Merge pull request #295 from carlaKC/205-autoout

liquidity: add budget-limited autoloop
pull/305/head
Carla Kirk-Cohen 4 years ago committed by GitHub
commit afc41adab0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -224,6 +224,30 @@ var setParamsCommand = cli.Command{
"previously had a failed swap will be " + "previously had a failed swap will be " +
"included in suggestions.", "included in suggestions.",
}, },
cli.BoolFlag{
Name: "autoout",
Usage: "set to true to enable automated dispatch " +
"of loop out swaps, limited to the budget " +
"set by autobudget",
},
cli.Uint64Flag{
Name: "autobudget",
Usage: "the maximum amount of fees in satoshis that " +
"automatically dispatched loop out swaps may " +
"spend",
},
cli.Uint64Flag{
Name: "budgetstart",
Usage: "the start time for the automated loop " +
"out budget, expressed as a unix timestamp " +
"in seconds",
},
cli.Uint64Flag{
Name: "autoinflight",
Usage: "the maximum number of automatically " +
"dispatched swaps that we allow to be in " +
"flight",
},
}, },
Action: setParams, Action: setParams,
} }
@ -304,6 +328,26 @@ func setParams(ctx *cli.Context) error {
flagSet = true flagSet = true
} }
if ctx.IsSet("autoout") {
params.AutoLoopOut = ctx.Bool("autoout")
flagSet = true
}
if ctx.IsSet("autobudget") {
params.AutoOutBudgetSat = ctx.Uint64("autobudget")
flagSet = true
}
if ctx.IsSet("budgetstart") {
params.AutoOutBudgetStartSec = ctx.Uint64("budgetstart")
flagSet = true
}
if ctx.IsSet("autoinflight") {
params.AutoMaxInFlight = ctx.Uint64("autoinflight")
flagSet = true
}
if !flagSet { if !flagSet {
return fmt.Errorf("at least one flag required to set params") return fmt.Errorf("at least one flag required to set params")
} }

@ -16,6 +16,7 @@ require (
github.com/lightningnetwork/lnd/cert v1.0.3 github.com/lightningnetwork/lnd/cert v1.0.3
github.com/lightningnetwork/lnd/clock v1.0.1 github.com/lightningnetwork/lnd/clock v1.0.1
github.com/lightningnetwork/lnd/queue v1.0.4 github.com/lightningnetwork/lnd/queue v1.0.4
github.com/lightningnetwork/lnd/ticker v1.0.0
github.com/stretchr/testify v1.5.1 github.com/stretchr/testify v1.5.1
github.com/urfave/cli v1.20.0 github.com/urfave/cli v1.20.0
golang.org/x/net v0.0.0-20191002035440-2ec189313ef0 golang.org/x/net v0.0.0-20191002035440-2ec189313ef0

@ -1,10 +1,14 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e h1:F2x1bq7RaNCIuqYpswggh1+c1JmwdnkHNC9wy1KDip0=
git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo= git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e h1:n+DcnTNkQnHlwpsrHoQtkrJIO7CBx029fw6oR4vIob4=
github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ= github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ=
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82 h1:MG93+PZYs9PyEsj/n5/haQu2gK0h4tUtSy9ejtMwWa0=
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:GbuBk21JqF+driLX3XtJYNZjGa45YDoa9IqCTzNSfEc= github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:GbuBk21JqF+driLX3XtJYNZjGa45YDoa9IqCTzNSfEc=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2 h1:2be4ykKKov3M1yISM2E8gnGXZ/N2SsPawfnGiXxaYEU=
github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
@ -131,7 +135,9 @@ github.com/grpc-ecosystem/grpc-gateway v1.14.3 h1:OCJlWkOUoTnl0neNGlf4fUm3TmbEtg
github.com/grpc-ecosystem/grpc-gateway v1.14.3/go.mod h1:6CwZWGDSPRJidgKAtJVvND6soZe6fT7iteq8wDPdhb0= github.com/grpc-ecosystem/grpc-gateway v1.14.3/go.mod h1:6CwZWGDSPRJidgKAtJVvND6soZe6fT7iteq8wDPdhb0=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jackpal/gateway v1.0.5 h1:qzXWUJfuMdlLMtt0a3Dgt+xkWQiA5itDEITVJtuSwMc=
github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA= github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA=
github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad h1:heFfj7z0pGsNCekUlsFhO2jstxO4b5iQ665LjwM5mDc=
github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@ -253,6 +259,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E=
github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY= github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY=
github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=

@ -2,6 +2,8 @@ package labels
import ( import (
"errors" "errors"
"fmt"
"strings"
) )
const ( const (
@ -11,6 +13,10 @@ const (
// Reserved is used as a prefix to separate labels that are created by // Reserved is used as a prefix to separate labels that are created by
// loopd from those created by users. // loopd from those created by users.
Reserved = "[reserved]" Reserved = "[reserved]"
// autoOut is the label used for loop out swaps that are automatically
// dispatched.
autoOut = "autoloop-out"
) )
var ( var (
@ -22,6 +28,12 @@ var (
ErrReservedPrefix = errors.New("label contains reserved prefix") ErrReservedPrefix = errors.New("label contains reserved prefix")
) )
// AutoOutLabel returns a label with the reserved prefix that identifies
// automatically dispatched loop outs.
func AutoOutLabel() string {
return fmt.Sprintf("%v: %v", Reserved, autoOut)
}
// Validate checks that a label is of appropriate length and is not in our list // Validate checks that a label is of appropriate length and is not in our list
// of reserved labels. // of reserved labels.
func Validate(label string) error { func Validate(label string) error {
@ -29,16 +41,10 @@ func Validate(label string) error {
return ErrLabelTooLong return ErrLabelTooLong
} }
// If the label is shorter than our reserved prefix, it cannot contain
// it.
if len(label) < len(Reserved) {
return nil
}
// Check if our label begins with our reserved prefix. We don't mind if // Check if our label begins with our reserved prefix. We don't mind if
// it has our reserved prefix in another case, we just need to be able // it has our reserved prefix in another case, we just need to be able
// to reserve a subset of labels with this prefix. // to reserve a subset of labels with this prefix.
if label[0:len(Reserved)] == Reserved { if strings.HasPrefix(label, Reserved) {
return ErrReservedPrefix return ErrReservedPrefix
} }

@ -0,0 +1,290 @@
package liquidity
import (
"testing"
"time"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
)
// TestAutoLoopDisabled tests the case where we need to perform a swap, but
// autoloop is not enabled.
func TestAutoLoopDisabled(t *testing.T) {
defer test.Guard(t)()
// Set parameters for a channel that will require a swap.
channels := []lndclient.ChannelInfo{
channel1,
}
params := defaultParameters
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule,
}
c := newAutoloopTestCtx(t, params, channels)
c.start()
// We expect a single quote to be required for our swap on channel 1.
// We set its quote to have acceptable fees for our current limit.
quotes := []quoteRequestResp{
{
request: &loop.LoopOutQuoteRequest{
Amount: chan1Rec.Amount,
SweepConfTarget: chan1Rec.SweepConfTarget,
},
quote: testQuote,
},
}
// Trigger an autoloop attempt for our test context with no existing
// loop in/out swaps. We expect a swap for our channel to be suggested,
// but do not expect any swaps to be executed, since autoloop is
// disabled by default.
c.autoloop(1, chan1Rec.Amount+1, nil, quotes, nil)
// Trigger another autoloop, this time setting our server restrictions
// to have a minimum swap amount greater than the amount that we need
// to swap. In this case we don't even expect to get a quote, because
// our suggested swap is beneath the minimum swap size.
c.autoloop(chan1Rec.Amount+1, chan1Rec.Amount+2, nil, nil, nil)
c.stop()
}
// TestAutoLoopEnabled tests enabling the liquidity manger's autolooper. To keep
// the test simple, we do not update actual lnd channel balances, but rather
// run our mock with two channels that will always require a loop out according
// to our rules. This allows us to test the other restrictions placed on the
// autolooper (such as balance, and in-flight swaps) rather than need to worry
// about calculating swap amounts and thresholds.
func TestAutoLoopEnabled(t *testing.T) {
defer test.Guard(t)()
channels := []lndclient.ChannelInfo{
channel1, channel2,
}
// Create a set of parameters with autoloop enabled. The autoloop budget
// is set to allow exactly 2 swaps at the prices that we set in our
// test quotes.
params := Parameters{
AutoOut: true,
AutoFeeBudget: 40066,
AutoFeeStartDate: testTime,
MaxAutoInFlight: 2,
FailureBackOff: time.Hour,
SweepFeeRateLimit: 20000,
SweepConfTarget: 10,
MaximumPrepay: 20000,
MaximumSwapFeePPM: 1000,
MaximumRoutingFeePPM: 1000,
MaximumPrepayRoutingFeePPM: 1000,
MaximumMinerFee: 20000,
ChannelRules: map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule,
chanID2: chanRule,
},
}
c := newAutoloopTestCtx(t, params, channels)
c.start()
// Calculate our maximum allowed fees and create quotes that fall within
// our budget.
var (
amt = chan1Rec.Amount
maxSwapFee = ppmToSat(amt, params.MaximumSwapFeePPM)
// Create a quote that is within our limits. We do not set miner
// fee because this value is not actually set by the server.
quote1 = &loop.LoopOutQuote{
SwapFee: maxSwapFee,
PrepayAmount: params.MaximumPrepay - 10,
}
quote2 = &loop.LoopOutQuote{
SwapFee: maxSwapFee,
PrepayAmount: params.MaximumPrepay - 20,
}
quoteRequest = &loop.LoopOutQuoteRequest{
Amount: amt,
SweepConfTarget: params.SweepConfTarget,
}
quotes = []quoteRequestResp{
{
request: quoteRequest,
quote: quote1,
},
{
request: quoteRequest,
quote: quote2,
},
}
maxRouteFee = ppmToSat(amt, params.MaximumRoutingFeePPM)
chan1Swap = &loop.OutRequest{
Amount: amt,
MaxSwapRoutingFee: maxRouteFee,
MaxPrepayRoutingFee: ppmToSat(
quote1.PrepayAmount,
params.MaximumPrepayRoutingFeePPM,
),
MaxSwapFee: quote1.SwapFee,
MaxPrepayAmount: quote1.PrepayAmount,
MaxMinerFee: params.MaximumMinerFee,
SweepConfTarget: params.SweepConfTarget,
OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()},
Label: labels.AutoOutLabel(),
}
chan2Swap = &loop.OutRequest{
Amount: amt,
MaxSwapRoutingFee: maxRouteFee,
MaxPrepayRoutingFee: ppmToSat(
quote2.PrepayAmount,
params.MaximumPrepayRoutingFeePPM,
),
MaxSwapFee: quote2.SwapFee,
MaxPrepayAmount: quote2.PrepayAmount,
MaxMinerFee: params.MaximumMinerFee,
SweepConfTarget: params.SweepConfTarget,
OutgoingChanSet: loopdb.ChannelSet{chanID2.ToUint64()},
Label: labels.AutoOutLabel(),
}
loopOuts = []loopOutRequestResp{
{
request: chan1Swap,
response: &loop.LoopOutSwapInfo{
SwapHash: lntypes.Hash{1},
},
},
{
request: chan2Swap,
response: &loop.LoopOutSwapInfo{
SwapHash: lntypes.Hash{2},
},
},
}
)
// Tick our autolooper with no existing swaps, we expect a loop out
// swap to be dispatched for each channel.
c.autoloop(1, amt+1, nil, quotes, loopOuts)
// Tick again with both of our swaps in progress. We haven't shifted our
// channel balances at all, so swaps should still be suggested, but we
// have 2 swaps in flight so we do not expect any suggestion.
existing := []*loopdb.LoopOut{
existingSwapFromRequest(chan1Swap, testTime, nil),
existingSwapFromRequest(chan2Swap, testTime, nil),
}
c.autoloop(1, amt+1, existing, nil, nil)
// Now, we update our channel 2 swap to have failed due to off chain
// failure and our first swap to have succeeded.
now := c.testClock.Now()
failedOffChain := []*loopdb.LoopEvent{
{
SwapStateData: loopdb.SwapStateData{
State: loopdb.StateFailOffchainPayments,
},
Time: now,
},
}
success := []*loopdb.LoopEvent{
{
SwapStateData: loopdb.SwapStateData{
State: loopdb.StateSuccess,
Cost: loopdb.SwapCost{
Server: quote1.SwapFee,
Onchain: params.MaximumMinerFee,
Offchain: maxRouteFee +
chan1Rec.MaxPrepayRoutingFee,
},
},
Time: now,
},
}
quotes = []quoteRequestResp{
{
request: quoteRequest,
quote: quote1,
},
}
loopOuts = []loopOutRequestResp{
{
request: chan1Swap,
response: &loop.LoopOutSwapInfo{
SwapHash: lntypes.Hash{3},
},
},
}
existing = []*loopdb.LoopOut{
existingSwapFromRequest(chan1Swap, testTime, success),
existingSwapFromRequest(chan2Swap, testTime, failedOffChain),
}
// We tick again, this time we expect another swap on channel 1 (which
// still has balances which reflect that we need to swap), but nothing
// for channel 2, since it has had a failure.
c.autoloop(1, amt+1, existing, quotes, loopOuts)
// Now, we progress our time so that we have sufficiently backed off
// for channel 2, and could perform another swap.
c.testClock.SetTime(now.Add(params.FailureBackOff))
// Our existing swaps (1 successful, one pending) have used our budget
// so we no longer expect any swaps to automatically dispatch.
existing = []*loopdb.LoopOut{
existingSwapFromRequest(chan1Swap, testTime, success),
existingSwapFromRequest(chan1Swap, c.testClock.Now(), nil),
existingSwapFromRequest(chan2Swap, testTime, failedOffChain),
}
c.autoloop(1, amt+1, existing, quotes, nil)
c.stop()
}
// existingSwapFromRequest is a helper function which returns the db
// representation of a loop out request with the event set provided.
func existingSwapFromRequest(request *loop.OutRequest, initTime time.Time,
events []*loopdb.LoopEvent) *loopdb.LoopOut {
return &loopdb.LoopOut{
Loop: loopdb.Loop{
Events: events,
},
Contract: &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
AmountRequested: request.Amount,
MaxSwapFee: request.MaxSwapFee,
MaxMinerFee: request.MaxMinerFee,
InitiationTime: initTime,
Label: request.Label,
},
SwapInvoice: "",
MaxSwapRoutingFee: request.MaxSwapRoutingFee,
SweepConfTarget: request.SweepConfTarget,
OutgoingChanSet: request.OutgoingChanSet,
MaxPrepayRoutingFee: request.MaxSwapRoutingFee,
},
}
}

