diff --git a/cmd/loop/liquidity.go b/cmd/loop/liquidity.go index 50e5121..5413b21 100644 --- a/cmd/loop/liquidity.go +++ b/cmd/loop/liquidity.go @@ -168,6 +168,7 @@ func setRule(ctx *cli.Context) error { newRule := &looprpc.LiquidityRule{ ChannelId: chanID, Type: looprpc.LiquidityRuleType_THRESHOLD, + SwapType: looprpc.SwapType_LOOP_OUT, } if pubkeyRule { diff --git a/liquidity/autoloop_test.go b/liquidity/autoloop_test.go index 0cb6480..72e0e5a 100644 --- a/liquidity/autoloop_test.go +++ b/liquidity/autoloop_test.go @@ -27,7 +27,7 @@ func TestAutoLoopDisabled(t *testing.T) { } params := defaultParameters - params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{ + params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{ chanID1: chanRule, } @@ -95,7 +95,7 @@ func TestAutoLoopEnabled(t *testing.T) { swapFeePPM, routeFeePPM, prepayFeePPM, maxMiner, prepayAmount, 20000, ), - ChannelRules: map[lnwire.ShortChannelID]*ThresholdRule{ + ChannelRules: map[lnwire.ShortChannelID]*SwapRule{ chanID1: chanRule, chanID2: chanRule, }, @@ -312,10 +312,10 @@ func TestCompositeRules(t *testing.T) { MaxAutoInFlight: 2, FailureBackOff: time.Hour, SweepConfTarget: 10, - ChannelRules: map[lnwire.ShortChannelID]*ThresholdRule{ + ChannelRules: map[lnwire.ShortChannelID]*SwapRule{ chanID1: chanRule, }, - PeerRules: map[route.Vertex]*ThresholdRule{ + PeerRules: map[route.Vertex]*SwapRule{ peer2: chanRule, }, } diff --git a/liquidity/fees.go b/liquidity/fees.go index b3e3419..86dc527 100644 --- a/liquidity/fees.go +++ b/liquidity/fees.go @@ -223,6 +223,27 @@ func (f *FeeCategoryLimit) loopOutLimits(amount btcutil.Amount, return nil } +func (f *FeeCategoryLimit) loopInLimits(amount btcutil.Amount, + quote *loop.LoopInQuote) error { + + maxServerFee := ppmToSat(amount, f.MaximumSwapFeePPM) + if quote.SwapFee > maxServerFee { + log.Debugf("quoted swap fee: %v > maximum swap fee: %v", + quote.SwapFee, maxServerFee) + + return newReasonError(ReasonSwapFee) + } + + if quote.MinerFee > f.MaximumMinerFee { + log.Debugf("quoted miner fee: %v > maximum miner "+ + "fee: %v", quote.MinerFee, f.MaximumMinerFee) + + return newReasonError(ReasonMinerFee) + } + + return nil +} + // loopOutFees returns the prepay and routing and miner fees we are willing to // pay for a loop out swap. func (f *FeeCategoryLimit) loopOutFees(amount btcutil.Amount, @@ -384,3 +405,42 @@ func splitOffChain(available, prepayAmt, func scaleMinerFee(estimate btcutil.Amount) btcutil.Amount { return estimate * btcutil.Amount(minerMultiplier) } + +func (f *FeePortion) loopInLimits(amount btcutil.Amount, + quote *loop.LoopInQuote) error { + + // Calculate the total amount that this swap may spend in fees, as a + // portion of the swap amount. + totalFeeSpend := ppmToSat(amount, f.PartsPerMillion) + + // Check individual fee components so that we can give more specific + // feedback. + if quote.MinerFee > totalFeeSpend { + log.Debugf("miner fee: %v greater than fee limit: %v, at "+ + "%v ppm", quote.MinerFee, totalFeeSpend, + f.PartsPerMillion) + + return newReasonError(ReasonMinerFee) + } + + if quote.SwapFee > totalFeeSpend { + log.Debugf("swap fee: %v greater than fee limit: %v, at "+ + "%v ppm", quote.SwapFee, totalFeeSpend, + f.PartsPerMillion) + + return newReasonError(ReasonSwapFee) + } + + fees := worstCaseInFees( + quote.MinerFee, quote.SwapFee, defaultLoopInSweepFee, + ) + + if fees > totalFeeSpend { + log.Debugf("total fees for swap: %v > fee limit: %v, at "+ + "%v ppm", fees, totalFeeSpend, f.PartsPerMillion) + + return newReasonError(ReasonFeePPMInsufficient) + } + + return nil +} diff --git a/liquidity/interface.go b/liquidity/interface.go index 047fe97..40ad166 100644 --- a/liquidity/interface.go +++ b/liquidity/interface.go @@ -32,6 +32,11 @@ type FeeLimit interface { // a swap amount and quote. loopOutFees(amount btcutil.Amount, quote *loop.LoopOutQuote) ( btcutil.Amount, btcutil.Amount, btcutil.Amount) + + // loopInLimits checks whether the quote provided is within our fee + // limits for the swap amount. + loopInLimits(amount btcutil.Amount, + quote *loop.LoopInQuote) error } // swapBuilder is an interface used to build our different swap types. diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index f1bfe0c..97548b9 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -81,6 +81,12 @@ const ( // autoloopSwapInitiator is the value we send in the initiator field of // a swap request when issuing an automatic swap. autoloopSwapInitiator = "autoloop" + + // We use a static fee rate to estimate our sweep fee, because we + // can't realistically estimate what our fee estimate will be by the + // time we reach timeout. We set this to a high estimate so that we can + // account for worst-case fees, (1250 * 4 / 1000) = 50 sat/byte. + defaultLoopInSweepFee = chainfee.SatPerKWeight(1250) ) var ( @@ -97,8 +103,8 @@ var ( defaultParameters = Parameters{ AutoFeeBudget: defaultBudget, MaxAutoInFlight: defaultMaxInFlight, - ChannelRules: make(map[lnwire.ShortChannelID]*ThresholdRule), - PeerRules: make(map[route.Vertex]*ThresholdRule), + ChannelRules: make(map[lnwire.ShortChannelID]*SwapRule), + PeerRules: make(map[route.Vertex]*SwapRule), FailureBackOff: defaultFailureBackoff, SweepConfTarget: defaultConfTarget, FeeLimit: defaultFeePortion(), @@ -216,13 +222,13 @@ type Parameters struct { // ChannelRules maps a short channel ID to a rule that describes how we // would like liquidity to be managed. These rules and PeerRules are // exclusively set to prevent overlap between peer and channel rules. - ChannelRules map[lnwire.ShortChannelID]*ThresholdRule + ChannelRules map[lnwire.ShortChannelID]*SwapRule // PeerRules maps a peer's pubkey to a rule that applies to all the // channels that we have with the peer collectively. These rules and // ChannelRules are exclusively set to prevent overlap between peer // and channel rules map to avoid ambiguity. - PeerRules map[route.Vertex]*ThresholdRule + PeerRules map[route.Vertex]*SwapRule } // String returns the string representation of our parameters. @@ -386,10 +392,6 @@ type Manager struct { // current liquidity balance. cfg *Config - // builder is the swap builder responsible for creating swaps of our - // chosen type for us. - builder swapBuilder - // params is the set of parameters we are currently using. These may be // updated at runtime. params Parameters @@ -428,9 +430,8 @@ func (m *Manager) Run(ctx context.Context) error { // NewManager creates a liquidity manager which has no rules set. func NewManager(cfg *Config) *Manager { return &Manager{ - cfg: cfg, - params: defaultParameters, - builder: newLoopOutBuilder(cfg), + cfg: cfg, + params: defaultParameters, } } @@ -473,7 +474,7 @@ func (m *Manager) SetParameters(ctx context.Context, params Parameters) error { func cloneParameters(params Parameters) Parameters { paramCopy := params paramCopy.ChannelRules = make( - map[lnwire.ShortChannelID]*ThresholdRule, + map[lnwire.ShortChannelID]*SwapRule, len(params.ChannelRules), ) @@ -483,7 +484,7 @@ func cloneParameters(params Parameters) Parameters { } paramCopy.PeerRules = make( - map[route.Vertex]*ThresholdRule, + map[route.Vertex]*SwapRule, len(params.PeerRules), ) @@ -617,23 +618,8 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( return m.singleReasonSuggestion(ReasonBudgetNotStarted), nil } - // Before we get any swap suggestions, we check what the current fee - // estimate is to sweep within our target number of confirmations. If - // This fee exceeds the fee limit we have set, we will not suggest any - // swaps at present. - if err := m.builder.maySwap(ctx, m.params); err != nil { - var reasonErr *reasonError - if errors.As(err, &reasonErr) { - return m.singleReasonSuggestion(reasonErr.reason), nil - - } - - return nil, err - } - - // Get the current server side restrictions, combined with the client - // set restrictions, if any. - restrictions, err := m.getSwapRestrictions(ctx, m.builder.swapType()) + // Get restrictions placed on swaps by the server. + outRestrictions, err := m.getSwapRestrictions(ctx, swap.TypeOut) if err != nil { return nil, err } @@ -653,7 +639,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // Get a summary of our existing swaps so that we can check our autoloop // budget. - summary, err := m.checkExistingAutoLoops(ctx, loopOut) + summary, err := m.checkExistingAutoLoops(ctx, loopOut, loopIn) if err != nil { return nil, err } @@ -721,7 +707,8 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( } suggestion, err := m.suggestSwap( - ctx, traffic, balances, rule, restrictions, autoloop, + ctx, traffic, balances, rule, outRestrictions, + autoloop, ) var reasonErr *reasonError if errors.As(err, &reasonErr) { @@ -746,7 +733,8 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( } suggestion, err := m.suggestSwap( - ctx, traffic, balance, rule, restrictions, autoloop, + ctx, traffic, balance, rule, outRestrictions, + autoloop, ) var reasonErr *reasonError @@ -841,12 +829,34 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // suggestSwap checks whether we can currently perform a swap, and creates a // swap request for the rule provided. func (m *Manager) suggestSwap(ctx context.Context, traffic *swapTraffic, - balance *balances, rule *ThresholdRule, restrictions *Restrictions, + balance *balances, rule *SwapRule, outRestrictions *Restrictions, autoloop bool) (swapSuggestion, error) { + var ( + builder swapBuilder + restrictions *Restrictions + ) + + switch rule.Type { + case swap.TypeOut: + builder = newLoopOutBuilder(m.cfg) + restrictions = outRestrictions + + default: + return nil, fmt.Errorf("unsupported swap type: %v", rule.Type) + } + + // Before we get any swap suggestions, we check what the current fee + // estimate is to sweep within our target number of confirmations. If + // This fee exceeds the fee limit we have set, we will not suggest any + // swaps at present. + if err := builder.maySwap(ctx, m.params); err != nil { + return nil, err + } + // First, check whether this peer/channel combination is already in use // for our swap. - err := m.builder.inUse(traffic, balance.pubkey, balance.channels) + err := builder.inUse(traffic, balance.pubkey, balance.channels) if err != nil { return nil, err } @@ -858,7 +868,7 @@ func (m *Manager) suggestSwap(ctx context.Context, traffic *swapTraffic, return nil, newReasonError(ReasonLiquidityOk) } - return m.builder.buildSwap( + return builder.buildSwap( ctx, balance.pubkey, balance.channels, amount, autoloop, m.params, ) @@ -948,7 +958,8 @@ func (e *existingAutoLoopSummary) totalFees() btcutil.Amount { // total for our set of ongoing, automatically dispatched swaps as well as a // current in-flight count. func (m *Manager) checkExistingAutoLoops(ctx context.Context, - loopOuts []*loopdb.LoopOut) (*existingAutoLoopSummary, error) { + loopOuts []*loopdb.LoopOut, loopIns []*loopdb.LoopIn) ( + *existingAutoLoopSummary, error) { var summary existingAutoLoopSummary @@ -987,6 +998,28 @@ func (m *Manager) checkExistingAutoLoops(ctx context.Context, } } + for _, in := range loopIns { + if in.Contract.Label != labels.AutoloopLabel(swap.TypeIn) { + continue + } + + pending := in.State().State.Type() == loopdb.StateTypePending + inBudget := !in.LastUpdateTime().Before(m.params.AutoFeeStartDate) + + // If an autoloop is in a pending state, we always count it in + // our current budget, and record the worst-case fees for it, + // because we do not know how it will resolve. + if pending { + summary.inFlightCount++ + summary.pendingFees += worstCaseInFees( + in.Contract.MaxMinerFee, in.Contract.MaxSwapFee, + defaultLoopInSweepFee, + ) + } else if inBudget { + summary.spentFees += in.State().Cost.Total() + } + } + return &summary, nil } @@ -1051,17 +1084,28 @@ func (m *Manager) currentSwapTraffic(loopOut []*loopdb.LoopOut, } for _, in := range loopIn { - // Skip completed swaps, they can't affect our channel balances. - if in.State().State.Type() != loopdb.StateTypePending { - continue - } - // Skip over swaps that may come through any peer. if in.Contract.LastHop == nil { continue } - traffic.ongoingLoopIn[*in.Contract.LastHop] = true + pubkey := *in.Contract.LastHop + + switch { + // Include any pending swaps in our ongoing set of swaps. + case in.State().State.Type() == loopdb.StateTypePending: + traffic.ongoingLoopIn[pubkey] = true + + // If a swap failed with an on-chain timeout, the server could + // not route to us. We add it to our backoff list so that + // there's some time for routing conditions to improve. + case in.State().State == loopdb.StateFailTimeout: + failedAt := in.LastUpdate().Time + + if failedAt.After(failureCutoff) { + traffic.failedLoopIn[pubkey] = failedAt + } + } } return traffic @@ -1072,6 +1116,7 @@ type swapTraffic struct { ongoingLoopOut map[lnwire.ShortChannelID]bool ongoingLoopIn map[route.Vertex]bool failedLoopOut map[lnwire.ShortChannelID]time.Time + failedLoopIn map[route.Vertex]time.Time } func newSwapTraffic() *swapTraffic { @@ -1079,6 +1124,7 @@ func newSwapTraffic() *swapTraffic { ongoingLoopOut: make(map[lnwire.ShortChannelID]bool), ongoingLoopIn: make(map[route.Vertex]bool), failedLoopOut: make(map[lnwire.ShortChannelID]time.Time), + failedLoopIn: make(map[route.Vertex]time.Time), } } diff --git a/liquidity/liquidity_test.go b/liquidity/liquidity_test.go index ccded3a..203e2c4 100644 --- a/liquidity/liquidity_test.go +++ b/liquidity/liquidity_test.go @@ -47,7 +47,10 @@ var ( } // chanRule is a rule that produces chan1Rec. - chanRule = NewThresholdRule(50, 0) + chanRule = &SwapRule{ + ThresholdRule: NewThresholdRule(50, 0), + Type: swap.TypeOut, + } testQuote = &loop.LoopOutQuote{ SwapFee: btcutil.Amount(5), @@ -104,6 +107,13 @@ var ( OutgoingChanSet: loopdb.ChannelSet{999}, } + autoInContract = &loopdb.LoopInContract{ + SwapContract: loopdb.SwapContract{ + Label: labels.AutoloopLabel(swap.TypeIn), + InitiationTime: testBudgetStart, + }, + } + testRestrictions = NewRestrictions(1, 10000) // noneDisqualified can be used in tests where we don't have any @@ -188,7 +198,10 @@ func TestParameters(t *testing.T) { require.Equal(t, defaultParameters, startParams) // Mutate the parameters returned by our get function. - startParams.ChannelRules[chanID] = NewThresholdRule(1, 1) + startParams.ChannelRules[chanID] = &SwapRule{ + ThresholdRule: NewThresholdRule(1, 1), + Type: swap.TypeOut, + } // Make sure that we have not mutated the liquidity manager's params // by making this change. @@ -197,9 +210,13 @@ func TestParameters(t *testing.T) { // Provide a valid set of parameters and validate assert that they are // set. - originalRule := NewThresholdRule(10, 10) + originalRule := &SwapRule{ + ThresholdRule: NewThresholdRule(10, 10), + Type: swap.TypeOut, + } + expected := defaultParameters - expected.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{ + expected.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{ chanID: originalRule, } @@ -208,15 +225,21 @@ func TestParameters(t *testing.T) { // Check that changing the parameters we just set does not mutate // our liquidity manager's parameters. - expected.ChannelRules[chanID] = NewThresholdRule(11, 11) + expected.ChannelRules[chanID] = &SwapRule{ + ThresholdRule: NewThresholdRule(11, 11), + Type: swap.TypeOut, + } params = manager.GetParameters() require.NoError(t, err) require.Equal(t, originalRule, params.ChannelRules[chanID]) // Set invalid parameters and assert that we fail. - expected.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{ - lnwire.NewShortChanIDFromInt(0): NewThresholdRule(1, 2), + expected.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{ + lnwire.NewShortChanIDFromInt(0): { + ThresholdRule: NewThresholdRule(1, 2), + Type: swap.TypeOut, + }, } err = manager.SetParameters(context.Background(), expected) require.Equal(t, ErrZeroChannelID, err) @@ -310,7 +333,7 @@ func TestRestrictedSuggestions(t *testing.T) { ), } - chanRules = map[lnwire.ShortChannelID]*ThresholdRule{ + chanRules = map[lnwire.ShortChannelID]*SwapRule{ chanID1: chanRule, chanID2: chanRule, } @@ -321,8 +344,8 @@ func TestRestrictedSuggestions(t *testing.T) { channels []lndclient.ChannelInfo loopOut []*loopdb.LoopOut loopIn []*loopdb.LoopIn - chanRules map[lnwire.ShortChannelID]*ThresholdRule - peerRules map[route.Vertex]*ThresholdRule + chanRules map[lnwire.ShortChannelID]*SwapRule + peerRules map[route.Vertex]*SwapRule expected *Suggestions }{ { @@ -511,8 +534,11 @@ func TestRestrictedSuggestions(t *testing.T) { Contract: chan1Out, }, }, - peerRules: map[route.Vertex]*ThresholdRule{ - peer1: NewThresholdRule(0, 50), + peerRules: map[route.Vertex]*SwapRule{ + peer1: { + ThresholdRule: NewThresholdRule(0, 50), + Type: swap.TypeOut, + }, }, expected: &Suggestions{ DisqualifiedChans: noneDisqualified, @@ -629,7 +655,7 @@ func TestSweepFeeLimit(t *testing.T) { ppmToSat(7500, defaultPrepayRoutingFeePPM) + ppmToSat(7500, defaultRoutingFeePPM) - params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{ + params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{ chanID1: chanRule, } @@ -654,21 +680,21 @@ func TestSuggestSwaps(t *testing.T) { tests := []struct { name string channels []lndclient.ChannelInfo - rules map[lnwire.ShortChannelID]*ThresholdRule - peerRules map[route.Vertex]*ThresholdRule + rules map[lnwire.ShortChannelID]*SwapRule + peerRules map[route.Vertex]*SwapRule suggestions *Suggestions err error }{ { name: "no rules", channels: singleChannel, - rules: map[lnwire.ShortChannelID]*ThresholdRule{}, + rules: map[lnwire.ShortChannelID]*SwapRule{}, err: ErrNoRules, }, { name: "loop out", channels: singleChannel, - rules: map[lnwire.ShortChannelID]*ThresholdRule{ + rules: map[lnwire.ShortChannelID]*SwapRule{ chanID1: chanRule, }, suggestions: &Suggestions{ @@ -682,8 +708,11 @@ func TestSuggestSwaps(t *testing.T) { { name: "no rule for channel", channels: singleChannel, - rules: map[lnwire.ShortChannelID]*ThresholdRule{ - chanID2: NewThresholdRule(10, 10), + rules: map[lnwire.ShortChannelID]*SwapRule{ + chanID2: { + ThresholdRule: NewThresholdRule(10, 10), + Type: swap.TypeOut, + }, }, suggestions: &Suggestions{ DisqualifiedChans: noneDisqualified, @@ -715,9 +744,15 @@ func TestSuggestSwaps(t *testing.T) { RemoteBalance: 3000, }, }, - peerRules: map[route.Vertex]*ThresholdRule{ - peer1: NewThresholdRule(80, 0), - peer2: NewThresholdRule(40, 50), + peerRules: map[route.Vertex]*SwapRule{ + peer1: { + ThresholdRule: NewThresholdRule(80, 0), + Type: swap.TypeOut, + }, + peer2: { + ThresholdRule: NewThresholdRule(40, 50), + Type: swap.TypeOut, + }, }, suggestions: &Suggestions{ OutSwaps: []loop.OutRequest{ @@ -869,7 +904,7 @@ func TestFeeLimits(t *testing.T) { ppmToSat(7500, defaultPrepayRoutingFeePPM) + ppmToSat(7500, defaultRoutingFeePPM) - params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{ + params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{ chanID1: chanRule, } @@ -1061,7 +1096,7 @@ func TestFeeBudget(t *testing.T) { } params := defaultParameters - params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{ + params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{ chanID1: chanRule, chanID2: chanRule, } @@ -1095,12 +1130,13 @@ func TestFeeBudget(t *testing.T) { // that are allowed. func TestInFlightLimit(t *testing.T) { tests := []struct { - name string - maxInFlight int - existingSwaps []*loopdb.LoopOut + name string + maxInFlight int + existingSwaps []*loopdb.LoopOut + existingInSwaps []*loopdb.LoopIn // peerRules will only be set (instead of test default values) // is it is non-nil. - peerRules map[route.Vertex]*ThresholdRule + peerRules map[route.Vertex]*SwapRule suggestions *Suggestions }{ { @@ -1166,8 +1202,10 @@ func TestInFlightLimit(t *testing.T) { { Contract: autoOutContract, }, + }, + existingInSwaps: []*loopdb.LoopIn{ { - Contract: autoOutContract, + Contract: autoInContract, }, }, suggestions: &Suggestions{ @@ -1189,9 +1227,15 @@ func TestInFlightLimit(t *testing.T) { // Create two peer-level rules, both in need of a swap, // but peer 1 needs a larger swap so will be // prioritized. - peerRules: map[route.Vertex]*ThresholdRule{ - peer1: NewThresholdRule(50, 0), - peer2: NewThresholdRule(40, 0), + peerRules: map[route.Vertex]*SwapRule{ + peer1: { + ThresholdRule: NewThresholdRule(50, 0), + Type: swap.TypeOut, + }, + peer2: { + ThresholdRule: NewThresholdRule(40, 0), + Type: swap.TypeOut, + }, }, suggestions: &Suggestions{ OutSwaps: []loop.OutRequest{ @@ -1213,6 +1257,9 @@ func TestInFlightLimit(t *testing.T) { cfg.ListLoopOut = func() ([]*loopdb.LoopOut, error) { return testCase.existingSwaps, nil } + cfg.ListLoopIn = func() ([]*loopdb.LoopIn, error) { + return testCase.existingInSwaps, nil + } lnd.Channels = []lndclient.ChannelInfo{ channel1, channel2, @@ -1224,7 +1271,7 @@ func TestInFlightLimit(t *testing.T) { params.PeerRules = testCase.peerRules } else { params.ChannelRules = - map[lnwire.ShortChannelID]*ThresholdRule{ + map[lnwire.ShortChannelID]*SwapRule{ chanID1: chanRule, chanID2: chanRule, } @@ -1364,7 +1411,7 @@ func TestSizeRestrictions(t *testing.T) { params := defaultParameters params.ClientRestrictions = testCase.clientRestrictions - params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{ + params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{ chanID1: chanRule, } @@ -1522,10 +1569,185 @@ func TestFeePercentage(t *testing.T) { params := defaultParameters params.FeeLimit = NewFeePortion(testCase.feePPM) - params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{ + params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{ + chanID1: chanRule, + } + + testSuggestSwaps( + t, newSuggestSwapsSetup(cfg, lnd, params), + testCase.suggestions, nil, + ) + }) + } +} + +// TestBudgetWithLoopin tests that our autoloop budget accounts for loop in +// swaps that have been automatically dispatched. It tests out swaps that have +// already completed and those that are pending, inside and outside of our +// budget period to ensure that we account for all relevant swaps. +func TestBudgetWithLoopin(t *testing.T) { + var ( + budget btcutil.Amount = 10000 + + outsideBudget = testBudgetStart.Add(-5) + insideBudget = testBudgetStart.Add(5) + + contractOutsideBudget = &loopdb.LoopInContract{ + SwapContract: loopdb.SwapContract{ + InitiationTime: outsideBudget, + MaxSwapFee: budget, + }, + Label: labels.AutoloopLabel(swap.TypeIn), + } + + // Set our spend equal to our budget so we don't need to + // calculate exact costs. + eventOutsideBudget = &loopdb.LoopEvent{ + SwapStateData: loopdb.SwapStateData{ + Cost: loopdb.SwapCost{ + Server: budget, + }, + State: loopdb.StateSuccess, + }, + Time: outsideBudget, + } + + successWithinBudget = &loopdb.LoopEvent{ + SwapStateData: loopdb.SwapStateData{ + Cost: loopdb.SwapCost{ + Server: budget, + }, + State: loopdb.StateSuccess, + }, + Time: insideBudget, + } + + okQuote = &loop.LoopOutQuote{ + SwapFee: 15, + PrepayAmount: 30, + MinerFee: 1, + } + + rec = loop.OutRequest{ + Amount: 7500, + OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()}, + MaxMinerFee: scaleMinerFee(okQuote.MinerFee), + MaxSwapFee: okQuote.SwapFee, + MaxPrepayAmount: okQuote.PrepayAmount, + SweepConfTarget: defaultConfTarget, + Initiator: autoloopSwapInitiator, + } + + testPPM uint64 = 100000 + ) + + rec.MaxPrepayRoutingFee, rec.MaxSwapRoutingFee = testPPMFees( + testPPM, okQuote, 7500, + ) + + tests := []struct { + name string + + // loopIns is the set of loop in swaps that the client has + // performed. + loopIns []*loopdb.LoopIn + + // suggestions is the set of swaps that we expect to be + // suggested given our current traffic. + suggestions *Suggestions + }{ + { + name: "completed swap outside of budget", + loopIns: []*loopdb.LoopIn{ + { + Loop: loopdb.Loop{ + Events: []*loopdb.LoopEvent{ + eventOutsideBudget, + }, + }, + Contract: contractOutsideBudget, + }, + }, + suggestions: &Suggestions{ + OutSwaps: []loop.OutRequest{ + rec, + }, + DisqualifiedChans: noneDisqualified, + DisqualifiedPeers: noPeersDisqualified, + }, + }, + { + name: "completed within budget", + loopIns: []*loopdb.LoopIn{ + { + Loop: loopdb.Loop{ + Events: []*loopdb.LoopEvent{ + successWithinBudget, + }, + }, + Contract: contractOutsideBudget, + }, + }, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonBudgetElapsed, + }, + DisqualifiedPeers: noPeersDisqualified, + }, + }, + { + name: "pending created before budget", + loopIns: []*loopdb.LoopIn{ + { + Contract: contractOutsideBudget, + }, + }, + suggestions: &Suggestions{ + DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ + chanID1: ReasonBudgetElapsed, + }, + DisqualifiedPeers: noPeersDisqualified, + }, + }, + } + + for _, testCase := range tests { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + cfg, lnd := newTestConfig() + + // Set our channel and rules so that we will need to + // swap 7500 sats and our fee limit is 10% of that + // amount (750 sats). + lnd.Channels = []lndclient.ChannelInfo{ + channel1, + } + + cfg.ListLoopIn = func() ([]*loopdb.LoopIn, error) { + return testCase.loopIns, nil + } + + cfg.LoopOutQuote = func(_ context.Context, + _ *loop.LoopOutQuoteRequest) (*loop.LoopOutQuote, + error) { + + return okQuote, nil + } + + params := defaultParameters + params.AutoFeeBudget = budget + params.AutoFeeStartDate = testBudgetStart + + params.FeeLimit = NewFeePortion(testPPM) + params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{ chanID1: chanRule, } + // Allow more than one in flight swap, to ensure that + // we restrict based on budget, not in-flight. + params.MaxAutoInFlight = 2 + testSuggestSwaps( t, newSuggestSwapsSetup(cfg, lnd, params), testCase.suggestions, nil, @@ -1572,7 +1794,7 @@ func testSuggestSwaps(t *testing.T, setup *testSuggestSwapsSetup, } params := defaultParameters - params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{ + params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{ chanID1: chanRule, chanID2: chanRule, } @@ -1595,3 +1817,184 @@ func testSuggestSwaps(t *testing.T, setup *testSuggestSwapsSetup, require.Equal(t, expectedErr, err) require.Equal(t, expected, actual) } + +// TestCurrentTraffic tests recording of our current set of ongoing swaps. +func TestCurrentTraffic(t *testing.T) { + var ( + backoff = time.Hour * 5 + withinBackoff = testTime.Add(time.Hour * -1) + outsideBackoff = testTime.Add(backoff * -2) + + success = []*loopdb.LoopEvent{ + { + SwapStateData: loopdb.SwapStateData{ + State: loopdb.StateSuccess, + }, + }, + } + + failedInBackoff = []*loopdb.LoopEvent{ + { + SwapStateData: loopdb.SwapStateData{ + State: loopdb.StateFailOffchainPayments, + }, + Time: withinBackoff, + }, + } + + failedOutsideBackoff = []*loopdb.LoopEvent{ + { + SwapStateData: loopdb.SwapStateData{ + State: loopdb.StateFailOffchainPayments, + }, + Time: outsideBackoff, + }, + } + + failedTimeoutInBackoff = []*loopdb.LoopEvent{ + { + SwapStateData: loopdb.SwapStateData{ + State: loopdb.StateFailTimeout, + }, + Time: withinBackoff, + }, + } + + failedTimeoutOutsideBackoff = []*loopdb.LoopEvent{ + { + SwapStateData: loopdb.SwapStateData{ + State: loopdb.StateFailTimeout, + }, + Time: outsideBackoff, + }, + } + ) + + tests := []struct { + name string + loopOut []*loopdb.LoopOut + loopIn []*loopdb.LoopIn + expected *swapTraffic + }{ + { + name: "completed swaps ignored", + loopOut: []*loopdb.LoopOut{ + { + Loop: loopdb.Loop{ + Events: success, + }, + Contract: &loopdb.LoopOutContract{}, + }, + }, + loopIn: []*loopdb.LoopIn{ + { + Loop: loopdb.Loop{ + Events: success, + }, + Contract: &loopdb.LoopInContract{}, + }, + }, + expected: newSwapTraffic(), + }, + { + // No events indicates that the swap is still pending. + name: "pending swaps included", + loopOut: []*loopdb.LoopOut{ + { + Contract: &loopdb.LoopOutContract{ + OutgoingChanSet: []uint64{ + chanID1.ToUint64(), + }, + }, + }, + }, + loopIn: []*loopdb.LoopIn{ + { + Contract: &loopdb.LoopInContract{ + LastHop: &peer2, + }, + }, + }, + expected: &swapTraffic{ + ongoingLoopOut: map[lnwire.ShortChannelID]bool{ + chanID1: true, + }, + ongoingLoopIn: map[route.Vertex]bool{ + peer2: true, + }, + // Make empty maps so that we can assert equal. + failedLoopOut: make( + map[lnwire.ShortChannelID]time.Time, + ), + failedLoopIn: make(map[route.Vertex]time.Time), + }, + }, + { + name: "failure backoff included", + loopOut: []*loopdb.LoopOut{ + { + Contract: &loopdb.LoopOutContract{ + OutgoingChanSet: []uint64{ + chanID1.ToUint64(), + }, + }, + Loop: loopdb.Loop{ + Events: failedInBackoff, + }, + }, + { + Contract: &loopdb.LoopOutContract{ + OutgoingChanSet: []uint64{ + chanID2.ToUint64(), + }, + }, + Loop: loopdb.Loop{ + Events: failedOutsideBackoff, + }, + }, + }, + loopIn: []*loopdb.LoopIn{ + { + Contract: &loopdb.LoopInContract{ + LastHop: &peer1, + }, + Loop: loopdb.Loop{ + Events: failedTimeoutInBackoff, + }, + }, + { + Contract: &loopdb.LoopInContract{ + LastHop: &peer2, + }, + Loop: loopdb.Loop{ + Events: failedTimeoutOutsideBackoff, + }, + }, + }, + expected: &swapTraffic{ + ongoingLoopOut: make( + map[lnwire.ShortChannelID]bool, + ), + ongoingLoopIn: make(map[route.Vertex]bool), + failedLoopOut: map[lnwire.ShortChannelID]time.Time{ + chanID1: withinBackoff, + }, + failedLoopIn: map[route.Vertex]time.Time{ + peer1: withinBackoff, + }, + }, + }, + } + + for _, testCase := range tests { + cfg, _ := newTestConfig() + m := NewManager(cfg) + + params := m.GetParameters() + params.FailureBackOff = backoff + require.NoError(t, m.SetParameters(context.Background(), params)) + + actual := m.currentSwapTraffic(testCase.loopOut, testCase.loopIn) + require.Equal(t, testCase.expected, actual) + } +} diff --git a/liquidity/loopin.go b/liquidity/loopin.go new file mode 100644 index 0000000..5591e09 --- /dev/null +++ b/liquidity/loopin.go @@ -0,0 +1,82 @@ +package liquidity + +import ( + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/loop" + "github.com/lightninglabs/loop/swap" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" +) + +// Compile time assertion that loop in suggestions satisfy our interface. +var _ swapSuggestion = (*loopInSwapSuggestion)(nil) + +type loopInSwapSuggestion struct { + loop.LoopInRequest +} + +// amount returns the amount of the swap suggestion. +func (l *loopInSwapSuggestion) amount() btcutil.Amount { + return l.Amount +} + +// fees returns the highest fees that we could pay for the swap suggestion. +func (l *loopInSwapSuggestion) fees() btcutil.Amount { + return worstCaseInFees( + l.MaxMinerFee, l.MaxSwapFee, defaultLoopInSweepFee, + ) +} + +// channels returns no channels for loop in swap suggestions because we do not +// restrict loop in swaps by channel id. +func (l *loopInSwapSuggestion) channels() []lnwire.ShortChannelID { + return nil +} + +// peers returns the peer that a loop in swap is restricted to, if it is set. +func (l *loopInSwapSuggestion) peers(_ map[uint64]route.Vertex) []route.Vertex { + if l.LastHop == nil { + return nil + } + + return []route.Vertex{ + *l.LastHop, + } +} + +// worstCaseInFees returns the largest possible fees for a loop in swap. +func worstCaseInFees(maxMinerFee, swapFee btcutil.Amount, + sweepEst chainfee.SatPerKWeight) btcutil.Amount { + + failureFee := maxMinerFee + loopInSweepFee(sweepEst) + successFee := maxMinerFee + swapFee + + if failureFee > successFee { + return failureFee + } + + return successFee +} + +// loopInSweepFee provides an estimated fee for our sweep transaction, based +// on the fee rate provided. We can calculate our fees for htlcv2 and p2wkh +// timeout addresses because automated loop ins will be handled entirely by the +// client, so we know what types will be used. +func loopInSweepFee(fee chainfee.SatPerKWeight) btcutil.Amount { + var estimator input.TxWeightEstimator + + // We sweep loop in swaps to wpkh addresses provided by lnd. + estimator.AddP2WKHOutput() + + // Create a htlcv2, which is what all autoloops will use, so that we + // can get our maximum timeout witness size. + htlc := swap.HtlcScriptV2{} + maxSize := htlc.MaxTimeoutWitnessSize() + + estimator.AddWitnessInput(maxSize) + weight := int64(estimator.Weight()) + + return fee.FeeForWeight(weight) +} diff --git a/liquidity/threshold_rule.go b/liquidity/threshold_rule.go index 47ef735..8c09fe3 100644 --- a/liquidity/threshold_rule.go +++ b/liquidity/threshold_rule.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/btcsuite/btcutil" + "github.com/lightninglabs/loop/swap" ) var ( @@ -19,6 +20,12 @@ var ( "percentages must be < 100") ) +// SwapRule is a liquidity rule with a specific swap type. +type SwapRule struct { + *ThresholdRule + swap.Type +} + // ThresholdRule is a liquidity rule that implements minimum incoming and // outgoing liquidity threshold. type ThresholdRule struct { @@ -65,73 +72,99 @@ func (r *ThresholdRule) validate() error { // swapAmount suggests a swap based on the liquidity thresholds configured, // returning zero if no swap is recommended. func (r *ThresholdRule) swapAmount(channel *balances, - outRestrictions *Restrictions) btcutil.Amount { + restrictions *Restrictions) btcutil.Amount { + + var ( + // For loop out swaps, we want to adjust our incoming liquidity + // so the channel's incoming balance is our target. + targetBalance = channel.incoming + + // For loop out swaps, we target a minimum amount of incoming + // liquidity, so the minimum incoming threshold is our target + // percentage. + targetPercentage = uint64(r.MinimumIncoming) + + // For loop out swaps, we may want to preserve some of our + // outgoing balance, so the channel's outgoing balance is our + // reserve. + reserveBalance = channel.outgoing + + // For loop out swaps, we may want to preserve some percentage + // of our outgoing balance, so the minimum outgoing threshold + // is our reserve percentage. + reservePercentage = uint64(r.MinimumOutgoing) + ) // Examine our total balance and required ratios to decide whether we // need to swap. - amount := loopOutSwapAmount( - channel, r.MinimumIncoming, r.MinimumOutgoing, + amount := calculateSwapAmount( + targetBalance, reserveBalance, channel.capacity, + targetPercentage, reservePercentage, ) // Limit our swap amount by the minimum/maximum thresholds set. switch { - case amount < outRestrictions.Minimum: + case amount < restrictions.Minimum: return 0 - case amount > outRestrictions.Maximum: - return outRestrictions.Maximum + case amount > restrictions.Maximum: + return restrictions.Maximum default: return amount } } -// loopOutSwapAmount determines whether we can perform a loop out swap, and -// returns the amount we need to swap to reach the desired liquidity balance -// specified by the incoming and outgoing thresholds. -func loopOutSwapAmount(balances *balances, incomingThresholdPercent, - outgoingThresholdPercent int) btcutil.Amount { - - minimumIncoming := btcutil.Amount(uint64( - balances.capacity) * - uint64(incomingThresholdPercent) / 100, +// calculateSwapAmount calculates amount for a swap based on thresholds. +// This function can be used for loop out or loop in, but the concept is the +// same - we want liquidity in one (target) direction, while preserving some +// minimum in the other (reserve) direction. +// * target: this is the side of the channel(s) where we want to acquire some +// liquidity. We aim for this liquidity to reach the threshold amount set. +// * reserve: this is the side of the channel(s) that we will move liquidity +// away from. This may not drop below a certain reserve threshold. +func calculateSwapAmount(targetAmount, reserveAmount, + capacity btcutil.Amount, targetThresholdPercentage, + reserveThresholdPercentage uint64) btcutil.Amount { + + targetGoal := btcutil.Amount( + uint64(capacity) * targetThresholdPercentage / 100, ) - minimumOutgoing := btcutil.Amount( - uint64(balances.capacity) * - uint64(outgoingThresholdPercent) / 100, + reserveMinimum := btcutil.Amount( + uint64(capacity) * reserveThresholdPercentage / 100, ) switch { - // If we have sufficient incoming capacity, we do not need to loop out. - case balances.incoming >= minimumIncoming: + // If we have sufficient target capacity, we do not need to swap. + case targetAmount >= targetGoal: return 0 - // If we are already below the threshold set for outgoing capacity, we + // If we are already below the threshold set for reserve capacity, we // cannot take any further action. - case balances.outgoing <= minimumOutgoing: + case reserveAmount <= reserveMinimum: return 0 } - // Express our minimum outgoing amount as a maximum incoming amount. + // Express our minimum reserve amount as a maximum target amount. // We will use this value to limit the amount that we swap, so that we - // do not dip below our outgoing threshold. - maximumIncoming := balances.capacity - minimumOutgoing + // do not dip below our reserve threshold. + maximumTarget := capacity - reserveMinimum - // Calculate the midpoint between our minimum and maximum incoming - // values. We will aim to swap this amount so that we do not tip our - // outgoing balance beneath the desired level. - midpoint := (minimumIncoming + maximumIncoming) / 2 + // Calculate the midpoint between our minimum and maximum target values. + // We will aim to swap this amount so that we do not tip our reserve + // balance beneath the desired level. + midpoint := (targetGoal + maximumTarget) / 2 - // Calculate the amount of incoming balance we need to shift to reach + // Calculate the amount of target balance we need to shift to reach // this desired midpoint. - required := midpoint - balances.incoming + required := midpoint - targetAmount // Since we can have pending htlcs on our channel, we check the amount - // of outbound capacity that we can shift before we fall below our + // of reserve capacity that we can shift before we fall below our // threshold. - available := balances.outgoing - minimumOutgoing + available := reserveAmount - reserveMinimum // If we do not have enough balance available to reach our midpoint, we // take no action. This is the case when we have a large portion of diff --git a/liquidity/threshold_rule_test.go b/liquidity/threshold_rule_test.go index b90c6c6..a460def 100644 --- a/liquidity/threshold_rule_test.go +++ b/liquidity/threshold_rule_test.go @@ -93,18 +93,18 @@ func TestValidateThreshold(t *testing.T) { } } -// TestLoopOutAmount tests assessing of a set of balances to determine whether -// we should perform a loop out. -func TestLoopOutAmount(t *testing.T) { +// TestCalculateAmount tests calculation of the amount we recommend for a given +// set of balances and threshold rule. +func TestCalculateAmount(t *testing.T) { tests := []struct { name string - minIncoming int - minOutgoing int + minIncoming uint64 + minOutgoing uint64 balances *balances amt btcutil.Amount }{ { - name: "insufficient surplus", + name: "insufficient outgoing", balances: &balances{ capacity: 100, incoming: 20, @@ -166,8 +166,9 @@ func TestLoopOutAmount(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() - amt := loopOutSwapAmount( - test.balances, test.minIncoming, + amt := calculateSwapAmount( + test.balances.incoming, test.balances.outgoing, + test.balances.capacity, test.minIncoming, test.minOutgoing, ) require.Equal(t, test.amt, amt) diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index 9318bca..752c2ce 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -750,15 +750,22 @@ func (s *swapClientServer) GetLiquidityParams(_ context.Context, } func newRPCRule(channelID uint64, peer []byte, - rule *liquidity.ThresholdRule) *looprpc.LiquidityRule { + rule *liquidity.SwapRule) *looprpc.LiquidityRule { - return &looprpc.LiquidityRule{ + rpcRule := &looprpc.LiquidityRule{ ChannelId: channelID, Pubkey: peer, Type: looprpc.LiquidityRuleType_THRESHOLD, IncomingThreshold: uint32(rule.MinimumIncoming), OutgoingThreshold: uint32(rule.MinimumOutgoing), + SwapType: looprpc.SwapType_LOOP_OUT, } + + if rule.Type == swap.TypeIn { + rpcRule.SwapType = looprpc.SwapType_LOOP_IN + } + + return rpcRule } // SetLiquidityParams attempts to set our current liquidity manager's @@ -781,10 +788,10 @@ func (s *swapClientServer) SetLiquidityParams(ctx context.Context, AutoFeeBudget: btcutil.Amount(in.Parameters.AutoloopBudgetSat), MaxAutoInFlight: int(in.Parameters.AutoMaxInFlight), ChannelRules: make( - map[lnwire.ShortChannelID]*liquidity.ThresholdRule, + map[lnwire.ShortChannelID]*liquidity.SwapRule, ), PeerRules: make( - map[route.Vertex]*liquidity.ThresholdRule, + map[route.Vertex]*liquidity.SwapRule, ), ClientRestrictions: liquidity.Restrictions{ Minimum: btcutil.Amount(in.Parameters.MinSwapAmount), @@ -890,16 +897,24 @@ func rpcToFee(req *looprpc.LiquidityParameters) (liquidity.FeeLimit, } // rpcToRule switches on rpc rule type to convert to our rule interface. -func rpcToRule(rule *looprpc.LiquidityRule) (*liquidity.ThresholdRule, error) { +func rpcToRule(rule *looprpc.LiquidityRule) (*liquidity.SwapRule, error) { + swapType := swap.TypeOut + if rule.SwapType == looprpc.SwapType_LOOP_IN { + swapType = swap.TypeIn + } + switch rule.Type { case looprpc.LiquidityRuleType_UNKNOWN: return nil, fmt.Errorf("rule type field must be set") case looprpc.LiquidityRuleType_THRESHOLD: - return liquidity.NewThresholdRule( - int(rule.IncomingThreshold), - int(rule.OutgoingThreshold), - ), nil + return &liquidity.SwapRule{ + ThresholdRule: liquidity.NewThresholdRule( + int(rule.IncomingThreshold), + int(rule.OutgoingThreshold), + ), + Type: swapType, + }, nil default: return nil, fmt.Errorf("unknown rule: %T", rule) diff --git a/looprpc/client.pb.go b/looprpc/client.pb.go index f4c7258..a4f21e5 100644 --- a/looprpc/client.pb.go +++ b/looprpc/client.pb.go @@ -2299,6 +2299,8 @@ type LiquidityRule struct { //The short channel ID of the channel that this rule should be applied to. //This field may not be set when the pubkey field is set. ChannelId uint64 `protobuf:"varint,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` + // The type of swap that will be dispatched for this rule. + SwapType SwapType `protobuf:"varint,6,opt,name=swap_type,json=swapType,proto3,enum=looprpc.SwapType" json:"swap_type,omitempty"` // //The public key of the peer that this rule should be applied to. This field //may not be set when the channel id field is set. @@ -2358,6 +2360,13 @@ func (x *LiquidityRule) GetChannelId() uint64 { return 0 } +func (x *LiquidityRule) GetSwapType() SwapType { + if x != nil { + return x.SwapType + } + return SwapType_LOOP_OUT +} + func (x *LiquidityRule) GetPubkey() []byte { if x != nil { return x.Pubkey @@ -2905,10 +2914,13 @@ var file_client_proto_rawDesc = []byte{ 0x6e, 0x53, 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x6d, 0x61, 0x78, 0x53, 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f, - 0x75, 0x6e, 0x74, 0x22, 0xd4, 0x01, 0x0a, 0x0d, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, + 0x75, 0x6e, 0x74, 0x22, 0x84, 0x02, 0x0a, 0x0d, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, - 0x65, 0x6c, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x05, + 0x65, 0x6c, 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, + 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, 0x65, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x52, 0x75, @@ -3122,44 +3134,45 @@ var file_client_proto_depIdxs = []int32{ 32, // 5: looprpc.ProbeRequest.route_hints:type_name -> looprpc.RouteHint 23, // 6: looprpc.TokensResponse.tokens:type_name -> looprpc.LsatToken 26, // 7: looprpc.LiquidityParameters.rules:type_name -> looprpc.LiquidityRule - 3, // 8: looprpc.LiquidityRule.type:type_name -> looprpc.LiquidityRuleType - 25, // 9: looprpc.SetLiquidityParamsRequest.parameters:type_name -> looprpc.LiquidityParameters - 4, // 10: looprpc.Disqualified.reason:type_name -> looprpc.AutoReason - 5, // 11: looprpc.SuggestSwapsResponse.loop_out:type_name -> looprpc.LoopOutRequest - 30, // 12: looprpc.SuggestSwapsResponse.disqualified:type_name -> looprpc.Disqualified - 5, // 13: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest - 6, // 14: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest - 8, // 15: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest - 10, // 16: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest - 12, // 17: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest - 13, // 18: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest - 16, // 19: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest - 13, // 20: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest - 16, // 21: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest - 19, // 22: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest - 21, // 23: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest - 24, // 24: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest - 27, // 25: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest - 29, // 26: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest - 7, // 27: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse - 7, // 28: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse - 9, // 29: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus - 11, // 30: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse - 9, // 31: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus - 15, // 32: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse - 18, // 33: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse - 14, // 34: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse - 17, // 35: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse - 20, // 36: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse - 22, // 37: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse - 25, // 38: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters - 28, // 39: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse - 31, // 40: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse - 27, // [27:41] is the sub-list for method output_type - 13, // [13:27] is the sub-list for method input_type - 13, // [13:13] is the sub-list for extension type_name - 13, // [13:13] is the sub-list for extension extendee - 0, // [0:13] is the sub-list for field type_name + 0, // 8: looprpc.LiquidityRule.swap_type:type_name -> looprpc.SwapType + 3, // 9: looprpc.LiquidityRule.type:type_name -> looprpc.LiquidityRuleType + 25, // 10: looprpc.SetLiquidityParamsRequest.parameters:type_name -> looprpc.LiquidityParameters + 4, // 11: looprpc.Disqualified.reason:type_name -> looprpc.AutoReason + 5, // 12: looprpc.SuggestSwapsResponse.loop_out:type_name -> looprpc.LoopOutRequest + 30, // 13: looprpc.SuggestSwapsResponse.disqualified:type_name -> looprpc.Disqualified + 5, // 14: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest + 6, // 15: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest + 8, // 16: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest + 10, // 17: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest + 12, // 18: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest + 13, // 19: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest + 16, // 20: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest + 13, // 21: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest + 16, // 22: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest + 19, // 23: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest + 21, // 24: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest + 24, // 25: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest + 27, // 26: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest + 29, // 27: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest + 7, // 28: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse + 7, // 29: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse + 9, // 30: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus + 11, // 31: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse + 9, // 32: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus + 15, // 33: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse + 18, // 34: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse + 14, // 35: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse + 17, // 36: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse + 20, // 37: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse + 22, // 38: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse + 25, // 39: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters + 28, // 40: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse + 31, // 41: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse + 28, // [28:42] is the sub-list for method output_type + 14, // [14:28] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name } func init() { file_client_proto_init() } diff --git a/looprpc/client.proto b/looprpc/client.proto index f90d3f9..5d2dfee 100644 --- a/looprpc/client.proto +++ b/looprpc/client.proto @@ -843,6 +843,9 @@ message LiquidityRule { */ uint64 channel_id = 1; + // The type of swap that will be dispatched for this rule. + SwapType swap_type = 6; + /* The public key of the peer that this rule should be applied to. This field may not be set when the channel id field is set. diff --git a/looprpc/client.swagger.json b/looprpc/client.swagger.json index 95a5288..11b2ad8 100644 --- a/looprpc/client.swagger.json +++ b/looprpc/client.swagger.json @@ -672,6 +672,10 @@ "format": "uint64", "description": "The short channel ID of the channel that this rule should be applied to.\nThis field may not be set when the pubkey field is set." }, + "swap_type": { + "$ref": "#/definitions/looprpcSwapType", + "description": "The type of swap that will be dispatched for this rule." + }, "pubkey": { "type": "string", "format": "byte",