diff --git a/liquidity/balances.go b/liquidity/balances.go index 4975380..051cff5 100644 --- a/liquidity/balances.go +++ b/liquidity/balances.go @@ -2,6 +2,7 @@ package liquidity import ( "github.com/btcsuite/btcutil" + "github.com/lightninglabs/lndclient" "github.com/lightningnetwork/lnd/lnwire" ) @@ -20,3 +21,13 @@ type balances struct { // channelID is the channel that has these balances. channelID lnwire.ShortChannelID } + +// newBalances creates a balances struct from lndclient channel information. +func newBalances(info lndclient.ChannelInfo) *balances { + return &balances{ + capacity: info.Capacity, + incoming: info.RemoteBalance, + outgoing: info.LocalBalance, + channelID: lnwire.NewShortChanIDFromInt(info.ChannelID), + } +} diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index c9e9aa4..f90fa7c 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -9,6 +9,7 @@ import ( "strings" "sync" + "github.com/lightninglabs/lndclient" "github.com/lightningnetwork/lnd/lnwire" ) @@ -23,6 +24,9 @@ type Config struct { // LoopOutRestrictions returns the restrictions that the server applies // to loop out swaps. LoopOutRestrictions func(ctx context.Context) (*Restrictions, error) + + // Lnd provides us with access to lnd's main rpc. + Lnd lndclient.LightningClient } // Parameters is a set of parameters provided by the user which guide @@ -131,3 +135,51 @@ func cloneParameters(params Parameters) Parameters { return paramCopy } + +// 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) ( + []*LoopOutRecommendation, error) { + + m.paramsLock.Lock() + defer m.paramsLock.Unlock() + + // If we have no rules set, exit early to avoid unnecessary calls to + // lnd and the server. + if len(m.params.ChannelRules) == 0 { + return nil, nil + } + + channels, err := m.cfg.Lnd.ListChannels(ctx) + if err != nil { + return nil, err + } + + // Get the current server side restrictions. + outRestrictions, err := m.cfg.LoopOutRestrictions(ctx) + if err != nil { + return nil, err + } + + var suggestions []*LoopOutRecommendation + for _, channel := range channels { + channelID := lnwire.NewShortChanIDFromInt(channel.ChannelID) + rule, ok := m.params.ChannelRules[channelID] + if !ok { + continue + } + + balance := newBalances(channel) + + suggestion := rule.suggestSwap(balance, outRestrictions) + + // 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) + } + } + + return suggestions, nil +} diff --git a/liquidity/liquidity_test.go b/liquidity/liquidity_test.go index 774109d..67fe1f8 100644 --- a/liquidity/liquidity_test.go +++ b/liquidity/liquidity_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" ) @@ -16,6 +18,7 @@ func newTestConfig() *Config { return NewRestrictions(1, 10000), nil }, + Lnd: test.NewMockLnd().Client, } } @@ -64,3 +67,89 @@ func TestParameters(t *testing.T) { err = manager.SetParameters(expected) require.Equal(t, ErrZeroChannelID, err) } + +// TestSuggestSwaps tests getting of swap suggestions. +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: "no rules", + channels: nil, + parameters: newParameters(), + }, + { + name: "loop out", + channels: []lndclient.ChannelInfo{ + { + ChannelID: 1, + Capacity: 1000, + LocalBalance: 1000, + RemoteBalance: 0, + }, + }, + parameters: Parameters{ + ChannelRules: map[lnwire.ShortChannelID]*ThresholdRule{ + chanID1: NewThresholdRule( + 10, 10, + ), + }, + }, + swaps: []*LoopOutRecommendation{ + { + Channel: chanID1, + Amount: 500, + }, + }, + }, + { + 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), + }, + }, + swaps: nil, + }, + } + + for _, testCase := range tests { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + cfg := 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 + + 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) + }) + } +} diff --git a/loopd/utils.go b/loopd/utils.go index b0116a4..42278bc 100644 --- a/loopd/utils.go +++ b/loopd/utils.go @@ -46,6 +46,7 @@ func getLiquidityManager(client *loop.Client) *liquidity.Manager { outTerms.MinSwapAmount, outTerms.MaxSwapAmount, ), nil }, + Lnd: client.LndServices.Client, } return liquidity.NewManager(mngrCfg)