@ -0,0 +1,211 @@
package liquidity
import (
"context"
"testing"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/ticker"
"github.com/stretchr/testify/assert"
)
type autoloopTestCtx struct {
t *testing.T
manager *Manager
lnd *test.LndMockServices
testClock *clock.TestClock
// quoteRequests is a channel that requests for quotes are pushed into.
quoteRequest chan *loop.LoopOutQuoteRequest
// quotes is a channel that we get loop out quote requests on.
quotes chan *loop.LoopOutQuote
// loopOutRestrictions is a channel that we get the server's
// restrictions on.
loopOutRestrictions chan *Restrictions
// loopOuts is a channel that we get existing loop out swaps on.
loopOuts chan []*loopdb.LoopOut
// loopIns is a channel that we get existing loop in swaps on.
loopIns chan []*loopdb.LoopIn
// restrictions is a channel that we get swap restrictions on.
restrictions chan *Restrictions
// outRequest is a channel that requests to dispatch loop outs are
// pushed into.
outRequest chan *loop.OutRequest
// loopOut is a channel that we return loop out responses on.
loopOut chan *loop.LoopOutSwapInfo
// errChan is a channel that we send run errors into.
errChan chan error
// cancelCtx cancels the context that our liquidity manager is run with.
// This can be used to cleanly shutdown the test. Note that this will be
// nil until the test context has been started.
cancelCtx func()
}
// newAutoloopTestCtx creates a test context with custom liquidity manager
// parameters and lnd channels.
func newAutoloopTestCtx(t *testing.T, parameters Parameters,
channels []lndclient.ChannelInfo) *autoloopTestCtx {
// Create a mock lnd and set our expected fee rate for sweeps to our
// sweep fee rate limit value.
lnd := test.NewMockLnd()
lnd.SetFeeEstimate(
defaultParameters.SweepConfTarget,
defaultParameters.SweepFeeRateLimit,
)
testCtx := &autoloopTestCtx{
t: t,
testClock: clock.NewTestClock(testTime),
lnd: lnd,
quoteRequest: make(chan *loop.LoopOutQuoteRequest),
quotes: make(chan *loop.LoopOutQuote),
loopOutRestrictions: make(chan *Restrictions),
loopOuts: make(chan []*loopdb.LoopOut),
loopIns: make(chan []*loopdb.LoopIn),
restrictions: make(chan *Restrictions),
outRequest: make(chan *loop.OutRequest),
loopOut: make(chan *loop.LoopOutSwapInfo),
errChan: make(chan error, 1),
}
// Set lnd's channels to equal the set of channels we want for our
// test.
testCtx.lnd.Channels = channels
cfg := &Config{
AutoOutTicker: ticker.NewForce(DefaultAutoOutTicker),
LoopOutRestrictions: func(context.Context) (*Restrictions, error) {
return <-testCtx.loopOutRestrictions, nil
},
ListLoopOut: func() ([]*loopdb.LoopOut, error) {
return <-testCtx.loopOuts, nil
},
ListLoopIn: func() ([]*loopdb.LoopIn, error) {
return <-testCtx.loopIns, nil
},
LoopOutQuote: func(_ context.Context,
req *loop.LoopOutQuoteRequest) (*loop.LoopOutQuote,
error) {
testCtx.quoteRequest <- req
return <-testCtx.quotes, nil
},
LoopOut: func(_ context.Context,
req *loop.OutRequest) (*loop.LoopOutSwapInfo,
error) {
testCtx.outRequest <- req
return <-testCtx.loopOut, nil
},
MinimumConfirmations: loop.DefaultSweepConfTarget,
Lnd: &testCtx.lnd.LndServices,
Clock: testCtx.testClock,
}
// Create a manager with our test config and set our starting set of
// parameters.
testCtx.manager = NewManager(cfg)
assert.NoError(t, testCtx.manager.SetParameters(parameters))
return testCtx
}
// start starts our liquidity manager's run loop in a goroutine. Tests should
// be run with test.Guard() to ensure that this does not leak.
func (c *autoloopTestCtx) start() {
ctx := context.Background()
ctx, c.cancelCtx = context.WithCancel(ctx)
go func() {
c.errChan <- c.manager.Run(ctx)
}()
}
// stop shuts down our test context and asserts that we have exited with a
// context-cancelled error.
func (c *autoloopTestCtx) stop() {
c.cancelCtx()
assert.Equal(c.t, context.Canceled, <-c.errChan)
}
// quoteRequestResp pairs an expected swap quote request with the response we
// would like to provide the liquidity manager with.
type quoteRequestResp struct {
request *loop.LoopOutQuoteRequest
quote *loop.LoopOutQuote
}
// loopOutRequestResp pairs an expected loop out request with the response we
// would like the server to respond with.
type loopOutRequestResp struct {
request *loop.OutRequest
response *loop.LoopOutSwapInfo
}
// autoloop walks our test context through the process of triggering our
// autoloop functionality, providing mocked values as required. The set of
// quotes provided indicates that we expect swap suggestions to be made (since
// we will query for a quote for each suggested swap). The set of expected
// swaps indicates whether we expect any of these swap suggestions to actually
// be dispatched by the autolooper.
func (c *autoloopTestCtx) autoloop(minAmt, maxAmt btcutil.Amount,
existingOut []*loopdb.LoopOut, quotes []quoteRequestResp,
expectedSwaps []loopOutRequestResp) {
// Tick our autoloop ticker to force assessing whether we want to loop.
c.manager.cfg.AutoOutTicker.Force <- testTime
// Send a mocked response from the server with the swap size limits.
c.loopOutRestrictions <- NewRestrictions(minAmt, maxAmt)
// Provide the liquidity manager with our desired existing set of swaps.
c.loopOuts <- existingOut
c.loopIns <- nil
// Assert that we query the server for a quote for each of our
// recommended swaps. Note that this differs from our set of expected
// swaps because we may get quotes for suggested swaps but then just
// log them.
for _, expected := range quotes {
request := <-c.quoteRequest
assert.Equal(
c.t, expected.request.Amount, request.Amount,
)
assert.Equal(
c.t, expected.request.SweepConfTarget,
request.SweepConfTarget,
)
c.quotes <- expected.quote
}
// Assert that we dispatch the expected set of swaps.
for _, expected := range expectedSwaps {
actual := <-c.outRequest
// Set our destination address to nil so that we do not need to
// provide the address that is obtained by the mock wallet kit.
actual.DestAddr = nil
assert.Equal(c.t, expected.request, actual)
c.loopOut <- expected.response
}
}

@ -36,6 +36,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"sort"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -43,11 +44,14 @@ import (
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/lightninglabs/lndclient" "github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop" "github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/ticker"
) )
const ( const (
@ -85,12 +89,35 @@ const (
// defaultSweepFeeRateLimit is the default limit we place on estimated // defaultSweepFeeRateLimit is the default limit we place on estimated
// sweep fees, (750 * 4 /1000 = 3 sat/vByte). // sweep fees, (750 * 4 /1000 = 3 sat/vByte).
defaultSweepFeeRateLimit = chainfee.SatPerKWeight(750) defaultSweepFeeRateLimit = chainfee.SatPerKWeight(750)
// defaultMaxInFlight is the default number of in-flight automatically
// dispatched swaps we allow. Note that this does not enable automated
// swaps itself (because we want non-zero values to be expressed in
// suggestions as a dry-run).
defaultMaxInFlight = 1
// DefaultAutoOutTicker is the default amount of time between automated
// loop out checks.
DefaultAutoOutTicker = time.Minute * 10
) )
var ( var (
// defaultBudget is the default autoloop budget we set. This budget will
// only be used for automatically dispatched swaps if autoloop is
// explicitly enabled, so we are happy to set a non-zero value here. The
// amount chosen simply uses the current defaults to provide budget for
// a single swap. We don't have a swap amount to calculate our maximum
// routing fee, so we use 0.16 BTC for now.
defaultBudget = defaultMaximumMinerFee +
ppmToSat(lnd.MaxBtcFundingAmount, defaultSwapFeePPM) +
ppmToSat(defaultMaximumPrepay, defaultPrepayRoutingFeePPM) +
ppmToSat(lnd.MaxBtcFundingAmount, defaultRoutingFeePPM)
// defaultParameters contains the default parameters that we start our // defaultParameters contains the default parameters that we start our
// liquidity manger with. // liquidity manger with.
defaultParameters = Parameters{ defaultParameters = Parameters{
AutoFeeBudget: defaultBudget,
MaxAutoInFlight: defaultMaxInFlight,
ChannelRules: make(map[lnwire.ShortChannelID]*ThresholdRule), ChannelRules: make(map[lnwire.ShortChannelID]*ThresholdRule),
FailureBackOff: defaultFailureBackoff, FailureBackOff: defaultFailureBackoff,
SweepFeeRateLimit: defaultSweepFeeRateLimit, SweepFeeRateLimit: defaultSweepFeeRateLimit,
@ -125,11 +152,22 @@ var (
// ErrZeroPrepay is returned if a zero maximum prepay is set. // ErrZeroPrepay is returned if a zero maximum prepay is set.
ErrZeroPrepay = errors.New("maximum prepay must be non-zero") ErrZeroPrepay = errors.New("maximum prepay must be non-zero")
// ErrNegativeBudget is returned if a negative swap budget is set.
ErrNegativeBudget = errors.New("swap budget must be >= 0")
// ErrZeroInFlight is returned is a zero in flight swaps value is set.
ErrZeroInFlight = errors.New("max in flight swaps must be >=0")
) )
// Config contains the external functionality required to run the // Config contains the external functionality required to run the
// liquidity manager. // liquidity manager.
type Config struct { type Config struct {
// AutoOutTicker determines how often we should check whether we want
// to dispatch an automated loop out. We use a force ticker so that
// we can trigger autoloop in itests.
AutoOutTicker *ticker.Force
// LoopOutRestrictions returns the restrictions that the server applies // LoopOutRestrictions returns the restrictions that the server applies
// to loop out swaps. // to loop out swaps.
LoopOutRestrictions func(ctx context.Context) (*Restrictions, error) LoopOutRestrictions func(ctx context.Context) (*Restrictions, error)
@ -148,6 +186,10 @@ type Config struct {
LoopOutQuote func(ctx context.Context, LoopOutQuote func(ctx context.Context,
request *loop.LoopOutQuoteRequest) (*loop.LoopOutQuote, error) request *loop.LoopOutQuoteRequest) (*loop.LoopOutQuote, error)
// LoopOut dispatches a loop out.
LoopOut func(ctx context.Context, request *loop.OutRequest) (
*loop.LoopOutSwapInfo, error)
// Clock allows easy mocking of time in unit tests. // Clock allows easy mocking of time in unit tests.
Clock clock.Clock Clock clock.Clock
@ -159,6 +201,23 @@ type Config struct {
// Parameters is a set of parameters provided by the user which guide // Parameters is a set of parameters provided by the user which guide
// how we assess liquidity. // how we assess liquidity.
type Parameters struct { type Parameters struct {
// AutoOut enables automatic dispatch of loop out swaps.
AutoOut bool
// AutoFeeBudget is the total amount we allow to be spent on
// automatically dispatched swaps. Once this budget has been used, we
// will stop dispatching swaps until the budget is increased or the
// start date is moved.
AutoFeeBudget btcutil.Amount
// AutoFeeStartDate is the date from which we will include automatically
// dispatched swaps in our current budget, inclusive.
AutoFeeStartDate time.Time
// MaxAutoInFlight is the maximum number of in-flight automatically
// dispatched swaps we allow.
MaxAutoInFlight int
// FailureBackOff is the amount of time that we require passes after a // FailureBackOff is the amount of time that we require passes after a
// channel has been part of a failed loop out swap before we suggest // channel has been part of a failed loop out swap before we suggest
// using it again. // using it again.
@ -219,12 +278,13 @@ func (p Parameters) String() string {
return fmt.Sprintf("channel rules: %v, failure backoff: %v, sweep "+ return fmt.Sprintf("channel rules: %v, failure backoff: %v, sweep "+
"fee rate limit: %v, sweep conf target: %v, maximum prepay: "+ "fee rate limit: %v, sweep conf target: %v, maximum prepay: "+
"%v, maximum miner fee: %v, maximum swap fee ppm: %v, maximum "+ "%v, maximum miner fee: %v, maximum swap fee ppm: %v, maximum "+
"routing fee ppm: %v, maximum prepay routing fee ppm: %v", "routing fee ppm: %v, maximum prepay routing fee ppm: %v, "+
"auto budget: %v, budget start: %v, max auto in flight: %v",
strings.Join(channelRules, ","), p.FailureBackOff, strings.Join(channelRules, ","), p.FailureBackOff,
p.SweepFeeRateLimit, p.SweepConfTarget, p.MaximumPrepay, p.SweepFeeRateLimit, p.SweepConfTarget, p.MaximumPrepay,
p.MaximumMinerFee, p.MaximumSwapFeePPM, p.MaximumMinerFee, p.MaximumSwapFeePPM,
p.MaximumRoutingFeePPM, p.MaximumPrepayRoutingFeePPM, p.MaximumRoutingFeePPM, p.MaximumPrepayRoutingFeePPM,
) p.AutoFeeBudget, p.AutoFeeStartDate, p.MaxAutoInFlight)
} }
// validate checks whether a set of parameters is valid. It takes the minimum // validate checks whether a set of parameters is valid. It takes the minimum
@ -275,6 +335,14 @@ func (p Parameters) validate(minConfs int32) error {
return ErrZeroMinerFee return ErrZeroMinerFee
} }
if p.AutoFeeBudget < 0 {
return ErrNegativeBudget
}
if p.MaxAutoInFlight <= 0 {
return ErrZeroInFlight
}
return nil return nil
} }
@ -293,6 +361,26 @@ type Manager struct {
paramsLock sync.Mutex paramsLock sync.Mutex
} }
// Run periodically checks whether we should automatically dispatch a loop out.
// We run this loop even if automated swaps are not currently enabled rather
// than managing starting and stopping the ticker as our parameters are updated.
func (m *Manager) Run(ctx context.Context) error {
m.cfg.AutoOutTicker.Resume()
defer m.cfg.AutoOutTicker.Stop()
for {
select {
case <-m.cfg.AutoOutTicker.Ticks():
if err := m.autoloop(ctx); err != nil {
log.Errorf("autoloop failed: %v", err)
}
case <-ctx.Done():
return ctx.Err()
}
}
}
// NewManager creates a liquidity manager which has no rules set. // NewManager creates a liquidity manager which has no rules set.
func NewManager(cfg *Config) *Manager { func NewManager(cfg *Config) *Manager {
return &Manager{ return &Manager{
@ -341,10 +429,37 @@ func cloneParameters(params Parameters) Parameters {
return paramCopy return paramCopy
} }
// autoloop gets a set of suggested swaps and dispatches them automatically if
// we have automated looping enabled.
func (m *Manager) autoloop(ctx context.Context) error {
swaps, err := m.SuggestSwaps(ctx, true)
if err != nil {
return err
}
for _, swap := range swaps {
// Create a copy of our range var so that we can reference it.
swap := swap
loopOut, err := m.cfg.LoopOut(ctx, &swap)
if err != nil {
return err
}
log.Infof("loop out automatically dispatched: hash: %v, "+
"address: %v", loopOut.SwapHash,
loopOut.HtlcAddressP2WSH)
}
return nil
}
// SuggestSwaps returns a set of swap suggestions based on our current liquidity // SuggestSwaps returns a set of swap suggestions based on our current liquidity
// balance for the set of rules configured for the manager, failing if there are // balance for the set of rules configured for the manager, failing if there are
// no rules set. // no rules set. It takes an autoOut boolean that indicates whether the
func (m *Manager) SuggestSwaps(ctx context.Context) ( // suggestions are being used for our internal autolooper. This boolean is used
// to determine the information we add to our swap suggestion and whether we
// return any suggestions.
func (m *Manager) SuggestSwaps(ctx context.Context, autoOut bool) (
[]loop.OutRequest, error) { []loop.OutRequest, error) {
m.paramsLock.Lock() m.paramsLock.Lock()
@ -356,6 +471,16 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
return nil, nil return nil, nil
} }
// If our start date is in the future, we interpret this as meaning that
// we should start using our budget at this date. This means that we
// have no budget for the present, so we just return.
if m.params.AutoFeeStartDate.After(m.cfg.Clock.Now()) {
log.Debugf("autoloop fee budget start time: %v is in "+
"the future", m.params.AutoFeeStartDate)
return nil, nil
}
// Before we get any swap suggestions, we check what the current fee // Before we get any swap suggestions, we check what the current fee
// estimate is to sweep within our target number of confirmations. If // 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 // This fee exceeds the fee limit we have set, we will not suggest any
@ -396,6 +521,32 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
return nil, err return nil, err
} }
// Get a summary of our existing swaps so that we can check our autoloop
// budget.
summary, err := m.checkExistingAutoLoops(ctx, loopOut)
if err != nil {
return nil, err
}
if summary.totalFees() >= m.params.AutoFeeBudget {
log.Debugf("autoloop fee budget: %v exhausted, %v spent on "+
"completed swaps, %v reserved for ongoing swaps "+
"(upper limit)",
m.params.AutoFeeBudget, summary.spentFees,
summary.pendingFees)
return nil, nil
}
// If we have already reached our total allowed number of in flight
// swaps, we do not suggest any more at the moment.
allowedSwaps := m.params.MaxAutoInFlight - summary.inFlightCount
if allowedSwaps <= 0 {
log.Debugf("%v autoloops allowed, %v in flight",
m.params.MaxAutoInFlight, summary.inFlightCount)
return nil, nil
}
eligible, err := m.getEligibleChannels(ctx, loopOut, loopIn) eligible, err := m.getEligibleChannels(ctx, loopOut, loopIn)
if err != nil { if err != nil {
return nil, err return nil, err
@ -445,11 +596,67 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
continue continue
} }
outRequest := m.makeLoopOutRequest(suggestion, quote) outRequest, err := m.makeLoopOutRequest(
ctx, suggestion, quote, autoOut,
)
if err != nil {
return nil, err
}
suggestions = append(suggestions, outRequest) suggestions = append(suggestions, outRequest)
} }
return suggestions, nil // If we have no suggestions after we have applied all of our limits,
// just return.
if len(suggestions) == 0 {
return nil, nil
}
// Sort suggestions by amount in descending order.
sort.SliceStable(suggestions, func(i, j int) bool {
return suggestions[i].Amount > suggestions[j].Amount
})
// Run through our suggested swaps in descending order of amount and
// return all of the swaps which will fit within our remaining budget.
var (
available = m.params.AutoFeeBudget - summary.totalFees()
inBudget []loop.OutRequest
)
for _, swap := range suggestions {
fees := worstCaseOutFees(
swap.MaxPrepayRoutingFee, swap.MaxSwapRoutingFee,
swap.MaxSwapFee, swap.MaxMinerFee, swap.MaxPrepayAmount,
)
// If the maximum fee we expect our swap to use is less than the
// amount we have available, we add it to our set of swaps that
// fall within the budget and decrement our available amount.
if fees <= available {
available -= fees
inBudget = append(inBudget, swap)
}
// If we're out of budget, or we have hit the max number of
// swaps that we want to dispatch at one time, exit early.
if available == 0 || allowedSwaps == len(inBudget) {
break
}
}
// If we are getting suggestions for automatically dispatched swaps,
// and they are not enabled in our parameters, we just log the swap
// suggestions and return an empty set of suggestions.
if autoOut && !m.params.AutoOut {
for _, swap := range inBudget {
log.Debugf("recommended autoloop: %v sats over "+
"%v", swap.Amount, swap.OutgoingChanSet)
}
return nil, nil
}
return inBudget, nil
} }
// makeLoopOutRequest creates a loop out request from a suggestion. Since we // makeLoopOutRequest creates a loop out request from a suggestion. Since we
@ -459,9 +666,13 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
// route-independent, which is a very poor estimation so we don't bother with // route-independent, which is a very poor estimation so we don't bother with
// checking against this inaccurate constant. We use the exact prepay amount // checking against this inaccurate constant. We use the exact prepay amount
// and swap fee given to us by the server, but use our maximum miner fee anyway // and swap fee given to us by the server, but use our maximum miner fee anyway
// to give us some leeway when performing the swap. // to give us some leeway when performing the swap. We take an auto-out which
func (m *Manager) makeLoopOutRequest(suggestion *LoopOutRecommendation, // determines whether we set a label identifying this swap as automatically
quote *loop.LoopOutQuote) loop.OutRequest { // dispatched, and decides whether we set a sweep address (we don't bother for
// non-auto requests, because the client api will set it anyway).
func (m *Manager) makeLoopOutRequest(ctx context.Context,
suggestion *LoopOutRecommendation, quote *loop.LoopOutQuote,
autoOut bool) (loop.OutRequest, error) {
prepayMaxFee := ppmToSat( prepayMaxFee := ppmToSat(
quote.PrepayAmount, m.params.MaximumPrepayRoutingFeePPM, quote.PrepayAmount, m.params.MaximumPrepayRoutingFeePPM,
@ -471,7 +682,7 @@ func (m *Manager) makeLoopOutRequest(suggestion *LoopOutRecommendation,
suggestion.Amount, m.params.MaximumRoutingFeePPM, suggestion.Amount, m.params.MaximumRoutingFeePPM,
) )
return loop.OutRequest{ request := loop.OutRequest{
Amount: suggestion.Amount, Amount: suggestion.Amount,
OutgoingChanSet: loopdb.ChannelSet{ OutgoingChanSet: loopdb.ChannelSet{
suggestion.Channel.ToUint64(), suggestion.Channel.ToUint64(),
@ -483,6 +694,109 @@ func (m *Manager) makeLoopOutRequest(suggestion *LoopOutRecommendation,
MaxPrepayAmount: quote.PrepayAmount, MaxPrepayAmount: quote.PrepayAmount,
SweepConfTarget: m.params.SweepConfTarget, SweepConfTarget: m.params.SweepConfTarget,
} }
if autoOut {
request.Label = labels.AutoOutLabel()
addr, err := m.cfg.Lnd.WalletKit.NextAddr(ctx)
if err != nil {
return loop.OutRequest{}, err
}
request.DestAddr = addr
}
return request, nil
}
// worstCaseOutFees calculates the largest possible fees for a loop out swap,
// comparing the fees for a successful swap to the cost when the client pays
// the prepay because they failed to sweep the on chain htlc. This is unlikely,
// because we expect clients to be online to sweep, but we want to account for
// every outcome so we include it.
func worstCaseOutFees(prepayRouting, swapRouting, swapFee, minerFee,
prepayAmount btcutil.Amount) btcutil.Amount {
var (
successFees = prepayRouting + minerFee + swapFee + swapRouting
noShowFees = prepayRouting + prepayAmount
)
if noShowFees > successFees {
return noShowFees
}
return successFees
}
// existingAutoLoopSummary provides a summary of the existing autoloops which
// were dispatched during our current budget period.
type existingAutoLoopSummary struct {
// spentFees is the amount we have spent on completed swaps.
spentFees btcutil.Amount
// pendingFees is the worst-case amount of fees we could spend on in
// flight autoloops.
pendingFees btcutil.Amount
// inFlightCount is the total number of automated swaps that are
// currently in flight. Note that this may race with swap completion,
// but not with initiation of new automated swaps, this is ok, because
// it can only lead to dispatching fewer swaps than we could have (not
// too many).
inFlightCount int
}
// totalFees returns the total amount of fees that automatically dispatched
// swaps may consume.
func (e *existingAutoLoopSummary) totalFees() btcutil.Amount {
return e.spentFees + e.pendingFees
}
// checkExistingAutoLoops calculates the total amount that has been spent by
// automatically dispatched swaps that have completed, and the worst-case fee
// 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) {
var summary existingAutoLoopSummary
for _, out := range loopOuts {
if out.Contract.Label != labels.AutoOutLabel() {
continue
}
// If we have a pending swap, we are uncertain of the fees that
// it will end up paying. We use the worst-case estimate based
// on the maximum values we set for each fee category. This will
// likely over-estimate our fees (because we probably won't
// spend our maximum miner amount). If a swap is not pending,
// it has succeeded or failed so we just record our actual fees
// for the swap provided that the swap completed after our
// budget start date.
if out.State().State.Type() == loopdb.StateTypePending {
summary.inFlightCount++
prepay, err := m.cfg.Lnd.Client.DecodePaymentRequest(
ctx, out.Contract.PrepayInvoice,
)
if err != nil {
return nil, err
}
summary.pendingFees += worstCaseOutFees(
out.Contract.MaxPrepayRoutingFee,
out.Contract.MaxSwapRoutingFee,
out.Contract.MaxSwapFee,
out.Contract.MaxMinerFee,
mSatToSatoshis(prepay.Value),
)
} else if !out.LastUpdateTime().Before(m.params.AutoFeeStartDate) {
summary.spentFees += out.State().Cost.Total()
}
}
return &summary, nil
} }
// getEligibleChannels takes lists of our existing loop out and in swaps, and // getEligibleChannels takes lists of our existing loop out and in swaps, and
@ -653,3 +967,7 @@ func satPerKwToSatPerVByte(satPerKw chainfee.SatPerKWeight) int64 {
func ppmToSat(amount btcutil.Amount, ppm int) btcutil.Amount { func ppmToSat(amount btcutil.Amount, ppm int) btcutil.Amount {
return btcutil.Amount(uint64(amount) * uint64(ppm) / FeeBase) return btcutil.Amount(uint64(amount) * uint64(ppm) / FeeBase)
} }
func mSatToSatoshis(amount lnwire.MilliSatoshi) btcutil.Amount {
return btcutil.Amount(amount / 1000)
}

@ -8,6 +8,7 @@ import (
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/lightninglabs/lndclient" "github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop" "github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/test" "github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/clock"
@ -18,7 +19,8 @@ import (
) )
var ( var (
testTime = time.Date(2020, 02, 13, 0, 0, 0, 0, time.UTC) testTime = time.Date(2020, 02, 13, 0, 0, 0, 0, time.UTC)
testBudgetStart = testTime.Add(time.Hour * -1)
chanID1 = lnwire.NewShortChanIDFromInt(1) chanID1 = lnwire.NewShortChanIDFromInt(1)
chanID2 = lnwire.NewShortChanIDFromInt(2) chanID2 = lnwire.NewShortChanIDFromInt(2)
@ -89,6 +91,17 @@ var (
}, },
), ),
} }
// autoOutContract is a contract for an existing loop out that was
// automatically dispatched. This swap is within our test budget period,
// and restricted to a channel that we do not use in our tests.
autoOutContract = &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
Label: labels.AutoOutLabel(),
InitiationTime: testBudgetStart,
},
OutgoingChanSet: loopdb.ChannelSet{999},
}
) )
// newTestConfig creates a default test config. // newTestConfig creates a default test config.
@ -351,13 +364,16 @@ func TestRestrictedSuggestions(t *testing.T) {
return testCase.loopIn, nil return testCase.loopIn, nil
} }
rules := map[lnwire.ShortChannelID]*ThresholdRule{ lnd.Channels = testCase.channels
params := defaultParameters
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule, chanID1: chanRule,
chanID2: chanRule, chanID2: chanRule,
} }
testSuggestSwaps( testSuggestSwaps(
t, cfg, lnd, testCase.channels, rules, t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.expected, testCase.expected,
) )
}) })
@ -397,16 +413,18 @@ func TestSweepFeeLimit(t *testing.T) {
loop.DefaultSweepConfTarget, testCase.feeRate, loop.DefaultSweepConfTarget, testCase.feeRate,
) )
channels := []lndclient.ChannelInfo{ lnd.Channels = []lndclient.ChannelInfo{
channel1, channel1,
} }
rules := map[lnwire.ShortChannelID]*ThresholdRule{ params := defaultParameters
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule, chanID1: chanRule,
} }
testSuggestSwaps( testSuggestSwaps(
t, cfg, lnd, channels, rules, testCase.swaps, t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.swaps,
) )
}) })
} }
@ -448,12 +466,15 @@ func TestSuggestSwaps(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
cfg, lnd := newTestConfig() cfg, lnd := newTestConfig()
channels := []lndclient.ChannelInfo{ lnd.Channels = []lndclient.ChannelInfo{
channel1, channel1,
} }
params := defaultParameters
params.ChannelRules = testCase.rules
testSuggestSwaps( testSuggestSwaps(
t, cfg, lnd, channels, testCase.rules, t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.swaps, testCase.swaps,
) )
}) })
@ -513,43 +534,331 @@ func TestFeeLimits(t *testing.T) {
return testCase.quote, nil return testCase.quote, nil
} }
channels := []lndclient.ChannelInfo{ lnd.Channels = []lndclient.ChannelInfo{
channel1, channel1,
} }
rules := map[lnwire.ShortChannelID]*ThresholdRule{
params := defaultParameters
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule, chanID1: chanRule,
} }
testSuggestSwaps( testSuggestSwaps(
t, cfg, lnd, channels, rules, testCase.expected, t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.expected,
) )
}) })
} }
} }
// testSuggestSwaps tests getting swap suggestions. // TestFeeBudget tests limiting of swap suggestions to a fee budget, with and
func testSuggestSwaps(t *testing.T, cfg *Config, lnd *test.LndMockServices, // without existing swaps. This test uses example channels and rules which need
channels []lndclient.ChannelInfo, // a 7500 sat loop out. With our default parameters, and our test quote with
rules map[lnwire.ShortChannelID]*ThresholdRule, // a prepay of 500, our total fees are (rounded due to int multiplication):
// swap fee: 1 (as set in test quote)
// route fee: 7500 * 0.005 = 37
// prepay route: 500 * 0.005 = 2 sat
// max miner: set by default params
// Since our routing fees are calculated as a portion of our swap/prepay
// amounts, we use our max miner fee to shift swap cost to values above/below
// our budget, fixing our other fees at 114 sat for simplicity.
func TestFeeBudget(t *testing.T) {
tests := []struct {
name string
// budget is our autoloop budget.
budget btcutil.Amount
// maxMinerFee is the maximum miner fee we will pay for swaps.
maxMinerFee btcutil.Amount
// existingSwaps represents our existing swaps, mapping their
// last update time to their total cost.
existingSwaps map[time.Time]btcutil.Amount
// expectedSwaps is the set of swaps we expect to be suggested.
expectedSwaps []loop.OutRequest
}{
{
// Two swaps will cost (78+5000)*2, set exactly 10156
// budget.
name: "budget for 2 swaps, no existing",
budget: 10156,
maxMinerFee: 5000,
expectedSwaps: []loop.OutRequest{
chan1Rec, chan2Rec,
},
},
{
// Two swaps will cost (78+5000)*2, set 10155 so we can
// only afford one swap.
name: "budget for 1 swaps, no existing",
budget: 10155,
maxMinerFee: 5000,
expectedSwaps: []loop.OutRequest{
chan1Rec,
},
},
{
// Set an existing swap which would limit us to a single
// swap if it were in our period.
name: "existing swaps, before budget period",
budget: 10156,
maxMinerFee: 5000,
existingSwaps: map[time.Time]btcutil.Amount{
testBudgetStart.Add(time.Hour * -1): 200,
},
expectedSwaps: []loop.OutRequest{
chan1Rec, chan2Rec,
},
},
{
// Add an existing swap in our budget period such that
// we only have budget left for one more swap.
name: "existing swaps, in budget period",
budget: 10156,
maxMinerFee: 5000,
existingSwaps: map[time.Time]btcutil.Amount{
testBudgetStart.Add(time.Hour): 500,
},
expectedSwaps: []loop.OutRequest{
chan1Rec,
},
},
{
name: "existing swaps, budget used",
budget: 500,
maxMinerFee: 1000,
existingSwaps: map[time.Time]btcutil.Amount{
testBudgetStart.Add(time.Hour): 500,
},
expectedSwaps: nil,
},
}
for _, testCase := range tests {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
cfg, lnd := newTestConfig()
// Create a swap set of existing swaps with our set of
// existing swap timestamps.
swaps := make(
[]*loopdb.LoopOut, 0,
len(testCase.existingSwaps),
)
// Add an event with the timestamp and budget set by
// our test case.
for ts, amt := range testCase.existingSwaps {
event := &loopdb.LoopEvent{
SwapStateData: loopdb.SwapStateData{
Cost: loopdb.SwapCost{
Server: amt,
},
State: loopdb.StateSuccess,
},
Time: ts,
}
swaps = append(swaps, &loopdb.LoopOut{
Loop: loopdb.Loop{
Events: []*loopdb.LoopEvent{
event,
},
},
Contract: autoOutContract,
})
}
cfg.ListLoopOut = func() ([]*loopdb.LoopOut, error) {
return swaps, nil
}
// Set two channels that need swaps.
lnd.Channels = []lndclient.ChannelInfo{
channel1,
channel2,
}
params := defaultParameters
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule,
chanID2: chanRule,
}
params.AutoFeeStartDate = testBudgetStart
params.AutoFeeBudget = testCase.budget
params.MaximumMinerFee = testCase.maxMinerFee
params.MaxAutoInFlight = 2
// Set our custom max miner fee on each expected swap,
// rather than having to create multiple vars for
// different rates.
for i := range testCase.expectedSwaps {
testCase.expectedSwaps[i].MaxMinerFee =
testCase.maxMinerFee
}
testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.expectedSwaps,
)
})
}
}
// TestInFlightLimit tests the limit we place on the number of in-flight swaps
// that are allowed.
func TestInFlightLimit(t *testing.T) {
tests := []struct {
name string
maxInFlight int
existingSwaps []*loopdb.LoopOut
expectedSwaps []loop.OutRequest
}{
{
name: "none in flight, extra space",
maxInFlight: 3,
expectedSwaps: []loop.OutRequest{
chan1Rec, chan2Rec,
},
},
{
name: "none in flight, exact match",
maxInFlight: 2,
expectedSwaps: []loop.OutRequest{
chan1Rec, chan2Rec,
},
},
{
name: "one in flight, one allowed",
maxInFlight: 2,
existingSwaps: []*loopdb.LoopOut{
{
Contract: autoOutContract,
},
},
expectedSwaps: []loop.OutRequest{
chan1Rec,
},
},
{
name: "max in flight",
maxInFlight: 1,
existingSwaps: []*loopdb.LoopOut{
{
Contract: autoOutContract,
},
},
expectedSwaps: nil,
},
{
name: "max swaps exceeded",
maxInFlight: 1,
existingSwaps: []*loopdb.LoopOut{
{
Contract: autoOutContract,
},
{
Contract: autoOutContract,
},
},
expectedSwaps: nil,
},
}
for _, testCase := range tests {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
cfg, lnd := newTestConfig()
cfg.ListLoopOut = func() ([]*loopdb.LoopOut, error) {
return testCase.existingSwaps, nil
}
lnd.Channels = []lndclient.ChannelInfo{
channel1, channel2,
}
params := defaultParameters
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule,
chanID2: chanRule,
}
params.MaxAutoInFlight = testCase.maxInFlight
// By default we only have budget for one swap, increase
// our budget so that we could recommend more than one
// swap at a time.
params.AutoFeeBudget = defaultBudget * 2
testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.expectedSwaps,
)
})
}
}
// testSuggestSwapsSetup contains the elements that are used to create a
// suggest swaps test.
type testSuggestSwapsSetup struct {
cfg *Config
lnd *test.LndMockServices
params Parameters
}
// newSuggestSwapsSetup creates a suggest swaps setup struct.
func newSuggestSwapsSetup(cfg *Config, lnd *test.LndMockServices,
params Parameters) *testSuggestSwapsSetup {
return &testSuggestSwapsSetup{
cfg: cfg,
lnd: lnd,
params: params,
}
}
// testSuggestSwaps tests getting swap suggestions. It takes a setup struct
// which contains custom setup for the test. If this struct is nil, it will
// use the default parameters and setup two channels (channel1 + channel2) with
// chanRule set for each.
func testSuggestSwaps(t *testing.T, setup *testSuggestSwapsSetup,
expected []loop.OutRequest) { expected []loop.OutRequest) {
t.Parallel() t.Parallel()
// Create a mock lnd with the set of channels set in our test case and // If our setup struct is nil, we replace it with our default test
// update our test case lnd to use these channels. // values.
lnd.Channels = channels if setup == nil {
cfg, lnd := newTestConfig()
lnd.Channels = []lndclient.ChannelInfo{
channel1, channel2,
}
params := defaultParameters
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule,
chanID2: chanRule,
}
setup = &testSuggestSwapsSetup{
cfg: cfg,
lnd: lnd,
params: params,
}
}
// Create a new manager, get our current set of parameters and update // Create a new manager, get our current set of parameters and update
// them to use the rules set by the test. // them to use the rules set by the test.
manager := NewManager(cfg) manager := NewManager(setup.cfg)
currentParams := manager.GetParameters()
currentParams.ChannelRules = rules
err := manager.SetParameters(currentParams) err := manager.SetParameters(setup.params)
require.NoError(t, err) require.NoError(t, err)
actual, err := manager.SuggestSwaps(context.Background()) actual, err := manager.SuggestSwaps(context.Background(), false)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected, actual) require.Equal(t, expected, actual)
} }

