2
0
mirror of https://github.com/lightninglabs/loop synced 2024-11-11 13:11:12 +00:00

multi: add opt-in automated swap dispatch to liquidity manager

This commit is contained in:
carla 2020-10-12 13:34:55 +02:00
parent fd17580213
commit 8166d936e1
No known key found for this signature in database
GPG Key ID: 4CA7FE54A6213C91
8 changed files with 625 additions and 12 deletions

1
go.mod
View File

@ -16,6 +16,7 @@ require (
github.com/lightningnetwork/lnd/cert v1.0.3 github.com/lightningnetwork/lnd/cert v1.0.3
github.com/lightningnetwork/lnd/clock v1.0.1 github.com/lightningnetwork/lnd/clock v1.0.1
github.com/lightningnetwork/lnd/queue v1.0.4 github.com/lightningnetwork/lnd/queue v1.0.4
github.com/lightningnetwork/lnd/ticker v1.0.0
github.com/stretchr/testify v1.5.1 github.com/stretchr/testify v1.5.1
github.com/urfave/cli v1.20.0 github.com/urfave/cli v1.20.0
golang.org/x/net v0.0.0-20191002035440-2ec189313ef0 golang.org/x/net v0.0.0-20191002035440-2ec189313ef0

290
liquidity/autoloop_test.go Normal file
View File

@ -0,0 +1,290 @@
package liquidity
import (
"testing"
"time"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
)
// TestAutoLoopDisabled tests the case where we need to perform a swap, but
// autoloop is not enabled.
func TestAutoLoopDisabled(t *testing.T) {
defer test.Guard(t)()
// Set parameters for a channel that will require a swap.
channels := []lndclient.ChannelInfo{
channel1,
}
params := defaultParameters
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule,
}
c := newAutoloopTestCtx(t, params, channels)
c.start()
// We expect a single quote to be required for our swap on channel 1.
// We set its quote to have acceptable fees for our current limit.
quotes := []quoteRequestResp{
{
request: &loop.LoopOutQuoteRequest{
Amount: chan1Rec.Amount,
SweepConfTarget: chan1Rec.SweepConfTarget,
},
quote: testQuote,
},
}
// Trigger an autoloop attempt for our test context with no existing
// loop in/out swaps. We expect a swap for our channel to be suggested,
// but do not expect any swaps to be executed, since autoloop is
// disabled by default.
c.autoloop(1, chan1Rec.Amount+1, nil, quotes, nil)
// Trigger another autoloop, this time setting our server restrictions
// to have a minimum swap amount greater than the amount that we need
// to swap. In this case we don't even expect to get a quote, because
// our suggested swap is beneath the minimum swap size.
c.autoloop(chan1Rec.Amount+1, chan1Rec.Amount+2, nil, nil, nil)
c.stop()
}
// TestAutoLoopEnabled tests enabling the liquidity manger's autolooper. To keep
// the test simple, we do not update actual lnd channel balances, but rather
// run our mock with two channels that will always require a loop out according
// to our rules. This allows us to test the other restrictions placed on the
// autolooper (such as balance, and in-flight swaps) rather than need to worry
// about calculating swap amounts and thresholds.
func TestAutoLoopEnabled(t *testing.T) {
defer test.Guard(t)()
channels := []lndclient.ChannelInfo{
channel1, channel2,
}
// Create a set of parameters with autoloop enabled. The autoloop budget
// is set to allow exactly 2 swaps at the prices that we set in our
// test quotes.
params := Parameters{
AutoOut: true,
AutoFeeBudget: 40066,
AutoFeeStartDate: testTime,
MaxAutoInFlight: 2,
FailureBackOff: time.Hour,
SweepFeeRateLimit: 20000,
SweepConfTarget: 10,
MaximumPrepay: 20000,
MaximumSwapFeePPM: 1000,
MaximumRoutingFeePPM: 1000,
MaximumPrepayRoutingFeePPM: 1000,
MaximumMinerFee: 20000,
ChannelRules: map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule,
chanID2: chanRule,
},
}
c := newAutoloopTestCtx(t, params, channels)
c.start()
// Calculate our maximum allowed fees and create quotes that fall within
// our budget.
var (
amt = chan1Rec.Amount
maxSwapFee = ppmToSat(amt, params.MaximumSwapFeePPM)
// Create a quote that is within our limits. We do not set miner
// fee because this value is not actually set by the server.
quote1 = &loop.LoopOutQuote{
SwapFee: maxSwapFee,
PrepayAmount: params.MaximumPrepay - 10,
}
quote2 = &loop.LoopOutQuote{
SwapFee: maxSwapFee,
PrepayAmount: params.MaximumPrepay - 20,
}
quoteRequest = &loop.LoopOutQuoteRequest{
Amount: amt,
SweepConfTarget: params.SweepConfTarget,
}
quotes = []quoteRequestResp{
{
request: quoteRequest,
quote: quote1,
},
{
request: quoteRequest,
quote: quote2,
},
}
maxRouteFee = ppmToSat(amt, params.MaximumRoutingFeePPM)
chan1Swap = &loop.OutRequest{
Amount: amt,
MaxSwapRoutingFee: maxRouteFee,
MaxPrepayRoutingFee: ppmToSat(
quote1.PrepayAmount,
params.MaximumPrepayRoutingFeePPM,
),
MaxSwapFee: quote1.SwapFee,
MaxPrepayAmount: quote1.PrepayAmount,
MaxMinerFee: params.MaximumMinerFee,
SweepConfTarget: params.SweepConfTarget,
OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()},
Label: labels.AutoOutLabel(),
}
chan2Swap = &loop.OutRequest{
Amount: amt,
MaxSwapRoutingFee: maxRouteFee,
MaxPrepayRoutingFee: ppmToSat(
quote2.PrepayAmount,
params.MaximumPrepayRoutingFeePPM,
),
MaxSwapFee: quote2.SwapFee,
MaxPrepayAmount: quote2.PrepayAmount,
MaxMinerFee: params.MaximumMinerFee,
SweepConfTarget: params.SweepConfTarget,
OutgoingChanSet: loopdb.ChannelSet{chanID2.ToUint64()},
Label: labels.AutoOutLabel(),
}
loopOuts = []loopOutRequestResp{
{
request: chan1Swap,
response: &loop.LoopOutSwapInfo{
SwapHash: lntypes.Hash{1},
},
},
{
request: chan2Swap,
response: &loop.LoopOutSwapInfo{
SwapHash: lntypes.Hash{2},
},
},
}
)
// Tick our autolooper with no existing swaps, we expect a loop out
// swap to be dispatched for each channel.
c.autoloop(1, amt+1, nil, quotes, loopOuts)
// Tick again with both of our swaps in progress. We haven't shifted our
// channel balances at all, so swaps should still be suggested, but we
// have 2 swaps in flight so we do not expect any suggestion.
existing := []*loopdb.LoopOut{
existingSwapFromRequest(chan1Swap, testTime, nil),
existingSwapFromRequest(chan2Swap, testTime, nil),
}
c.autoloop(1, amt+1, existing, nil, nil)
// Now, we update our channel 2 swap to have failed due to off chain
// failure and our first swap to have succeeded.
now := c.testClock.Now()
failedOffChain := []*loopdb.LoopEvent{
{
SwapStateData: loopdb.SwapStateData{
State: loopdb.StateFailOffchainPayments,
},
Time: now,
},
}
success := []*loopdb.LoopEvent{
{
SwapStateData: loopdb.SwapStateData{
State: loopdb.StateSuccess,
Cost: loopdb.SwapCost{
Server: quote1.SwapFee,
Onchain: params.MaximumMinerFee,
Offchain: maxRouteFee +
chan1Rec.MaxPrepayRoutingFee,
},
},
Time: now,
},
}
quotes = []quoteRequestResp{
{
request: quoteRequest,
quote: quote1,
},
}
loopOuts = []loopOutRequestResp{
{
request: chan1Swap,
response: &loop.LoopOutSwapInfo{
SwapHash: lntypes.Hash{3},
},
},
}
existing = []*loopdb.LoopOut{
existingSwapFromRequest(chan1Swap, testTime, success),
existingSwapFromRequest(chan2Swap, testTime, failedOffChain),
}
// We tick again, this time we expect another swap on channel 1 (which
// still has balances which reflect that we need to swap), but nothing
// for channel 2, since it has had a failure.
c.autoloop(1, amt+1, existing, quotes, loopOuts)
// Now, we progress our time so that we have sufficiently backed off
// for channel 2, and could perform another swap.
c.testClock.SetTime(now.Add(params.FailureBackOff))
// Our existing swaps (1 successful, one pending) have used our budget
// so we no longer expect any swaps to automatically dispatch.
existing = []*loopdb.LoopOut{
existingSwapFromRequest(chan1Swap, testTime, success),
existingSwapFromRequest(chan1Swap, c.testClock.Now(), nil),
existingSwapFromRequest(chan2Swap, testTime, failedOffChain),
}
c.autoloop(1, amt+1, existing, quotes, nil)
c.stop()
}
// existingSwapFromRequest is a helper function which returns the db
// representation of a loop out request with the event set provided.
func existingSwapFromRequest(request *loop.OutRequest, initTime time.Time,
events []*loopdb.LoopEvent) *loopdb.LoopOut {
return &loopdb.LoopOut{
Loop: loopdb.Loop{
Events: events,
},
Contract: &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
AmountRequested: request.Amount,
MaxSwapFee: request.MaxSwapFee,
MaxMinerFee: request.MaxMinerFee,
InitiationTime: initTime,
Label: request.Label,
},
SwapInvoice: "",
MaxSwapRoutingFee: request.MaxSwapRoutingFee,
SweepConfTarget: request.SweepConfTarget,
OutgoingChanSet: request.OutgoingChanSet,
MaxPrepayRoutingFee: request.MaxSwapRoutingFee,
},
}
}

