liquidity: add swap builder interface

pull/418/head
carla 3 years ago
parent 4df6a4e7ca
commit 8bd7ee35be
No known key found for this signature in database
GPG Key ID: 4CA7FE54A6213C91

@ -1,8 +1,11 @@
package liquidity
import (
"context"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
@ -31,6 +34,32 @@ type FeeLimit interface {
btcutil.Amount, btcutil.Amount, btcutil.Amount)
}
// swapBuilder is an interface used to build our different swap types.
type swapBuilder interface {
// swapType returns the swap type that the builder is responsible for
// creating.
swapType() swap.Type
// maySwap checks whether we can currently execute a swap, examining
// the current on-chain fee conditions against relevant to our swap
// type against our fee restrictions.
maySwap(ctx context.Context, params Parameters) error
// inUse examines our current swap traffic to determine whether we
// should suggest the builder's type of swap for the peer and channels
// suggested.
inUse(traffic *swapTraffic, peer route.Vertex,
channels []lnwire.ShortChannelID) error
// buildSwap creates a swap for the target peer/channels provided. The
// autoloop boolean indicates whether this swap will actually be
// executed, because there are some calls we can leave out if this swap
// is just for a dry run.
buildSwap(ctx context.Context, peer route.Vertex,
channels []lnwire.ShortChannelID, amount btcutil.Amount,
autoloop bool, params Parameters) (swapSuggestion, error)
}
// swapSuggestion is an interface implemented by suggested swaps for our
// different swap types. This interface is used to allow us to handle different
// swap types with the same autoloop logic.

@ -386,6 +386,10 @@ 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
@ -424,8 +428,9 @@ 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,
cfg: cfg,
params: defaultParameters,
builder: newLoopOutBuilder(cfg),
}
}
@ -616,14 +621,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
// 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.
estimate, err := m.cfg.Lnd.WalletKit.EstimateFee(
ctx, m.params.SweepConfTarget,
)
if err != nil {
return nil, err
}
if err := m.params.FeeLimit.mayLoopOut(estimate); err != nil {
if err := m.builder.maySwap(ctx, m.params); err != nil {
var reasonErr *reasonError
if errors.As(err, &reasonErr) {
return m.singleReasonSuggestion(reasonErr.reason), nil
@ -635,7 +633,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
// Get the current server side restrictions, combined with the client
// set restrictions, if any.
restrictions, err := m.getSwapRestrictions(ctx, swap.TypeOut)
restrictions, err := m.getSwapRestrictions(ctx, m.builder.swapType())
if err != nil {
return nil, err
}
@ -846,65 +844,24 @@ func (m *Manager) suggestSwap(ctx context.Context, traffic *swapTraffic,
balance *balances, rule *ThresholdRule, restrictions *Restrictions,
autoloop bool) (swapSuggestion, error) {
// Check whether we can perform a swap.
err := traffic.maySwap(balance.pubkey, balance.channels)
// First, check whether this peer/channel combination is already in use
// for our swap.
err := m.builder.inUse(traffic, balance.pubkey, balance.channels)
if err != nil {
return nil, err
}
// We can have nil suggestions in the case where no action is
// required, so we skip over them.
// Next, get the amount that we need to swap for this entity, skipping
// over it if no change in liquidity is required.
amount := rule.swapAmount(balance, restrictions)
if amount == 0 {
return nil, newReasonError(ReasonLiquidityOk)
}
swap, err := m.loopOutSwap(ctx, amount, balance, autoloop)
if err != nil {
return nil, err
}
return &loopOutSwapSuggestion{
OutRequest: *swap,
}, nil
}
// loopOutSwap creates a loop out swap with the amount provided for the balance
// described by the balance set provided. A reason that indicates whether we
// can swap is returned. If this value is not ReasonNone, there is no possible
// swap and the loop out request returned will be nil.
func (m *Manager) loopOutSwap(ctx context.Context, amount btcutil.Amount,
balance *balances, autoloop bool) (*loop.OutRequest, error) {
quote, err := m.cfg.LoopOutQuote(
ctx, &loop.LoopOutQuoteRequest{
Amount: amount,
SweepConfTarget: m.params.SweepConfTarget,
SwapPublicationDeadline: m.cfg.Clock.Now(),
},
)
if err != nil {
return nil, err
}
log.Debugf("quote for suggestion: %v, swap fee: %v, "+
"miner fee: %v, prepay: %v", amount, quote.SwapFee,
quote.MinerFee, quote.PrepayAmount)
// Check that the estimated fees for the suggested swap are
// below the fee limits configured by the manager.
if err := m.params.FeeLimit.loopOutLimits(amount, quote); err != nil {
return nil, err
}
outRequest, err := m.makeLoopOutRequest(
ctx, amount, balance, quote, autoloop,
return m.builder.buildSwap(
ctx, balance.pubkey, balance.channels, amount, autoloop,
m.params,
)
if err != nil {
return nil, err
}
return &outRequest, nil
}
// getSwapRestrictions queries the server for its latest swap size restrictions,
@ -942,58 +899,6 @@ func (m *Manager) getSwapRestrictions(ctx context.Context, swapType swap.Type) (
return restrictions, nil
}
// makeLoopOutRequest creates a loop out request from a suggestion. Since we
// do not get any information about our off-chain routing fees when we request
// a quote, we just set our prepay and route maximum fees directly from the
// amounts we expect to route. The estimation we use elsewhere is the repo is
// route-independent, which is a very poor estimation so we don't bother with
// checking against this inaccurate constant. We use the exact prepay amount
// and swap fee given to us by the server, but use our maximum miner fee anyway
// to give us some leeway when performing the swap. We take an auto-out which
// determines whether we set a label identifying this swap as automatically
// dispatched, and decides whether we set a sweep address (we don't bother for
// non-auto requests, because the client api will set it anyway).
func (m *Manager) makeLoopOutRequest(ctx context.Context,
amount btcutil.Amount, balance *balances, quote *loop.LoopOutQuote,
autoloop bool) (loop.OutRequest, error) {
prepayMaxFee, routeMaxFee, minerFee := m.params.FeeLimit.loopOutFees(
amount, quote,
)
var chanSet loopdb.ChannelSet
for _, channel := range balance.channels {
chanSet = append(chanSet, channel.ToUint64())
}
// Create a request with our calculated routing fees. We can use the
// swap fee, prepay amount and miner fee from the quote because we have
// already validated them.
request := loop.OutRequest{
Amount: amount,
OutgoingChanSet: chanSet,
MaxPrepayRoutingFee: prepayMaxFee,
MaxSwapRoutingFee: routeMaxFee,
MaxMinerFee: minerFee,
MaxSwapFee: quote.SwapFee,
MaxPrepayAmount: quote.PrepayAmount,
SweepConfTarget: m.params.SweepConfTarget,
Initiator: autoloopSwapInitiator,
}
if autoloop {
request.Label = labels.AutoloopLabel(swap.TypeOut)
addr, err := m.cfg.Lnd.WalletKit.NextAddr(ctx)
if err != nil {
return loop.OutRequest{}, err
}
request.DestAddr = addr
}
return request, nil
}
// worstCaseOutFees calculates the largest possible fees for a loop out swap,
// comparing the fees for a successful swap to the cost when the client pays
// the prepay because they failed to sweep the on chain htlc. This is unlikely,
@ -1177,38 +1082,6 @@ func newSwapTraffic() *swapTraffic {
}
}
// maySwap returns a boolean that indicates whether we may perform a swap for a
// peer and its set of channels.
func (s *swapTraffic) maySwap(peer route.Vertex,
channels []lnwire.ShortChannelID) error {
for _, chanID := range channels {
lastFail, recentFail := s.failedLoopOut[chanID]
if recentFail {
log.Debugf("Channel: %v not eligible for suggestions, was "+
"part of a failed swap at: %v", chanID, lastFail)
return newReasonError(ReasonFailureBackoff)
}
if s.ongoingLoopOut[chanID] {
log.Debugf("Channel: %v not eligible for suggestions, "+
"ongoing loop out utilizing channel", chanID)
return newReasonError(ReasonLoopOut)
}
}
if s.ongoingLoopIn[peer] {
log.Debugf("Peer: %x not eligible for suggestions ongoing "+
"loop in utilizing peer", peer)
return newReasonError(ReasonLoopIn)
}
return nil
}
// satPerKwToSatPerVByte converts sat per kWeight to sat per vByte.
func satPerKwToSatPerVByte(satPerKw chainfee.SatPerKWeight) int64 {
return int64(satPerKw.FeePerKVByte() / 1000)

@ -0,0 +1,162 @@
package liquidity
import (
"context"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
)
// Compile-time assertion that loopOutBuilder satisfies the swapBuilder
// interface.
var _ swapBuilder = (*loopOutBuilder)(nil)
func newLoopOutBuilder(cfg *Config) *loopOutBuilder {
return &loopOutBuilder{
cfg: cfg,
}
}
type loopOutBuilder struct {
// cfg contains all the external functionality we require to create
// swaps.
cfg *Config
}
// swapType returns the swap type that the builder is responsible for creating.
func (b *loopOutBuilder) swapType() swap.Type {
return swap.TypeOut
}
// maySwap checks whether we can currently execute a swap, examining the
// current on-chain fee conditions relevant to our swap type against our fee
// restrictions.
//
// For loop out, we check whether the fees required for our on-chain sweep
// transaction exceed our fee limits.
func (b *loopOutBuilder) maySwap(ctx context.Context, params Parameters) error {
estimate, err := b.cfg.Lnd.WalletKit.EstimateFee(
ctx, params.SweepConfTarget,
)
if err != nil {
return err
}
return params.FeeLimit.mayLoopOut(estimate)
}
// inUse examines our current swap traffic to determine whether we should
// we can perform a swap for the peer/ channels provided.
func (b *loopOutBuilder) inUse(traffic *swapTraffic, peer route.Vertex,
channels []lnwire.ShortChannelID) error {
for _, chanID := range channels {
lastFail, recentFail := traffic.failedLoopOut[chanID]
if recentFail {
log.Debugf("Channel: %v not eligible for suggestions, "+
"was part of a failed swap at: %v", chanID,
lastFail)
return newReasonError(ReasonFailureBackoff)
}
if traffic.ongoingLoopOut[chanID] {
log.Debugf("Channel: %v not eligible for suggestions, "+
"ongoing loop out utilizing channel", chanID)
return newReasonError(ReasonLoopOut)
}
}
if traffic.ongoingLoopIn[peer] {
log.Debugf("Peer: %x not eligible for suggestions ongoing "+
"loop in utilizing peer", peer)
return newReasonError(ReasonLoopIn)
}
return nil
}
// buildSwap creates a swap for the target peer/channels provided. The autoloop
// boolean indicates whether this swap will actually be executed, because there
// are some calls we can leave out if this swap is just for a dry run (ie, when
// we are just demonstrating the actions that autoloop _would_ take, but not
// actually executing the swap).
//
// For loop out, we don't bother generating a new wallet address if this is a
// dry-run, and we do not add the autoloop label to the recommended swap.
func (b *loopOutBuilder) buildSwap(ctx context.Context, pubkey route.Vertex,
channels []lnwire.ShortChannelID, amount btcutil.Amount,
autoloop bool, params Parameters) (swapSuggestion, error) {
quote, err := b.cfg.LoopOutQuote(
ctx, &loop.LoopOutQuoteRequest{
Amount: amount,
SweepConfTarget: params.SweepConfTarget,
SwapPublicationDeadline: b.cfg.Clock.Now(),
},
)
if err != nil {
return nil, err
}
log.Debugf("quote for suggestion: %v, swap fee: %v, "+
"miner fee: %v, prepay: %v", amount, quote.SwapFee,
quote.MinerFee, quote.PrepayAmount)
// Check that the estimated fees for the suggested swap are below the
// fee limits configured.
if err := params.FeeLimit.loopOutLimits(amount, quote); err != nil {
return nil, err
}
// Break down our fees into appropriate categories for our swap. Our
// quote does not provide any off-chain routing estimates for us, so
// we just set our fees from the amounts that we expect to route. We
// don't have any off-chain fee estimation, so we just use the exact
// prepay, swap and miner fee provided by the server and split our
// remaining fees up from there.
prepayMaxFee, routeMaxFee, minerFee := params.FeeLimit.loopOutFees(
amount, quote,
)
var chanSet loopdb.ChannelSet
for _, channel := range channels {
chanSet = append(chanSet, channel.ToUint64())
}
// Create a request with our calculated routing fees. We can use the
// swap fee, prepay amount and miner fee from the quote because we have
// already validated them.
request := loop.OutRequest{
Amount: amount,
OutgoingChanSet: chanSet,
MaxPrepayRoutingFee: prepayMaxFee,
MaxSwapRoutingFee: routeMaxFee,
MaxMinerFee: minerFee,
MaxSwapFee: quote.SwapFee,
MaxPrepayAmount: quote.PrepayAmount,
SweepConfTarget: params.SweepConfTarget,
Initiator: autoloopSwapInitiator,
}
if autoloop {
request.Label = labels.AutoloopLabel(swap.TypeOut)
addr, err := b.cfg.Lnd.WalletKit.NextAddr(ctx)
if err != nil {
return nil, err
}
request.DestAddr = addr
}
return &loopOutSwapSuggestion{
OutRequest: request,
}, nil
}
Loading…
Cancel
Save