@ -98,10 +98,10 @@ func New(config *Config, lisCfg *listenerCfg) *Daemon {
cfg: config, cfg: config,
listenerCfg: lisCfg, listenerCfg: lisCfg,
// We have 3 goroutines that could potentially send an error. // We have 4 goroutines that could potentially send an error.
// We react on the first error but in case more than one exits // We react on the first error but in case more than one exits
// with an error we don't want them to block. // with an error we don't want them to block.
internalErrChan: make(chan error, 3), internalErrChan: make(chan error, 4),
} }
} }
@ -408,6 +408,19 @@ func (d *Daemon) initialize() error {
d.processStatusUpdates(d.mainCtx) d.processStatusUpdates(d.mainCtx)
}() }()
d.wg.Add(1)
go func() {
defer d.wg.Done()
log.Info("Starting liquidity manager")
err := d.liquidityMgr.Run(d.mainCtx)
if err != nil && err != context.Canceled {
d.internalErrChan <- err
}
log.Info("Liquidity manager stopped")
}()
// Last, start our internal error handler. This will return exactly one // Last, start our internal error handler. This will return exactly one
// error or nil on the main error channel to inform the caller that // error or nil on the main error channel to inform the caller that
// something went wrong or that shutdown is complete. We don't add to // something went wrong or that shutdown is complete. We don't add to

