mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-16 13:00:37 +02:00
Merge pull request #433 from carlaKC/419-swaptype
liquidity: add swap type and loop in fee logic
This commit is contained in:
commit
2e6adeaa3f
13 changed files with 838 additions and 172 deletions
|
|
@ -168,6 +168,7 @@ func setRule(ctx *cli.Context) error {
|
|||
newRule := &looprpc.LiquidityRule{
|
||||
ChannelId: chanID,
|
||||
Type: looprpc.LiquidityRuleType_THRESHOLD,
|
||||
SwapType: looprpc.SwapType_LOOP_OUT,
|
||||
}
|
||||
|
||||
if pubkeyRule {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ func TestAutoLoopDisabled(t *testing.T) {
|
|||
}
|
||||
|
||||
params := defaultParameters
|
||||
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
|
||||
params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{
|
||||
chanID1: chanRule,
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ func TestAutoLoopEnabled(t *testing.T) {
|
|||
swapFeePPM, routeFeePPM, prepayFeePPM, maxMiner,
|
||||
prepayAmount, 20000,
|
||||
),
|
||||
ChannelRules: map[lnwire.ShortChannelID]*ThresholdRule{
|
||||
ChannelRules: map[lnwire.ShortChannelID]*SwapRule{
|
||||
chanID1: chanRule,
|
||||
chanID2: chanRule,
|
||||
},
|
||||
|
|
@ -312,10 +312,10 @@ func TestCompositeRules(t *testing.T) {
|
|||
MaxAutoInFlight: 2,
|
||||
FailureBackOff: time.Hour,
|
||||
SweepConfTarget: 10,
|
||||
ChannelRules: map[lnwire.ShortChannelID]*ThresholdRule{
|
||||
ChannelRules: map[lnwire.ShortChannelID]*SwapRule{
|
||||
chanID1: chanRule,
|
||||
},
|
||||
PeerRules: map[route.Vertex]*ThresholdRule{
|
||||
PeerRules: map[route.Vertex]*SwapRule{
|
||||
peer2: chanRule,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -223,6 +223,27 @@ func (f *FeeCategoryLimit) loopOutLimits(amount btcutil.Amount,
|
|||
return nil
|
||||
}
|
||||
|
||||
func (f *FeeCategoryLimit) loopInLimits(amount btcutil.Amount,
|
||||
quote *loop.LoopInQuote) error {
|
||||
|
||||
maxServerFee := ppmToSat(amount, f.MaximumSwapFeePPM)
|
||||
if quote.SwapFee > maxServerFee {
|
||||
log.Debugf("quoted swap fee: %v > maximum swap fee: %v",
|
||||
quote.SwapFee, maxServerFee)
|
||||
|
||||
return newReasonError(ReasonSwapFee)
|
||||
}
|
||||
|
||||
if quote.MinerFee > f.MaximumMinerFee {
|
||||
log.Debugf("quoted miner fee: %v > maximum miner "+
|
||||
"fee: %v", quote.MinerFee, f.MaximumMinerFee)
|
||||
|
||||
return newReasonError(ReasonMinerFee)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loopOutFees returns the prepay and routing and miner fees we are willing to
|
||||
// pay for a loop out swap.
|
||||
func (f *FeeCategoryLimit) loopOutFees(amount btcutil.Amount,
|
||||
|
|
@ -384,3 +405,42 @@ func splitOffChain(available, prepayAmt,
|
|||
func scaleMinerFee(estimate btcutil.Amount) btcutil.Amount {
|
||||
return estimate * btcutil.Amount(minerMultiplier)
|
||||
}
|
||||
|
||||
func (f *FeePortion) loopInLimits(amount btcutil.Amount,
|
||||
quote *loop.LoopInQuote) error {
|
||||
|
||||
// Calculate the total amount that this swap may spend in fees, as a
|
||||
// portion of the swap amount.
|
||||
totalFeeSpend := ppmToSat(amount, f.PartsPerMillion)
|
||||
|
||||
// Check individual fee components so that we can give more specific
|
||||
// feedback.
|
||||
if quote.MinerFee > totalFeeSpend {
|
||||
log.Debugf("miner fee: %v greater than fee limit: %v, at "+
|
||||
"%v ppm", quote.MinerFee, totalFeeSpend,
|
||||
f.PartsPerMillion)
|
||||
|
||||
return newReasonError(ReasonMinerFee)
|
||||
}
|
||||
|
||||
if quote.SwapFee > totalFeeSpend {
|
||||
log.Debugf("swap fee: %v greater than fee limit: %v, at "+
|
||||
"%v ppm", quote.SwapFee, totalFeeSpend,
|
||||
f.PartsPerMillion)
|
||||
|
||||
return newReasonError(ReasonSwapFee)
|
||||
}
|
||||
|
||||
fees := worstCaseInFees(
|
||||
quote.MinerFee, quote.SwapFee, defaultLoopInSweepFee,
|
||||
)
|
||||
|
||||
if fees > totalFeeSpend {
|
||||
log.Debugf("total fees for swap: %v > fee limit: %v, at "+
|
||||
"%v ppm", fees, totalFeeSpend, f.PartsPerMillion)
|
||||
|
||||
return newReasonError(ReasonFeePPMInsufficient)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ type FeeLimit interface {
|
|||
// a swap amount and quote.
|
||||
loopOutFees(amount btcutil.Amount, quote *loop.LoopOutQuote) (
|
||||
btcutil.Amount, btcutil.Amount, btcutil.Amount)
|
||||
|
||||
// loopInLimits checks whether the quote provided is within our fee
|
||||
// limits for the swap amount.
|
||||
loopInLimits(amount btcutil.Amount,
|
||||
quote *loop.LoopInQuote) error
|
||||
}
|
||||
|
||||
// swapBuilder is an interface used to build our different swap types.
|
||||
|
|
|
|||
|
|
@ -81,6 +81,12 @@ const (
|
|||
// autoloopSwapInitiator is the value we send in the initiator field of
|
||||
// a swap request when issuing an automatic swap.
|
||||
autoloopSwapInitiator = "autoloop"
|
||||
|
||||
// We use a static fee rate to estimate our sweep fee, because we
|
||||
// can't realistically estimate what our fee estimate will be by the
|
||||
// time we reach timeout. We set this to a high estimate so that we can
|
||||
// account for worst-case fees, (1250 * 4 / 1000) = 50 sat/byte.
|
||||
defaultLoopInSweepFee = chainfee.SatPerKWeight(1250)
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -97,8 +103,8 @@ var (
|
|||
defaultParameters = Parameters{
|
||||
AutoFeeBudget: defaultBudget,
|
||||
MaxAutoInFlight: defaultMaxInFlight,
|
||||
ChannelRules: make(map[lnwire.ShortChannelID]*ThresholdRule),
|
||||
PeerRules: make(map[route.Vertex]*ThresholdRule),
|
||||
ChannelRules: make(map[lnwire.ShortChannelID]*SwapRule),
|
||||
PeerRules: make(map[route.Vertex]*SwapRule),
|
||||
FailureBackOff: defaultFailureBackoff,
|
||||
SweepConfTarget: defaultConfTarget,
|
||||
FeeLimit: defaultFeePortion(),
|
||||
|
|
@ -216,13 +222,13 @@ type Parameters struct {
|
|||
// ChannelRules maps a short channel ID to a rule that describes how we
|
||||
// would like liquidity to be managed. These rules and PeerRules are
|
||||
// exclusively set to prevent overlap between peer and channel rules.
|
||||
ChannelRules map[lnwire.ShortChannelID]*ThresholdRule
|
||||
ChannelRules map[lnwire.ShortChannelID]*SwapRule
|
||||
|
||||
// PeerRules maps a peer's pubkey to a rule that applies to all the
|
||||
// channels that we have with the peer collectively. These rules and
|
||||
// ChannelRules are exclusively set to prevent overlap between peer
|
||||
// and channel rules map to avoid ambiguity.
|
||||
PeerRules map[route.Vertex]*ThresholdRule
|
||||
PeerRules map[route.Vertex]*SwapRule
|
||||
}
|
||||
|
||||
// String returns the string representation of our parameters.
|
||||
|
|
@ -386,10 +392,6 @@ 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
|
||||
|
|
@ -428,9 +430,8 @@ 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,
|
||||
builder: newLoopOutBuilder(cfg),
|
||||
cfg: cfg,
|
||||
params: defaultParameters,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -473,7 +474,7 @@ func (m *Manager) SetParameters(ctx context.Context, params Parameters) error {
|
|||
func cloneParameters(params Parameters) Parameters {
|
||||
paramCopy := params
|
||||
paramCopy.ChannelRules = make(
|
||||
map[lnwire.ShortChannelID]*ThresholdRule,
|
||||
map[lnwire.ShortChannelID]*SwapRule,
|
||||
len(params.ChannelRules),
|
||||
)
|
||||
|
||||
|
|
@ -483,7 +484,7 @@ func cloneParameters(params Parameters) Parameters {
|
|||
}
|
||||
|
||||
paramCopy.PeerRules = make(
|
||||
map[route.Vertex]*ThresholdRule,
|
||||
map[route.Vertex]*SwapRule,
|
||||
len(params.PeerRules),
|
||||
)
|
||||
|
||||
|
|
@ -617,23 +618,8 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
|
|||
return m.singleReasonSuggestion(ReasonBudgetNotStarted), nil
|
||||
}
|
||||
|
||||
// Before we get any swap suggestions, we check what the current fee
|
||||
// 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.
|
||||
if err := m.builder.maySwap(ctx, m.params); err != nil {
|
||||
var reasonErr *reasonError
|
||||
if errors.As(err, &reasonErr) {
|
||||
return m.singleReasonSuggestion(reasonErr.reason), nil
|
||||
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the current server side restrictions, combined with the client
|
||||
// set restrictions, if any.
|
||||
restrictions, err := m.getSwapRestrictions(ctx, m.builder.swapType())
|
||||
// Get restrictions placed on swaps by the server.
|
||||
outRestrictions, err := m.getSwapRestrictions(ctx, swap.TypeOut)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -653,7 +639,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
|
|||
|
||||
// Get a summary of our existing swaps so that we can check our autoloop
|
||||
// budget.
|
||||
summary, err := m.checkExistingAutoLoops(ctx, loopOut)
|
||||
summary, err := m.checkExistingAutoLoops(ctx, loopOut, loopIn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -721,7 +707,8 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
|
|||
}
|
||||
|
||||
suggestion, err := m.suggestSwap(
|
||||
ctx, traffic, balances, rule, restrictions, autoloop,
|
||||
ctx, traffic, balances, rule, outRestrictions,
|
||||
autoloop,
|
||||
)
|
||||
var reasonErr *reasonError
|
||||
if errors.As(err, &reasonErr) {
|
||||
|
|
@ -746,7 +733,8 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
|
|||
}
|
||||
|
||||
suggestion, err := m.suggestSwap(
|
||||
ctx, traffic, balance, rule, restrictions, autoloop,
|
||||
ctx, traffic, balance, rule, outRestrictions,
|
||||
autoloop,
|
||||
)
|
||||
|
||||
var reasonErr *reasonError
|
||||
|
|
@ -841,12 +829,34 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
|
|||
// suggestSwap checks whether we can currently perform a swap, and creates a
|
||||
// swap request for the rule provided.
|
||||
func (m *Manager) suggestSwap(ctx context.Context, traffic *swapTraffic,
|
||||
balance *balances, rule *ThresholdRule, restrictions *Restrictions,
|
||||
balance *balances, rule *SwapRule, outRestrictions *Restrictions,
|
||||
autoloop bool) (swapSuggestion, error) {
|
||||
|
||||
var (
|
||||
builder swapBuilder
|
||||
restrictions *Restrictions
|
||||
)
|
||||
|
||||
switch rule.Type {
|
||||
case swap.TypeOut:
|
||||
builder = newLoopOutBuilder(m.cfg)
|
||||
restrictions = outRestrictions
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported swap type: %v", rule.Type)
|
||||
}
|
||||
|
||||
// Before we get any swap suggestions, we check what the current fee
|
||||
// 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.
|
||||
if err := builder.maySwap(ctx, m.params); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// First, check whether this peer/channel combination is already in use
|
||||
// for our swap.
|
||||
err := m.builder.inUse(traffic, balance.pubkey, balance.channels)
|
||||
err := builder.inUse(traffic, balance.pubkey, balance.channels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -858,7 +868,7 @@ func (m *Manager) suggestSwap(ctx context.Context, traffic *swapTraffic,
|
|||
return nil, newReasonError(ReasonLiquidityOk)
|
||||
}
|
||||
|
||||
return m.builder.buildSwap(
|
||||
return builder.buildSwap(
|
||||
ctx, balance.pubkey, balance.channels, amount, autoloop,
|
||||
m.params,
|
||||
)
|
||||
|
|
@ -948,7 +958,8 @@ func (e *existingAutoLoopSummary) totalFees() btcutil.Amount {
|
|||
// total for our set of ongoing, automatically dispatched swaps as well as a
|
||||
// current in-flight count.
|
||||
func (m *Manager) checkExistingAutoLoops(ctx context.Context,
|
||||
loopOuts []*loopdb.LoopOut) (*existingAutoLoopSummary, error) {
|
||||
loopOuts []*loopdb.LoopOut, loopIns []*loopdb.LoopIn) (
|
||||
*existingAutoLoopSummary, error) {
|
||||
|
||||
var summary existingAutoLoopSummary
|
||||
|
||||
|
|
@ -987,6 +998,28 @@ func (m *Manager) checkExistingAutoLoops(ctx context.Context,
|
|||
}
|
||||
}
|
||||
|
||||
for _, in := range loopIns {
|
||||
if in.Contract.Label != labels.AutoloopLabel(swap.TypeIn) {
|
||||
continue
|
||||
}
|
||||
|
||||
pending := in.State().State.Type() == loopdb.StateTypePending
|
||||
inBudget := !in.LastUpdateTime().Before(m.params.AutoFeeStartDate)
|
||||
|
||||
// If an autoloop is in a pending state, we always count it in
|
||||
// our current budget, and record the worst-case fees for it,
|
||||
// because we do not know how it will resolve.
|
||||
if pending {
|
||||
summary.inFlightCount++
|
||||
summary.pendingFees += worstCaseInFees(
|
||||
in.Contract.MaxMinerFee, in.Contract.MaxSwapFee,
|
||||
defaultLoopInSweepFee,
|
||||
)
|
||||
} else if inBudget {
|
||||
summary.spentFees += in.State().Cost.Total()
|
||||
}
|
||||
}
|
||||
|
||||
return &summary, nil
|
||||
}
|
||||
|
||||
|
|
@ -1051,17 +1084,28 @@ func (m *Manager) currentSwapTraffic(loopOut []*loopdb.LoopOut,
|
|||
}
|
||||
|
||||
for _, in := range loopIn {
|
||||
// Skip completed swaps, they can't affect our channel balances.
|
||||
if in.State().State.Type() != loopdb.StateTypePending {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip over swaps that may come through any peer.
|
||||
if in.Contract.LastHop == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
traffic.ongoingLoopIn[*in.Contract.LastHop] = true
|
||||
pubkey := *in.Contract.LastHop
|
||||
|
||||
switch {
|
||||
// Include any pending swaps in our ongoing set of swaps.
|
||||
case in.State().State.Type() == loopdb.StateTypePending:
|
||||
traffic.ongoingLoopIn[pubkey] = true
|
||||
|
||||
// If a swap failed with an on-chain timeout, the server could
|
||||
// not route to us. We add it to our backoff list so that
|
||||
// there's some time for routing conditions to improve.
|
||||
case in.State().State == loopdb.StateFailTimeout:
|
||||
failedAt := in.LastUpdate().Time
|
||||
|
||||
if failedAt.After(failureCutoff) {
|
||||
traffic.failedLoopIn[pubkey] = failedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return traffic
|
||||
|
|
@ -1072,6 +1116,7 @@ type swapTraffic struct {
|
|||
ongoingLoopOut map[lnwire.ShortChannelID]bool
|
||||
ongoingLoopIn map[route.Vertex]bool
|
||||
failedLoopOut map[lnwire.ShortChannelID]time.Time
|
||||
failedLoopIn map[route.Vertex]time.Time
|
||||
}
|
||||
|
||||
func newSwapTraffic() *swapTraffic {
|
||||
|
|
@ -1079,6 +1124,7 @@ func newSwapTraffic() *swapTraffic {
|
|||
ongoingLoopOut: make(map[lnwire.ShortChannelID]bool),
|
||||
ongoingLoopIn: make(map[route.Vertex]bool),
|
||||
failedLoopOut: make(map[lnwire.ShortChannelID]time.Time),
|
||||
failedLoopIn: make(map[route.Vertex]time.Time),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,10 @@ var (
|
|||
}
|
||||
|
||||
// chanRule is a rule that produces chan1Rec.
|
||||
chanRule = NewThresholdRule(50, 0)
|
||||
chanRule = &SwapRule{
|
||||
ThresholdRule: NewThresholdRule(50, 0),
|
||||
Type: swap.TypeOut,
|
||||
}
|
||||
|
||||
testQuote = &loop.LoopOutQuote{
|
||||
SwapFee: btcutil.Amount(5),
|
||||
|
|
@ -104,6 +107,13 @@ var (
|
|||
OutgoingChanSet: loopdb.ChannelSet{999},
|
||||
}
|
||||
|
||||
autoInContract = &loopdb.LoopInContract{
|
||||
SwapContract: loopdb.SwapContract{
|
||||
Label: labels.AutoloopLabel(swap.TypeIn),
|
||||
InitiationTime: testBudgetStart,
|
||||
},
|
||||
}
|
||||
|
||||
testRestrictions = NewRestrictions(1, 10000)
|
||||
|
||||
// noneDisqualified can be used in tests where we don't have any
|
||||
|
|
@ -188,7 +198,10 @@ func TestParameters(t *testing.T) {
|
|||
require.Equal(t, defaultParameters, startParams)
|
||||
|
||||
// Mutate the parameters returned by our get function.
|
||||
startParams.ChannelRules[chanID] = NewThresholdRule(1, 1)
|
||||
startParams.ChannelRules[chanID] = &SwapRule{
|
||||
ThresholdRule: NewThresholdRule(1, 1),
|
||||
Type: swap.TypeOut,
|
||||
}
|
||||
|
||||
// Make sure that we have not mutated the liquidity manager's params
|
||||
// by making this change.
|
||||
|
|
@ -197,9 +210,13 @@ func TestParameters(t *testing.T) {
|
|||
|
||||
// Provide a valid set of parameters and validate assert that they are
|
||||
// set.
|
||||
originalRule := NewThresholdRule(10, 10)
|
||||
originalRule := &SwapRule{
|
||||
ThresholdRule: NewThresholdRule(10, 10),
|
||||
Type: swap.TypeOut,
|
||||
}
|
||||
|
||||
expected := defaultParameters
|
||||
expected.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
|
||||
expected.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{
|
||||
chanID: originalRule,
|
||||
}
|
||||
|
||||
|
|
@ -208,15 +225,21 @@ func TestParameters(t *testing.T) {
|
|||
|
||||
// Check that changing the parameters we just set does not mutate
|
||||
// our liquidity manager's parameters.
|
||||
expected.ChannelRules[chanID] = NewThresholdRule(11, 11)
|
||||
expected.ChannelRules[chanID] = &SwapRule{
|
||||
ThresholdRule: NewThresholdRule(11, 11),
|
||||
Type: swap.TypeOut,
|
||||
}
|
||||
|
||||
params = manager.GetParameters()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, originalRule, params.ChannelRules[chanID])
|
||||
|
||||
// Set invalid parameters and assert that we fail.
|
||||
expected.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
|
||||
lnwire.NewShortChanIDFromInt(0): NewThresholdRule(1, 2),
|
||||
expected.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{
|
||||
lnwire.NewShortChanIDFromInt(0): {
|
||||
ThresholdRule: NewThresholdRule(1, 2),
|
||||
Type: swap.TypeOut,
|
||||
},
|
||||
}
|
||||
err = manager.SetParameters(context.Background(), expected)
|
||||
require.Equal(t, ErrZeroChannelID, err)
|
||||
|
|
@ -310,7 +333,7 @@ func TestRestrictedSuggestions(t *testing.T) {
|
|||
),
|
||||
}
|
||||
|
||||
chanRules = map[lnwire.ShortChannelID]*ThresholdRule{
|
||||
chanRules = map[lnwire.ShortChannelID]*SwapRule{
|
||||
chanID1: chanRule,
|
||||
chanID2: chanRule,
|
||||
}
|
||||
|
|
@ -321,8 +344,8 @@ func TestRestrictedSuggestions(t *testing.T) {
|
|||
channels []lndclient.ChannelInfo
|
||||
loopOut []*loopdb.LoopOut
|
||||
loopIn []*loopdb.LoopIn
|
||||
chanRules map[lnwire.ShortChannelID]*ThresholdRule
|
||||
peerRules map[route.Vertex]*ThresholdRule
|
||||
chanRules map[lnwire.ShortChannelID]*SwapRule
|
||||
peerRules map[route.Vertex]*SwapRule
|
||||
expected *Suggestions
|
||||
}{
|
||||
{
|
||||
|
|
@ -511,8 +534,11 @@ func TestRestrictedSuggestions(t *testing.T) {
|
|||
Contract: chan1Out,
|
||||
},
|
||||
},
|
||||
peerRules: map[route.Vertex]*ThresholdRule{
|
||||
peer1: NewThresholdRule(0, 50),
|
||||
peerRules: map[route.Vertex]*SwapRule{
|
||||
peer1: {
|
||||
ThresholdRule: NewThresholdRule(0, 50),
|
||||
Type: swap.TypeOut,
|
||||
},
|
||||
},
|
||||
expected: &Suggestions{
|
||||
DisqualifiedChans: noneDisqualified,
|
||||
|
|
@ -629,7 +655,7 @@ func TestSweepFeeLimit(t *testing.T) {
|
|||
ppmToSat(7500, defaultPrepayRoutingFeePPM) +
|
||||
ppmToSat(7500, defaultRoutingFeePPM)
|
||||
|
||||
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
|
||||
params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{
|
||||
chanID1: chanRule,
|
||||
}
|
||||
|
||||
|
|
@ -654,21 +680,21 @@ func TestSuggestSwaps(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
channels []lndclient.ChannelInfo
|
||||
rules map[lnwire.ShortChannelID]*ThresholdRule
|
||||
peerRules map[route.Vertex]*ThresholdRule
|
||||
rules map[lnwire.ShortChannelID]*SwapRule
|
||||
peerRules map[route.Vertex]*SwapRule
|
||||
suggestions *Suggestions
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "no rules",
|
||||
channels: singleChannel,
|
||||
rules: map[lnwire.ShortChannelID]*ThresholdRule{},
|
||||
rules: map[lnwire.ShortChannelID]*SwapRule{},
|
||||
err: ErrNoRules,
|
||||
},
|
||||
{
|
||||
name: "loop out",
|
||||
channels: singleChannel,
|
||||
rules: map[lnwire.ShortChannelID]*ThresholdRule{
|
||||
rules: map[lnwire.ShortChannelID]*SwapRule{
|
||||
chanID1: chanRule,
|
||||
},
|
||||
suggestions: &Suggestions{
|
||||
|
|
@ -682,8 +708,11 @@ func TestSuggestSwaps(t *testing.T) {
|
|||
{
|
||||
name: "no rule for channel",
|
||||
channels: singleChannel,
|
||||
rules: map[lnwire.ShortChannelID]*ThresholdRule{
|
||||
chanID2: NewThresholdRule(10, 10),
|
||||
rules: map[lnwire.ShortChannelID]*SwapRule{
|
||||
chanID2: {
|
||||
ThresholdRule: NewThresholdRule(10, 10),
|
||||
Type: swap.TypeOut,
|
||||
},
|
||||
},
|
||||
suggestions: &Suggestions{
|
||||
DisqualifiedChans: noneDisqualified,
|
||||
|
|
@ -715,9 +744,15 @@ func TestSuggestSwaps(t *testing.T) {
|
|||
RemoteBalance: 3000,
|
||||
},
|
||||
},
|
||||
peerRules: map[route.Vertex]*ThresholdRule{
|
||||
peer1: NewThresholdRule(80, 0),
|
||||
peer2: NewThresholdRule(40, 50),
|
||||
peerRules: map[route.Vertex]*SwapRule{
|
||||
peer1: {
|
||||
ThresholdRule: NewThresholdRule(80, 0),
|
||||
Type: swap.TypeOut,
|
||||
},
|
||||
peer2: {
|
||||
ThresholdRule: NewThresholdRule(40, 50),
|
||||
Type: swap.TypeOut,
|
||||
},
|
||||
},
|
||||
suggestions: &Suggestions{
|
||||
OutSwaps: []loop.OutRequest{
|
||||
|
|
@ -869,7 +904,7 @@ func TestFeeLimits(t *testing.T) {
|
|||
ppmToSat(7500, defaultPrepayRoutingFeePPM) +
|
||||
ppmToSat(7500, defaultRoutingFeePPM)
|
||||
|
||||
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
|
||||
params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{
|
||||
chanID1: chanRule,
|
||||
}
|
||||
|
||||
|
|
@ -1061,7 +1096,7 @@ func TestFeeBudget(t *testing.T) {
|
|||
}
|
||||
|
||||
params := defaultParameters
|
||||
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
|
||||
params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{
|
||||
chanID1: chanRule,
|
||||
chanID2: chanRule,
|
||||
}
|
||||
|
|
@ -1095,12 +1130,13 @@ func TestFeeBudget(t *testing.T) {
|
|||
// that are allowed.
|
||||
func TestInFlightLimit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
maxInFlight int
|
||||
existingSwaps []*loopdb.LoopOut
|
||||
name string
|
||||
maxInFlight int
|
||||
existingSwaps []*loopdb.LoopOut
|
||||
existingInSwaps []*loopdb.LoopIn
|
||||
// peerRules will only be set (instead of test default values)
|
||||
// is it is non-nil.
|
||||
peerRules map[route.Vertex]*ThresholdRule
|
||||
peerRules map[route.Vertex]*SwapRule
|
||||
suggestions *Suggestions
|
||||
}{
|
||||
{
|
||||
|
|
@ -1166,8 +1202,10 @@ func TestInFlightLimit(t *testing.T) {
|
|||
{
|
||||
Contract: autoOutContract,
|
||||
},
|
||||
},
|
||||
existingInSwaps: []*loopdb.LoopIn{
|
||||
{
|
||||
Contract: autoOutContract,
|
||||
Contract: autoInContract,
|
||||
},
|
||||
},
|
||||
suggestions: &Suggestions{
|
||||
|
|
@ -1189,9 +1227,15 @@ func TestInFlightLimit(t *testing.T) {
|
|||
// Create two peer-level rules, both in need of a swap,
|
||||
// but peer 1 needs a larger swap so will be
|
||||
// prioritized.
|
||||
peerRules: map[route.Vertex]*ThresholdRule{
|
||||
peer1: NewThresholdRule(50, 0),
|
||||
peer2: NewThresholdRule(40, 0),
|
||||
peerRules: map[route.Vertex]*SwapRule{
|
||||
peer1: {
|
||||
ThresholdRule: NewThresholdRule(50, 0),
|
||||
Type: swap.TypeOut,
|
||||
},
|
||||
peer2: {
|
||||
ThresholdRule: NewThresholdRule(40, 0),
|
||||
Type: swap.TypeOut,
|
||||
},
|
||||
},
|
||||
suggestions: &Suggestions{
|
||||
OutSwaps: []loop.OutRequest{
|
||||
|
|
@ -1213,6 +1257,9 @@ func TestInFlightLimit(t *testing.T) {
|
|||
cfg.ListLoopOut = func() ([]*loopdb.LoopOut, error) {
|
||||
return testCase.existingSwaps, nil
|
||||
}
|
||||
cfg.ListLoopIn = func() ([]*loopdb.LoopIn, error) {
|
||||
return testCase.existingInSwaps, nil
|
||||
}
|
||||
|
||||
lnd.Channels = []lndclient.ChannelInfo{
|
||||
channel1, channel2,
|
||||
|
|
@ -1224,7 +1271,7 @@ func TestInFlightLimit(t *testing.T) {
|
|||
params.PeerRules = testCase.peerRules
|
||||
} else {
|
||||
params.ChannelRules =
|
||||
map[lnwire.ShortChannelID]*ThresholdRule{
|
||||
map[lnwire.ShortChannelID]*SwapRule{
|
||||
chanID1: chanRule,
|
||||
chanID2: chanRule,
|
||||
}
|
||||
|
|
@ -1364,7 +1411,7 @@ func TestSizeRestrictions(t *testing.T) {
|
|||
|
||||
params := defaultParameters
|
||||
params.ClientRestrictions = testCase.clientRestrictions
|
||||
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
|
||||
params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{
|
||||
chanID1: chanRule,
|
||||
}
|
||||
|
||||
|
|
@ -1522,7 +1569,7 @@ func TestFeePercentage(t *testing.T) {
|
|||
|
||||
params := defaultParameters
|
||||
params.FeeLimit = NewFeePortion(testCase.feePPM)
|
||||
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
|
||||
params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{
|
||||
chanID1: chanRule,
|
||||
}
|
||||
|
||||
|
|
@ -1534,6 +1581,181 @@ func TestFeePercentage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestBudgetWithLoopin tests that our autoloop budget accounts for loop in
|
||||
// swaps that have been automatically dispatched. It tests out swaps that have
|
||||
// already completed and those that are pending, inside and outside of our
|
||||
// budget period to ensure that we account for all relevant swaps.
|
||||
func TestBudgetWithLoopin(t *testing.T) {
|
||||
var (
|
||||
budget btcutil.Amount = 10000
|
||||
|
||||
outsideBudget = testBudgetStart.Add(-5)
|
||||
insideBudget = testBudgetStart.Add(5)
|
||||
|
||||
contractOutsideBudget = &loopdb.LoopInContract{
|
||||
SwapContract: loopdb.SwapContract{
|
||||
InitiationTime: outsideBudget,
|
||||
MaxSwapFee: budget,
|
||||
},
|
||||
Label: labels.AutoloopLabel(swap.TypeIn),
|
||||
}
|
||||
|
||||
// Set our spend equal to our budget so we don't need to
|
||||
// calculate exact costs.
|
||||
eventOutsideBudget = &loopdb.LoopEvent{
|
||||
SwapStateData: loopdb.SwapStateData{
|
||||
Cost: loopdb.SwapCost{
|
||||
Server: budget,
|
||||
},
|
||||
State: loopdb.StateSuccess,
|
||||
},
|
||||
Time: outsideBudget,
|
||||
}
|
||||
|
||||
successWithinBudget = &loopdb.LoopEvent{
|
||||
SwapStateData: loopdb.SwapStateData{
|
||||
Cost: loopdb.SwapCost{
|
||||
Server: budget,
|
||||
},
|
||||
State: loopdb.StateSuccess,
|
||||
},
|
||||
Time: insideBudget,
|
||||
}
|
||||
|
||||
okQuote = &loop.LoopOutQuote{
|
||||
SwapFee: 15,
|
||||
PrepayAmount: 30,
|
||||
MinerFee: 1,
|
||||
}
|
||||
|
||||
rec = loop.OutRequest{
|
||||
Amount: 7500,
|
||||
OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()},
|
||||
MaxMinerFee: scaleMinerFee(okQuote.MinerFee),
|
||||
MaxSwapFee: okQuote.SwapFee,
|
||||
MaxPrepayAmount: okQuote.PrepayAmount,
|
||||
SweepConfTarget: defaultConfTarget,
|
||||
Initiator: autoloopSwapInitiator,
|
||||
}
|
||||
|
||||
testPPM uint64 = 100000
|
||||
)
|
||||
|
||||
rec.MaxPrepayRoutingFee, rec.MaxSwapRoutingFee = testPPMFees(
|
||||
testPPM, okQuote, 7500,
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
// loopIns is the set of loop in swaps that the client has
|
||||
// performed.
|
||||
loopIns []*loopdb.LoopIn
|
||||
|
||||
// suggestions is the set of swaps that we expect to be
|
||||
// suggested given our current traffic.
|
||||
suggestions *Suggestions
|
||||
}{
|
||||
{
|
||||
name: "completed swap outside of budget",
|
||||
loopIns: []*loopdb.LoopIn{
|
||||
{
|
||||
Loop: loopdb.Loop{
|
||||
Events: []*loopdb.LoopEvent{
|
||||
eventOutsideBudget,
|
||||
},
|
||||
},
|
||||
Contract: contractOutsideBudget,
|
||||
},
|
||||
},
|
||||
suggestions: &Suggestions{
|
||||
OutSwaps: []loop.OutRequest{
|
||||
rec,
|
||||
},
|
||||
DisqualifiedChans: noneDisqualified,
|
||||
DisqualifiedPeers: noPeersDisqualified,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "completed within budget",
|
||||
loopIns: []*loopdb.LoopIn{
|
||||
{
|
||||
Loop: loopdb.Loop{
|
||||
Events: []*loopdb.LoopEvent{
|
||||
successWithinBudget,
|
||||
},
|
||||
},
|
||||
Contract: contractOutsideBudget,
|
||||
},
|
||||
},
|
||||
suggestions: &Suggestions{
|
||||
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
|
||||
chanID1: ReasonBudgetElapsed,
|
||||
},
|
||||
DisqualifiedPeers: noPeersDisqualified,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pending created before budget",
|
||||
loopIns: []*loopdb.LoopIn{
|
||||
{
|
||||
Contract: contractOutsideBudget,
|
||||
},
|
||||
},
|
||||
suggestions: &Suggestions{
|
||||
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
|
||||
chanID1: ReasonBudgetElapsed,
|
||||
},
|
||||
DisqualifiedPeers: noPeersDisqualified,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range tests {
|
||||
testCase := testCase
|
||||
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
cfg, lnd := newTestConfig()
|
||||
|
||||
// Set our channel and rules so that we will need to
|
||||
// swap 7500 sats and our fee limit is 10% of that
|
||||
// amount (750 sats).
|
||||
lnd.Channels = []lndclient.ChannelInfo{
|
||||
channel1,
|
||||
}
|
||||
|
||||
cfg.ListLoopIn = func() ([]*loopdb.LoopIn, error) {
|
||||
return testCase.loopIns, nil
|
||||
}
|
||||
|
||||
cfg.LoopOutQuote = func(_ context.Context,
|
||||
_ *loop.LoopOutQuoteRequest) (*loop.LoopOutQuote,
|
||||
error) {
|
||||
|
||||
return okQuote, nil
|
||||
}
|
||||
|
||||
params := defaultParameters
|
||||
params.AutoFeeBudget = budget
|
||||
params.AutoFeeStartDate = testBudgetStart
|
||||
|
||||
params.FeeLimit = NewFeePortion(testPPM)
|
||||
params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{
|
||||
chanID1: chanRule,
|
||||
}
|
||||
|
||||
// Allow more than one in flight swap, to ensure that
|
||||
// we restrict based on budget, not in-flight.
|
||||
params.MaxAutoInFlight = 2
|
||||
|
||||
testSuggestSwaps(
|
||||
t, newSuggestSwapsSetup(cfg, lnd, params),
|
||||
testCase.suggestions, nil,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// testSuggestSwapsSetup contains the elements that are used to create a
|
||||
// suggest swaps test.
|
||||
type testSuggestSwapsSetup struct {
|
||||
|
|
@ -1572,7 +1794,7 @@ func testSuggestSwaps(t *testing.T, setup *testSuggestSwapsSetup,
|
|||
}
|
||||
|
||||
params := defaultParameters
|
||||
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
|
||||
params.ChannelRules = map[lnwire.ShortChannelID]*SwapRule{
|
||||
chanID1: chanRule,
|
||||
chanID2: chanRule,
|
||||
}
|
||||
|
|
@ -1595,3 +1817,184 @@ func testSuggestSwaps(t *testing.T, setup *testSuggestSwapsSetup,
|
|||
require.Equal(t, expectedErr, err)
|
||||
require.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
// TestCurrentTraffic tests recording of our current set of ongoing swaps.
|
||||
func TestCurrentTraffic(t *testing.T) {
|
||||
var (
|
||||
backoff = time.Hour * 5
|
||||
withinBackoff = testTime.Add(time.Hour * -1)
|
||||
outsideBackoff = testTime.Add(backoff * -2)
|
||||
|
||||
success = []*loopdb.LoopEvent{
|
||||
{
|
||||
SwapStateData: loopdb.SwapStateData{
|
||||
State: loopdb.StateSuccess,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
failedInBackoff = []*loopdb.LoopEvent{
|
||||
{
|
||||
SwapStateData: loopdb.SwapStateData{
|
||||
State: loopdb.StateFailOffchainPayments,
|
||||
},
|
||||
Time: withinBackoff,
|
||||
},
|
||||
}
|
||||
|
||||
failedOutsideBackoff = []*loopdb.LoopEvent{
|
||||
{
|
||||
SwapStateData: loopdb.SwapStateData{
|
||||
State: loopdb.StateFailOffchainPayments,
|
||||
},
|
||||
Time: outsideBackoff,
|
||||
},
|
||||
}
|
||||
|
||||
failedTimeoutInBackoff = []*loopdb.LoopEvent{
|
||||
{
|
||||
SwapStateData: loopdb.SwapStateData{
|
||||
State: loopdb.StateFailTimeout,
|
||||
},
|
||||
Time: withinBackoff,
|
||||
},
|
||||
}
|
||||
|
||||
failedTimeoutOutsideBackoff = []*loopdb.LoopEvent{
|
||||
{
|
||||
SwapStateData: loopdb.SwapStateData{
|
||||
State: loopdb.StateFailTimeout,
|
||||
},
|
||||
Time: outsideBackoff,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
loopOut []*loopdb.LoopOut
|
||||
loopIn []*loopdb.LoopIn
|
||||
expected *swapTraffic
|
||||
}{
|
||||
{
|
||||
name: "completed swaps ignored",
|
||||
loopOut: []*loopdb.LoopOut{
|
||||
{
|
||||
Loop: loopdb.Loop{
|
||||
Events: success,
|
||||
},
|
||||
Contract: &loopdb.LoopOutContract{},
|
||||
},
|
||||
},
|
||||
loopIn: []*loopdb.LoopIn{
|
||||
{
|
||||
Loop: loopdb.Loop{
|
||||
Events: success,
|
||||
},
|
||||
Contract: &loopdb.LoopInContract{},
|
||||
},
|
||||
},
|
||||
expected: newSwapTraffic(),
|
||||
},
|
||||
{
|
||||
// No events indicates that the swap is still pending.
|
||||
name: "pending swaps included",
|
||||
loopOut: []*loopdb.LoopOut{
|
||||
{
|
||||
Contract: &loopdb.LoopOutContract{
|
||||
OutgoingChanSet: []uint64{
|
||||
chanID1.ToUint64(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
loopIn: []*loopdb.LoopIn{
|
||||
{
|
||||
Contract: &loopdb.LoopInContract{
|
||||
LastHop: &peer2,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &swapTraffic{
|
||||
ongoingLoopOut: map[lnwire.ShortChannelID]bool{
|
||||
chanID1: true,
|
||||
},
|
||||
ongoingLoopIn: map[route.Vertex]bool{
|
||||
peer2: true,
|
||||
},
|
||||
// Make empty maps so that we can assert equal.
|
||||
failedLoopOut: make(
|
||||
map[lnwire.ShortChannelID]time.Time,
|
||||
),
|
||||
failedLoopIn: make(map[route.Vertex]time.Time),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "failure backoff included",
|
||||
loopOut: []*loopdb.LoopOut{
|
||||
{
|
||||
Contract: &loopdb.LoopOutContract{
|
||||
OutgoingChanSet: []uint64{
|
||||
chanID1.ToUint64(),
|
||||
},
|
||||
},
|
||||
Loop: loopdb.Loop{
|
||||
Events: failedInBackoff,
|
||||
},
|
||||
},
|
||||
{
|
||||
Contract: &loopdb.LoopOutContract{
|
||||
OutgoingChanSet: []uint64{
|
||||
chanID2.ToUint64(),
|
||||
},
|
||||
},
|
||||
Loop: loopdb.Loop{
|
||||
Events: failedOutsideBackoff,
|
||||
},
|
||||
},
|
||||
},
|
||||
loopIn: []*loopdb.LoopIn{
|
||||
{
|
||||
Contract: &loopdb.LoopInContract{
|
||||
LastHop: &peer1,
|
||||
},
|
||||
Loop: loopdb.Loop{
|
||||
Events: failedTimeoutInBackoff,
|
||||
},
|
||||
},
|
||||
{
|
||||
Contract: &loopdb.LoopInContract{
|
||||
LastHop: &peer2,
|
||||
},
|
||||
Loop: loopdb.Loop{
|
||||
Events: failedTimeoutOutsideBackoff,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &swapTraffic{
|
||||
ongoingLoopOut: make(
|
||||
map[lnwire.ShortChannelID]bool,
|
||||
),
|
||||
ongoingLoopIn: make(map[route.Vertex]bool),
|
||||
failedLoopOut: map[lnwire.ShortChannelID]time.Time{
|
||||
chanID1: withinBackoff,
|
||||
},
|
||||
failedLoopIn: map[route.Vertex]time.Time{
|
||||
peer1: withinBackoff,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range tests {
|
||||
cfg, _ := newTestConfig()
|
||||
m := NewManager(cfg)
|
||||
|
||||
params := m.GetParameters()
|
||||
params.FailureBackOff = backoff
|
||||
require.NoError(t, m.SetParameters(context.Background(), params))
|
||||
|
||||
actual := m.currentSwapTraffic(testCase.loopOut, testCase.loopIn)
|
||||
require.Equal(t, testCase.expected, actual)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
82
liquidity/loopin.go
Normal file
82
liquidity/loopin.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package liquidity
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightninglabs/loop"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
)
|
||||
|
||||
// Compile time assertion that loop in suggestions satisfy our interface.
|
||||
var _ swapSuggestion = (*loopInSwapSuggestion)(nil)
|
||||
|
||||
type loopInSwapSuggestion struct {
|
||||
loop.LoopInRequest
|
||||
}
|
||||
|
||||
// amount returns the amount of the swap suggestion.
|
||||
func (l *loopInSwapSuggestion) amount() btcutil.Amount {
|
||||
return l.Amount
|
||||
}
|
||||
|
||||
// fees returns the highest fees that we could pay for the swap suggestion.
|
||||
func (l *loopInSwapSuggestion) fees() btcutil.Amount {
|
||||
return worstCaseInFees(
|
||||
l.MaxMinerFee, l.MaxSwapFee, defaultLoopInSweepFee,
|
||||
)
|
||||
}
|
||||
|
||||
// channels returns no channels for loop in swap suggestions because we do not
|
||||
// restrict loop in swaps by channel id.
|
||||
func (l *loopInSwapSuggestion) channels() []lnwire.ShortChannelID {
|
||||
return nil
|
||||
}
|
||||
|
||||
// peers returns the peer that a loop in swap is restricted to, if it is set.
|
||||
func (l *loopInSwapSuggestion) peers(_ map[uint64]route.Vertex) []route.Vertex {
|
||||
if l.LastHop == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []route.Vertex{
|
||||
*l.LastHop,
|
||||
}
|
||||
}
|
||||
|
||||
// worstCaseInFees returns the largest possible fees for a loop in swap.
|
||||
func worstCaseInFees(maxMinerFee, swapFee btcutil.Amount,
|
||||
sweepEst chainfee.SatPerKWeight) btcutil.Amount {
|
||||
|
||||
failureFee := maxMinerFee + loopInSweepFee(sweepEst)
|
||||
successFee := maxMinerFee + swapFee
|
||||
|
||||
if failureFee > successFee {
|
||||
return failureFee
|
||||
}
|
||||
|
||||
return successFee
|
||||
}
|
||||
|
||||
// loopInSweepFee provides an estimated fee for our sweep transaction, based
|
||||
// on the fee rate provided. We can calculate our fees for htlcv2 and p2wkh
|
||||
// timeout addresses because automated loop ins will be handled entirely by the
|
||||
// client, so we know what types will be used.
|
||||
func loopInSweepFee(fee chainfee.SatPerKWeight) btcutil.Amount {
|
||||
var estimator input.TxWeightEstimator
|
||||
|
||||
// We sweep loop in swaps to wpkh addresses provided by lnd.
|
||||
estimator.AddP2WKHOutput()
|
||||
|
||||
// Create a htlcv2, which is what all autoloops will use, so that we
|
||||
// can get our maximum timeout witness size.
|
||||
htlc := swap.HtlcScriptV2{}
|
||||
maxSize := htlc.MaxTimeoutWitnessSize()
|
||||
|
||||
estimator.AddWitnessInput(maxSize)
|
||||
weight := int64(estimator.Weight())
|
||||
|
||||
return fee.FeeForWeight(weight)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -19,6 +20,12 @@ var (
|
|||
"percentages must be < 100")
|
||||
)
|
||||
|
||||
// SwapRule is a liquidity rule with a specific swap type.
|
||||
type SwapRule struct {
|
||||
*ThresholdRule
|
||||
swap.Type
|
||||
}
|
||||
|
||||
// ThresholdRule is a liquidity rule that implements minimum incoming and
|
||||
// outgoing liquidity threshold.
|
||||
type ThresholdRule struct {
|
||||
|
|
@ -65,73 +72,99 @@ func (r *ThresholdRule) validate() error {
|
|||
// swapAmount suggests a swap based on the liquidity thresholds configured,
|
||||
// returning zero if no swap is recommended.
|
||||
func (r *ThresholdRule) swapAmount(channel *balances,
|
||||
outRestrictions *Restrictions) btcutil.Amount {
|
||||
restrictions *Restrictions) btcutil.Amount {
|
||||
|
||||
var (
|
||||
// For loop out swaps, we want to adjust our incoming liquidity
|
||||
// so the channel's incoming balance is our target.
|
||||
targetBalance = channel.incoming
|
||||
|
||||
// For loop out swaps, we target a minimum amount of incoming
|
||||
// liquidity, so the minimum incoming threshold is our target
|
||||
// percentage.
|
||||
targetPercentage = uint64(r.MinimumIncoming)
|
||||
|
||||
// For loop out swaps, we may want to preserve some of our
|
||||
// outgoing balance, so the channel's outgoing balance is our
|
||||
// reserve.
|
||||
reserveBalance = channel.outgoing
|
||||
|
||||
// For loop out swaps, we may want to preserve some percentage
|
||||
// of our outgoing balance, so the minimum outgoing threshold
|
||||
// is our reserve percentage.
|
||||
reservePercentage = uint64(r.MinimumOutgoing)
|
||||
)
|
||||
|
||||
// Examine our total balance and required ratios to decide whether we
|
||||
// need to swap.
|
||||
amount := loopOutSwapAmount(
|
||||
channel, r.MinimumIncoming, r.MinimumOutgoing,
|
||||
amount := calculateSwapAmount(
|
||||
targetBalance, reserveBalance, channel.capacity,
|
||||
targetPercentage, reservePercentage,
|
||||
)
|
||||
|
||||
// Limit our swap amount by the minimum/maximum thresholds set.
|
||||
switch {
|
||||
case amount < outRestrictions.Minimum:
|
||||
case amount < restrictions.Minimum:
|
||||
return 0
|
||||
|
||||
case amount > outRestrictions.Maximum:
|
||||
return outRestrictions.Maximum
|
||||
case amount > restrictions.Maximum:
|
||||
return restrictions.Maximum
|
||||
|
||||
default:
|
||||
return amount
|
||||
}
|
||||
}
|
||||
|
||||
// loopOutSwapAmount determines whether we can perform a loop out swap, and
|
||||
// returns the amount we need to swap to reach the desired liquidity balance
|
||||
// specified by the incoming and outgoing thresholds.
|
||||
func loopOutSwapAmount(balances *balances, incomingThresholdPercent,
|
||||
outgoingThresholdPercent int) btcutil.Amount {
|
||||
// calculateSwapAmount calculates amount for a swap based on thresholds.
|
||||
// This function can be used for loop out or loop in, but the concept is the
|
||||
// same - we want liquidity in one (target) direction, while preserving some
|
||||
// minimum in the other (reserve) direction.
|
||||
// * target: this is the side of the channel(s) where we want to acquire some
|
||||
// liquidity. We aim for this liquidity to reach the threshold amount set.
|
||||
// * reserve: this is the side of the channel(s) that we will move liquidity
|
||||
// away from. This may not drop below a certain reserve threshold.
|
||||
func calculateSwapAmount(targetAmount, reserveAmount,
|
||||
capacity btcutil.Amount, targetThresholdPercentage,
|
||||
reserveThresholdPercentage uint64) btcutil.Amount {
|
||||
|
||||
minimumIncoming := btcutil.Amount(uint64(
|
||||
balances.capacity) *
|
||||
uint64(incomingThresholdPercent) / 100,
|
||||
targetGoal := btcutil.Amount(
|
||||
uint64(capacity) * targetThresholdPercentage / 100,
|
||||
)
|
||||
|
||||
minimumOutgoing := btcutil.Amount(
|
||||
uint64(balances.capacity) *
|
||||
uint64(outgoingThresholdPercent) / 100,
|
||||
reserveMinimum := btcutil.Amount(
|
||||
uint64(capacity) * reserveThresholdPercentage / 100,
|
||||
)
|
||||
|
||||
switch {
|
||||
// If we have sufficient incoming capacity, we do not need to loop out.
|
||||
case balances.incoming >= minimumIncoming:
|
||||
// If we have sufficient target capacity, we do not need to swap.
|
||||
case targetAmount >= targetGoal:
|
||||
return 0
|
||||
|
||||
// If we are already below the threshold set for outgoing capacity, we
|
||||
// If we are already below the threshold set for reserve capacity, we
|
||||
// cannot take any further action.
|
||||
case balances.outgoing <= minimumOutgoing:
|
||||
case reserveAmount <= reserveMinimum:
|
||||
return 0
|
||||
|
||||
}
|
||||
|
||||
// Express our minimum outgoing amount as a maximum incoming amount.
|
||||
// Express our minimum reserve amount as a maximum target amount.
|
||||
// We will use this value to limit the amount that we swap, so that we
|
||||
// do not dip below our outgoing threshold.
|
||||
maximumIncoming := balances.capacity - minimumOutgoing
|
||||
// do not dip below our reserve threshold.
|
||||
maximumTarget := capacity - reserveMinimum
|
||||
|
||||
// Calculate the midpoint between our minimum and maximum incoming
|
||||
// values. We will aim to swap this amount so that we do not tip our
|
||||
// outgoing balance beneath the desired level.
|
||||
midpoint := (minimumIncoming + maximumIncoming) / 2
|
||||
// Calculate the midpoint between our minimum and maximum target values.
|
||||
// We will aim to swap this amount so that we do not tip our reserve
|
||||
// balance beneath the desired level.
|
||||
midpoint := (targetGoal + maximumTarget) / 2
|
||||
|
||||
// Calculate the amount of incoming balance we need to shift to reach
|
||||
// Calculate the amount of target balance we need to shift to reach
|
||||
// this desired midpoint.
|
||||
required := midpoint - balances.incoming
|
||||
required := midpoint - targetAmount
|
||||
|
||||
// Since we can have pending htlcs on our channel, we check the amount
|
||||
// of outbound capacity that we can shift before we fall below our
|
||||
// of reserve capacity that we can shift before we fall below our
|
||||
// threshold.
|
||||
available := balances.outgoing - minimumOutgoing
|
||||
available := reserveAmount - reserveMinimum
|
||||
|
||||
// If we do not have enough balance available to reach our midpoint, we
|
||||
// take no action. This is the case when we have a large portion of
|
||||
|
|
|
|||
|
|
@ -93,18 +93,18 @@ func TestValidateThreshold(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestLoopOutAmount tests assessing of a set of balances to determine whether
|
||||
// we should perform a loop out.
|
||||
func TestLoopOutAmount(t *testing.T) {
|
||||
// TestCalculateAmount tests calculation of the amount we recommend for a given
|
||||
// set of balances and threshold rule.
|
||||
func TestCalculateAmount(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
minIncoming int
|
||||
minOutgoing int
|
||||
minIncoming uint64
|
||||
minOutgoing uint64
|
||||
balances *balances
|
||||
amt btcutil.Amount
|
||||
}{
|
||||
{
|
||||
name: "insufficient surplus",
|
||||
name: "insufficient outgoing",
|
||||
balances: &balances{
|
||||
capacity: 100,
|
||||
incoming: 20,
|
||||
|
|
@ -166,8 +166,9 @@ func TestLoopOutAmount(t *testing.T) {
|
|||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
amt := loopOutSwapAmount(
|
||||
test.balances, test.minIncoming,
|
||||
amt := calculateSwapAmount(
|
||||
test.balances.incoming, test.balances.outgoing,
|
||||
test.balances.capacity, test.minIncoming,
|
||||
test.minOutgoing,
|
||||
)
|
||||
require.Equal(t, test.amt, amt)
|
||||
|
|
|
|||
|
|
@ -750,15 +750,22 @@ func (s *swapClientServer) GetLiquidityParams(_ context.Context,
|
|||
}
|
||||
|
||||
func newRPCRule(channelID uint64, peer []byte,
|
||||
rule *liquidity.ThresholdRule) *looprpc.LiquidityRule {
|
||||
rule *liquidity.SwapRule) *looprpc.LiquidityRule {
|
||||
|
||||
return &looprpc.LiquidityRule{
|
||||
rpcRule := &looprpc.LiquidityRule{
|
||||
ChannelId: channelID,
|
||||
Pubkey: peer,
|
||||
Type: looprpc.LiquidityRuleType_THRESHOLD,
|
||||
IncomingThreshold: uint32(rule.MinimumIncoming),
|
||||
OutgoingThreshold: uint32(rule.MinimumOutgoing),
|
||||
SwapType: looprpc.SwapType_LOOP_OUT,
|
||||
}
|
||||
|
||||
if rule.Type == swap.TypeIn {
|
||||
rpcRule.SwapType = looprpc.SwapType_LOOP_IN
|
||||
}
|
||||
|
||||
return rpcRule
|
||||
}
|
||||
|
||||
// SetLiquidityParams attempts to set our current liquidity manager's
|
||||
|
|
@ -781,10 +788,10 @@ func (s *swapClientServer) SetLiquidityParams(ctx context.Context,
|
|||
AutoFeeBudget: btcutil.Amount(in.Parameters.AutoloopBudgetSat),
|
||||
MaxAutoInFlight: int(in.Parameters.AutoMaxInFlight),
|
||||
ChannelRules: make(
|
||||
map[lnwire.ShortChannelID]*liquidity.ThresholdRule,
|
||||
map[lnwire.ShortChannelID]*liquidity.SwapRule,
|
||||
),
|
||||
PeerRules: make(
|
||||
map[route.Vertex]*liquidity.ThresholdRule,
|
||||
map[route.Vertex]*liquidity.SwapRule,
|
||||
),
|
||||
ClientRestrictions: liquidity.Restrictions{
|
||||
Minimum: btcutil.Amount(in.Parameters.MinSwapAmount),
|
||||
|
|
@ -890,16 +897,24 @@ func rpcToFee(req *looprpc.LiquidityParameters) (liquidity.FeeLimit,
|
|||
}
|
||||
|
||||
// rpcToRule switches on rpc rule type to convert to our rule interface.
|
||||
func rpcToRule(rule *looprpc.LiquidityRule) (*liquidity.ThresholdRule, error) {
|
||||
func rpcToRule(rule *looprpc.LiquidityRule) (*liquidity.SwapRule, error) {
|
||||
swapType := swap.TypeOut
|
||||
if rule.SwapType == looprpc.SwapType_LOOP_IN {
|
||||
swapType = swap.TypeIn
|
||||
}
|
||||
|
||||
switch rule.Type {
|
||||
case looprpc.LiquidityRuleType_UNKNOWN:
|
||||
return nil, fmt.Errorf("rule type field must be set")
|
||||
|
||||
case looprpc.LiquidityRuleType_THRESHOLD:
|
||||
return liquidity.NewThresholdRule(
|
||||
int(rule.IncomingThreshold),
|
||||
int(rule.OutgoingThreshold),
|
||||
), nil
|
||||
return &liquidity.SwapRule{
|
||||
ThresholdRule: liquidity.NewThresholdRule(
|
||||
int(rule.IncomingThreshold),
|
||||
int(rule.OutgoingThreshold),
|
||||
),
|
||||
Type: swapType,
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown rule: %T", rule)
|
||||
|
|
|
|||
|
|
@ -2299,6 +2299,8 @@ type LiquidityRule struct {
|
|||
//The short channel ID of the channel that this rule should be applied to.
|
||||
//This field may not be set when the pubkey field is set.
|
||||
ChannelId uint64 `protobuf:"varint,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"`
|
||||
// The type of swap that will be dispatched for this rule.
|
||||
SwapType SwapType `protobuf:"varint,6,opt,name=swap_type,json=swapType,proto3,enum=looprpc.SwapType" json:"swap_type,omitempty"`
|
||||
//
|
||||
//The public key of the peer that this rule should be applied to. This field
|
||||
//may not be set when the channel id field is set.
|
||||
|
|
@ -2358,6 +2360,13 @@ func (x *LiquidityRule) GetChannelId() uint64 {
|
|||
return 0
|
||||
}
|
||||
|
||||
func (x *LiquidityRule) GetSwapType() SwapType {
|
||||
if x != nil {
|
||||
return x.SwapType
|
||||
}
|
||||
return SwapType_LOOP_OUT
|
||||
}
|
||||
|
||||
func (x *LiquidityRule) GetPubkey() []byte {
|
||||
if x != nil {
|
||||
return x.Pubkey
|
||||
|
|
@ -2905,10 +2914,13 @@ var file_client_proto_rawDesc = []byte{
|
|||
0x6e, 0x53, 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6d,
|
||||
0x61, 0x78, 0x5f, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0f,
|
||||
0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x6d, 0x61, 0x78, 0x53, 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f,
|
||||
0x75, 0x6e, 0x74, 0x22, 0xd4, 0x01, 0x0a, 0x0d, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74,
|
||||
0x75, 0x6e, 0x74, 0x22, 0x84, 0x02, 0x0a, 0x0d, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74,
|
||||
0x79, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c,
|
||||
0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e,
|
||||
0x65, 0x6c, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x05,
|
||||
0x65, 0x6c, 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x74, 0x79, 0x70,
|
||||
0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70,
|
||||
0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, 0x65, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70,
|
||||
0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x05,
|
||||
0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x04,
|
||||
0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x6c, 0x6f, 0x6f,
|
||||
0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x52, 0x75,
|
||||
|
|
@ -3122,44 +3134,45 @@ var file_client_proto_depIdxs = []int32{
|
|||
32, // 5: looprpc.ProbeRequest.route_hints:type_name -> looprpc.RouteHint
|
||||
23, // 6: looprpc.TokensResponse.tokens:type_name -> looprpc.LsatToken
|
||||
26, // 7: looprpc.LiquidityParameters.rules:type_name -> looprpc.LiquidityRule
|
||||
3, // 8: looprpc.LiquidityRule.type:type_name -> looprpc.LiquidityRuleType
|
||||
25, // 9: looprpc.SetLiquidityParamsRequest.parameters:type_name -> looprpc.LiquidityParameters
|
||||
4, // 10: looprpc.Disqualified.reason:type_name -> looprpc.AutoReason
|
||||
5, // 11: looprpc.SuggestSwapsResponse.loop_out:type_name -> looprpc.LoopOutRequest
|
||||
30, // 12: looprpc.SuggestSwapsResponse.disqualified:type_name -> looprpc.Disqualified
|
||||
5, // 13: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest
|
||||
6, // 14: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest
|
||||
8, // 15: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest
|
||||
10, // 16: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest
|
||||
12, // 17: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest
|
||||
13, // 18: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest
|
||||
16, // 19: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest
|
||||
13, // 20: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest
|
||||
16, // 21: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest
|
||||
19, // 22: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest
|
||||
21, // 23: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest
|
||||
24, // 24: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest
|
||||
27, // 25: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest
|
||||
29, // 26: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest
|
||||
7, // 27: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse
|
||||
7, // 28: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse
|
||||
9, // 29: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus
|
||||
11, // 30: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse
|
||||
9, // 31: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus
|
||||
15, // 32: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse
|
||||
18, // 33: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse
|
||||
14, // 34: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse
|
||||
17, // 35: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse
|
||||
20, // 36: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse
|
||||
22, // 37: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse
|
||||
25, // 38: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters
|
||||
28, // 39: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse
|
||||
31, // 40: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse
|
||||
27, // [27:41] is the sub-list for method output_type
|
||||
13, // [13:27] is the sub-list for method input_type
|
||||
13, // [13:13] is the sub-list for extension type_name
|
||||
13, // [13:13] is the sub-list for extension extendee
|
||||
0, // [0:13] is the sub-list for field type_name
|
||||
0, // 8: looprpc.LiquidityRule.swap_type:type_name -> looprpc.SwapType
|
||||
3, // 9: looprpc.LiquidityRule.type:type_name -> looprpc.LiquidityRuleType
|
||||
25, // 10: looprpc.SetLiquidityParamsRequest.parameters:type_name -> looprpc.LiquidityParameters
|
||||
4, // 11: looprpc.Disqualified.reason:type_name -> looprpc.AutoReason
|
||||
5, // 12: looprpc.SuggestSwapsResponse.loop_out:type_name -> looprpc.LoopOutRequest
|
||||
30, // 13: looprpc.SuggestSwapsResponse.disqualified:type_name -> looprpc.Disqualified
|
||||
5, // 14: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest
|
||||
6, // 15: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest
|
||||
8, // 16: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest
|
||||
10, // 17: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest
|
||||
12, // 18: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest
|
||||
13, // 19: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest
|
||||
16, // 20: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest
|
||||
13, // 21: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest
|
||||
16, // 22: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest
|
||||
19, // 23: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest
|
||||
21, // 24: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest
|
||||
24, // 25: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest
|
||||
27, // 26: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest
|
||||
29, // 27: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest
|
||||
7, // 28: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse
|
||||
7, // 29: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse
|
||||
9, // 30: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus
|
||||
11, // 31: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse
|
||||
9, // 32: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus
|
||||
15, // 33: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse
|
||||
18, // 34: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse
|
||||
14, // 35: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse
|
||||
17, // 36: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse
|
||||
20, // 37: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse
|
||||
22, // 38: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse
|
||||
25, // 39: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters
|
||||
28, // 40: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse
|
||||
31, // 41: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse
|
||||
28, // [28:42] is the sub-list for method output_type
|
||||
14, // [14:28] is the sub-list for method input_type
|
||||
14, // [14:14] is the sub-list for extension type_name
|
||||
14, // [14:14] is the sub-list for extension extendee
|
||||
0, // [0:14] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_client_proto_init() }
|
||||
|
|
|
|||
|
|
@ -843,6 +843,9 @@ message LiquidityRule {
|
|||
*/
|
||||
uint64 channel_id = 1;
|
||||
|
||||
// The type of swap that will be dispatched for this rule.
|
||||
SwapType swap_type = 6;
|
||||
|
||||
/*
|
||||
The public key of the peer that this rule should be applied to. This field
|
||||
may not be set when the channel id field is set.
|
||||
|
|
|
|||
|
|
@ -672,6 +672,10 @@
|
|||
"format": "uint64",
|
||||
"description": "The short channel ID of the channel that this rule should be applied to.\nThis field may not be set when the pubkey field is set."
|
||||
},
|
||||
"swap_type": {
|
||||
"$ref": "#/definitions/looprpcSwapType",
|
||||
"description": "The type of swap that will be dispatched for this rule."
|
||||
},
|
||||
"pubkey": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue