You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
loop/liquidity/liquidity.go

186 lines
4.8 KiB
Go

// 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
}