@ -11,6 +11,7 @@ import (
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/lightninglabs/lndclient" "github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop" "github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/liquidity" "github.com/lightninglabs/loop/liquidity"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/looprpc" "github.com/lightninglabs/loop/looprpc"
@ -79,6 +80,11 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
} }
} }
// Check that the label is valid.
if err := labels.Validate(in.Label); err != nil {
return nil, err
}
req := &loop.OutRequest{ req := &loop.OutRequest{
Amount: btcutil.Amount(in.Amt), Amount: btcutil.Amount(in.Amt),
DestAddr: sweepAddr, DestAddr: sweepAddr,
@ -477,6 +483,11 @@ func (s *swapClientServer) LoopIn(ctx context.Context,
return nil, err return nil, err
} }
// Check that the label is valid.
if err := labels.Validate(in.Label); err != nil {
return nil, err
}
req := &loop.LoopInRequest{ req := &loop.LoopInRequest{
Amount: btcutil.Amount(in.Amt), Amount: btcutil.Amount(in.Amt),
MaxMinerFee: btcutil.Amount(in.MaxMinerFee), MaxMinerFee: btcutil.Amount(in.MaxMinerFee),
@ -567,11 +578,22 @@ func (s *swapClientServer) GetLiquidityParams(_ context.Context,
SweepFeeRateSatPerVbyte: uint64(satPerByte), SweepFeeRateSatPerVbyte: uint64(satPerByte),
SweepConfTarget: cfg.SweepConfTarget, SweepConfTarget: cfg.SweepConfTarget,
FailureBackoffSec: uint64(cfg.FailureBackOff.Seconds()), FailureBackoffSec: uint64(cfg.FailureBackOff.Seconds()),
AutoLoopOut: cfg.AutoOut,
AutoOutBudgetSat: uint64(cfg.AutoFeeBudget),
AutoMaxInFlight: uint64(cfg.MaxAutoInFlight),
Rules: make( Rules: make(
[]*looprpc.LiquidityRule, 0, len(cfg.ChannelRules), []*looprpc.LiquidityRule, 0, len(cfg.ChannelRules),
), ),
} }
// Zero golang time is different to a zero unix time, so we only set
// our start date if it is non-zero.
if !cfg.AutoFeeStartDate.IsZero() {
rpcCfg.AutoOutBudgetStartSec = uint64(
cfg.AutoFeeStartDate.Unix(),
)
}
for channel, rule := range cfg.ChannelRules { for channel, rule := range cfg.ChannelRules {
rpcRule := &looprpc.LiquidityRule{ rpcRule := &looprpc.LiquidityRule{
ChannelId: channel.ToUint64(), ChannelId: channel.ToUint64(),
@ -606,12 +628,22 @@ func (s *swapClientServer) SetLiquidityParams(_ context.Context,
SweepConfTarget: in.Parameters.SweepConfTarget, SweepConfTarget: in.Parameters.SweepConfTarget,
FailureBackOff: time.Duration(in.Parameters.FailureBackoffSec) * FailureBackOff: time.Duration(in.Parameters.FailureBackoffSec) *
time.Second, time.Second,
AutoOut: in.Parameters.AutoLoopOut,
AutoFeeBudget: btcutil.Amount(in.Parameters.AutoOutBudgetSat),
MaxAutoInFlight: int(in.Parameters.AutoMaxInFlight),
ChannelRules: make( ChannelRules: make(
map[lnwire.ShortChannelID]*liquidity.ThresholdRule, map[lnwire.ShortChannelID]*liquidity.ThresholdRule,
len(in.Parameters.Rules), len(in.Parameters.Rules),
), ),
} }
// Zero unix time is different to zero golang time.
if in.Parameters.AutoOutBudgetStartSec != 0 {
params.AutoFeeStartDate = time.Unix(
int64(in.Parameters.AutoOutBudgetStartSec), 0,
)
}
for _, rule := range in.Parameters.Rules { for _, rule := range in.Parameters.Rules {
var ( var (
shortID = lnwire.NewShortChanIDFromInt(rule.ChannelId) shortID = lnwire.NewShortChanIDFromInt(rule.ChannelId)
@ -661,7 +693,7 @@ func rpcToRule(rule *looprpc.LiquidityRule) (*liquidity.ThresholdRule, error) {
func (s *swapClientServer) SuggestSwaps(ctx context.Context, func (s *swapClientServer) SuggestSwaps(ctx context.Context,
_ *looprpc.SuggestSwapsRequest) (*looprpc.SuggestSwapsResponse, error) { _ *looprpc.SuggestSwapsRequest) (*looprpc.SuggestSwapsResponse, error) {
swaps, err := s.liquidityMgr.SuggestSwaps(ctx) swaps, err := s.liquidityMgr.SuggestSwaps(ctx, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }

@ -8,6 +8,7 @@ import (
"github.com/lightninglabs/loop" "github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/liquidity" "github.com/lightninglabs/loop/liquidity"
"github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/ticker"
) )
// getClient returns an instance of the swap client. // getClient returns an instance of the swap client.
@ -35,6 +36,8 @@ func getClient(config *Config, lnd *lndclient.LndServices) (*loop.Client,
func getLiquidityManager(client *loop.Client) *liquidity.Manager { func getLiquidityManager(client *loop.Client) *liquidity.Manager {
mngrCfg := &liquidity.Config{ mngrCfg := &liquidity.Config{
AutoOutTicker: ticker.NewForce(liquidity.DefaultAutoOutTicker),
LoopOut: client.LoopOut,
LoopOutRestrictions: func(ctx context.Context) ( LoopOutRestrictions: func(ctx context.Context) (
*liquidity.Restrictions, error) { *liquidity.Restrictions, error) {

@ -125,8 +125,9 @@ func putLabel(bucket *bbolt.Bucket, label string) error {
return nil return nil
} }
if err := labels.Validate(label); err != nil { // Check that the label does not exceed our maximum length.
return err if len(label) > labels.MaxLength {
return labels.ErrLabelTooLong
} }
return bucket.Put(labelKey, []byte(label)) return bucket.Put(labelKey, []byte(label))

@ -151,6 +151,11 @@ type SwapCost struct {
Offchain btcutil.Amount Offchain btcutil.Amount
} }
// Total returns the total costs represented by swap costs.
func (s SwapCost) Total() btcutil.Amount {
return s.Server + s.Onchain + s.Offchain
}
// SwapStateData is all persistent data to describe the current swap state. // SwapStateData is all persistent data to describe the current swap state.
type SwapStateData struct { type SwapStateData struct {
// SwapState is the state the swap is in. // SwapState is the state the swap is in.

@ -14,7 +14,6 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient" "github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
@ -79,11 +78,6 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
currentHeight int32, request *LoopInRequest) (*loopInInitResult, currentHeight int32, request *LoopInRequest) (*loopInInitResult,
error) { error) {
// Before we start, check that the label is valid.
if err := labels.Validate(request.Label); err != nil {
return nil, err
}
// Request current server loop in terms and use these to calculate the // Request current server loop in terms and use these to calculate the
// swap fee that we should subtract from the swap amount in the payment // swap fee that we should subtract from the swap amount in the payment
// request that we send to the server. // request that we send to the server.

@ -13,7 +13,6 @@ import (
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/lightninglabs/lndclient" "github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/sweep" "github.com/lightninglabs/loop/sweep"
@ -88,11 +87,6 @@ type loopOutInitResult struct {
func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig, func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
currentHeight int32, request *OutRequest) (*loopOutInitResult, error) { currentHeight int32, request *OutRequest) (*loopOutInitResult, error) {
// Before we start, check that the label is valid.
if err := labels.Validate(request.Label); err != nil {
return nil, err
}
// Generate random preimage. // Generate random preimage.
var swapPreimage [32]byte var swapPreimage [32]byte
if _, err := rand.Read(swapPreimage[:]); err != nil { if _, err := rand.Read(swapPreimage[:]); err != nil {

@ -1579,7 +1579,26 @@ type LiquidityParameters struct {
//The amount of time we require pass since a channel was part of a failed //The amount of time we require pass since a channel was part of a failed
//swap due to off chain payment failure until it will be considered for swap //swap due to off chain payment failure until it will be considered for swap
//suggestions again, expressed in seconds. //suggestions again, expressed in seconds.
FailureBackoffSec uint64 `protobuf:"varint,9,opt,name=failure_backoff_sec,json=failureBackoffSec,proto3" json:"failure_backoff_sec,omitempty"` FailureBackoffSec uint64 `protobuf:"varint,9,opt,name=failure_backoff_sec,json=failureBackoffSec,proto3" json:"failure_backoff_sec,omitempty"`
//
//Set to true to enable automatic dispatch of loop out swaps. All swaps will
//be limited to the fee categories set by these parameters, and total
//expenditure will be limited to the auto out budget.
AutoLoopOut bool `protobuf:"varint,10,opt,name=auto_loop_out,json=autoLoopOut,proto3" json:"auto_loop_out,omitempty"`
//
//The total budget for automatically dispatched swaps since the budget start
//time, expressed in satoshis.
AutoOutBudgetSat uint64 `protobuf:"varint,11,opt,name=auto_out_budget_sat,json=autoOutBudgetSat,proto3" json:"auto_out_budget_sat,omitempty"`
//
//The start time for auto-out budget, expressed as a unix timestamp in
//seconds. If this value is 0, the budget will be applied for all
//automatically dispatched swaps. Swaps that were completed before this date
//will not be included in budget calculations.
AutoOutBudgetStartSec uint64 `protobuf:"varint,12,opt,name=auto_out_budget_start_sec,json=autoOutBudgetStartSec,proto3" json:"auto_out_budget_start_sec,omitempty"`
//
//The maximum number of automatically dispatched swaps that we allow to be in
//flight at any point in time.
AutoMaxInFlight uint64 `protobuf:"varint,13,opt,name=auto_max_in_flight,json=autoMaxInFlight,proto3" json:"auto_max_in_flight,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"` XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"` XXX_sizecache int32 `json:"-"`
@ -1673,6 +1692,34 @@ func (m *LiquidityParameters) GetFailureBackoffSec() uint64 {
return 0 return 0
} }
func (m *LiquidityParameters) GetAutoLoopOut() bool {
if m != nil {
return m.AutoLoopOut
}
return false
}
func (m *LiquidityParameters) GetAutoOutBudgetSat() uint64 {
if m != nil {
return m.AutoOutBudgetSat
}
return 0
}
func (m *LiquidityParameters) GetAutoOutBudgetStartSec() uint64 {
if m != nil {
return m.AutoOutBudgetStartSec
}
return 0
}
func (m *LiquidityParameters) GetAutoMaxInFlight() uint64 {
if m != nil {
return m.AutoMaxInFlight
}
return 0
}
type LiquidityRule struct { type LiquidityRule struct {
// //
//The short channel ID of the channel that this rule should be applied to. //The short channel ID of the channel that this rule should be applied to.
@ -1930,150 +1977,155 @@ func init() {
func init() { proto.RegisterFile("client.proto", fileDescriptor_014de31d7ac8c57c) } func init() { proto.RegisterFile("client.proto", fileDescriptor_014de31d7ac8c57c) }
var fileDescriptor_014de31d7ac8c57c = []byte{ var fileDescriptor_014de31d7ac8c57c = []byte{
// 2280 bytes of a gzipped FileDescriptorProto // 2365 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x58, 0xcf, 0x6f, 0x22, 0xc9, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x58, 0xcd, 0x6e, 0x23, 0xc7,
0xf5, 0x1f, 0xa0, 0x31, 0xf0, 0x68, 0xa0, 0x5d, 0x9e, 0xb1, 0x31, 0xeb, 0xd5, 0x7a, 0x7a, 0x76, 0xf1, 0x5f, 0x7e, 0x89, 0x64, 0xf1, 0x6b, 0xd4, 0xda, 0x95, 0x28, 0x5a, 0x86, 0xb5, 0x63, 0xef,
0xbe, 0xeb, 0xf1, 0x77, 0xc7, 0x64, 0xbd, 0xa7, 0x8c, 0x76, 0x23, 0x31, 0x18, 0xaf, 0x71, 0x6c, 0xdf, 0xb2, 0x6c, 0x2f, 0xff, 0x96, 0x2f, 0x89, 0x61, 0x07, 0xd0, 0x52, 0x94, 0xc5, 0x8d, 0x44,
0x20, 0x0d, 0x9e, 0xd5, 0x44, 0x91, 0x5a, 0x65, 0x28, 0xdb, 0xad, 0xa5, 0x7f, 0x4c, 0x77, 0x31, 0x32, 0x43, 0x6a, 0x0d, 0x07, 0x01, 0x06, 0x2d, 0xb2, 0x25, 0x0d, 0xcc, 0xf9, 0xd8, 0x99, 0xe6,
0x63, 0x6b, 0x95, 0x44, 0xca, 0x3f, 0xb0, 0x87, 0xfc, 0x07, 0xf9, 0x1b, 0x72, 0x4b, 0x6e, 0xb9, 0xae, 0x04, 0x23, 0x09, 0x90, 0x17, 0xf0, 0x21, 0x6f, 0x90, 0x67, 0xc8, 0x2d, 0x79, 0x84, 0x9c,
0xe6, 0x94, 0x1c, 0x73, 0x8d, 0x14, 0xe5, 0x90, 0xff, 0x21, 0xaa, 0x57, 0xdd, 0x4d, 0x37, 0x06, 0x92, 0x63, 0xae, 0x01, 0x82, 0x1c, 0xf2, 0x08, 0x01, 0x82, 0xaa, 0x9e, 0x19, 0xce, 0x50, 0x94,
0x47, 0x39, 0xe4, 0x46, 0xbf, 0xf7, 0xa9, 0x57, 0xf5, 0x7e, 0xd6, 0xa7, 0x00, 0x75, 0x3c, 0xb5, 0x82, 0x1c, 0x72, 0xe3, 0x54, 0xfd, 0xba, 0xba, 0xeb, 0xbb, 0x8a, 0x50, 0x9d, 0xcc, 0x2c, 0xe1,
0x98, 0xc3, 0x0f, 0x3c, 0xdf, 0xe5, 0x2e, 0x29, 0x4c, 0x5d, 0xd7, 0xf3, 0xbd, 0x71, 0x63, 0xe7, 0xc8, 0xe7, 0x9e, 0xef, 0x4a, 0x97, 0x15, 0x67, 0xae, 0xeb, 0xf9, 0xde, 0xa4, 0xb5, 0x73, 0xe5,
0xda, 0x75, 0xaf, 0xa7, 0xac, 0x49, 0x3d, 0xab, 0x49, 0x1d, 0xc7, 0xe5, 0x94, 0x5b, 0xae, 0x13, 0xba, 0x57, 0x33, 0xd1, 0xe6, 0x9e, 0xd5, 0xe6, 0x8e, 0xe3, 0x4a, 0x2e, 0x2d, 0xd7, 0x09, 0x14,
0x48, 0x98, 0xfe, 0x83, 0x02, 0xd5, 0x33, 0xd7, 0xf5, 0xfa, 0x33, 0x6e, 0xb0, 0x77, 0x33, 0x16, 0x4c, 0xff, 0x21, 0x0f, 0xf5, 0x53, 0xd7, 0xf5, 0x06, 0x73, 0x69, 0x88, 0xd7, 0x73, 0x11, 0x48,
0x70, 0xa2, 0x41, 0x8e, 0xda, 0xbc, 0x9e, 0xd9, 0xcd, 0xec, 0xe5, 0x0c, 0xf1, 0x93, 0x10, 0x50, 0xa6, 0x41, 0x8e, 0xdb, 0xb2, 0x99, 0xd9, 0xcd, 0xec, 0xe5, 0x0c, 0xfc, 0xc9, 0x18, 0xe4, 0xa7,
0x26, 0x2c, 0xe0, 0xf5, 0xec, 0x6e, 0x66, 0xaf, 0x64, 0xe0, 0x6f, 0xd2, 0x84, 0xc7, 0x36, 0xbd, 0x22, 0x90, 0xcd, 0xec, 0x6e, 0x66, 0xaf, 0x6c, 0xd0, 0x6f, 0xd6, 0x86, 0xc7, 0x36, 0xbf, 0x31,
0x35, 0x83, 0x0f, 0xd4, 0x33, 0x7d, 0x77, 0xc6, 0x2d, 0xe7, 0xda, 0xbc, 0x62, 0xac, 0x9e, 0xc3, 0x83, 0xb7, 0xdc, 0x33, 0x7d, 0x77, 0x2e, 0x2d, 0xe7, 0xca, 0xbc, 0x14, 0xa2, 0x99, 0xa3, 0x63,
0x65, 0xeb, 0x36, 0xbd, 0x1d, 0x7e, 0xa0, 0x9e, 0x21, 0x35, 0xc7, 0x8c, 0x91, 0x2f, 0x61, 0x53, 0xeb, 0x36, 0xbf, 0x19, 0xbd, 0xe5, 0x9e, 0xa1, 0x38, 0xc7, 0x42, 0xb0, 0xcf, 0x61, 0x13, 0x0f,
0x2c, 0xf0, 0x7c, 0xe6, 0xd1, 0xbb, 0xd4, 0x12, 0x05, 0x97, 0x6c, 0xd8, 0xf4, 0x76, 0x80, 0xca, 0x78, 0xbe, 0xf0, 0xf8, 0x6d, 0xea, 0x48, 0x9e, 0x8e, 0x6c, 0xd8, 0xfc, 0x66, 0x48, 0xcc, 0xc4,
0xc4, 0xa2, 0x5d, 0x50, 0xe3, 0x5d, 0x04, 0x34, 0x8f, 0x50, 0x08, 0xad, 0x0b, 0xc4, 0xa7, 0x50, 0xa1, 0x5d, 0xa8, 0xc6, 0xb7, 0x20, 0xb4, 0x40, 0x50, 0x08, 0xa5, 0x23, 0xe2, 0x03, 0xa8, 0x27,
0x4d, 0x98, 0x15, 0x07, 0x5f, 0x43, 0x8c, 0x1a, 0x9b, 0x6b, 0xd9, 0x9c, 0xe8, 0x50, 0x11, 0x28, 0xc4, 0xe2, 0xc3, 0xd7, 0x08, 0x53, 0x8d, 0xc5, 0x1d, 0xda, 0x92, 0xe9, 0x50, 0x43, 0x94, 0x6d,
0xdb, 0x72, 0x98, 0x8f, 0x86, 0x0a, 0x08, 0x2a, 0xdb, 0xf4, 0xf6, 0x5c, 0xc8, 0x84, 0xa5, 0xcf, 0x39, 0xc2, 0x27, 0x41, 0x45, 0x02, 0x55, 0x6c, 0x7e, 0x73, 0x86, 0x34, 0x94, 0xf4, 0x09, 0x68,
0x41, 0x13, 0x31, 0x33, 0xdd, 0x19, 0x37, 0xc7, 0x37, 0xd4, 0x71, 0xd8, 0xb4, 0x5e, 0xdc, 0xcd, 0x68, 0x33, 0xd3, 0x9d, 0x4b, 0x73, 0x72, 0xcd, 0x1d, 0x47, 0xcc, 0x9a, 0xa5, 0xdd, 0xcc, 0x5e,
0xec, 0x29, 0xaf, 0xb3, 0xf5, 0x8c, 0x51, 0x9d, 0xca, 0x28, 0xb5, 0xa5, 0x86, 0xec, 0xc3, 0xba, 0xfe, 0x45, 0xb6, 0x99, 0x31, 0xea, 0x33, 0x65, 0xa5, 0x8e, 0xe2, 0xb0, 0x7d, 0x58, 0x77, 0xe7,
0x3b, 0xe3, 0xd7, 0xae, 0x70, 0x42, 0xa0, 0xcd, 0x80, 0xf1, 0x7a, 0x79, 0x37, 0xb7, 0xa7, 0x18, 0xf2, 0xca, 0x45, 0x25, 0x10, 0x6d, 0x06, 0x42, 0x36, 0x2b, 0xbb, 0xb9, 0xbd, 0xbc, 0xd1, 0x88,
0xb5, 0x48, 0x21, 0xb0, 0x43, 0xc6, 0x05, 0x36, 0xf8, 0xc0, 0x98, 0x67, 0x8e, 0x5d, 0xe7, 0xca, 0x18, 0x88, 0x1d, 0x09, 0x89, 0xd8, 0xe0, 0xad, 0x10, 0x9e, 0x39, 0x71, 0x9d, 0x4b, 0x53, 0x72,
0xe4, 0xd4, 0xbf, 0x66, 0xbc, 0x5e, 0xda, 0xcd, 0xec, 0xe5, 0x8d, 0x1a, 0x2a, 0xda, 0xae, 0x73, 0xff, 0x4a, 0xc8, 0x66, 0x79, 0x37, 0xb3, 0x57, 0x30, 0x1a, 0xc4, 0xe8, 0xb8, 0xce, 0xe5, 0x98,
0x35, 0x42, 0x31, 0x79, 0x09, 0xe4, 0x86, 0x4f, 0xc7, 0x08, 0xb5, 0x7c, 0x5b, 0x26, 0xab, 0x5e, 0xc8, 0xec, 0x53, 0x60, 0xd7, 0x72, 0x36, 0x21, 0xa8, 0xe5, 0xdb, 0xca, 0x59, 0xcd, 0x1a, 0x81,
0x41, 0xf0, 0xba, 0xd0, 0xb4, 0x93, 0x0a, 0xf2, 0x0a, 0xb6, 0x31, 0x38, 0xde, 0xec, 0x72, 0x6a, 0xd7, 0x91, 0xd3, 0x49, 0x32, 0xd8, 0x17, 0xb0, 0x4d, 0xc6, 0xf1, 0xe6, 0x17, 0x33, 0x6b, 0x42,
0x8d, 0x51, 0x68, 0x4e, 0x18, 0x9d, 0x4c, 0x2d, 0x87, 0xd5, 0x41, 0x9c, 0xde, 0xd8, 0x12, 0x80, 0x44, 0x73, 0x2a, 0xf8, 0x74, 0x66, 0x39, 0xa2, 0x09, 0xf8, 0x7a, 0x63, 0x0b, 0x01, 0xc3, 0x05,
0xc1, 0x5c, 0x7f, 0x14, 0xaa, 0xc9, 0x63, 0xc8, 0x4f, 0xe9, 0x25, 0x9b, 0xd6, 0x55, 0xcc, 0xab, 0xff, 0x28, 0x64, 0xb3, 0xc7, 0x50, 0x98, 0xf1, 0x0b, 0x31, 0x6b, 0x56, 0xc9, 0xaf, 0xea, 0x43,
0xfc, 0xd0, 0xff, 0x91, 0x81, 0x8a, 0xa8, 0x88, 0xae, 0xb3, 0xba, 0x20, 0x16, 0xd3, 0x92, 0xbd, 0xff, 0x7b, 0x06, 0x6a, 0x18, 0x11, 0x3d, 0xe7, 0xfe, 0x80, 0x58, 0x76, 0x4b, 0xf6, 0x8e, 0x5b,
0x97, 0x96, 0x7b, 0x01, 0xcf, 0xdd, 0x0f, 0xf8, 0x36, 0x14, 0xa7, 0x34, 0xe0, 0xe6, 0x8d, 0xeb, 0xee, 0x18, 0x3c, 0x77, 0xd7, 0xe0, 0xdb, 0x50, 0x9a, 0xf1, 0x40, 0x9a, 0xd7, 0xae, 0x47, 0x31,
0x61, 0x0d, 0xa8, 0x46, 0x41, 0x7c, 0x9f, 0xb8, 0x1e, 0x79, 0x06, 0x15, 0x76, 0xcb, 0x99, 0xef, 0x50, 0x35, 0x8a, 0xf8, 0x7d, 0xe2, 0x7a, 0xec, 0x7d, 0xa8, 0x89, 0x1b, 0x29, 0x7c, 0x87, 0xcf,
0xd0, 0xa9, 0x29, 0x9c, 0xc6, 0xc4, 0x17, 0x0d, 0x35, 0x12, 0x9e, 0xf0, 0xe9, 0x98, 0xec, 0x81, 0x4c, 0x54, 0x9a, 0x1c, 0x5f, 0x32, 0xaa, 0x11, 0xf1, 0x44, 0xce, 0x26, 0x6c, 0x0f, 0xb4, 0xd8,
0x16, 0x87, 0x2a, 0x8a, 0xea, 0x1a, 0x06, 0xaa, 0x1a, 0x05, 0x2a, 0x0c, 0x6a, 0xec, 0x69, 0x21, 0x54, 0x91, 0x55, 0xd7, 0xc8, 0x50, 0xf5, 0xc8, 0x50, 0xa1, 0x51, 0x63, 0x4d, 0x8b, 0x49, 0x4d,
0xe9, 0xe9, 0x3f, 0x33, 0xa0, 0x62, 0x91, 0xb2, 0xc0, 0x73, 0x9d, 0x80, 0x11, 0x02, 0x59, 0x6b, 0xff, 0x91, 0x81, 0x2a, 0x05, 0xa9, 0x08, 0x3c, 0xd7, 0x09, 0x04, 0x63, 0x90, 0xb5, 0xa6, 0xa4,
0x82, 0x7e, 0x96, 0x30, 0xe7, 0x59, 0x6b, 0x22, 0x0e, 0x69, 0x4d, 0xcc, 0xcb, 0x3b, 0xce, 0x02, 0x67, 0x99, 0x7c, 0x9e, 0xb5, 0xa6, 0xf8, 0x48, 0x6b, 0x6a, 0x5e, 0xdc, 0x4a, 0x11, 0x90, 0x0e,
0xf4, 0x41, 0x35, 0x0a, 0xd6, 0xe4, 0xb5, 0xf8, 0x24, 0xcf, 0x41, 0xc5, 0xfd, 0xe9, 0x64, 0xe2, 0x55, 0xa3, 0x68, 0x4d, 0x5f, 0xe0, 0x27, 0x7b, 0x06, 0x55, 0xba, 0x9f, 0x4f, 0xa7, 0xbe, 0x08,
0xb3, 0x20, 0x90, 0xed, 0x81, 0x0b, 0xcb, 0x42, 0xde, 0x92, 0x62, 0x72, 0x00, 0x1b, 0x49, 0x98, 0x02, 0x95, 0x1e, 0x74, 0xb0, 0x82, 0xf4, 0x43, 0x45, 0x66, 0xcf, 0x61, 0x23, 0x09, 0x33, 0x1d,
0xe9, 0x78, 0x87, 0x1f, 0x82, 0x1b, 0xf4, 0xb8, 0x24, 0x53, 0x1a, 0x22, 0x7b, 0xa8, 0x20, 0x9f, 0xef, 0xe0, 0x6d, 0x70, 0x4d, 0x1a, 0x97, 0x95, 0x4b, 0x43, 0x64, 0x9f, 0x18, 0xec, 0x93, 0x30,
0x87, 0x15, 0x10, 0xe1, 0x25, 0x3c, 0x8f, 0x70, 0x2d, 0x01, 0x1f, 0x20, 0xfa, 0x39, 0x54, 0x03, 0x02, 0x22, 0xbc, 0x82, 0x17, 0x08, 0xae, 0x25, 0xe0, 0x43, 0x42, 0x3f, 0x83, 0x7a, 0x20, 0xfc,
0xe6, 0xbf, 0x67, 0xbe, 0x69, 0xb3, 0x20, 0xa0, 0xd7, 0x0c, 0x43, 0x50, 0x32, 0x2a, 0x52, 0x7a, 0x37, 0xc2, 0x37, 0x6d, 0x11, 0x04, 0xfc, 0x4a, 0x90, 0x09, 0xca, 0x46, 0x4d, 0x51, 0xcf, 0x14,
0x2e, 0x85, 0xba, 0x06, 0xd5, 0x73, 0xd7, 0xb1, 0xb8, 0xeb, 0x87, 0x59, 0xd5, 0x7f, 0xaf, 0x00, 0x51, 0xd7, 0xa0, 0x7e, 0xe6, 0x3a, 0x96, 0x74, 0xfd, 0xd0, 0xab, 0xfa, 0xef, 0xf3, 0x00, 0xa8,
0x08, 0xef, 0x87, 0x9c, 0xf2, 0x59, 0xb0, 0xb4, 0xeb, 0x45, 0x34, 0xb2, 0x2b, 0xa3, 0x51, 0x5e, 0xfd, 0x48, 0x72, 0x39, 0x0f, 0x56, 0x66, 0x3d, 0x5a, 0x23, 0x7b, 0xaf, 0x35, 0x2a, 0xcb, 0xd6,
0x8c, 0x86, 0xc2, 0xef, 0x3c, 0x99, 0xe8, 0xea, 0xe1, 0xfa, 0x41, 0x38, 0x7f, 0x0e, 0xc4, 0x1e, 0xc8, 0xcb, 0x5b, 0x4f, 0x39, 0xba, 0x7e, 0xb0, 0xfe, 0x3c, 0xac, 0x3f, 0xcf, 0xf1, 0x8e, 0xf1,
0xa3, 0x3b, 0x8f, 0x19, 0xa8, 0x26, 0x7b, 0x90, 0x0f, 0x38, 0xe5, 0xb2, 0xeb, 0xab, 0x87, 0x24, 0xad, 0x27, 0x0c, 0x62, 0xb3, 0x3d, 0x28, 0x04, 0x92, 0x4b, 0x95, 0xf5, 0xf5, 0x03, 0x96, 0xc2,
0x85, 0x13, 0x67, 0x61, 0x86, 0x04, 0x90, 0xaf, 0xa1, 0x7a, 0x45, 0xad, 0xe9, 0xcc, 0x67, 0xa6, 0xe1, 0x5b, 0x84, 0xa1, 0x00, 0xec, 0x2b, 0xa8, 0x5f, 0x72, 0x6b, 0x36, 0xf7, 0x85, 0xe9, 0x0b,
0xcf, 0x68, 0xe0, 0x3a, 0xf5, 0x2a, 0x2e, 0xd9, 0x8c, 0x97, 0x1c, 0x4b, 0xb5, 0x81, 0x5a, 0xa3, 0x1e, 0xb8, 0x4e, 0xb3, 0x4e, 0x47, 0x36, 0xe3, 0x23, 0xc7, 0x8a, 0x6d, 0x10, 0xd7, 0xa8, 0x5d,
0x72, 0x95, 0xfc, 0x24, 0x9f, 0x41, 0xcd, 0x72, 0x2c, 0x6e, 0xc9, 0x9e, 0xe0, 0x96, 0x1d, 0x4d, 0x26, 0x3f, 0xd9, 0x87, 0xd0, 0xb0, 0x1c, 0x4b, 0x5a, 0x2a, 0x27, 0xa4, 0x65, 0x47, 0xd5, 0xa3,
0x8f, 0xea, 0x5c, 0x3c, 0xb2, 0x6c, 0x71, 0x22, 0x0d, 0xcb, 0x70, 0xe6, 0x4d, 0x28, 0x67, 0x12, 0xbe, 0x20, 0x8f, 0x2d, 0x1b, 0x5f, 0xa4, 0x51, 0x18, 0xce, 0xbd, 0x29, 0x97, 0x42, 0x21, 0x55,
0x29, 0x67, 0x48, 0x55, 0xc8, 0x2f, 0x50, 0x8c, 0xc8, 0xc5, 0x84, 0x17, 0x96, 0x27, 0x7c, 0x79, 0x0d, 0xa9, 0x23, 0xfd, 0x9c, 0xc8, 0x84, 0x5c, 0x76, 0x78, 0x71, 0xb5, 0xc3, 0x57, 0x3b, 0xb0,
0x02, 0xd5, 0x15, 0x09, 0x5c, 0x51, 0x1e, 0x95, 0x55, 0xe5, 0xf1, 0x09, 0x94, 0xc7, 0x6e, 0xc0, 0x7a, 0x8f, 0x03, 0xef, 0x09, 0x8f, 0xda, 0x7d, 0xe1, 0xf1, 0x1e, 0x54, 0x26, 0x6e, 0x20, 0x4d,
0x4d, 0x99, 0x5f, 0x9c, 0x50, 0x39, 0x03, 0x84, 0x68, 0x88, 0x12, 0xf2, 0x14, 0x54, 0x04, 0xb8, 0xe5, 0x5f, 0xaa, 0x50, 0x39, 0x03, 0x90, 0x34, 0x22, 0x0a, 0x7b, 0x0a, 0x55, 0x02, 0xb8, 0xce,
0xce, 0xf8, 0x86, 0x5a, 0x0e, 0x0e, 0x9a, 0x9c, 0x81, 0x8b, 0xfa, 0x52, 0x24, 0xda, 0x4b, 0x42, 0xe4, 0x9a, 0x5b, 0x0e, 0x15, 0x9a, 0x9c, 0x41, 0x87, 0x06, 0x8a, 0x84, 0xe9, 0xa5, 0x20, 0x97,
0xae, 0xae, 0x24, 0x06, 0xe4, 0xcc, 0x44, 0x4c, 0x28, 0x9b, 0x37, 0x4d, 0x2d, 0xd9, 0x34, 0x04, 0x97, 0x0a, 0x03, 0xaa, 0x66, 0x12, 0x26, 0xa4, 0x2d, 0x92, 0xa6, 0x91, 0x4c, 0x1a, 0x06, 0xda,
0xb4, 0x33, 0x2b, 0xe0, 0x22, 0x5b, 0x41, 0x54, 0x4a, 0x3f, 0x81, 0xf5, 0x84, 0x2c, 0x6c, 0xa6, 0xa9, 0x15, 0x48, 0xf4, 0x56, 0x10, 0x85, 0xd2, 0x4f, 0x60, 0x3d, 0x41, 0x0b, 0x93, 0xe9, 0x23,
0x17, 0x90, 0x17, 0xf3, 0x21, 0xa8, 0x67, 0x76, 0x73, 0x7b, 0xe5, 0xc3, 0x8d, 0x7b, 0x89, 0x9e, 0x28, 0x60, 0x7d, 0x08, 0x9a, 0x99, 0xdd, 0xdc, 0x5e, 0xe5, 0x60, 0xe3, 0x8e, 0xa3, 0xe7, 0x81,
0x05, 0x86, 0x44, 0xe8, 0x4f, 0xa1, 0x26, 0x84, 0x5d, 0xe7, 0xca, 0x8d, 0x66, 0x4e, 0x35, 0x6e, 0xa1, 0x10, 0xfa, 0x53, 0x68, 0x20, 0xb1, 0xe7, 0x5c, 0xba, 0x51, 0xcd, 0xa9, 0xc7, 0xa9, 0x58,
0x45, 0x55, 0x14, 0x9e, 0x5e, 0x05, 0x75, 0xc4, 0x7c, 0x3b, 0xde, 0xf2, 0xd7, 0x50, 0xeb, 0x3a, 0xc5, 0xc0, 0xd3, 0xeb, 0x50, 0x1d, 0x0b, 0xdf, 0x8e, 0xaf, 0xfc, 0x35, 0x34, 0x7a, 0x4e, 0x48,
0xa1, 0x24, 0xdc, 0xf0, 0xff, 0xa0, 0x66, 0x5b, 0x8e, 0x1c, 0x4a, 0xd4, 0x76, 0x67, 0x0e, 0x0f, 0x09, 0x2f, 0xfc, 0x3f, 0x68, 0xd8, 0x96, 0xa3, 0x8a, 0x12, 0xb7, 0xdd, 0xb9, 0x23, 0x43, 0x87,
0x13, 0x5e, 0xb1, 0x2d, 0x47, 0xd8, 0x6f, 0xa1, 0x10, 0x71, 0xd1, 0xf0, 0x0a, 0x71, 0x6b, 0x21, 0xd7, 0x6c, 0xcb, 0x41, 0xf9, 0x87, 0x44, 0x24, 0x5c, 0x54, 0xbc, 0x42, 0xdc, 0x5a, 0x88, 0x53,
0x4e, 0xce, 0x2f, 0x89, 0x3b, 0x55, 0x8a, 0x19, 0x2d, 0x7b, 0xaa, 0x14, 0xb3, 0x5a, 0xee, 0x54, 0xf5, 0x4b, 0xe1, 0x5e, 0xe6, 0x4b, 0x19, 0x2d, 0xfb, 0x32, 0x5f, 0xca, 0x6a, 0xb9, 0x97, 0xf9,
0x29, 0xe6, 0x34, 0xe5, 0x54, 0x29, 0x2a, 0x5a, 0xfe, 0x54, 0x29, 0x16, 0xb4, 0xa2, 0xfe, 0xe7, 0x52, 0x4e, 0xcb, 0xbf, 0xcc, 0x97, 0xf2, 0x5a, 0xe1, 0x65, 0xbe, 0x54, 0xd4, 0x4a, 0xfa, 0x9f,
0x0c, 0x68, 0xfd, 0x19, 0xff, 0x9f, 0x1e, 0x01, 0x2f, 0x37, 0xcb, 0x31, 0xc7, 0x53, 0xfe, 0xde, 0x32, 0xa0, 0x0d, 0xe6, 0xf2, 0x7f, 0xfa, 0x04, 0x6a, 0x6e, 0x96, 0x63, 0x4e, 0x66, 0xf2, 0x8d,
0x9c, 0xb0, 0x29, 0xa7, 0x98, 0xee, 0xbc, 0xa1, 0xda, 0x96, 0xd3, 0x9e, 0xf2, 0xf7, 0x47, 0x42, 0x39, 0x15, 0x33, 0xc9, 0xc9, 0xdd, 0x05, 0xa3, 0x6a, 0x5b, 0x4e, 0x67, 0x26, 0xdf, 0x1c, 0x21,
0x16, 0x5d, 0x81, 0x09, 0x54, 0x29, 0x44, 0xd1, 0xdb, 0x18, 0xf5, 0x1f, 0xdc, 0xf9, 0x5d, 0x06, 0x2d, 0x6a, 0x81, 0x09, 0x54, 0x39, 0x44, 0xf1, 0x9b, 0x18, 0xf5, 0x1f, 0xd4, 0xf9, 0x5d, 0x06,
0xd4, 0x9f, 0xcd, 0x5c, 0xce, 0x56, 0x0f, 0x7d, 0x2c, 0xbc, 0xf9, 0xa4, 0xcd, 0xe2, 0x1e, 0x30, 0xaa, 0x3f, 0x9b, 0xbb, 0x52, 0xdc, 0x5f, 0xf4, 0x29, 0xf0, 0x16, 0x95, 0x36, 0x4b, 0x77, 0xc0,
0x9e, 0x4f, 0xd9, 0x7b, 0x43, 0x3b, 0xb7, 0x64, 0x68, 0x3f, 0x78, 0x61, 0x29, 0x0f, 0x5e, 0x58, 0x64, 0x51, 0x65, 0xef, 0x14, 0xed, 0xdc, 0x8a, 0xa2, 0xfd, 0x60, 0xc3, 0xca, 0x3f, 0xd8, 0xb0,
0xfa, 0x0f, 0x19, 0x91, 0xf5, 0xf0, 0x98, 0x61, 0xc8, 0x77, 0x41, 0x8d, 0xae, 0x21, 0x33, 0xa0, 0xf4, 0x1f, 0x32, 0xe8, 0xf5, 0xf0, 0x99, 0xa1, 0xc9, 0x77, 0xa1, 0x1a, 0xb5, 0x21, 0x33, 0xe0,
0xd1, 0x81, 0x21, 0x90, 0xf7, 0xd0, 0x90, 0x22, 0x53, 0xc1, 0x06, 0xc3, 0x1d, 0x83, 0x9b, 0x18, 0xd1, 0x83, 0x21, 0x50, 0x7d, 0x68, 0xc4, 0x69, 0x52, 0xa1, 0x04, 0xa3, 0x1b, 0x83, 0xeb, 0x18,
0x19, 0x32, 0x15, 0xa1, 0x1b, 0x48, 0x55, 0xb8, 0xe0, 0x63, 0x80, 0x44, 0x2c, 0xf3, 0xe8, 0x67, 0x19, 0x4e, 0x2a, 0xc8, 0x1b, 0x2a, 0x56, 0x78, 0xe0, 0x5d, 0x80, 0x84, 0x2d, 0x0b, 0xa4, 0x67,
0x69, 0x9c, 0x08, 0xa4, 0x0c, 0xa1, 0xa2, 0xe5, 0xf5, 0xbf, 0xc8, 0x2a, 0xf8, 0x6f, 0x8f, 0xf4, 0x79, 0x92, 0x30, 0xa4, 0x32, 0x61, 0x5e, 0x2b, 0xe8, 0x7f, 0x56, 0x51, 0xf0, 0xdf, 0x3e, 0xe9,
0x29, 0x54, 0xe7, 0x84, 0x05, 0x31, 0xf2, 0x06, 0x55, 0xbd, 0x88, 0xb1, 0x08, 0xd4, 0xff, 0x87, 0x03, 0xa8, 0x2f, 0x06, 0x16, 0xc2, 0xa8, 0x0e, 0x5a, 0xf5, 0xa2, 0x89, 0x05, 0x51, 0x1f, 0x87,
0x73, 0x44, 0x72, 0x87, 0xf4, 0xb1, 0x6b, 0x42, 0x33, 0x14, 0x8a, 0xd0, 0x24, 0x72, 0x0c, 0x11, 0x75, 0x44, 0xcd, 0x0e, 0xe9, 0x67, 0x37, 0x90, 0x33, 0x42, 0x46, 0x28, 0x92, 0x66, 0x0c, 0xb4,
0x57, 0x7a, 0x67, 0x33, 0x87, 0x9b, 0x48, 0xd8, 0xe4, 0xad, 0x5a, 0xc3, 0x78, 0x4a, 0xf9, 0x91, 0x2b, 0xbf, 0xb5, 0x85, 0x23, 0x4d, 0x1a, 0xd8, 0x54, 0x57, 0x6d, 0x90, 0x3d, 0x15, 0xfd, 0x08,
0xc8, 0xed, 0xc3, 0x0e, 0xea, 0x35, 0xa8, 0x8c, 0xdc, 0xef, 0x98, 0x13, 0x37, 0xdb, 0x57, 0x50, 0x7d, 0xfb, 0xb0, 0x82, 0x7a, 0x03, 0x6a, 0x63, 0xf7, 0x3b, 0xe1, 0xc4, 0xc9, 0xf6, 0x25, 0xd4,
0x8d, 0x04, 0xa1, 0x8b, 0xfb, 0xb0, 0xc6, 0x51, 0x12, 0x76, 0xf7, 0x7c, 0x8c, 0x9f, 0x05, 0x94, 0x23, 0x42, 0xa8, 0xe2, 0x3e, 0xac, 0x49, 0xa2, 0x84, 0xd9, 0xbd, 0x28, 0xe3, 0xa7, 0x01, 0x97,
0x23, 0xd8, 0x08, 0x11, 0xfa, 0x1f, 0xb2, 0x50, 0x8a, 0xa5, 0xa2, 0x48, 0x2e, 0x69, 0xc0, 0x4c, 0x04, 0x36, 0x42, 0x84, 0xfe, 0x87, 0x2c, 0x94, 0x63, 0x2a, 0x06, 0xc9, 0x05, 0x0f, 0x84, 0x69,
0x9b, 0x8e, 0xa9, 0xef, 0xba, 0x4e, 0xd8, 0xe3, 0xaa, 0x10, 0x9e, 0x87, 0x32, 0x31, 0xc2, 0x22, 0xf3, 0x09, 0xf7, 0x5d, 0xd7, 0x09, 0x73, 0xbc, 0x8a, 0xc4, 0xb3, 0x90, 0x86, 0x25, 0x2c, 0xd2,
0x3f, 0x6e, 0x68, 0x70, 0x83, 0xd1, 0x51, 0x8d, 0x72, 0x28, 0x3b, 0xa1, 0xc1, 0x0d, 0x79, 0x01, 0xe3, 0x9a, 0x07, 0xd7, 0x64, 0x9d, 0xaa, 0x51, 0x09, 0x69, 0x27, 0x3c, 0xb8, 0x66, 0x1f, 0x81,
0x5a, 0x04, 0xf1, 0x7c, 0x66, 0xd9, 0xe2, 0xe6, 0x93, 0xf7, 0x73, 0x2d, 0x94, 0x0f, 0x42, 0xb1, 0x16, 0x41, 0x3c, 0x5f, 0x58, 0x36, 0x76, 0x3e, 0xd5, 0x9f, 0x1b, 0x21, 0x7d, 0x18, 0x92, 0xb1,
0x18, 0xf0, 0xb2, 0xc9, 0x4c, 0x8f, 0x5a, 0x13, 0xd3, 0x16, 0x51, 0x94, 0x9c, 0xb3, 0x2a, 0xe5, 0xc0, 0xab, 0x24, 0x33, 0x3d, 0x6e, 0x4d, 0x4d, 0x1b, 0xad, 0xa8, 0x66, 0xce, 0xba, 0xa2, 0x0f,
0x03, 0x6a, 0x4d, 0xce, 0x03, 0xca, 0xc9, 0x17, 0xf0, 0x24, 0x41, 0x4c, 0x13, 0x70, 0xd9, 0xc5, 0xb9, 0x35, 0x3d, 0x0b, 0xb8, 0x64, 0x9f, 0xc1, 0x93, 0xc4, 0x60, 0x9a, 0x80, 0xab, 0x2c, 0x66,
0xc4, 0x8f, 0x99, 0x69, 0xbc, 0xe4, 0x29, 0xa8, 0xe2, 0xc6, 0x30, 0xc7, 0x3e, 0xa3, 0x9c, 0x4d, 0x7e, 0x3c, 0x99, 0xc6, 0x47, 0x9e, 0x42, 0x15, 0x3b, 0x86, 0x39, 0xf1, 0x05, 0x97, 0x62, 0x1a,
0xc2, 0x3e, 0x2e, 0x0b, 0x59, 0x5b, 0x8a, 0x48, 0x1d, 0x0a, 0xec, 0xd6, 0xb3, 0x7c, 0x36, 0xc1, 0xe6, 0x71, 0x05, 0x69, 0x1d, 0x45, 0x62, 0x4d, 0x28, 0x8a, 0x1b, 0xcf, 0xf2, 0xc5, 0x94, 0x3a,
0x1b, 0xa3, 0x68, 0x44, 0x9f, 0x62, 0x71, 0xc0, 0x5d, 0x9f, 0x5e, 0x33, 0xd3, 0xa1, 0x36, 0xc3, 0x46, 0xc9, 0x88, 0x3e, 0xf1, 0x70, 0x20, 0x5d, 0x9f, 0x5f, 0x09, 0xd3, 0xe1, 0xb6, 0xa0, 0xec,
0xee, 0x2e, 0x19, 0xe5, 0x50, 0xd6, 0xa3, 0x36, 0xd3, 0x3f, 0x82, 0xed, 0x6f, 0x18, 0x3f, 0xb3, 0x2e, 0x1b, 0x95, 0x90, 0xd6, 0xe7, 0xb6, 0xd0, 0xdf, 0x81, 0xed, 0xaf, 0x85, 0x3c, 0xb5, 0x5e,
0xde, 0xcd, 0xac, 0x89, 0xc5, 0xef, 0x06, 0xd4, 0xa7, 0xf3, 0x29, 0xf8, 0xa7, 0x1c, 0x6c, 0xa4, 0xcf, 0xad, 0xa9, 0x25, 0x6f, 0x87, 0xdc, 0xe7, 0x8b, 0x2a, 0xf8, 0xaf, 0x3c, 0x6c, 0xa4, 0x59,
0x55, 0x8c, 0x33, 0x5f, 0xdc, 0x40, 0x79, 0x7f, 0x36, 0x65, 0x51, 0x76, 0xe6, 0x37, 0x66, 0x0c, 0x42, 0x0a, 0x1f, 0x3b, 0x50, 0xc1, 0x9f, 0xcf, 0x44, 0xe4, 0x9d, 0x45, 0xc7, 0x8c, 0xc1, 0xc6,
0x36, 0x66, 0x53, 0x66, 0x48, 0x10, 0xf9, 0x1a, 0x76, 0xe6, 0x25, 0xe6, 0x8b, 0x3b, 0x30, 0xa0, 0x7c, 0x26, 0x0c, 0x05, 0x62, 0x5f, 0xc1, 0xce, 0x22, 0xc4, 0x7c, 0xec, 0x81, 0x01, 0x97, 0xa6,
0xdc, 0xf4, 0x98, 0x6f, 0xbe, 0x17, 0x37, 0x3d, 0x46, 0x1f, 0xbb, 0x52, 0x56, 0x9b, 0x41, 0xb9, 0x27, 0x7c, 0xf3, 0x0d, 0x76, 0x7a, 0xb2, 0x3e, 0x65, 0xa5, 0x8a, 0x36, 0x83, 0x4b, 0x8c, 0xb8,
0xa8, 0xb8, 0x01, 0xf3, 0xdf, 0x08, 0x35, 0xf9, 0x0c, 0xb4, 0x24, 0x19, 0x34, 0x3d, 0xcf, 0xc6, 0xa1, 0xf0, 0x5f, 0x21, 0x9b, 0x7d, 0x08, 0x5a, 0x72, 0x18, 0x34, 0x3d, 0xcf, 0x26, 0x4f, 0xe4,
0x4c, 0x28, 0xf1, 0x34, 0x13, 0xf1, 0xf2, 0x6c, 0xf2, 0x12, 0x04, 0xc7, 0x37, 0x53, 0x11, 0xf6, 0xe3, 0x6a, 0x86, 0xf6, 0xf2, 0x6c, 0xf6, 0x29, 0xe0, 0x8c, 0x6f, 0xa6, 0x2c, 0xec, 0xd9, 0x61,
0xec, 0xb0, 0xe9, 0x85, 0x8d, 0x39, 0xf1, 0x17, 0xf0, 0x57, 0xd0, 0x58, 0xfe, 0x60, 0xc0, 0x55, 0xd2, 0xa3, 0x8c, 0xc5, 0xe0, 0x8f, 0xf0, 0x2f, 0xa0, 0xb5, 0x7a, 0x61, 0xa0, 0x53, 0x05, 0x3a,
0x79, 0x5c, 0xb5, 0xb9, 0xe4, 0xd1, 0x20, 0xd6, 0xa6, 0x5f, 0x05, 0x22, 0x83, 0x6b, 0x88, 0x9f, 0xb5, 0xb9, 0x62, 0x69, 0xc0, 0xb3, 0xe9, 0xad, 0x00, 0x3d, 0xb8, 0x46, 0xf8, 0xc5, 0x56, 0x80,
0xbf, 0x0a, 0x44, 0xcf, 0xbc, 0x80, 0xf5, 0x14, 0x49, 0x45, 0x60, 0x01, 0x81, 0xd5, 0x04, 0x51, 0x39, 0xf3, 0x11, 0xac, 0xa7, 0x86, 0x54, 0x02, 0x16, 0x09, 0x58, 0x4f, 0x0c, 0xaa, 0x71, 0x7a,
0x8d, 0xdb, 0x6b, 0x91, 0xc2, 0x17, 0x97, 0x53, 0xf8, 0x03, 0xd8, 0x88, 0x88, 0xcb, 0x25, 0x1d, 0x2d, 0x8f, 0xf0, 0xa5, 0xd5, 0x23, 0xfc, 0x73, 0xd8, 0x88, 0x06, 0x97, 0x0b, 0x3e, 0xf9, 0xce,
0x7f, 0xe7, 0x5e, 0x5d, 0x99, 0x01, 0x1b, 0xe3, 0x50, 0x56, 0x8c, 0xf5, 0x50, 0xf5, 0x5a, 0x6a, 0xbd, 0xbc, 0x34, 0x03, 0x31, 0xa1, 0xa2, 0x9c, 0x37, 0xd6, 0x43, 0xd6, 0x0b, 0xc5, 0x19, 0x89,
0x86, 0x6c, 0xac, 0xff, 0x51, 0x30, 0xee, 0x64, 0x62, 0xb0, 0x41, 0xe5, 0x3b, 0xc3, 0x0c, 0x6f, 0x09, 0xce, 0xca, 0x7c, 0x2e, 0x5d, 0x33, 0xda, 0x3e, 0xa8, 0x1b, 0x97, 0x8c, 0x0a, 0x12, 0xc3,
0x41, 0xc5, 0x28, 0x85, 0x92, 0xee, 0x84, 0x1c, 0x84, 0x54, 0x2b, 0x8b, 0x7c, 0xa8, 0xb1, 0x3c, 0xdd, 0x0c, 0x6d, 0x47, 0x18, 0x5c, 0x4e, 0x2e, 0xe6, 0xd3, 0x2b, 0xa1, 0xca, 0x46, 0x45, 0xd9,
0xbb, 0x09, 0xce, 0xf5, 0x12, 0x88, 0xe5, 0x8c, 0x5d, 0x5b, 0xc4, 0x8f, 0xdf, 0xf8, 0x2c, 0xb8, 0x0e, 0x59, 0x83, 0xb9, 0x7c, 0x41, 0x0c, 0x7c, 0xee, 0x8f, 0x60, 0xfb, 0x0e, 0x5c, 0x72, 0x5f,
0x71, 0xa7, 0x13, 0xcc, 0x51, 0xc5, 0x58, 0x8f, 0x34, 0xa3, 0x48, 0x21, 0xe0, 0xf1, 0xd3, 0x66, 0xd2, 0x43, 0xaa, 0x74, 0xe8, 0x49, 0xfa, 0x10, 0x72, 0xf1, 0x31, 0x1f, 0x03, 0xa3, 0x93, 0x68,
0x0e, 0x57, 0x24, 0x3c, 0xd2, 0xc4, 0x70, 0xfd, 0x2d, 0x6c, 0x0f, 0x57, 0x55, 0x28, 0xf9, 0x0a, 0x18, 0xcb, 0x31, 0x2f, 0x67, 0xd6, 0xd5, 0xb5, 0xa4, 0x69, 0x24, 0x6f, 0x34, 0x90, 0x73, 0xc6,
0xc0, 0x8b, 0xeb, 0x12, 0x3d, 0x29, 0x1f, 0xee, 0xdc, 0x3f, 0xf0, 0xbc, 0x76, 0x8d, 0x04, 0x5e, 0x6f, 0x7a, 0xce, 0x31, 0x91, 0xf5, 0x3f, 0xe2, 0xae, 0x90, 0x0c, 0x29, 0x2a, 0x2d, 0x6a, 0x43,
0xdf, 0x81, 0xc6, 0x32, 0xd3, 0x72, 0x08, 0xe9, 0x4f, 0x60, 0x63, 0x38, 0xbb, 0xbe, 0x66, 0x0b, 0x32, 0xc3, 0xfe, 0x9d, 0x37, 0xca, 0x21, 0xa5, 0x37, 0x65, 0xcf, 0xc3, 0x21, 0x31, 0x4b, 0x93,
0x6c, 0xe4, 0x14, 0x1e, 0xa7, 0xc5, 0xe1, 0xcc, 0x3a, 0x84, 0x62, 0xf4, 0xbe, 0x0b, 0xfb, 0x62, 0x5c, 0x6b, 0x75, 0x5c, 0x26, 0xa6, 0xc5, 0x4f, 0x81, 0x59, 0xce, 0xc4, 0xb5, 0xd1, 0xf3, 0xf2,
0x6b, 0x7e, 0x90, 0xd4, 0x13, 0xd8, 0x28, 0x84, 0x8f, 0xbd, 0xfd, 0xe7, 0x50, 0x8c, 0xf8, 0x2b, 0xda, 0x17, 0xc1, 0xb5, 0x3b, 0x9b, 0x52, 0x74, 0xd5, 0x8c, 0xf5, 0x88, 0x33, 0x8e, 0x18, 0x08,
0x51, 0xa1, 0x78, 0xd6, 0xef, 0x0f, 0xcc, 0xfe, 0xc5, 0x48, 0x7b, 0x44, 0xca, 0x50, 0xc0, 0xaf, 0x8f, 0x97, 0xb2, 0x05, 0x3c, 0xaf, 0xe0, 0x11, 0x27, 0x86, 0xeb, 0xdf, 0xc2, 0xf6, 0xe8, 0xbe,
0x6e, 0x4f, 0xcb, 0xec, 0x07, 0x50, 0x8a, 0xe9, 0x2b, 0xa9, 0x40, 0xa9, 0xdb, 0xeb, 0x8e, 0xba, 0xdc, 0x62, 0x5f, 0x02, 0x78, 0x71, 0x46, 0x91, 0x26, 0x95, 0x83, 0x9d, 0xbb, 0x0f, 0x5e, 0x64,
0xad, 0x51, 0xe7, 0x48, 0x7b, 0x44, 0x9e, 0xc0, 0xfa, 0xc0, 0xe8, 0x74, 0xcf, 0x5b, 0xdf, 0x74, 0x9d, 0x91, 0xc0, 0xeb, 0x3b, 0xd0, 0x5a, 0x25, 0x5a, 0x95, 0x4f, 0xfd, 0x09, 0x6c, 0x8c, 0xe6,
0x4c, 0xa3, 0xf3, 0xa6, 0xd3, 0x3a, 0xeb, 0x1c, 0x69, 0x19, 0x42, 0xa0, 0x7a, 0x32, 0x3a, 0x6b, 0x57, 0x57, 0x62, 0x69, 0x8e, 0x7a, 0x09, 0x8f, 0xd3, 0xe4, 0xb0, 0xda, 0x1e, 0x40, 0x29, 0x8e,
0x9b, 0x83, 0x8b, 0xd7, 0x67, 0xdd, 0xe1, 0x49, 0xe7, 0x48, 0xcb, 0x0a, 0x9b, 0xc3, 0x8b, 0x76, 0x0d, 0x95, 0xd1, 0x5b, 0x8b, 0x87, 0xa4, 0x96, 0x77, 0xa3, 0x18, 0xae, 0xa9, 0xfb, 0xcf, 0xa0,
0xbb, 0x33, 0x1c, 0x6a, 0x39, 0x02, 0xb0, 0x76, 0xdc, 0xea, 0x0a, 0xb0, 0x42, 0x36, 0xa0, 0xd6, 0x14, 0x4d, 0xde, 0xac, 0x0a, 0xa5, 0xd3, 0xc1, 0x60, 0x68, 0x0e, 0xce, 0xc7, 0xda, 0x23, 0x56,
0xed, 0xbd, 0xe9, 0x77, 0xdb, 0x1d, 0x73, 0xd8, 0x19, 0x8d, 0x84, 0x30, 0xbf, 0xff, 0xaf, 0x0c, 0x81, 0x22, 0x7d, 0xf5, 0xfa, 0x5a, 0x66, 0x3f, 0x80, 0x72, 0x3c, 0x78, 0xb3, 0x1a, 0x94, 0x7b,
0x54, 0x52, 0x0c, 0x98, 0x6c, 0xc1, 0x86, 0x58, 0x72, 0x61, 0x88, 0x9d, 0x5a, 0xc3, 0x7e, 0xcf, 0xfd, 0xde, 0xb8, 0x77, 0x38, 0xee, 0x1e, 0x69, 0x8f, 0xd8, 0x13, 0x58, 0x1f, 0x1a, 0xdd, 0xde,
0xec, 0xf5, 0x7b, 0x1d, 0xed, 0x11, 0xf9, 0x08, 0xb6, 0x16, 0x14, 0xfd, 0xe3, 0xe3, 0xf6, 0x49, 0xd9, 0xe1, 0xd7, 0x5d, 0xd3, 0xe8, 0xbe, 0xea, 0x1e, 0x9e, 0x76, 0x8f, 0xb4, 0x0c, 0x63, 0x50,
0x4b, 0x1c, 0x9e, 0x34, 0x60, 0x73, 0x41, 0x39, 0xea, 0x9e, 0x77, 0x84, 0x97, 0x59, 0xb2, 0x0b, 0x3f, 0x19, 0x9f, 0x76, 0xcc, 0xe1, 0xf9, 0x8b, 0xd3, 0xde, 0xe8, 0xa4, 0x7b, 0xa4, 0x65, 0x51,
0x3b, 0x0b, 0xba, 0xe1, 0xb7, 0x9d, 0xce, 0x20, 0x46, 0xe4, 0xc8, 0x73, 0x78, 0xba, 0x80, 0xe8, 0xe6, 0xe8, 0xbc, 0xd3, 0xe9, 0x8e, 0x46, 0x5a, 0x8e, 0x01, 0xac, 0x1d, 0x1f, 0xf6, 0x10, 0x9c,
0xf6, 0x86, 0x17, 0xc7, 0xc7, 0xdd, 0x76, 0xb7, 0xd3, 0x1b, 0x99, 0x6f, 0x5a, 0x67, 0x17, 0x1d, 0x67, 0x1b, 0xd0, 0xe8, 0xf5, 0x5f, 0x0d, 0x7a, 0x9d, 0xae, 0x39, 0xea, 0x8e, 0xc7, 0x48, 0x2c,
0x4d, 0x21, 0x3b, 0x50, 0x5f, 0xdc, 0xa4, 0x73, 0x3e, 0xe8, 0x1b, 0x2d, 0xe3, 0xad, 0x96, 0x27, 0xec, 0xff, 0x33, 0x03, 0xb5, 0xd4, 0xec, 0xce, 0xb6, 0x60, 0x03, 0x8f, 0x9c, 0x1b, 0x78, 0xd3,
0xcf, 0xe0, 0x93, 0x7b, 0x46, 0xda, 0x7d, 0xc3, 0xe8, 0xb4, 0x47, 0x66, 0xeb, 0xbc, 0x7f, 0xd1, 0xe1, 0x68, 0xd0, 0x37, 0xfb, 0x83, 0x7e, 0x57, 0x7b, 0xc4, 0xde, 0x81, 0xad, 0x25, 0xc6, 0xe0,
0x1b, 0x69, 0x6b, 0xfb, 0x4d, 0xc1, 0x32, 0x17, 0x0a, 0x5c, 0x84, 0xec, 0xa2, 0xf7, 0xd3, 0x5e, 0xf8, 0xb8, 0x73, 0x72, 0x88, 0x8f, 0x67, 0x2d, 0xd8, 0x5c, 0x62, 0x8e, 0x7b, 0x67, 0x5d, 0xd4,
0xff, 0xdb, 0x9e, 0xf6, 0x48, 0x44, 0x7e, 0x74, 0x62, 0x74, 0x86, 0x27, 0xfd, 0xb3, 0x23, 0x2d, 0x32, 0xcb, 0x76, 0x61, 0x67, 0x89, 0x37, 0xfa, 0xa6, 0xdb, 0x1d, 0xc6, 0x88, 0x1c, 0x7b, 0x06,
0x73, 0xf8, 0xb7, 0x92, 0x7c, 0xe1, 0xb4, 0xf1, 0x7f, 0x11, 0x62, 0x40, 0x21, 0x4c, 0x33, 0x59, 0x4f, 0x97, 0x10, 0xbd, 0xfe, 0xe8, 0xfc, 0xf8, 0xb8, 0xd7, 0xe9, 0x75, 0xfb, 0x63, 0xf3, 0xd5,
0x95, 0xf8, 0xc6, 0x93, 0x14, 0x4b, 0x8d, 0x2b, 0x6d, 0xeb, 0x37, 0x7f, 0xfd, 0xfb, 0x6f, 0xb3, 0xe1, 0xe9, 0x79, 0x57, 0xcb, 0xb3, 0x1d, 0x68, 0x2e, 0x5f, 0xd2, 0x3d, 0x1b, 0x0e, 0x8c, 0x43,
0xeb, 0xba, 0xda, 0x7c, 0xff, 0x45, 0x53, 0x20, 0x9a, 0xee, 0x8c, 0xbf, 0xca, 0xec, 0x93, 0x3e, 0xe3, 0x5b, 0xad, 0xc0, 0xde, 0x87, 0xf7, 0xee, 0x08, 0xe9, 0x0c, 0x0c, 0xa3, 0xdb, 0x19, 0x9b,
0xac, 0xc9, 0xb7, 0x32, 0xd9, 0x4c, 0x99, 0x8c, 0x1f, 0xcf, 0xab, 0x2c, 0x6e, 0xa2, 0x45, 0x4d, 0x87, 0x67, 0x83, 0xf3, 0xfe, 0x58, 0x5b, 0xdb, 0x6f, 0xe3, 0x7c, 0xbc, 0x14, 0xe0, 0x68, 0xb2,
0x2f, 0xc7, 0x16, 0x2d, 0x47, 0x18, 0xfc, 0x31, 0x14, 0xc2, 0x77, 0x5a, 0xe2, 0x90, 0xe9, 0x97, 0xf3, 0xfe, 0x4f, 0xfb, 0x83, 0x6f, 0xfa, 0xda, 0x23, 0xb4, 0xfc, 0xf8, 0xc4, 0xe8, 0x8e, 0x4e,
0x5b, 0x63, 0x19, 0x95, 0xfe, 0x51, 0x86, 0xfc, 0x1c, 0x4a, 0x31, 0x0b, 0x27, 0xdb, 0x89, 0x1e, 0x06, 0xa7, 0x47, 0x5a, 0xe6, 0xe0, 0xaf, 0x65, 0xb5, 0x9b, 0x75, 0xe8, 0x1f, 0x1d, 0x66, 0x40,
0x4b, 0xf7, 0x47, 0xa3, 0xb1, 0x4c, 0x95, 0x3e, 0x16, 0xa9, 0xc6, 0xc7, 0x42, 0x86, 0x4e, 0x2e, 0x31, 0xaa, 0x03, 0xf7, 0x39, 0xbe, 0xf5, 0x24, 0x35, 0x5f, 0xc7, 0x91, 0xb6, 0xf5, 0x9b, 0xbf,
0x64, 0x1f, 0x08, 0x86, 0x4e, 0xea, 0xa9, 0xed, 0x13, 0xa4, 0x7d, 0xe9, 0xc1, 0xf4, 0x06, 0x9a, 0xfc, 0xed, 0xb7, 0xd9, 0x75, 0xbd, 0xda, 0x7e, 0xf3, 0x59, 0x1b, 0x11, 0x6d, 0x77, 0x2e, 0xbf,
0x7c, 0x4c, 0x48, 0xca, 0x64, 0xf3, 0x7b, 0x6b, 0xf2, 0x4b, 0xf2, 0x0b, 0x50, 0xc3, 0x04, 0x20, 0xc8, 0xec, 0xb3, 0x01, 0xac, 0xa9, 0x2d, 0x9f, 0x6d, 0xa6, 0x44, 0xc6, 0x6b, 0xff, 0x7d, 0x12,
0x8f, 0x26, 0xf3, 0x60, 0x25, 0xc9, 0x7e, 0x63, 0xee, 0xcc, 0x22, 0xe3, 0x5e, 0x62, 0xdd, 0x9d, 0x37, 0x49, 0xa2, 0xa6, 0x57, 0x62, 0x89, 0x96, 0x83, 0x02, 0x7f, 0x0c, 0xc5, 0x70, 0xc3, 0x4c,
0xf1, 0x26, 0x47, 0x6b, 0x97, 0xb1, 0x75, 0xe4, 0x67, 0x09, 0xeb, 0x49, 0xa6, 0x9b, 0xb6, 0x9e, 0x3c, 0x32, 0xbd, 0x73, 0xb6, 0x56, 0x2d, 0x01, 0xff, 0x9f, 0x61, 0x3f, 0x87, 0x72, 0xbc, 0x3f,
0x62, 0x72, 0xfa, 0x2e, 0x5a, 0x6f, 0x90, 0x7a, 0xca, 0xfa, 0x3b, 0x81, 0x69, 0x7e, 0x4f, 0x6d, 0xb0, 0xed, 0x44, 0x8e, 0xa5, 0xf3, 0xa3, 0xd5, 0x5a, 0xc5, 0x4a, 0x3f, 0x8b, 0xd5, 0xe3, 0x67,
0x2e, 0x3c, 0xa8, 0x8a, 0xeb, 0x19, 0x53, 0xfe, 0xa0, 0x0f, 0xf3, 0xa8, 0x2d, 0xbc, 0x5b, 0xf4, 0xd1, 0x6e, 0xc1, 0xce, 0x55, 0x1e, 0xe0, 0x6e, 0xc1, 0x9a, 0xa9, 0xeb, 0x13, 0xeb, 0xc6, 0xca,
0x6d, 0xdc, 0x64, 0x83, 0xac, 0x27, 0x4a, 0x21, 0xf6, 0x60, 0x6e, 0xfd, 0x41, 0x1f, 0x92, 0xd6, 0x87, 0xe9, 0x2d, 0x12, 0xf9, 0x98, 0xb1, 0x94, 0xc8, 0xf6, 0xf7, 0xd6, 0xf4, 0x97, 0xec, 0x17,
0xd3, 0x2e, 0x7c, 0x82, 0xd6, 0xb7, 0xc9, 0x56, 0xd2, 0x7a, 0xd2, 0x83, 0xb7, 0x50, 0x11, 0x7b, 0x50, 0x0d, 0x1d, 0x40, 0x1b, 0x00, 0x5b, 0x18, 0x2b, 0xb9, 0xa6, 0xb4, 0x16, 0xca, 0x2c, 0xef,
0x44, 0x04, 0x2d, 0x48, 0x54, 0x72, 0x8a, 0x05, 0x36, 0xb6, 0xee, 0xc9, 0xd3, 0xdd, 0x41, 0x6a, 0x0a, 0x2b, 0xa4, 0xbb, 0x73, 0xd9, 0x96, 0x24, 0xed, 0x22, 0x96, 0x4e, 0x93, 0x65, 0x42, 0x7a,
0xb8, 0x45, 0x40, 0x79, 0x53, 0x32, 0x3f, 0xc2, 0x81, 0xdc, 0xe7, 0x2e, 0x44, 0x8f, 0xed, 0xac, 0x72, 0x46, 0x4f, 0x4b, 0x4f, 0xcd, 0xa0, 0xfa, 0x2e, 0x49, 0x6f, 0xb1, 0x66, 0x4a, 0xfa, 0x6b,
0x24, 0x36, 0x8d, 0x07, 0xaf, 0x08, 0x7d, 0x07, 0x37, 0xdc, 0x24, 0x8f, 0x71, 0xc3, 0x08, 0xd0, 0xc4, 0xb4, 0xbf, 0xe7, 0xb6, 0x44, 0x0d, 0xea, 0x38, 0x58, 0x90, 0xcb, 0x1f, 0xd4, 0x61, 0x61,
0xf4, 0xa4, 0xfd, 0x5f, 0x01, 0x19, 0x3e, 0xb4, 0xeb, 0xca, 0xcb, 0xaa, 0xf1, 0xec, 0x41, 0x4c, 0xb5, 0xa5, 0x8d, 0x4b, 0xdf, 0xa6, 0x4b, 0x36, 0xd8, 0x7a, 0x22, 0x14, 0x62, 0x0d, 0x16, 0xd2,
0x3a, 0xa0, 0xfa, 0xd2, 0xcd, 0x45, 0x0b, 0x33, 0x50, 0x93, 0xf7, 0x0f, 0x99, 0xfb, 0xb2, 0xe4, 0x1f, 0xd4, 0x21, 0x29, 0x3d, 0xad, 0xc2, 0x7b, 0x24, 0x7d, 0x9b, 0x6d, 0x25, 0xa5, 0x27, 0x35,
0xb6, 0x6a, 0x7c, 0xbc, 0x42, 0x1b, 0xee, 0x56, 0xc7, 0xdd, 0x08, 0xd1, 0xc4, 0x6e, 0x74, 0xc6, 0xf8, 0x16, 0x6a, 0x78, 0x47, 0x34, 0x5a, 0x06, 0x89, 0x48, 0x4e, 0xcd, 0xaf, 0xad, 0xad, 0x3b,
0xdd, 0x66, 0x20, 0x61, 0x97, 0x6b, 0xf8, 0x07, 0xee, 0x97, 0xff, 0x0e, 0x00, 0x00, 0xff, 0xff, 0xf4, 0x74, 0x76, 0xb0, 0x06, 0x5d, 0x11, 0x70, 0xd9, 0x56, 0x33, 0x2b, 0x93, 0xc0, 0xee, 0x4e,
0xff, 0xa6, 0xdb, 0xca, 0xf7, 0x15, 0x00, 0x00, 0x5d, 0x4c, 0x8f, 0xe5, 0xdc, 0x3b, 0x92, 0xb5, 0x1e, 0x6c, 0x11, 0xfa, 0x0e, 0x5d, 0xb8, 0xc9,
0x1e, 0xd3, 0x85, 0x11, 0xa0, 0xed, 0x29, 0xf9, 0xbf, 0x02, 0x36, 0x7a, 0xe8, 0xd6, 0x7b, 0x9b,
0x55, 0xeb, 0xfd, 0x07, 0x31, 0x69, 0x83, 0xea, 0x2b, 0x2f, 0xc7, 0x14, 0x16, 0x50, 0x4d, 0xf6,
0x1f, 0xb6, 0xd0, 0x65, 0x45, 0xb7, 0x6a, 0xbd, 0x7b, 0x0f, 0x37, 0xbc, 0xad, 0x49, 0xb7, 0x31,
0xa6, 0xe1, 0x6d, 0x38, 0x38, 0xb4, 0x03, 0x05, 0xbb, 0x58, 0xa3, 0xbf, 0x9e, 0x3f, 0xff, 0x77,
0x00, 0x00, 0x00, 0xff, 0xff, 0xd9, 0x97, 0x5c, 0x27, 0xb1, 0x16, 0x00, 0x00,
} }
// Reference imports to suppress errors if they are not otherwise used. // Reference imports to suppress errors if they are not otherwise used.

@ -762,6 +762,33 @@ message LiquidityParameters{
suggestions again, expressed in seconds. suggestions again, expressed in seconds.
*/ */
uint64 failure_backoff_sec = 9; uint64 failure_backoff_sec = 9;
/*
Set to true to enable automatic dispatch of loop out swaps. All swaps will
be limited to the fee categories set by these parameters, and total
expenditure will be limited to the auto out budget.
*/
bool auto_loop_out = 10;
/*
The total budget for automatically dispatched swaps since the budget start
time, expressed in satoshis.
*/
uint64 auto_out_budget_sat = 11;
/*
The start time for auto-out budget, expressed as a unix timestamp in
seconds. If this value is 0, the budget will be applied for all
automatically dispatched swaps. Swaps that were completed before this date
will not be included in budget calculations.
*/
uint64 auto_out_budget_start_sec = 12;
/*
The maximum number of automatically dispatched swaps that we allow to be in
flight at any point in time.
*/
uint64 auto_max_in_flight = 13;
} }
enum LiquidityRuleType{ enum LiquidityRuleType{

@ -493,6 +493,26 @@
"type": "string", "type": "string",
"format": "uint64", "format": "uint64",
"description": "The amount of time we require pass since a channel was part of a failed\nswap due to off chain payment failure until it will be considered for swap\nsuggestions again, expressed in seconds." "description": "The amount of time we require pass since a channel was part of a failed\nswap due to off chain payment failure until it will be considered for swap\nsuggestions again, expressed in seconds."
},
"auto_loop_out": {
"type": "boolean",
"format": "boolean",
"description": "Set to true to enable automatic dispatch of loop out swaps. All swaps will\nbe limited to the fee categories set by these parameters, and total\nexpenditure will be limited to the auto out budget."
},
"auto_out_budget_sat": {
"type": "string",
"format": "uint64",
"description": "The total budget for automatically dispatched swaps since the budget start\ntime, expressed in satoshis."
},
"auto_out_budget_start_sec": {
"type": "string",
"format": "uint64",
"description": "The start time for auto-out budget, expressed as a unix timestamp in\nseconds. If this value is 0, the budget will be applied for all\nautomatically dispatched swaps. Swaps that were completed before this date\nwill not be included in budget calculations."
},
"auto_max_in_flight": {
"type": "string",
"format": "uint64",
"description": "The maximum number of automatically dispatched swaps that we allow to be in\nflight at any point in time."
} }
} }
}, },

@ -33,6 +33,18 @@ This file tracks release notes for the loop client.
value is configurable). value is configurable).
* The `debug` logging level is recommended if using this feature. * The `debug` logging level is recommended if using this feature.
##### Introducing Autoloop
* This release includes support for opt-in automatic dispatch of loop out swaps,
based on the output of the `Suggestions` endpoint.
* To enable the autolooper, the following command can be used:
`loop setparams --autoout=true --autobudget={budget in sats} --budgetstart={start time for budget}`
* Automatically dispatched swaps are identified in the output of the
`ListSwaps` with the label `[reserved]: autoloop-out`.
* If autoloop is not enabled, the client will log the actions that the
autolooper would have taken if it was enabled, and the `Suggestions` endpoint
can be used to view the exact set of swaps that the autolooper would make if
enabled.
#### Breaking Changes #### Breaking Changes
* Macaroon authentication has been enabled for the `loopd` gRPC and REST * Macaroon authentication has been enabled for the `loopd` gRPC and REST

@ -42,6 +42,13 @@ func (h *mockLightningClient) PayInvoice(ctx context.Context, invoice string,
return done return done
} }
// DecodePaymentRequest returns a non-nil payment request.
func (h *mockLightningClient) DecodePaymentRequest(_ context.Context,
_ string) (*lndclient.PaymentRequest, error) {
return &lndclient.PaymentRequest{}, nil
}
func (h *mockLightningClient) WaitForFinished() { func (h *mockLightningClient) WaitForFinished() {
h.wg.Wait() h.wg.Wait()
} }

Loading…
Cancel
Save