Merge pull request #433 from carlaKC/419-swaptype

liquidity: add swap type and loop in fee logic
pull/434/head
Carla Kirk-Cohen 2 years ago committed by GitHub
commit 2e6adeaa3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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,10 +1569,185 @@ 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,
}
testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.suggestions, nil,
)
})
}
}
// 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,
@ -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)
}
}

@ -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 {
minimumIncoming := btcutil.Amount(uint64(
balances.capacity) *
uint64(incomingThresholdPercent) / 100,
// 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 {
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…
Cancel
Save