View File

@ -0,0 +1,211 @@
package liquidity
import (
"context"
"testing"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/ticker"
"github.com/stretchr/testify/assert"
)
type autoloopTestCtx struct {
t *testing.T
manager *Manager
lnd *test.LndMockServices
testClock *clock.TestClock
// quoteRequests is a channel that requests for quotes are pushed into.
quoteRequest chan *loop.LoopOutQuoteRequest
// quotes is a channel that we get loop out quote requests on.
quotes chan *loop.LoopOutQuote
// loopOutRestrictions is a channel that we get the server's
// restrictions on.
loopOutRestrictions chan *Restrictions
// loopOuts is a channel that we get existing loop out swaps on.
loopOuts chan []*loopdb.LoopOut
// loopIns is a channel that we get existing loop in swaps on.
loopIns chan []*loopdb.LoopIn
// restrictions is a channel that we get swap restrictions on.
restrictions chan *Restrictions
// outRequest is a channel that requests to dispatch loop outs are
// pushed into.
outRequest chan *loop.OutRequest
// loopOut is a channel that we return loop out responses on.
loopOut chan *loop.LoopOutSwapInfo
// errChan is a channel that we send run errors into.
errChan chan error
// cancelCtx cancels the context that our liquidity manager is run with.
// This can be used to cleanly shutdown the test. Note that this will be
// nil until the test context has been started.
cancelCtx func()
}
// newAutoloopTestCtx creates a test context with custom liquidity manager
// parameters and lnd channels.
func newAutoloopTestCtx(t *testing.T, parameters Parameters,
channels []lndclient.ChannelInfo) *autoloopTestCtx {
// Create a mock lnd and set our expected fee rate for sweeps to our
// sweep fee rate limit value.
lnd := test.NewMockLnd()
lnd.SetFeeEstimate(
defaultParameters.SweepConfTarget,
defaultParameters.SweepFeeRateLimit,
)
testCtx := &autoloopTestCtx{
t: t,
testClock: clock.NewTestClock(testTime),
lnd: lnd,
quoteRequest: make(chan *loop.LoopOutQuoteRequest),
quotes: make(chan *loop.LoopOutQuote),
loopOutRestrictions: make(chan *Restrictions),
loopOuts: make(chan []*loopdb.LoopOut),
loopIns: make(chan []*loopdb.LoopIn),
restrictions: make(chan *Restrictions),
outRequest: make(chan *loop.OutRequest),
loopOut: make(chan *loop.LoopOutSwapInfo),
errChan: make(chan error, 1),
}
// Set lnd's channels to equal the set of channels we want for our
// test.
testCtx.lnd.Channels = channels
cfg := &Config{
AutoOutTicker: ticker.NewForce(DefaultAutoOutTicker),
LoopOutRestrictions: func(context.Context) (*Restrictions, error) {
return <-testCtx.loopOutRestrictions, nil
},
ListLoopOut: func() ([]*loopdb.LoopOut, error) {
return <-testCtx.loopOuts, nil
},
ListLoopIn: func() ([]*loopdb.LoopIn, error) {
return <-testCtx.loopIns, nil
},
LoopOutQuote: func(_ context.Context,
req *loop.LoopOutQuoteRequest) (*loop.LoopOutQuote,
error) {
testCtx.quoteRequest <- req
return <-testCtx.quotes, nil
},
LoopOut: func(_ context.Context,
req *loop.OutRequest) (*loop.LoopOutSwapInfo,
error) {
testCtx.outRequest <- req
return <-testCtx.loopOut, nil
},
MinimumConfirmations: loop.DefaultSweepConfTarget,
Lnd: &testCtx.lnd.LndServices,
Clock: testCtx.testClock,
}
// Create a manager with our test config and set our starting set of
// parameters.
testCtx.manager = NewManager(cfg)
assert.NoError(t, testCtx.manager.SetParameters(parameters))
return testCtx
}
// start starts our liquidity manager's run loop in a goroutine. Tests should
// be run with test.Guard() to ensure that this does not leak.
func (c *autoloopTestCtx) start() {
ctx := context.Background()
ctx, c.cancelCtx = context.WithCancel(ctx)
go func() {
c.errChan <- c.manager.Run(ctx)
}()
}
// stop shuts down our test context and asserts that we have exited with a
// context-cancelled error.
func (c *autoloopTestCtx) stop() {
c.cancelCtx()
assert.Equal(c.t, context.Canceled, <-c.errChan)
}
// quoteRequestResp pairs an expected swap quote request with the response we
// would like to provide the liquidity manager with.
type quoteRequestResp struct {
request *loop.LoopOutQuoteRequest
quote *loop.LoopOutQuote
}
// loopOutRequestResp pairs an expected loop out request with the response we
// would like the server to respond with.
type loopOutRequestResp struct {
request *loop.OutRequest
response *loop.LoopOutSwapInfo
}
// autoloop walks our test context through the process of triggering our
// autoloop functionality, providing mocked values as required. The set of
// quotes provided indicates that we expect swap suggestions to be made (since
// we will query for a quote for each suggested swap). The set of expected
// swaps indicates whether we expect any of these swap suggestions to actually
// be dispatched by the autolooper.
func (c *autoloopTestCtx) autoloop(minAmt, maxAmt btcutil.Amount,
existingOut []*loopdb.LoopOut, quotes []quoteRequestResp,
expectedSwaps []loopOutRequestResp) {
// Tick our autoloop ticker to force assessing whether we want to loop.
c.manager.cfg.AutoOutTicker.Force <- testTime
// Send a mocked response from the server with the swap size limits.
c.loopOutRestrictions <- NewRestrictions(minAmt, maxAmt)
// Provide the liquidity manager with our desired existing set of swaps.
c.loopOuts <- existingOut
c.loopIns <- nil
// Assert that we query the server for a quote for each of our
// recommended swaps. Note that this differs from our set of expected
// swaps because we may get quotes for suggested swaps but then just
// log them.
for _, expected := range quotes {
request := <-c.quoteRequest
assert.Equal(
c.t, expected.request.Amount, request.Amount,
)
assert.Equal(
c.t, expected.request.SweepConfTarget,
request.SweepConfTarget,
)
c.quotes <- expected.quote
}
// Assert that we dispatch the expected set of swaps.
for _, expected := range expectedSwaps {
actual := <-c.outRequest
// Set our destination address to nil so that we do not need to
// provide the address that is obtained by the mock wallet kit.
actual.DestAddr = nil
assert.Equal(c.t, expected.request, actual)
c.loopOut <- expected.response
}
}

