diff --git a/go.mod b/go.mod index 6dee319..8ca6f9f 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/lightningnetwork/lnd/cert v1.0.3 github.com/lightningnetwork/lnd/clock v1.0.1 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/urfave/cli v1.20.0 golang.org/x/net v0.0.0-20191002035440-2ec189313ef0 diff --git a/liquidity/autoloop_test.go b/liquidity/autoloop_test.go new file mode 100644 index 0000000..030a55e --- /dev/null +++ b/liquidity/autoloop_test.go @@ -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, + }, + } +} diff --git a/liquidity/autoloop_testcontext_test.go b/liquidity/autoloop_testcontext_test.go new file mode 100644 index 0000000..464b811 --- /dev/null +++ b/liquidity/autoloop_testcontext_test.go @@ -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 + } +} diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index 6f1e73d..72ecec5 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -51,6 +51,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/ticker" ) const ( @@ -93,7 +94,11 @@ const ( // dispatched swaps we allow. Note that this does not enable automated // swaps itself (because we want non-zero values to be expressed in // 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 ( @@ -158,6 +163,11 @@ var ( // Config contains the external functionality required to run the // liquidity manager. 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 // to loop out swaps. LoopOutRestrictions func(ctx context.Context) (*Restrictions, error) @@ -176,6 +186,10 @@ type Config struct { LoopOutQuote func(ctx context.Context, 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 clock.Clock @@ -187,6 +201,9 @@ type Config struct { // Parameters is a set of parameters provided by the user which guide // how we assess liquidity. type Parameters struct { + // AutoOut enables automatic dispatch of loop out swaps. + AutoOut bool + // AutoFeeBudget is the total amount we allow to be spent on // automatically dispatched swaps. Once this budget has been used, we // will stop dispatching swaps until the budget is increased or the @@ -344,6 +361,24 @@ type Manager struct { 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. func NewManager(cfg *Config) *Manager { return &Manager{ @@ -392,10 +427,37 @@ func cloneParameters(params Parameters) Parameters { 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 // balance for the set of rules configured for the manager, failing if there are -// no rules set. -func (m *Manager) SuggestSwaps(ctx context.Context) ( +// no rules set. It takes an autoOut boolean that indicates whether the +// 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) { m.paramsLock.Lock() @@ -532,7 +594,12 @@ func (m *Manager) SuggestSwaps(ctx context.Context) ( continue } - outRequest := m.makeLoopOutRequest(suggestion, quote) + outRequest, err := m.makeLoopOutRequest( + ctx, suggestion, quote, autoOut, + ) + if err != nil { + return nil, err + } 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 } @@ -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 // 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 -// to give us some leeway when performing the swap. -func (m *Manager) makeLoopOutRequest(suggestion *LoopOutRecommendation, - quote *loop.LoopOutQuote) loop.OutRequest { +// to give us some leeway when performing the swap. We take an auto-out which +// determines whether we set a label identifying this swap as automatically +// 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( quote.PrepayAmount, m.params.MaximumPrepayRoutingFeePPM, @@ -597,7 +680,7 @@ func (m *Manager) makeLoopOutRequest(suggestion *LoopOutRecommendation, suggestion.Amount, m.params.MaximumRoutingFeePPM, ) - return loop.OutRequest{ + request := loop.OutRequest{ Amount: suggestion.Amount, OutgoingChanSet: loopdb.ChannelSet{ suggestion.Channel.ToUint64(), @@ -609,6 +692,18 @@ func (m *Manager) makeLoopOutRequest(suggestion *LoopOutRecommendation, MaxPrepayAmount: quote.PrepayAmount, 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, diff --git a/liquidity/liquidity_test.go b/liquidity/liquidity_test.go index 06ef481..26b938e 100644 --- a/liquidity/liquidity_test.go +++ b/liquidity/liquidity_test.go @@ -858,7 +858,7 @@ func testSuggestSwaps(t *testing.T, setup *testSuggestSwapsSetup, err := manager.SetParameters(setup.params) require.NoError(t, err) - actual, err := manager.SuggestSwaps(context.Background()) + actual, err := manager.SuggestSwaps(context.Background(), false) require.NoError(t, err) require.Equal(t, expected, actual) } diff --git a/loopd/daemon.go b/loopd/daemon.go index f806788..4295c10 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -98,10 +98,10 @@ func New(config *Config, lisCfg *listenerCfg) *Daemon { cfg: config, 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 // 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.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 // 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 diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index af0feed..f1bf396 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -672,7 +672,7 @@ func rpcToRule(rule *looprpc.LiquidityRule) (*liquidity.ThresholdRule, error) { func (s *swapClientServer) SuggestSwaps(ctx context.Context, _ *looprpc.SuggestSwapsRequest) (*looprpc.SuggestSwapsResponse, error) { - swaps, err := s.liquidityMgr.SuggestSwaps(ctx) + swaps, err := s.liquidityMgr.SuggestSwaps(ctx, false) if err != nil { return nil, err } diff --git a/loopd/utils.go b/loopd/utils.go index b70cab2..90b329a 100644 --- a/loopd/utils.go +++ b/loopd/utils.go @@ -8,6 +8,7 @@ import ( "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/liquidity" "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/ticker" ) // 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 { mngrCfg := &liquidity.Config{ + AutoOutTicker: ticker.NewForce(liquidity.DefaultAutoOutTicker), + LoopOut: client.LoopOut, LoopOutRestrictions: func(ctx context.Context) ( *liquidity.Restrictions, error) {