// Package liquidity is responsible for monitoring our node's liquidity. It // allows setting of a liquidity rule which describes the desired liquidity // balance on a per-channel basis. package liquidity import ( "context" "fmt" "strings" "sync" "github.com/lightninglabs/lndclient" "github.com/lightningnetwork/lnd/lnwire" ) var ( // ErrZeroChannelID is returned if we get a rule for a 0 channel ID. ErrZeroChannelID = fmt.Errorf("zero channel ID not allowed") ) // Config contains the external functionality required to run the // liquidity manager. 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 // how we assess liquidity. type Parameters struct { // ChannelRules maps a short channel ID to a rule that describes how we // would like liquidity to be managed. ChannelRules map[lnwire.ShortChannelID]*ThresholdRule } // newParameters creates an empty set of parameters. func newParameters() Parameters { return Parameters{ ChannelRules: make(map[lnwire.ShortChannelID]*ThresholdRule), } } // String returns the string representation of our parameters. func (p Parameters) String() string { channelRules := make([]string, 0, len(p.ChannelRules)) for channel, rule := range p.ChannelRules { channelRules = append( channelRules, fmt.Sprintf("%v: %v", channel, rule), ) } return fmt.Sprintf("channel rules: %v", strings.Join(channelRules, ",")) } // validate checks whether a set of parameters is valid. func (p Parameters) validate() error { for channel, rule := range p.ChannelRules { if channel.ToUint64() == 0 { return ErrZeroChannelID } if err := rule.validate(); err != nil { return fmt.Errorf("channel: %v has invalid rule: %v", channel.ToUint64(), err) } } return nil } // Manager contains a set of desired liquidity rules for our channel // balances. type Manager struct { // cfg contains the external functionality we require to determine our // current liquidity balance. cfg *Config // params is the set of parameters we are currently using. These may be // updated at runtime. params Parameters // paramsLock is a lock for our current set of parameters. paramsLock sync.Mutex } // NewManager creates a liquidity manager which has no rules set. func NewManager(cfg *Config) *Manager { return &Manager{ cfg: cfg, params: newParameters(), } } // GetParameters returns a copy of our current parameters. func (m *Manager) GetParameters() Parameters { m.paramsLock.Lock() defer m.paramsLock.Unlock() return cloneParameters(m.params) } // SetParameters updates our current set of parameters if the new parameters // provided are valid. func (m *Manager) SetParameters(params Parameters) error { if err := params.validate(); err != nil { return err } m.paramsLock.Lock() defer m.paramsLock.Unlock() m.params = cloneParameters(params) return nil } // cloneParameters creates a deep clone of a parameters struct so that callers // cannot mutate our parameters. Although our parameters struct itself is not // a reference, we still need to clone the contents of maps. func cloneParameters(params Parameters) Parameters { paramCopy := Parameters{ ChannelRules: make(map[lnwire.ShortChannelID]*ThresholdRule, len(params.ChannelRules)), } for channel, rule := range params.ChannelRules { ruleCopy := *rule paramCopy.ChannelRules[channel] = &ruleCopy } 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 }