diff --git a/liquidity/autoloop_test.go b/liquidity/autoloop_test.go index 253b12b..b682c5c 100644 --- a/liquidity/autoloop_test.go +++ b/liquidity/autoloop_test.go @@ -28,6 +28,7 @@ func TestAutoLoopDisabled(t *testing.T) { } params := defaultParameters + params.AutoloopBudgetLastRefresh = testBudgetStart params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{ chanID1: chanRule, } @@ -95,12 +96,13 @@ func TestAutoLoopEnabled(t *testing.T) { // autoloop budget is set to allow exactly 2 swaps at the prices // that we set in our test quotes. params = Parameters{ - Autoloop: true, - AutoFeeBudget: 40066, - AutoFeeRefreshPeriod: testBudgetRefresh, - MaxAutoInFlight: 2, - FailureBackOff: time.Hour, - SweepConfTarget: 10, + Autoloop: true, + AutoFeeBudget: 40066, + AutoFeeRefreshPeriod: testBudgetRefresh, + AutoloopBudgetLastRefresh: testBudgetStart, + MaxAutoInFlight: 2, + FailureBackOff: time.Hour, + SweepConfTarget: 10, FeeLimit: NewFeeCategoryLimit( swapFeePPM, routeFeePPM, prepayFeePPM, maxMiner, prepayAmount, 20000, @@ -353,13 +355,14 @@ func TestAutoloopAddress(t *testing.T) { // Create some dummy parameters for autoloop and also specify an // destination address. params = Parameters{ - Autoloop: true, - AutoFeeBudget: 40066, - DestAddr: addr, - AutoFeeRefreshPeriod: testBudgetRefresh, - MaxAutoInFlight: 2, - FailureBackOff: time.Hour, - SweepConfTarget: 10, + Autoloop: true, + AutoFeeBudget: 40066, + DestAddr: addr, + AutoFeeRefreshPeriod: testBudgetRefresh, + AutoloopBudgetLastRefresh: testBudgetStart, + MaxAutoInFlight: 2, + FailureBackOff: time.Hour, + SweepConfTarget: 10, FeeLimit: NewFeeCategoryLimit( swapFeePPM, routeFeePPM, prepayFeePPM, maxMiner, prepayAmount, 20000, @@ -523,12 +526,13 @@ func TestCompositeRules(t *testing.T) { swapFeePPM, routeFeePPM, prepayFeePPM, maxMiner, prepayAmount, 20000, ), - Autoloop: true, - AutoFeeBudget: 100000, - AutoFeeRefreshPeriod: testBudgetRefresh, - MaxAutoInFlight: 2, - FailureBackOff: time.Hour, - SweepConfTarget: 10, + Autoloop: true, + AutoFeeBudget: 100000, + AutoFeeRefreshPeriod: testBudgetRefresh, + AutoloopBudgetLastRefresh: testBudgetStart, + MaxAutoInFlight: 2, + FailureBackOff: time.Hour, + SweepConfTarget: 10, ChannelRules: map[lnwire.ShortChannelID]*SwapRule{ chanID1: chanRule, }, @@ -715,13 +719,14 @@ func TestAutoLoopInEnabled(t *testing.T) { peer2MaxFee = ppmToSat(peer2ExpectedAmt, swapFeePPM) params = Parameters{ - Autoloop: true, - AutoFeeBudget: peer1MaxFee + peer2MaxFee + 1, - AutoFeeRefreshPeriod: testBudgetRefresh, - MaxAutoInFlight: 2, - FailureBackOff: time.Hour, - FeeLimit: NewFeePortion(swapFeePPM), - ChannelRules: make(map[lnwire.ShortChannelID]*SwapRule), + Autoloop: true, + AutoFeeBudget: peer1MaxFee + peer2MaxFee + 1, + AutoFeeRefreshPeriod: testBudgetRefresh, + AutoloopBudgetLastRefresh: testBudgetStart, + MaxAutoInFlight: 2, + FailureBackOff: time.Hour, + FeeLimit: NewFeePortion(swapFeePPM), + ChannelRules: make(map[lnwire.ShortChannelID]*SwapRule), PeerRules: map[route.Vertex]*SwapRule{ peer1: rule, peer2: rule, @@ -898,12 +903,13 @@ func TestAutoloopBothTypes(t *testing.T) { loopInMaxFee = ppmToSat(loopInAmount, swapFeePPM) params = Parameters{ - Autoloop: true, - AutoFeeBudget: loopOutMaxFee + loopInMaxFee + 1, - AutoFeeRefreshPeriod: testBudgetRefresh, - MaxAutoInFlight: 2, - FailureBackOff: time.Hour, - FeeLimit: NewFeePortion(swapFeePPM), + Autoloop: true, + AutoFeeBudget: loopOutMaxFee + loopInMaxFee + 1, + AutoFeeRefreshPeriod: testBudgetRefresh, + AutoloopBudgetLastRefresh: testBudgetStart, + MaxAutoInFlight: 2, + FailureBackOff: time.Hour, + FeeLimit: NewFeePortion(swapFeePPM), ChannelRules: map[lnwire.ShortChannelID]*SwapRule{ chanID1: outRule, }, @@ -1041,12 +1047,13 @@ func TestAutoLoopRecurringBudget(t *testing.T) { maxMiner = btcutil.Amount(20000) params = Parameters{ - Autoloop: true, - AutoFeeBudget: 36000, - AutoFeeRefreshPeriod: time.Hour * 3, - MaxAutoInFlight: 2, - FailureBackOff: time.Hour, - SweepConfTarget: 10, + Autoloop: true, + AutoFeeBudget: 36000, + AutoFeeRefreshPeriod: time.Hour * 3, + AutoloopBudgetLastRefresh: testBudgetStart, + MaxAutoInFlight: 2, + FailureBackOff: time.Hour, + SweepConfTarget: 10, FeeLimit: NewFeeCategoryLimit( swapFeePPM, routeFeePPM, prepayFeePPM, maxMiner, prepayAmount, 20000, diff --git a/liquidity/autoloop_testcontext_test.go b/liquidity/autoloop_testcontext_test.go index 8379fa9..8ecf27e 100644 --- a/liquidity/autoloop_testcontext_test.go +++ b/liquidity/autoloop_testcontext_test.go @@ -129,8 +129,7 @@ func newAutoloopTestCtx(t *testing.T, parameters Parameters, testCtx.lnd.Channels = channels cfg := &Config{ - AutoloopTicker: ticker.NewForce(DefaultAutoloopTicker), - AutoloopBudgetLastRefresh: testBudgetStart, + AutoloopTicker: ticker.NewForce(DefaultAutoloopTicker), Restrictions: func(_ context.Context, swapType swap.Type) (*Restrictions, error) { diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index 4f6a45d..5b47826 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -166,10 +166,6 @@ type Config struct { // trigger autoloop in itests. AutoloopTicker *ticker.Force - // AutoloopBudgetLastRefresh is the last time at which we refreshed - // our budget. - AutoloopBudgetLastRefresh time.Time - // Restrictions returns the restrictions that the server applies to // swaps. Restrictions func(ctx context.Context, swapType swap.Type) ( @@ -301,7 +297,7 @@ func (m *Manager) GetParameters() Parameters { func (m *Manager) SetParameters(ctx context.Context, req *clientrpc.LiquidityParameters) error { - params, err := rpcToParameters(req) + params, err := RpcToParameters(req) if err != nil { return err } @@ -743,7 +739,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( } } else { refreshTime := m.params.AutoFeeRefreshPeriod - - time.Since(m.cfg.AutoloopBudgetLastRefresh) + time.Since(m.params.AutoloopBudgetLastRefresh) log.Infof("Swap fee exceeds budget, remaining budget: "+ "%v, swap fee %v, next budget refresh: %v", @@ -929,7 +925,7 @@ func (m *Manager) checkExistingAutoLoops(ctx context.Context, mSatToSatoshis(prepay.Value), ) } else if out.LastUpdateTime().After( - m.cfg.AutoloopBudgetLastRefresh, + m.params.AutoloopBudgetLastRefresh, ) { summary.spentFees += out.State().Cost.Total() @@ -943,7 +939,7 @@ func (m *Manager) checkExistingAutoLoops(ctx context.Context, pending := in.State().State.Type() == loopdb.StateTypePending inBudget := !in.LastUpdateTime(). - Before(m.cfg.AutoloopBudgetLastRefresh) + Before(m.params.AutoloopBudgetLastRefresh) // If an autoloop is in a pending state, we always count it in // our current budget, and record the worst-case fees for it, @@ -1054,11 +1050,23 @@ func (m *Manager) currentSwapTraffic(loopOut []*loopdb.LoopOut, // budget refresh is greater than our configured refresh period. If so, the last // refresh timestamp. func (m *Manager) refreshAutoloopBudget(ctx context.Context) { - if time.Since(m.cfg.AutoloopBudgetLastRefresh) > + if time.Since(m.params.AutoloopBudgetLastRefresh) > m.params.AutoFeeRefreshPeriod { log.Debug("Refreshing autoloop budget") - m.cfg.AutoloopBudgetLastRefresh = m.cfg.Clock.Now() + m.params.AutoloopBudgetLastRefresh = m.cfg.Clock.Now() + + paramsRpc, err := ParametersToRpc(m.params) + if err != nil { + log.Errorf("Error converting parameters to rpc: %v", + err) + return + } + + err = m.saveParams(paramsRpc) + if err != nil { + log.Errorf("Error saving parameters: %v", err) + } } } diff --git a/liquidity/liquidity_test.go b/liquidity/liquidity_test.go index d195d8b..af8819b 100644 --- a/liquidity/liquidity_test.go +++ b/liquidity/liquidity_test.go @@ -148,9 +148,8 @@ func newTestConfig() (*Config, *test.LndMockServices) { return testRestrictions, nil }, - Lnd: &lnd.LndServices, - Clock: clock.NewTestClock(testTime), - AutoloopBudgetLastRefresh: testBudgetStart, + Lnd: &lnd.LndServices, + Clock: clock.NewTestClock(testTime), ListLoopOut: func() ([]*loopdb.LoopOut, error) { return nil, nil }, @@ -572,6 +571,7 @@ func TestRestrictedSuggestions(t *testing.T) { lnd.Channels = testCase.channels params := defaultParameters + params.AutoloopBudgetLastRefresh = testBudgetStart if testCase.chanRules != nil { params.ChannelRules = testCase.chanRules } @@ -652,6 +652,7 @@ func TestSweepFeeLimit(t *testing.T) { } params := defaultParameters + params.AutoloopBudgetLastRefresh = testBudgetStart params.FeeLimit = defaultFeeCategoryLimit() // Set our budget to cover a single swap with these @@ -794,6 +795,7 @@ func TestSuggestSwaps(t *testing.T) { lnd.Channels = testCase.channels params := defaultParameters + params.AutoloopBudgetLastRefresh = testBudgetStart if testCase.rules != nil { params.ChannelRules = testCase.rules } @@ -901,6 +903,7 @@ func TestFeeLimits(t *testing.T) { // Set our params to use individual fee limits. params := defaultParameters + params.AutoloopBudgetLastRefresh = testBudgetStart params.FeeLimit = defaultFeeCategoryLimit() // Set our budget to cover a single swap with these @@ -1108,6 +1111,7 @@ func TestFeeBudget(t *testing.T) { } params.AutoFeeBudget = testCase.budget params.AutoFeeRefreshPeriod = testBudgetRefresh + params.AutoloopBudgetLastRefresh = testBudgetStart params.MaxAutoInFlight = 2 params.FeeLimit = NewFeeCategoryLimit( defaultSwapFeePPM, defaultRoutingFeePPM, @@ -1272,6 +1276,7 @@ func TestInFlightLimit(t *testing.T) { } params := defaultParameters + params.AutoloopBudgetLastRefresh = testBudgetStart if testCase.peerRules != nil { params.PeerRules = testCase.peerRules @@ -1431,6 +1436,7 @@ func TestSizeRestrictions(t *testing.T) { } params := defaultParameters + params.AutoloopBudgetLastRefresh = testBudgetStart params.ClientRestrictions = testCase.clientRestrictions params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{ chanID1: chanRule, @@ -1593,6 +1599,7 @@ func TestFeePercentage(t *testing.T) { } params := defaultParameters + params.AutoloopBudgetLastRefresh = testBudgetStart params.FeeLimit = NewFeePortion(testCase.feePPM) params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{ chanID1: chanRule, @@ -1765,6 +1772,7 @@ func TestBudgetWithLoopin(t *testing.T) { params := defaultParameters params.AutoFeeBudget = budget params.AutoFeeRefreshPeriod = testBudgetRefresh + params.AutoloopBudgetLastRefresh = testBudgetStart params.FeeLimit = NewFeePortion(testPPM) params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{ @@ -1821,6 +1829,7 @@ func testSuggestSwaps(t *testing.T, setup *testSuggestSwapsSetup, } params := defaultParameters + params.AutoloopBudgetLastRefresh = testBudgetStart params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{ chanID1: chanRule, chanID2: chanRule, diff --git a/liquidity/parameters.go b/liquidity/parameters.go index 2b74c5c..c1781e4 100644 --- a/liquidity/parameters.go +++ b/liquidity/parameters.go @@ -20,16 +20,17 @@ var ( // defaultParameters contains the default parameters that we start our // liquidity manager with. defaultParameters = Parameters{ - AutoFeeBudget: defaultBudget, - AutoFeeRefreshPeriod: defaultBudgetRefreshPeriod, - DestAddr: nil, - MaxAutoInFlight: defaultMaxInFlight, - ChannelRules: make(map[lnwire.ShortChannelID]*SwapRule), - PeerRules: make(map[route.Vertex]*SwapRule), - FailureBackOff: defaultFailureBackoff, - SweepConfTarget: defaultConfTarget, - HtlcConfTarget: defaultHtlcConfTarget, - FeeLimit: defaultFeePortion(), + AutoFeeBudget: defaultBudget, + AutoFeeRefreshPeriod: defaultBudgetRefreshPeriod, + AutoloopBudgetLastRefresh: time.Now(), + DestAddr: nil, + MaxAutoInFlight: defaultMaxInFlight, + ChannelRules: make(map[lnwire.ShortChannelID]*SwapRule), + PeerRules: make(map[route.Vertex]*SwapRule), + FailureBackOff: defaultFailureBackoff, + SweepConfTarget: defaultConfTarget, + HtlcConfTarget: defaultHtlcConfTarget, + FeeLimit: defaultFeePortion(), } ) @@ -52,6 +53,10 @@ type Parameters struct { // auto fee budget is refreshed. AutoFeeRefreshPeriod time.Duration + // AutoloopBudgetLastRefresh is the last time at which we refreshed + // our budget. + AutoloopBudgetLastRefresh time.Time + // MaxAutoInFlight is the maximum number of in-flight automatically // dispatched swaps we allow. MaxAutoInFlight int @@ -348,7 +353,7 @@ func rpcToRule(rule *clientrpc.LiquidityRule) (*SwapRule, error) { // rpcToParameters takes a `LiquidityParameters` and creates a `Parameters` // from it. -func rpcToParameters(req *clientrpc.LiquidityParameters) (*Parameters, +func RpcToParameters(req *clientrpc.LiquidityParameters) (*Parameters, error) { feeLimit, err := rpcToFee(req) @@ -373,7 +378,10 @@ func rpcToParameters(req *clientrpc.LiquidityParameters) (*Parameters, SweepConfTarget: req.SweepConfTarget, FailureBackOff: time.Duration(req.FailureBackoffSec) * time.Second, - Autoloop: req.Autoloop, + Autoloop: req.Autoloop, + AutoloopBudgetLastRefresh: time.Unix( + int64(req.AutoloopBudgetLastRefresh), 0, + ), DestAddr: destaddr, AutoFeeBudget: btcutil.Amount(req.AutoloopBudgetSat), MaxAutoInFlight: int(req.AutoMaxInFlight), @@ -442,3 +450,90 @@ func rpcToParameters(req *clientrpc.LiquidityParameters) (*Parameters, return params, nil } + +// ParametersToRpc takes a `Parameters` and creates a `LiquidityParameters` +// from it. +func ParametersToRpc(cfg Parameters) (*clientrpc.LiquidityParameters, + error) { + + totalRules := len(cfg.ChannelRules) + len(cfg.PeerRules) + + var destaddr string + if cfg.DestAddr != nil { + destaddr = cfg.DestAddr.String() + } + + rpcCfg := &clientrpc.LiquidityParameters{ + SweepConfTarget: cfg.SweepConfTarget, + FailureBackoffSec: uint64(cfg.FailureBackOff.Seconds()), + Autoloop: cfg.Autoloop, + AutoloopBudgetSat: uint64(cfg.AutoFeeBudget), + AutoloopBudgetRefreshPeriodSec: uint64( + cfg.AutoFeeRefreshPeriod.Seconds(), + ), + AutoloopBudgetLastRefresh: uint64( + cfg.AutoloopBudgetLastRefresh.Unix(), + ), + AutoMaxInFlight: uint64(cfg.MaxAutoInFlight), + AutoloopDestAddress: destaddr, + Rules: make( + []*clientrpc.LiquidityRule, 0, totalRules, + ), + MinSwapAmount: uint64(cfg.ClientRestrictions.Minimum), + MaxSwapAmount: uint64(cfg.ClientRestrictions.Maximum), + HtlcConfTarget: cfg.HtlcConfTarget, + } + + switch f := cfg.FeeLimit.(type) { + case *FeeCategoryLimit: + satPerByte := f.SweepFeeRateLimit.FeePerKVByte() / 1000 + + rpcCfg.SweepFeeRateSatPerVbyte = uint64(satPerByte) + + rpcCfg.MaxMinerFeeSat = uint64(f.MaximumMinerFee) + rpcCfg.MaxSwapFeePpm = f.MaximumSwapFeePPM + rpcCfg.MaxRoutingFeePpm = f.MaximumRoutingFeePPM + rpcCfg.MaxPrepayRoutingFeePpm = f.MaximumPrepayRoutingFeePPM + rpcCfg.MaxPrepaySat = uint64(f.MaximumPrepay) + + case *FeePortion: + rpcCfg.FeePpm = f.PartsPerMillion + + default: + return nil, fmt.Errorf("unknown fee limit: %T", cfg.FeeLimit) + } + + for channel, rule := range cfg.ChannelRules { + rpcRule := newRPCRule(channel.ToUint64(), nil, rule) + rpcCfg.Rules = append(rpcCfg.Rules, rpcRule) + } + + for peer, rule := range cfg.PeerRules { + peer := peer + rpcRule := newRPCRule(0, peer[:], rule) + rpcCfg.Rules = append(rpcCfg.Rules, rpcRule) + } + + return rpcCfg, nil +} + +// newRPCRule is a helper function that creates a `LiquidityRule` based on the +// provided `SwapRule` for the given channelID or peer. +func newRPCRule(channelID uint64, peer []byte, + rule *SwapRule) *clientrpc.LiquidityRule { + + rpcRule := &clientrpc.LiquidityRule{ + ChannelId: channelID, + Pubkey: peer, + Type: clientrpc.LiquidityRuleType_THRESHOLD, + IncomingThreshold: uint32(rule.MinimumIncoming), + OutgoingThreshold: uint32(rule.MinimumOutgoing), + SwapType: clientrpc.SwapType_LOOP_OUT, + } + + if rule.Type == swap.TypeIn { + rpcRule.SwapType = clientrpc.SwapType_LOOP_IN + } + + return rpcRule +} diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index 578782b..d63ec55 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -745,83 +745,14 @@ func (s *swapClientServer) GetLiquidityParams(_ context.Context, cfg := s.liquidityMgr.GetParameters() - totalRules := len(cfg.ChannelRules) + len(cfg.PeerRules) - - var destaddr string - if cfg.DestAddr != nil { - destaddr = cfg.DestAddr.String() - } - - rpcCfg := &clientrpc.LiquidityParameters{ - SweepConfTarget: cfg.SweepConfTarget, - FailureBackoffSec: uint64(cfg.FailureBackOff.Seconds()), - Autoloop: cfg.Autoloop, - AutoloopBudgetSat: uint64(cfg.AutoFeeBudget), - AutoloopBudgetRefreshPeriodSec: uint64( - cfg.AutoFeeRefreshPeriod.Seconds(), - ), - AutoMaxInFlight: uint64(cfg.MaxAutoInFlight), - AutoloopDestAddress: destaddr, - Rules: make( - []*clientrpc.LiquidityRule, 0, totalRules, - ), - MinSwapAmount: uint64(cfg.ClientRestrictions.Minimum), - MaxSwapAmount: uint64(cfg.ClientRestrictions.Maximum), - HtlcConfTarget: cfg.HtlcConfTarget, - } - - switch f := cfg.FeeLimit.(type) { - case *liquidity.FeeCategoryLimit: - satPerByte := f.SweepFeeRateLimit.FeePerKVByte() / 1000 - - rpcCfg.SweepFeeRateSatPerVbyte = uint64(satPerByte) - - rpcCfg.MaxMinerFeeSat = uint64(f.MaximumMinerFee) - rpcCfg.MaxSwapFeePpm = f.MaximumSwapFeePPM - rpcCfg.MaxRoutingFeePpm = f.MaximumRoutingFeePPM - rpcCfg.MaxPrepayRoutingFeePpm = f.MaximumPrepayRoutingFeePPM - rpcCfg.MaxPrepaySat = uint64(f.MaximumPrepay) - - case *liquidity.FeePortion: - rpcCfg.FeePpm = f.PartsPerMillion - - default: - return nil, fmt.Errorf("unknown fee limit: %T", cfg.FeeLimit) - } - - for channel, rule := range cfg.ChannelRules { - rpcRule := newRPCRule(channel.ToUint64(), nil, rule) - rpcCfg.Rules = append(rpcCfg.Rules, rpcRule) - } - - for peer, rule := range cfg.PeerRules { - peer := peer - rpcRule := newRPCRule(0, peer[:], rule) - rpcCfg.Rules = append(rpcCfg.Rules, rpcRule) + rpcCfg, err := liquidity.ParametersToRpc(cfg) + if err != nil { + return nil, err } return rpcCfg, nil } -func newRPCRule(channelID uint64, peer []byte, - rule *liquidity.SwapRule) *clientrpc.LiquidityRule { - - rpcRule := &clientrpc.LiquidityRule{ - ChannelId: channelID, - Pubkey: peer, - Type: clientrpc.LiquidityRuleType_THRESHOLD, - IncomingThreshold: uint32(rule.MinimumIncoming), - OutgoingThreshold: uint32(rule.MinimumOutgoing), - SwapType: clientrpc.SwapType_LOOP_OUT, - } - - if rule.Type == swap.TypeIn { - rpcRule.SwapType = clientrpc.SwapType_LOOP_IN - } - - return rpcRule -} - // SetLiquidityParams attempts to set our current liquidity manager's // parameters. func (s *swapClientServer) SetLiquidityParams(ctx context.Context, diff --git a/loopd/utils.go b/loopd/utils.go index c21b781..67fdefc 100644 --- a/loopd/utils.go +++ b/loopd/utils.go @@ -2,7 +2,6 @@ package loopd import ( "context" - "time" "github.com/btcsuite/btcd/btcutil" "github.com/lightninglabs/lndclient" @@ -40,10 +39,9 @@ func getClient(config *Config, lnd *lndclient.LndServices) (*loop.Client, func getLiquidityManager(client *loop.Client) *liquidity.Manager { mngrCfg := &liquidity.Config{ - AutoloopTicker: ticker.NewForce(liquidity.DefaultAutoloopTicker), - AutoloopBudgetLastRefresh: time.Now(), - LoopOut: client.LoopOut, - LoopIn: client.LoopIn, + AutoloopTicker: ticker.NewForce(liquidity.DefaultAutoloopTicker), + LoopOut: client.LoopOut, + LoopIn: client.LoopIn, Restrictions: func(ctx context.Context, swapType swap.Type) (*liquidity.Restrictions, error) {