diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index f90fa7c0..47af6b10 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -9,10 +9,40 @@ import ( "strings" "sync" + "github.com/btcsuite/btcutil" "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop" + "github.com/lightninglabs/loop/loopdb" "github.com/lightningnetwork/lnd/lnwire" ) +const ( + // FeeBase is the base that we use to express fees. + FeeBase = 1e6 + + // defaultSwapFeePPM is the default limit we place on swap fees, + // expressed as parts per million of swap volume, 0.5%. + defaultSwapFeePPM = 5000 + + // defaultRoutingFeePPM is the default limit we place on routing fees + // for the swap invoice, expressed as parts per million of swap volume, + // 1%. + defaultRoutingFeePPM = 10000 + + // defaultRoutingFeePPM is the default limit we place on routing fees + // for the prepay invoice, expressed as parts per million of prepay + // volume, 0.5%. + defaultPrepayRoutingFeePPM = 5000 + + // defaultMaximumMinerFee is the default limit we place on miner fees + // per swap. + defaultMaximumMinerFee = 15000 + + // defaultMaximumPrepay is the default limit we place on prepay + // invoices. + defaultMaximumPrepay = 30000 +) + var ( // ErrZeroChannelID is returned if we get a rule for a 0 channel ID. ErrZeroChannelID = fmt.Errorf("zero channel ID not allowed") @@ -25,8 +55,8 @@ type Config struct { // to loop out swaps. LoopOutRestrictions func(ctx context.Context) (*Restrictions, error) - // Lnd provides us with access to lnd's main rpc. - Lnd lndclient.LightningClient + // Lnd provides us with access to lnd's rpc servers. + Lnd *lndclient.LndServices } // Parameters is a set of parameters provided by the user which guide @@ -140,7 +170,7 @@ func cloneParameters(params Parameters) Parameters { // balance for the set of rules configured for the manager, failing if there are // no rules set. func (m *Manager) SuggestSwaps(ctx context.Context) ( - []*LoopOutRecommendation, error) { + []loop.OutRequest, error) { m.paramsLock.Lock() defer m.paramsLock.Unlock() @@ -151,7 +181,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context) ( return nil, nil } - channels, err := m.cfg.Lnd.ListChannels(ctx) + channels, err := m.cfg.Lnd.Client.ListChannels(ctx) if err != nil { return nil, err } @@ -162,7 +192,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context) ( return nil, err } - var suggestions []*LoopOutRecommendation + var suggestions []loop.OutRequest for _, channel := range channels { channelID := lnwire.NewShortChanIDFromInt(channel.ChannelID) rule, ok := m.params.ChannelRules[channelID] @@ -177,9 +207,40 @@ func (m *Manager) SuggestSwaps(ctx context.Context) ( // We can have nil suggestions in the case where no action is // required, so only add non-nil suggestions. if suggestion != nil { - suggestions = append(suggestions, suggestion) + outRequest := makeLoopOutRequest(suggestion) + suggestions = append(suggestions, outRequest) } } return suggestions, nil } + +// makeLoopOutRequest creates a loop out request from a suggestion, setting fee +// limits defined by our default fee values. +func makeLoopOutRequest(suggestion *LoopOutRecommendation) loop.OutRequest { + prepayMaxFee := ppmToSat( + defaultMaximumPrepay, defaultPrepayRoutingFeePPM, + ) + + routeMaxFee := ppmToSat(suggestion.Amount, defaultRoutingFeePPM) + maxSwapFee := ppmToSat(suggestion.Amount, defaultSwapFeePPM) + + return loop.OutRequest{ + Amount: suggestion.Amount, + OutgoingChanSet: loopdb.ChannelSet{ + suggestion.Channel.ToUint64(), + }, + MaxPrepayRoutingFee: prepayMaxFee, + MaxSwapRoutingFee: routeMaxFee, + MaxMinerFee: defaultMaximumMinerFee, + MaxSwapFee: maxSwapFee, + MaxPrepayAmount: defaultMaximumPrepay, + SweepConfTarget: loop.DefaultSweepConfTarget, + } +} + +// ppmToSat takes an amount and a measure of parts per million for the amount +// and returns the amount that the ppm represents. +func ppmToSat(amount btcutil.Amount, ppm int) btcutil.Amount { + return btcutil.Amount(uint64(amount) * uint64(ppm) / FeeBase) +} diff --git a/liquidity/liquidity_test.go b/liquidity/liquidity_test.go index 67fe1f8c..386fc43b 100644 --- a/liquidity/liquidity_test.go +++ b/liquidity/liquidity_test.go @@ -5,26 +5,64 @@ import ( "testing" "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop" + "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" ) +var ( + chanID1 = lnwire.NewShortChanIDFromInt(1) + chanID2 = lnwire.NewShortChanIDFromInt(2) + + channel1 = lndclient.ChannelInfo{ + ChannelID: chanID1.ToUint64(), + LocalBalance: 10000, + RemoteBalance: 0, + Capacity: 10000, + } + + // chanRule is a rule that produces chan1Rec. + chanRule = NewThresholdRule(50, 0) + + prepayFee = ppmToSat( + defaultMaximumPrepay, defaultPrepayRoutingFeePPM, + ) + routingFee = ppmToSat(7500, defaultRoutingFeePPM) + swapFee = ppmToSat(7500, defaultSwapFeePPM) + + // chan1Rec is the suggested swap for channel 1 when we use chanRule. + chan1Rec = loop.OutRequest{ + Amount: 7500, + OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()}, + MaxPrepayRoutingFee: prepayFee, + MaxSwapRoutingFee: routingFee, + MaxMinerFee: defaultMaximumMinerFee, + MaxSwapFee: swapFee, + MaxPrepayAmount: defaultMaximumPrepay, + SweepConfTarget: loop.DefaultSweepConfTarget, + } +) + // newTestConfig creates a default test config. -func newTestConfig() *Config { +func newTestConfig() (*Config, *test.LndMockServices) { + lnd := test.NewMockLnd() + return &Config{ LoopOutRestrictions: func(_ context.Context) (*Restrictions, error) { return NewRestrictions(1, 10000), nil }, - Lnd: test.NewMockLnd().Client, - } + Lnd: &lnd.LndServices, + }, lnd } // TestParameters tests getting and setting of parameters for our manager. func TestParameters(t *testing.T) { - manager := NewManager(newTestConfig()) + cfg, _ := newTestConfig() + manager := NewManager(cfg) chanID := lnwire.NewShortChanIDFromInt(1) @@ -68,62 +106,31 @@ func TestParameters(t *testing.T) { require.Equal(t, ErrZeroChannelID, err) } -// TestSuggestSwaps tests getting of swap suggestions. +// TestSuggestSwaps tests getting of swap suggestions based on the rules set for +// the liquidity manager and the current set of channel balances. func TestSuggestSwaps(t *testing.T) { - var ( - chanID1 = lnwire.NewShortChanIDFromInt(1) - chanID2 = lnwire.NewShortChanIDFromInt(2) - ) - tests := []struct { - name string - channels []lndclient.ChannelInfo - parameters Parameters - swaps []*LoopOutRecommendation + name string + rules map[lnwire.ShortChannelID]*ThresholdRule + swaps []loop.OutRequest }{ { - name: "no rules", - channels: nil, - parameters: newParameters(), + name: "no rules", + rules: map[lnwire.ShortChannelID]*ThresholdRule{}, }, { name: "loop out", - channels: []lndclient.ChannelInfo{ - { - ChannelID: 1, - Capacity: 1000, - LocalBalance: 1000, - RemoteBalance: 0, - }, + rules: map[lnwire.ShortChannelID]*ThresholdRule{ + chanID1: chanRule, }, - parameters: Parameters{ - ChannelRules: map[lnwire.ShortChannelID]*ThresholdRule{ - chanID1: NewThresholdRule( - 10, 10, - ), - }, - }, - swaps: []*LoopOutRecommendation{ - { - Channel: chanID1, - Amount: 500, - }, + swaps: []loop.OutRequest{ + chan1Rec, }, }, { name: "no rule for channel", - channels: []lndclient.ChannelInfo{ - { - ChannelID: 1, - Capacity: 1000, - LocalBalance: 0, - RemoteBalance: 1000, - }, - }, - parameters: Parameters{ - ChannelRules: map[lnwire.ShortChannelID]*ThresholdRule{ - chanID2: NewThresholdRule(10, 10), - }, + rules: map[lnwire.ShortChannelID]*ThresholdRule{ + chanID2: NewThresholdRule(10, 10), }, swaps: nil, }, @@ -133,23 +140,43 @@ func TestSuggestSwaps(t *testing.T) { testCase := testCase t.Run(testCase.name, func(t *testing.T) { - cfg := newTestConfig() + cfg, lnd := newTestConfig() - // Create a mock lnd with the set of channels set in our - // test case. - mock := test.NewMockLnd() - mock.Channels = testCase.channels - cfg.Lnd = mock.Client + channels := []lndclient.ChannelInfo{ + channel1, + } - manager := NewManager(cfg) - - // Set our test case parameters. - err := manager.SetParameters(testCase.parameters) - require.NoError(t, err) - - swaps, err := manager.SuggestSwaps(context.Background()) - require.NoError(t, err) - require.Equal(t, testCase.swaps, swaps) + testSuggestSwaps( + t, cfg, lnd, channels, testCase.rules, + testCase.swaps, + ) }) } } + +// testSuggestSwaps tests getting swap suggestions. +func testSuggestSwaps(t *testing.T, cfg *Config, lnd *test.LndMockServices, + channels []lndclient.ChannelInfo, + rules map[lnwire.ShortChannelID]*ThresholdRule, + expected []loop.OutRequest) { + + t.Parallel() + + // Create a mock lnd with the set of channels set in our test case and + // update our test case lnd to use these channels. + lnd.Channels = channels + + // Create a new manager, get our current set of parameters and update + // them to use the rules set by the test. + manager := NewManager(cfg) + + currentParams := manager.GetParameters() + currentParams.ChannelRules = rules + + err := manager.SetParameters(currentParams) + require.NoError(t, err) + + actual, err := manager.SuggestSwaps(context.Background()) + require.NoError(t, err) + require.Equal(t, expected, actual) +} diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index b7a28c2a..64578e56 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -646,10 +646,14 @@ func (s *swapClientServer) SuggestSwaps(ctx context.Context, for _, swap := range swaps { loopOut = append(loopOut, &looprpc.LoopOutRequest{ - Amt: int64(swap.Amount), - OutgoingChanSet: []uint64{ - swap.Channel.ToUint64(), - }, + Amt: int64(swap.Amount), + OutgoingChanSet: swap.OutgoingChanSet, + MaxSwapFee: int64(swap.MaxSwapFee), + MaxMinerFee: int64(swap.MaxMinerFee), + MaxPrepayAmt: int64(swap.MaxPrepayAmount), + MaxSwapRoutingFee: int64(swap.MaxSwapRoutingFee), + MaxPrepayRoutingFee: int64(swap.MaxPrepayRoutingFee), + SweepConfTarget: swap.SweepConfTarget, }) } diff --git a/loopd/utils.go b/loopd/utils.go index 42278bc8..c283e91d 100644 --- a/loopd/utils.go +++ b/loopd/utils.go @@ -46,7 +46,7 @@ func getLiquidityManager(client *loop.Client) *liquidity.Manager { outTerms.MinSwapAmount, outTerms.MaxSwapAmount, ), nil }, - Lnd: client.LndServices.Client, + Lnd: client.LndServices, } return liquidity.NewManager(mngrCfg)