View File

@ -51,6 +51,7 @@ import (
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/ticker"
) )
const ( const (
@ -93,7 +94,11 @@ const (
// dispatched swaps we allow. Note that this does not enable automated // dispatched swaps we allow. Note that this does not enable automated
// swaps itself (because we want non-zero values to be expressed in // swaps itself (because we want non-zero values to be expressed in
// suggestions as a dry-run). // suggestions as a dry-run).
defaultMaxInFlight = 2 defaultMaxInFlight = 1
// DefaultAutoOutTicker is the default amount of time between automated
// loop out checks.
DefaultAutoOutTicker = time.Minute * 10
) )
var ( var (
@ -158,6 +163,11 @@ var (
// Config contains the external functionality required to run the // Config contains the external functionality required to run the
// liquidity manager. // liquidity manager.
type Config struct { type Config struct {
// AutoOutTicker determines how often we should check whether we want
// to dispatch an automated loop out. We use a force ticker so that
// we can trigger autoloop in itests.
AutoOutTicker *ticker.Force
// LoopOutRestrictions returns the restrictions that the server applies // LoopOutRestrictions returns the restrictions that the server applies
// to loop out swaps. // to loop out swaps.
LoopOutRestrictions func(ctx context.Context) (*Restrictions, error) LoopOutRestrictions func(ctx context.Context) (*Restrictions, error)
@ -176,6 +186,10 @@ type Config struct {
LoopOutQuote func(ctx context.Context, LoopOutQuote func(ctx context.Context,
request *loop.LoopOutQuoteRequest) (*loop.LoopOutQuote, error) request *loop.LoopOutQuoteRequest) (*loop.LoopOutQuote, error)
// LoopOut dispatches a loop out.
LoopOut func(ctx context.Context, request *loop.OutRequest) (
*loop.LoopOutSwapInfo, error)
// Clock allows easy mocking of time in unit tests. // Clock allows easy mocking of time in unit tests.
Clock clock.Clock Clock clock.Clock
@ -187,6 +201,9 @@ type Config struct {
// Parameters is a set of parameters provided by the user which guide // Parameters is a set of parameters provided by the user which guide
// how we assess liquidity. // how we assess liquidity.
type Parameters struct { type Parameters struct {
// AutoOut enables automatic dispatch of loop out swaps.
AutoOut bool
// AutoFeeBudget is the total amount we allow to be spent on // AutoFeeBudget is the total amount we allow to be spent on
// automatically dispatched swaps. Once this budget has been used, we // automatically dispatched swaps. Once this budget has been used, we
// will stop dispatching swaps until the budget is increased or the // will stop dispatching swaps until the budget is increased or the
@ -344,6 +361,24 @@ type Manager struct {
paramsLock sync.Mutex paramsLock sync.Mutex
} }
// Run periodically checks whether we should automatically dispatch a loop out.
func (m *Manager) Run(ctx context.Context) error {
m.cfg.AutoOutTicker.Resume()
defer m.cfg.AutoOutTicker.Stop()
for {
select {
case <-m.cfg.AutoOutTicker.Ticks():
if err := m.autoloop(ctx); err != nil {
log.Errorf("autoloop failed: %v", err)
}
case <-ctx.Done():
return ctx.Err()
}
}
}
// NewManager creates a liquidity manager which has no rules set. // NewManager creates a liquidity manager which has no rules set.
func NewManager(cfg *Config) *Manager { func NewManager(cfg *Config) *Manager {
return &Manager{ return &Manager{
@ -392,10 +427,37 @@ func cloneParameters(params Parameters) Parameters {
return paramCopy return paramCopy
} }
// autoloop gets a set of suggested swaps and dispatches them automatically if
// we have automated looping enabled.
func (m *Manager) autoloop(ctx context.Context) error {
swaps, err := m.SuggestSwaps(ctx, true)
if err != nil {
return err
}
for _, swap := range swaps {
// Create a copy of our range var so that we can reference it.
swap := swap
loopOut, err := m.cfg.LoopOut(ctx, &swap)
if err != nil {
return err
}
log.Infof("loop out automatically dispatched: hash: %v, "+
"address: %v", loopOut.SwapHash,
loopOut.HtlcAddressP2WSH)
}
return nil
}
// SuggestSwaps returns a set of swap suggestions based on our current liquidity // SuggestSwaps returns a set of swap suggestions based on our current liquidity
// balance for the set of rules configured for the manager, failing if there are // balance for the set of rules configured for the manager, failing if there are
// no rules set. // no rules set. It takes an autoOut boolean that indicates whether the
func (m *Manager) SuggestSwaps(ctx context.Context) ( // suggestions are being used for our internal autolooper. This boolean is used
// to determine the information we add to our swap suggestion and whether we
// return any suggestions.
func (m *Manager) SuggestSwaps(ctx context.Context, autoOut bool) (
[]loop.OutRequest, error) { []loop.OutRequest, error) {
m.paramsLock.Lock() m.paramsLock.Lock()
@ -532,7 +594,12 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
continue continue
} }
outRequest := m.makeLoopOutRequest(suggestion, quote) outRequest, err := m.makeLoopOutRequest(
ctx, suggestion, quote, autoOut,
)
if err != nil {
return nil, err
}
suggestions = append(suggestions, outRequest) suggestions = append(suggestions, outRequest)
} }
@ -575,6 +642,18 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
} }
} }
// If we are getting suggestions for automatically dispatched swaps,
// and they are not enabled in our parameters, we just log the swap
// suggestions and return an empty set of suggestions.
if autoOut && !m.params.AutoOut {
for _, swap := range inBudget {
log.Debugf("recommended autoloop: %v sats over "+
"%v", swap.Amount, swap.OutgoingChanSet)
}
return nil, nil
}
return inBudget, nil return inBudget, nil
} }
@ -585,9 +664,13 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
// route-independent, which is a very poor estimation so we don't bother with // route-independent, which is a very poor estimation so we don't bother with
// checking against this inaccurate constant. We use the exact prepay amount // checking against this inaccurate constant. We use the exact prepay amount
// and swap fee given to us by the server, but use our maximum miner fee anyway // and swap fee given to us by the server, but use our maximum miner fee anyway
// to give us some leeway when performing the swap. // to give us some leeway when performing the swap. We take an auto-out which
func (m *Manager) makeLoopOutRequest(suggestion *LoopOutRecommendation, // determines whether we set a label identifying this swap as automatically
quote *loop.LoopOutQuote) loop.OutRequest { // dispatched, and decides whether we set a sweep address (we don't bother for
// non-auto requests, because the client api will set it anyway).
func (m *Manager) makeLoopOutRequest(ctx context.Context,
suggestion *LoopOutRecommendation, quote *loop.LoopOutQuote,
autoOut bool) (loop.OutRequest, error) {
prepayMaxFee := ppmToSat( prepayMaxFee := ppmToSat(
quote.PrepayAmount, m.params.MaximumPrepayRoutingFeePPM, quote.PrepayAmount, m.params.MaximumPrepayRoutingFeePPM,
@ -597,7 +680,7 @@ func (m *Manager) makeLoopOutRequest(suggestion *LoopOutRecommendation,
suggestion.Amount, m.params.MaximumRoutingFeePPM, suggestion.Amount, m.params.MaximumRoutingFeePPM,
) )
return loop.OutRequest{ request := loop.OutRequest{
Amount: suggestion.Amount, Amount: suggestion.Amount,
OutgoingChanSet: loopdb.ChannelSet{ OutgoingChanSet: loopdb.ChannelSet{
suggestion.Channel.ToUint64(), suggestion.Channel.ToUint64(),
@ -609,6 +692,18 @@ func (m *Manager) makeLoopOutRequest(suggestion *LoopOutRecommendation,
MaxPrepayAmount: quote.PrepayAmount, MaxPrepayAmount: quote.PrepayAmount,
SweepConfTarget: m.params.SweepConfTarget, SweepConfTarget: m.params.SweepConfTarget,
} }
if autoOut {
request.Label = labels.AutoOutLabel()
addr, err := m.cfg.Lnd.WalletKit.NextAddr(ctx)
if err != nil {
return loop.OutRequest{}, err
}
request.DestAddr = addr
}
return request, nil
} }
// worstCaseOutFees calculates the largest possible fees for a loop out swap, // worstCaseOutFees calculates the largest possible fees for a loop out swap,

View File

@ -858,7 +858,7 @@ func testSuggestSwaps(t *testing.T, setup *testSuggestSwapsSetup,
err := manager.SetParameters(setup.params) err := manager.SetParameters(setup.params)
require.NoError(t, err) require.NoError(t, err)
actual, err := manager.SuggestSwaps(context.Background()) actual, err := manager.SuggestSwaps(context.Background(), false)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected, actual) require.Equal(t, expected, actual)
} }

View File

@ -98,10 +98,10 @@ func New(config *Config, lisCfg *listenerCfg) *Daemon {
cfg: config, cfg: config,
listenerCfg: lisCfg, listenerCfg: lisCfg,
// We have 3 goroutines that could potentially send an error. // We have 4 goroutines that could potentially send an error.
// We react on the first error but in case more than one exits // We react on the first error but in case more than one exits
// with an error we don't want them to block. // with an error we don't want them to block.
internalErrChan: make(chan error, 3), internalErrChan: make(chan error, 4),
} }
} }
@ -408,6 +408,19 @@ func (d *Daemon) initialize() error {
d.processStatusUpdates(d.mainCtx) d.processStatusUpdates(d.mainCtx)
}() }()
d.wg.Add(1)
go func() {
defer d.wg.Done()
log.Info("Starting liquidity manager")
err := d.liquidityMgr.Run(d.mainCtx)
if err != nil && err != context.Canceled {
d.internalErrChan <- err
}
log.Info("Liquidity manager stopped")
}()
// Last, start our internal error handler. This will return exactly one // Last, start our internal error handler. This will return exactly one
// error or nil on the main error channel to inform the caller that // error or nil on the main error channel to inform the caller that
// something went wrong or that shutdown is complete. We don't add to // something went wrong or that shutdown is complete. We don't add to

View File

@ -672,7 +672,7 @@ func rpcToRule(rule *looprpc.LiquidityRule) (*liquidity.ThresholdRule, error) {
func (s *swapClientServer) SuggestSwaps(ctx context.Context, func (s *swapClientServer) SuggestSwaps(ctx context.Context,
_ *looprpc.SuggestSwapsRequest) (*looprpc.SuggestSwapsResponse, error) { _ *looprpc.SuggestSwapsRequest) (*looprpc.SuggestSwapsResponse, error) {
swaps, err := s.liquidityMgr.SuggestSwaps(ctx) swaps, err := s.liquidityMgr.SuggestSwaps(ctx, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -8,6 +8,7 @@ import (
"github.com/lightninglabs/loop" "github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/liquidity" "github.com/lightninglabs/loop/liquidity"
"github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/ticker"
) )
// getClient returns an instance of the swap client. // getClient returns an instance of the swap client.
@ -35,6 +36,8 @@ func getClient(config *Config, lnd *lndclient.LndServices) (*loop.Client,
func getLiquidityManager(client *loop.Client) *liquidity.Manager { func getLiquidityManager(client *loop.Client) *liquidity.Manager {
mngrCfg := &liquidity.Config{ mngrCfg := &liquidity.Config{
AutoOutTicker: ticker.NewForce(liquidity.DefaultAutoOutTicker),
LoopOut: client.LoopOut,
LoopOutRestrictions: func(ctx context.Context) ( LoopOutRestrictions: func(ctx context.Context) (
*liquidity.Restrictions, error) { *liquidity.Restrictions, error) {