Merge pull request #340 from carlaKC/autoloop-feepercentage

liquidity: flat fee percentage for autoloop
pull/350/head
Carla Kirk-Cohen 3 years ago committed by GitHub
commit c5ee9843cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -212,6 +212,11 @@ var setParamsCommand = cli.Command{
Usage: "the limit placed on our estimated sweep fee " +
"in sat/vByte.",
},
cli.IntFlag{
Name: "feepercent",
Usage: "the maximum percentage of swap amount to be " +
"used across all fee categories",
},
cli.Float64Flag{
Name: "maxswapfee",
Usage: "the maximum percentage of swap volume we are " +
@ -307,8 +312,11 @@ func setParams(ctx *cli.Context) error {
return err
}
var flagSet bool
var flagSet, categoriesSet, feePercentSet bool
// Update our existing parameters with the values provided by cli flags.
// Our fee categories and fee percentage are exclusive, so track which
// flags are set to ensure that we don't have nonsensical overlap.
if ctx.IsSet("maxswapfee") {
feeRate := ctx.Float64("maxswapfee")
params.MaxSwapFeePpm, err = ppmFromPercentage(feeRate)
@ -317,6 +325,7 @@ func setParams(ctx *cli.Context) error {
}
flagSet = true
categoriesSet = true
}
if ctx.IsSet("sweeplimit") {
@ -324,6 +333,18 @@ func setParams(ctx *cli.Context) error {
params.SweepFeeRateSatPerVbyte = uint64(satPerVByte)
flagSet = true
categoriesSet = true
}
if ctx.IsSet("feepercent") {
feeRate := ctx.Float64("feepercent")
params.FeePpm, err = ppmFromPercentage(feeRate)
if err != nil {
return err
}
flagSet = true
feePercentSet = true
}
if ctx.IsSet("maxroutingfee") {
@ -334,6 +355,7 @@ func setParams(ctx *cli.Context) error {
}
flagSet = true
categoriesSet = true
}
if ctx.IsSet("maxprepayfee") {
@ -344,16 +366,19 @@ func setParams(ctx *cli.Context) error {
}
flagSet = true
categoriesSet = true
}
if ctx.IsSet("maxprepay") {
params.MaxPrepaySat = ctx.Uint64("maxprepay")
flagSet = true
categoriesSet = true
}
if ctx.IsSet("maxminer") {
params.MaxMinerFeeSat = ctx.Uint64("maxminer")
flagSet = true
categoriesSet = true
}
if ctx.IsSet("sweepconf") {
@ -400,6 +425,29 @@ func setParams(ctx *cli.Context) error {
return fmt.Errorf("at least one flag required to set params")
}
switch {
// Fail if fee params for both types of fee limit are set, since they
// cannot be used in conjunction.
case feePercentSet && categoriesSet:
return fmt.Errorf("feepercent cannot be set with specific " +
"fee category flags")
// If we are updating to fee percentage, we unset all other fee related
// params so that users do not need to manually unset them.
case feePercentSet:
params.SweepFeeRateSatPerVbyte = 0
params.MaxMinerFeeSat = 0
params.MaxPrepayRoutingFeePpm = 0
params.MaxPrepaySat = 0
params.MaxRoutingFeePpm = 0
params.MaxSwapFeePpm = 0
// If we are setting any of our fee categories, unset fee percentage
// so that it does not need to be manually updated.
case categoriesSet:
params.FeePpm = 0
}
// Update our parameters to our mutated values.
_, err = client.SetLiquidityParams(
context.Background(), &looprpc.SetLiquidityParamsRequest{

@ -55,10 +55,16 @@ loop setrule {short channel id/ peer pubkey} --clear
```
## Fees
Fee control is one of the most important features of the autolooper, so we expose
multiple fee related settings which can be used to tune the autolooper to your
preference. Note that these fees are expressed on a per-swap basis, rather than
as an overall budget.
The amount of fees that an automatically dispatched swap consumes can be limited
to a percentage of the swap amount using the fee percentage parameter:
```
loop setparams --feepercent={percentage of swap amount}
```
If you would like finer grained control over swap fees, there are multiple fee
related settings which can be used to tune the autolooper to your preference.
The sections that follow explain these settings in detail. Note that these fees
are expressed on a per-swap basis, rather than as an overall budget.
### On-Chain Fees
When performing a successful loop out swap, the loop client needs to sweep the
@ -277,6 +283,10 @@ following reasons will be displayed:
* Liquidity ok: if a channel's current liquidity balance is within the bound set
by the rule that it applies to, then a liquidity ok reason will be displayed
to indicate that no action is required for that channel.
* Fee insufficient: if the fees that a swap will cost are more than the
percentage of total swap amount that we allow, this reason will be displayed.
See [fees](#fees) to update this value.
Further details for all of these reasons can be found in loopd's debug level
logs.

@ -70,32 +70,37 @@ func TestAutoLoopDisabled(t *testing.T) {
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{
Autoloop: 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,
},
}
var (
channels = []lndclient.ChannelInfo{
channel1, channel2,
}
swapFeePPM uint64 = 1000
routeFeePPM uint64 = 1000
prepayFeePPM uint64 = 1000
prepayAmount = btcutil.Amount(20000)
maxMiner = btcutil.Amount(20000)
// 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{
Autoloop: true,
AutoFeeBudget: 40066,
AutoFeeStartDate: testTime,
MaxAutoInFlight: 2,
FailureBackOff: time.Hour,
SweepConfTarget: 10,
FeeLimit: NewFeeCategoryLimit(
swapFeePPM, routeFeePPM, prepayFeePPM, maxMiner,
prepayAmount, 20000,
),
ChannelRules: map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule,
chanID2: chanRule,
},
}
)
c := newAutoloopTestCtx(t, params, channels, testRestrictions)
c.start()
@ -104,18 +109,20 @@ func TestAutoLoopEnabled(t *testing.T) {
var (
amt = chan1Rec.Amount
maxSwapFee = ppmToSat(amt, params.MaximumSwapFeePPM)
maxSwapFee = ppmToSat(amt, swapFeePPM)
// 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,
PrepayAmount: prepayAmount - 10,
MinerFee: maxMiner - 10,
}
quote2 = &loop.LoopOutQuote{
SwapFee: maxSwapFee,
PrepayAmount: params.MaximumPrepay - 20,
PrepayAmount: prepayAmount - 20,
MinerFee: maxMiner - 10,
}
quoteRequest = &loop.LoopOutQuoteRequest{
@ -134,18 +141,17 @@ func TestAutoLoopEnabled(t *testing.T) {
},
}
maxRouteFee = ppmToSat(amt, params.MaximumRoutingFeePPM)
maxRouteFee = ppmToSat(amt, routeFeePPM)
chan1Swap = &loop.OutRequest{
Amount: amt,
MaxSwapRoutingFee: maxRouteFee,
MaxPrepayRoutingFee: ppmToSat(
quote1.PrepayAmount,
params.MaximumPrepayRoutingFeePPM,
quote1.PrepayAmount, prepayFeePPM,
),
MaxSwapFee: quote1.SwapFee,
MaxPrepayAmount: quote1.PrepayAmount,
MaxMinerFee: params.MaximumMinerFee,
MaxMinerFee: maxMiner,
SweepConfTarget: params.SweepConfTarget,
OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()},
Label: labels.AutoloopLabel(swap.TypeOut),
@ -156,12 +162,11 @@ func TestAutoLoopEnabled(t *testing.T) {
Amount: amt,
MaxSwapRoutingFee: maxRouteFee,
MaxPrepayRoutingFee: ppmToSat(
quote2.PrepayAmount,
params.MaximumPrepayRoutingFeePPM,
quote2.PrepayAmount, routeFeePPM,
),
MaxSwapFee: quote2.SwapFee,
MaxPrepayAmount: quote2.PrepayAmount,
MaxMinerFee: params.MaximumMinerFee,
MaxMinerFee: maxMiner,
SweepConfTarget: params.SweepConfTarget,
OutgoingChanSet: loopdb.ChannelSet{chanID2.ToUint64()},
Label: labels.AutoloopLabel(swap.TypeOut),
@ -216,7 +221,7 @@ func TestAutoLoopEnabled(t *testing.T) {
State: loopdb.StateSuccess,
Cost: loopdb.SwapCost{
Server: quote1.SwapFee,
Onchain: params.MaximumMinerFee,
Onchain: maxMiner,
Offchain: maxRouteFee +
chan1Rec.MaxPrepayRoutingFee,
},
@ -273,42 +278,48 @@ func TestAutoLoopEnabled(t *testing.T) {
func TestCompositeRules(t *testing.T) {
defer test.Guard(t)()
// Setup our channels so that we have two channels with peer 2, and
// a single channel with peer 1.
channel3 := lndclient.ChannelInfo{
ChannelID: chanID3.ToUint64(),
PubKeyBytes: peer2,
LocalBalance: 10000,
RemoteBalance: 0,
Capacity: 10000,
}
var (
// Setup our channels so that we have two channels with peer 2,
// and a single channel with peer 1.
channel3 = lndclient.ChannelInfo{
ChannelID: chanID3.ToUint64(),
PubKeyBytes: peer2,
LocalBalance: 10000,
RemoteBalance: 0,
Capacity: 10000,
}
channels := []lndclient.ChannelInfo{
channel1, channel2, channel3,
}
channels = []lndclient.ChannelInfo{
channel1, channel2, channel3,
}
// Create a set of parameters with autoloop enabled, set our budget to
// a value that will easily accommodate our two swaps.
params := Parameters{
Autoloop: true,
AutoFeeBudget: 100000,
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,
},
PeerRules: map[route.Vertex]*ThresholdRule{
peer2: chanRule,
},
}
swapFeePPM uint64 = 1000
routeFeePPM uint64 = 1000
prepayFeePPM uint64 = 1000
prepayAmount = btcutil.Amount(20000)
maxMiner = btcutil.Amount(20000)
// Create a set of parameters with autoloop enabled, set our
// budget to a value that will easily accommodate our two swaps.
params = Parameters{
FeeLimit: NewFeeCategoryLimit(
swapFeePPM, routeFeePPM, prepayFeePPM, maxMiner,
prepayAmount, 20000,
),
Autoloop: true,
AutoFeeBudget: 100000,
AutoFeeStartDate: testTime,
MaxAutoInFlight: 2,
FailureBackOff: time.Hour,
SweepConfTarget: 10,
ChannelRules: map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule,
},
PeerRules: map[route.Vertex]*ThresholdRule{
peer2: chanRule,
},
}
)
c := newAutoloopTestCtx(t, params, channels, testRestrictions)
c.start()
@ -320,11 +331,12 @@ func TestCompositeRules(t *testing.T) {
// our budget, with an amount which would balance the peer
/// across all of its channels.
peerAmount = btcutil.Amount(15000)
maxPeerSwapFee = ppmToSat(peerAmount, params.MaximumSwapFeePPM)
maxPeerSwapFee = ppmToSat(peerAmount, swapFeePPM)
peerSwapQuote = &loop.LoopOutQuote{
SwapFee: maxPeerSwapFee,
PrepayAmount: params.MaximumPrepay - 20,
PrepayAmount: prepayAmount - 20,
MinerFee: maxMiner - 10,
}
peerSwapQuoteRequest = &loop.LoopOutQuoteRequest{
@ -332,20 +344,17 @@ func TestCompositeRules(t *testing.T) {
SweepConfTarget: params.SweepConfTarget,
}
maxPeerRouteFee = ppmToSat(
peerAmount, params.MaximumRoutingFeePPM,
)
maxPeerRouteFee = ppmToSat(peerAmount, routeFeePPM)
peerSwap = &loop.OutRequest{
Amount: peerAmount,
MaxSwapRoutingFee: maxPeerRouteFee,
MaxPrepayRoutingFee: ppmToSat(
peerSwapQuote.PrepayAmount,
params.MaximumPrepayRoutingFeePPM,
peerSwapQuote.PrepayAmount, routeFeePPM,
),
MaxSwapFee: peerSwapQuote.SwapFee,
MaxPrepayAmount: peerSwapQuote.PrepayAmount,
MaxMinerFee: params.MaximumMinerFee,
MaxMinerFee: maxMiner,
SweepConfTarget: params.SweepConfTarget,
OutgoingChanSet: loopdb.ChannelSet{
chanID2.ToUint64(), chanID3.ToUint64(),
@ -356,11 +365,12 @@ func TestCompositeRules(t *testing.T) {
// Create a quote for our single channel swap that is within
// our budget.
chanAmount = chan1Rec.Amount
maxChanSwapFee = ppmToSat(chanAmount, params.MaximumSwapFeePPM)
maxChanSwapFee = ppmToSat(chanAmount, swapFeePPM)
channelSwapQuote = &loop.LoopOutQuote{
SwapFee: maxChanSwapFee,
PrepayAmount: params.MaximumPrepay - 10,
PrepayAmount: prepayAmount - 10,
MinerFee: maxMiner - 10,
}
chanSwapQuoteRequest = &loop.LoopOutQuoteRequest{
@ -368,20 +378,17 @@ func TestCompositeRules(t *testing.T) {
SweepConfTarget: params.SweepConfTarget,
}
maxChanRouteFee = ppmToSat(
chanAmount, params.MaximumRoutingFeePPM,
)
maxChanRouteFee = ppmToSat(chanAmount, routeFeePPM)
chanSwap = &loop.OutRequest{
Amount: chanAmount,
MaxSwapRoutingFee: maxChanRouteFee,
MaxPrepayRoutingFee: ppmToSat(
channelSwapQuote.PrepayAmount,
params.MaximumPrepayRoutingFeePPM,
channelSwapQuote.PrepayAmount, routeFeePPM,
),
MaxSwapFee: channelSwapQuote.SwapFee,
MaxPrepayAmount: channelSwapQuote.PrepayAmount,
MaxMinerFee: params.MaximumMinerFee,
MaxMinerFee: maxMiner,
SweepConfTarget: params.SweepConfTarget,
OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()},
Label: labels.AutoloopLabel(swap.TypeOut),

@ -65,10 +65,13 @@ func newAutoloopTestCtx(t *testing.T, parameters Parameters,
// 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,
)
categories, ok := parameters.FeeLimit.(*FeeCategoryLimit)
if ok {
lnd.SetFeeEstimate(
parameters.SweepConfTarget, categories.SweepFeeRateLimit,
)
}
testCtx := &autoloopTestCtx{
t: t,

@ -0,0 +1,386 @@
package liquidity
import (
"errors"
"fmt"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
const (
// defaultSwapFeePPM is the default limit we place on swap fees,
// expressed as parts per million of swap volume, 0.5%.
defaultSwapFeePPM = 5000
// defaultRoutingFeePPM is the default limit we place on routing fees
// for the swap invoice, expressed as parts per million of swap volume,
// 1%.
defaultRoutingFeePPM = 10000
// defaultRoutingFeePPM is the default limit we place on routing fees
// for the prepay invoice, expressed as parts per million of prepay
// volume, 0.5%.
defaultPrepayRoutingFeePPM = 5000
// defaultMaximumMinerFee is the default limit we place on miner fees
// per swap. We apply a multiplier to this default fee to guard against
// the case where we have broadcast the preimage, then fees spike and
// we need to sweep the preimage.
defaultMaximumMinerFee = 15000 * 100
// defaultMaximumPrepay is the default limit we place on prepay
// invoices.
defaultMaximumPrepay = 30000
// defaultSweepFeeRateLimit is the default limit we place on estimated
// sweep fees, (750 * 4 /1000 = 3 sat/vByte).
defaultSweepFeeRateLimit = chainfee.SatPerKWeight(750)
// minerMultiplier is a multiplier we use to scale our miner fee to
// ensure that we will still be able to complete our swap in the case
// of a severe fee spike.
minerMultiplier = 100
// defaultFeePPM is the default percentage of swap amount that we
// allocate to fees, 2%.
defaultFeePPM = 20000
)
var (
// ErrZeroMinerFee is returned if a zero maximum miner fee is set.
ErrZeroMinerFee = errors.New("maximum miner fee must be non-zero")
// ErrZeroSwapFeePPM is returned if a zero server fee ppm is set.
ErrZeroSwapFeePPM = errors.New("swap fee PPM must be non-zero")
// ErrZeroRoutingPPM is returned if a zero routing fee ppm is set.
ErrZeroRoutingPPM = errors.New("routing fee PPM must be non-zero")
// ErrZeroPrepayPPM is returned if a zero prepay routing fee ppm is set.
ErrZeroPrepayPPM = errors.New("prepay routing fee PPM must be non-zero")
// ErrZeroPrepay is returned if a zero maximum prepay is set.
ErrZeroPrepay = errors.New("maximum prepay must be non-zero")
// ErrInvalidPPM is returned is the parts per million for a fee rate
// are invalid.
ErrInvalidPPM = errors.New("invalid ppm")
// ErrInvalidSweepFeeRateLimit is returned if an invalid sweep fee limit
// is set.
ErrInvalidSweepFeeRateLimit = fmt.Errorf("sweep fee rate limit must "+
"be > %v sat/vByte",
satPerKwToSatPerVByte(chainfee.AbsoluteFeePerKwFloor))
)
// Compile time assertion that FeeCategoryLimit implements FeeLimit.
var _ FeeLimit = (*FeeCategoryLimit)(nil)
// FeeCategoryLimit is an implementation of the fee limit interface which sets
// a specific fee limit per fee category.
type FeeCategoryLimit struct {
// MaximumPrepay is the maximum prepay amount we are willing to pay per
// swap.
MaximumPrepay btcutil.Amount
// MaximumSwapFeePPM is the maximum server fee we are willing to pay per
// swap expressed as parts per million of the swap volume.
MaximumSwapFeePPM uint64
// MaximumRoutingFeePPM is the maximum off-chain routing fee we
// are willing to pay for off chain invoice routing fees per swap,
// expressed as parts per million of the swap amount.
MaximumRoutingFeePPM uint64
// MaximumPrepayRoutingFeePPM is the maximum off-chain routing fee we
// are willing to pay for off chain prepay routing fees per swap,
// expressed as parts per million of the prepay amount.
MaximumPrepayRoutingFeePPM uint64
// MaximumMinerFee is the maximum on chain fee that we cap our miner
// fee at in case where we need to claim on chain because we have
// revealed the preimage, but fees have spiked. We will not initiate a
// swap if we estimate that the sweep cost will be above our sweep
// fee limit, and we use fee estimates at time of sweep to set our fees,
// so this is just a sane cap covering the special case where we need to
// sweep during a fee spike.
MaximumMinerFee btcutil.Amount
// SweepFeeRateLimit is the limit that we place on our estimated sweep
// fee. A swap will not be suggested if estimated fee rate is above this
// value.
SweepFeeRateLimit chainfee.SatPerKWeight
}
// NewFeeCategoryLimit created a new fee limit struct which sets individual
// fee limits per category.
func NewFeeCategoryLimit(swapFeePPM, routingFeePPM, prepayFeePPM uint64,
minerFee, prepay btcutil.Amount,
sweepLimit chainfee.SatPerKWeight) *FeeCategoryLimit {
return &FeeCategoryLimit{
MaximumPrepay: prepay,
MaximumSwapFeePPM: swapFeePPM,
MaximumRoutingFeePPM: routingFeePPM,
MaximumPrepayRoutingFeePPM: prepayFeePPM,
MaximumMinerFee: minerFee,
SweepFeeRateLimit: sweepLimit,
}
}
func defaultFeeCategoryLimit() *FeeCategoryLimit {
return NewFeeCategoryLimit(defaultSwapFeePPM, defaultRoutingFeePPM,
defaultPrepayRoutingFeePPM, defaultMaximumMinerFee,
defaultMaximumPrepay, defaultSweepFeeRateLimit)
}
// String returns the string representation of our fee category limits.
func (f *FeeCategoryLimit) String() string {
return fmt.Sprintf("fee categories: maximum prepay: %v, maximum "+
"miner fee: %v, maximum swap fee ppm: %v, maximum "+
"routing fee ppm: %v, maximum prepay routing fee ppm: %v,"+
"sweep fee limit: %v", f.MaximumPrepay, f.MaximumMinerFee,
f.MaximumSwapFeePPM, f.MaximumRoutingFeePPM,
f.MaximumPrepayRoutingFeePPM, f.SweepFeeRateLimit,
)
}
func (f *FeeCategoryLimit) validate() error {
// Check that we have non-zero fee limits.
if f.MaximumSwapFeePPM == 0 {
return ErrZeroSwapFeePPM
}
if f.MaximumRoutingFeePPM == 0 {
return ErrZeroRoutingPPM
}
if f.MaximumPrepayRoutingFeePPM == 0 {
return ErrZeroPrepayPPM
}
if f.MaximumPrepay == 0 {
return ErrZeroPrepay
}
if f.MaximumMinerFee == 0 {
return ErrZeroMinerFee
}
// Check that our sweep limit is above our minimum fee rate. We use
// absolute fee floor rather than kw floor because we will allow users
// to specify fee rate is sat/vByte and want to allow 1 sat/vByte.
if f.SweepFeeRateLimit < chainfee.AbsoluteFeePerKwFloor {
return ErrInvalidSweepFeeRateLimit
}
return nil
}
// mayLoopOut checks our estimated loop out sweep fee against our sweep limit.
func (f *FeeCategoryLimit) mayLoopOut(estimate chainfee.SatPerKWeight) error {
if estimate > f.SweepFeeRateLimit {
log.Debugf("Current fee estimate to sweep: %v sat/vByte "+
"exceeds limit of: %v sat/vByte",
satPerKwToSatPerVByte(estimate),
satPerKwToSatPerVByte(f.SweepFeeRateLimit))
return newReasonError(ReasonSweepFees)
}
return nil
}
// loopOutLimits checks whether the quote provided is within our fee limits.
func (f *FeeCategoryLimit) loopOutLimits(amount btcutil.Amount,
quote *loop.LoopOutQuote) error {
maxFee := ppmToSat(amount, f.MaximumSwapFeePPM)
if quote.SwapFee > maxFee {
log.Debugf("quoted swap fee: %v > maximum swap fee: %v",
quote.SwapFee, maxFee)
return newReasonError(ReasonSwapFee)
}
if quote.MinerFee > f.MaximumMinerFee {
log.Debugf("quoted miner fee: %v > maximum miner "+
"fee: %v", quote.MinerFee, f.MaximumMinerFee)
return newReasonError(ReasonMinerFee)
}
if quote.PrepayAmount > f.MaximumPrepay {
log.Debugf("quoted prepay: %v > maximum prepay: %v",
quote.PrepayAmount, f.MaximumPrepay)
return newReasonError(ReasonPrepay)
}
return nil
}
// loopOutFees returns the prepay and routing and miner fees we are willing to
// pay for a loop out swap.
func (f *FeeCategoryLimit) loopOutFees(amount btcutil.Amount,
quote *loop.LoopOutQuote) (btcutil.Amount, btcutil.Amount,
btcutil.Amount) {
prepayMaxFee := ppmToSat(
quote.PrepayAmount, f.MaximumPrepayRoutingFeePPM,
)
routeMaxFee := ppmToSat(amount, f.MaximumRoutingFeePPM)
return prepayMaxFee, routeMaxFee, f.MaximumMinerFee
}
// Compile time assertion that FeePortion implements FeeLimit interface.
var _ FeeLimit = (*FeePortion)(nil)
// FeePortion is a fee limitation which limits fees to a set portion of
// the swap amount.
type FeePortion struct {
// PartsPerMillion is the total portion of the swap amount that the
// swap may consume.
PartsPerMillion uint64
}
func defaultFeePortion() *FeePortion {
return &FeePortion{
PartsPerMillion: defaultFeePPM,
}
}
// NewFeePortion creates a fee limit based on a flat percentage of swap amount.
func NewFeePortion(ppm uint64) *FeePortion {
return &FeePortion{
PartsPerMillion: ppm,
}
}
// String returns a string representation of the fee limit.
func (f *FeePortion) String() string {
return fmt.Sprintf("parts per million: %v", f.PartsPerMillion)
}
// validate returns an error if the values provided are invalid.
func (f *FeePortion) validate() error {
if f.PartsPerMillion <= 0 {
return ErrInvalidPPM
}
return nil
}
// mayLoopOut checks our estimated loop out sweep fee against our sweep limit.
// For fee percentage, we do not check anything because we need the full quote
// to determine whether we can perform a swap.
func (f *FeePortion) mayLoopOut(_ chainfee.SatPerKWeight) error {
return nil
}
// loopOutLimits checks whether the quote provided is within our fee
// limits for the swap amount.
func (f *FeePortion) loopOutLimits(swapAmt btcutil.Amount,
quote *loop.LoopOutQuote) error {
// First, check whether any of the individual fee categories provided
// by the server are more than our total limit. We do this so that we
// can provide more specific reasons for not executing swaps.
feeLimit := ppmToSat(swapAmt, f.PartsPerMillion)
minerFee := scaleMinerFee(quote.MinerFee)
if minerFee > feeLimit {
log.Debugf("miner fee: %v greater than fee limit: %v, at "+
"%v ppm", minerFee, feeLimit, f.PartsPerMillion)
return newReasonError(ReasonMinerFee)
}
if quote.SwapFee > feeLimit {
log.Debugf("swap fee: %v greater than fee limit: %v, at "+
"%v ppm", quote.SwapFee, feeLimit, f.PartsPerMillion)
return newReasonError(ReasonSwapFee)
}
if quote.PrepayAmount > feeLimit {
log.Debugf("prepay amount: %v greater than fee limit: %v, at "+
"%v ppm", quote.PrepayAmount, feeLimit, f.PartsPerMillion)
return newReasonError(ReasonPrepay)
}
// If our miner and swap fee equal our limit, we will have nothing left
// for off-chain fees, so we fail out early.
if minerFee+quote.SwapFee >= feeLimit {
log.Debugf("no budget for off-chain routing with miner fee: "+
"%v, swap fee: %v and fee limit: %v, at %v ppm",
minerFee, quote.SwapFee, feeLimit, f.PartsPerMillion)
return newReasonError(ReasonFeePPMInsufficient)
}
prepay, route, miner := f.loopOutFees(swapAmt, quote)
// Calculate the worst case fees that we could pay for this swap,
// ensuring that we are within our fee limit even if the swap fails.
fees := worstCaseOutFees(
prepay, route, quote.SwapFee, miner, quote.PrepayAmount,
)
if fees > feeLimit {
log.Debugf("total fees for swap: %v > fee limit: %v, at "+
"%v ppm", fees, feeLimit, f.PartsPerMillion)
return newReasonError(ReasonFeePPMInsufficient)
}
return nil
}
// loopOutFees return the maximum prepay and invoice routing fees for a swap
// amount and quote. Note that the fee portion implementation just returns
// the quote's miner fee, assuming that this value has already been validated.
// We also assume that the quote's minerfee + swapfee < fee limit, so that we
// have some fees left for off-chain routing.
func (f *FeePortion) loopOutFees(amount btcutil.Amount,
quote *loop.LoopOutQuote) (btcutil.Amount, btcutil.Amount,
btcutil.Amount) {
// Calculate the total amount we can spend in fees, and subtract the
// amounts provided by the quote to get the total available for
// off-chain fees.
feeLimit := ppmToSat(amount, f.PartsPerMillion)
minerFee := scaleMinerFee(quote.MinerFee)
available := feeLimit - minerFee - quote.SwapFee
prepayMaxFee, routeMaxFee := splitOffChain(
available, quote.PrepayAmount, amount,
)
return prepayMaxFee, routeMaxFee, minerFee
}
// splitOffChain takes an available fee budget and divides it among our prepay
// and swap payments proportional to their volume.
func splitOffChain(available, prepayAmt,
swapAmt btcutil.Amount) (btcutil.Amount, btcutil.Amount) {
total := swapAmt + prepayAmt
prepayMaxFee := available * prepayAmt / total
routeMaxFee := available * swapAmt / total
return prepayMaxFee, routeMaxFee
}
// scaleMinerFee scales our miner fee by our constant multiplier.
func scaleMinerFee(estimate btcutil.Amount) btcutil.Amount {
return estimate * btcutil.Amount(minerMultiplier)
}

@ -3,9 +3,33 @@ package liquidity
import (
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
)
// FeeLimit is an interface implemented by different strategies for limiting
// the fees we pay for autoloops.
type FeeLimit interface {
// String returns the string representation of fee limits.
String() string
// validate returns an error if the values provided are invalid.
validate() error
// mayLoopOut checks whether we may dispatch a loop out swap based on
// the current fee conditions.
mayLoopOut(estimate chainfee.SatPerKWeight) error
// loopOutLimits checks whether the quote provided is within our fee
// limits for the swap amount.
loopOutLimits(amount btcutil.Amount, quote *loop.LoopOutQuote) error
// loopOutFees return the maximum prepay and invoice routing fees for
// a swap amount and quote.
loopOutFees(amount btcutil.Amount, quote *loop.LoopOutQuote) (
btcutil.Amount, btcutil.Amount, btcutil.Amount)
}
// swapSuggestion is an interface implemented by suggested swaps for our
// different swap types. This interface is used to allow us to handle different
// swap types with the same autoloop logic.

@ -63,34 +63,6 @@ const (
// FeeBase is the base that we use to express fees.
FeeBase = 1e6
// defaultSwapFeePPM is the default limit we place on swap fees,
// expressed as parts per million of swap volume, 0.5%.
defaultSwapFeePPM = 5000
// defaultRoutingFeePPM is the default limit we place on routing fees
// for the swap invoice, expressed as parts per million of swap volume,
// 1%.
defaultRoutingFeePPM = 10000
// defaultRoutingFeePPM is the default limit we place on routing fees
// for the prepay invoice, expressed as parts per million of prepay
// volume, 0.5%.
defaultPrepayRoutingFeePPM = 5000
// defaultMaximumMinerFee is the default limit we place on miner fees
// per swap. We apply a multiplier to this default fee to guard against
// the case where we have broadcast the preimage, then fees spike and
// we need to sweep the preimage.
defaultMaximumMinerFee = 15000 * 100
// defaultMaximumPrepay is the default limit we place on prepay
// invoices.
defaultMaximumPrepay = 30000
// defaultSweepFeeRateLimit is the default limit we place on estimated
// sweep fees, (750 * 4 /1000 = 3 sat/vByte).
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
@ -121,44 +93,18 @@ var (
// defaultParameters contains the default parameters that we start our
// liquidity manger with.
defaultParameters = Parameters{
AutoFeeBudget: defaultBudget,
MaxAutoInFlight: defaultMaxInFlight,
ChannelRules: make(map[lnwire.ShortChannelID]*ThresholdRule),
PeerRules: make(map[route.Vertex]*ThresholdRule),
FailureBackOff: defaultFailureBackoff,
SweepFeeRateLimit: defaultSweepFeeRateLimit,
SweepConfTarget: loop.DefaultSweepConfTarget,
MaximumSwapFeePPM: defaultSwapFeePPM,
MaximumRoutingFeePPM: defaultRoutingFeePPM,
MaximumPrepayRoutingFeePPM: defaultPrepayRoutingFeePPM,
MaximumMinerFee: defaultMaximumMinerFee,
MaximumPrepay: defaultMaximumPrepay,
AutoFeeBudget: defaultBudget,
MaxAutoInFlight: defaultMaxInFlight,
ChannelRules: make(map[lnwire.ShortChannelID]*ThresholdRule),
PeerRules: make(map[route.Vertex]*ThresholdRule),
FailureBackOff: defaultFailureBackoff,
SweepConfTarget: loop.DefaultSweepConfTarget,
FeeLimit: defaultFeePortion(),
}
// ErrZeroChannelID is returned if we get a rule for a 0 channel ID.
ErrZeroChannelID = fmt.Errorf("zero channel ID not allowed")
// ErrInvalidSweepFeeRateLimit is returned if an invalid sweep fee limit
// is set.
ErrInvalidSweepFeeRateLimit = fmt.Errorf("sweep fee rate limit must "+
"be > %v sat/vByte",
satPerKwToSatPerVByte(chainfee.AbsoluteFeePerKwFloor))
// ErrZeroMinerFee is returned if a zero maximum miner fee is set.
ErrZeroMinerFee = errors.New("maximum miner fee must be non-zero")
// ErrZeroSwapFeePPM is returned if a zero server fee ppm is set.
ErrZeroSwapFeePPM = errors.New("swap fee PPM must be non-zero")
// ErrZeroRoutingPPM is returned if a zero routing fee ppm is set.
ErrZeroRoutingPPM = errors.New("routing fee PPM must be non-zero")
// ErrZeroPrepayPPM is returned if a zero prepay routing fee ppm is set.
ErrZeroPrepayPPM = errors.New("prepay routing fee PPM must be non-zero")
// ErrZeroPrepay is returned if a zero maximum prepay is set.
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")
@ -254,41 +200,12 @@ type Parameters struct {
// TODO(carla): add exponential backoff
FailureBackOff time.Duration
// SweepFeeRateLimit is the limit that we place on our estimated sweep
// fee. A swap will not be suggested if estimated fee rate is above this
// value.
SweepFeeRateLimit chainfee.SatPerKWeight
// SweepConfTarget is the number of blocks we aim to confirm our sweep
// transaction in. This value affects the on chain fees we will pay.
SweepConfTarget int32
// MaximumPrepay is the maximum prepay amount we are willing to pay per
// swap.
MaximumPrepay btcutil.Amount
// MaximumSwapFeePPM is the maximum server fee we are willing to pay per
// swap expressed as parts per million of the swap volume.
MaximumSwapFeePPM int
// MaximumRoutingFeePPM is the maximum off-chain routing fee we
// are willing to pay for off chain invoice routing fees per swap,
// expressed as parts per million of the swap amount.
MaximumRoutingFeePPM int
// MaximumPrepayRoutingFeePPM is the maximum off-chain routing fee we
// are willing to pay for off chain prepay routing fees per swap,
// expressed as parts per million of the prepay amount.
MaximumPrepayRoutingFeePPM int
// MaximumMinerFee is the maximum on chain fee that we cap our miner
// fee at in case where we need to claim on chain because we have
// revealed the preimage, but fees have spiked. We will not initiate a
// swap if we estimate that the sweep cost will be above our sweep
// fee limit, and we use fee estimates at time of sweep to set our fees,
// so this is just a sane cap covering the special case where we need to
// sweep during a fee spike.
MaximumMinerFee btcutil.Amount
// FeeLimit controls the fee limit we place on swaps.
FeeLimit FeeLimit
// ClientRestrictions are the restrictions placed on swap size by the
// client.
@ -324,15 +241,10 @@ func (p Parameters) String() string {
}
return fmt.Sprintf("rules: %v, failure backoff: %v, sweep "+
"fee rate limit: %v, sweep conf target: %v, maximum prepay: "+
"%v, maximum miner fee: %v, maximum swap fee ppm: %v, maximum "+
"routing fee ppm: %v, maximum prepay routing fee ppm: %v, "+
"auto budget: %v, budget start: %v, max auto in flight: %v, "+
"minimum swap size=%v, maximum swap size=%v",
strings.Join(ruleList, ","), p.FailureBackOff,
p.SweepFeeRateLimit, p.SweepConfTarget, p.MaximumPrepay,
p.MaximumMinerFee, p.MaximumSwapFeePPM,
p.MaximumRoutingFeePPM, p.MaximumPrepayRoutingFeePPM,
"sweep conf target: %v, fees: %v, auto budget: %v, budget "+
"start: %v, max auto in flight: %v, minimum swap size=%v, "+
"maximum swap size=%v", strings.Join(ruleList, ","),
p.FailureBackOff, p.SweepConfTarget, p.FeeLimit,
p.AutoFeeBudget, p.AutoFeeStartDate, p.MaxAutoInFlight,
p.ClientRestrictions.Minimum, p.ClientRestrictions.Maximum)
}
@ -403,38 +315,14 @@ func (p Parameters) validate(minConfs int32, openChans []lndclient.ChannelInfo,
}
}
// Check that our sweep limit is above our minimum fee rate. We use
// absolute fee floor rather than kw floor because we will allow users
// to specify fee rate is sat/vByte and want to allow 1 sat/vByte.
if p.SweepFeeRateLimit < chainfee.AbsoluteFeePerKwFloor {
return ErrInvalidSweepFeeRateLimit
}
// Check that our confirmation target is above our required minimum.
if p.SweepConfTarget < minConfs {
return fmt.Errorf("confirmation target must be at least: %v",
minConfs)
}
// Check that we have non-zero fee limits.
if p.MaximumSwapFeePPM == 0 {
return ErrZeroSwapFeePPM
}
if p.MaximumRoutingFeePPM == 0 {
return ErrZeroRoutingPPM
}
if p.MaximumPrepayRoutingFeePPM == 0 {
return ErrZeroPrepayPPM
}
if p.MaximumPrepay == 0 {
return ErrZeroPrepay
}
if p.MaximumMinerFee == 0 {
return ErrZeroMinerFee
if err := p.FeeLimit.validate(); err != nil {
return err
}
if p.AutoFeeBudget < 0 {
@ -733,14 +621,14 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
return nil, err
}
if estimate > m.params.SweepFeeRateLimit {
log.Debugf("Current fee estimate to sweep within: %v blocks "+
"%v sat/vByte exceeds limit of: %v sat/vByte",
m.params.SweepConfTarget,
satPerKwToSatPerVByte(estimate),
satPerKwToSatPerVByte(m.params.SweepFeeRateLimit))
if err := m.params.FeeLimit.mayLoopOut(estimate); err != nil {
var reasonErr *reasonError
if errors.As(err, &reasonErr) {
return m.singleReasonSuggestion(reasonErr.reason), nil
}
return m.singleReasonSuggestion(ReasonSweepFees), nil
return nil, err
}
// Get the current server side restrictions, combined with the client
@ -989,9 +877,8 @@ func (m *Manager) loopOutSwap(ctx context.Context, amount btcutil.Amount,
// Check that the estimated fees for the suggested swap are
// below the fee limits configured by the manager.
feeReason := m.checkFeeLimits(quote, amount)
if feeReason != ReasonNone {
return nil, newReasonError(feeReason)
if err := m.params.FeeLimit.loopOutLimits(amount, quote); err != nil {
return nil, err
}
outRequest, err := m.makeLoopOutRequest(
@ -1054,23 +941,24 @@ func (m *Manager) makeLoopOutRequest(ctx context.Context,
amount btcutil.Amount, balance *balances, quote *loop.LoopOutQuote,
autoloop bool) (loop.OutRequest, error) {
prepayMaxFee := ppmToSat(
quote.PrepayAmount, m.params.MaximumPrepayRoutingFeePPM,
prepayMaxFee, routeMaxFee, minerFee := m.params.FeeLimit.loopOutFees(
amount, quote,
)
routeMaxFee := ppmToSat(amount, m.params.MaximumRoutingFeePPM)
var chanSet loopdb.ChannelSet
for _, channel := range balance.channels {
chanSet = append(chanSet, channel.ToUint64())
}
// Create a request with our calculated routing fees. We can use the
// swap fee, prepay amount and miner fee from the quote because we have
// already validated them.
request := loop.OutRequest{
Amount: amount,
OutgoingChanSet: chanSet,
MaxPrepayRoutingFee: prepayMaxFee,
MaxSwapRoutingFee: routeMaxFee,
MaxMinerFee: m.params.MaximumMinerFee,
MaxMinerFee: minerFee,
MaxSwapFee: quote.SwapFee,
MaxPrepayAmount: quote.PrepayAmount,
SweepConfTarget: m.params.SweepConfTarget,
@ -1305,37 +1193,6 @@ func (s *swapTraffic) maySwap(peer route.Vertex,
return nil
}
// checkFeeLimits takes a set of fees for a swap and checks whether they exceed
// our swap limits.
func (m *Manager) checkFeeLimits(quote *loop.LoopOutQuote,
swapAmt btcutil.Amount) Reason {
maxFee := ppmToSat(swapAmt, m.params.MaximumSwapFeePPM)
if quote.SwapFee > maxFee {
log.Debugf("quoted swap fee: %v > maximum swap fee: %v",
quote.SwapFee, maxFee)
return ReasonSwapFee
}
if quote.MinerFee > m.params.MaximumMinerFee {
log.Debugf("quoted miner fee: %v > maximum miner "+
"fee: %v", quote.MinerFee, m.params.MaximumMinerFee)
return ReasonMinerFee
}
if quote.PrepayAmount > m.params.MaximumPrepay {
log.Debugf("quoted prepay: %v > maximum prepay: %v",
quote.PrepayAmount, m.params.MaximumPrepay)
return ReasonPrepay
}
return ReasonNone
}
// satPerKwToSatPerVByte converts sat per kWeight to sat per vByte.
func satPerKwToSatPerVByte(satPerKw chainfee.SatPerKWeight) int64 {
return int64(satPerKw.FeePerKVByte() / 1000)
@ -1343,8 +1200,8 @@ func satPerKwToSatPerVByte(satPerKw chainfee.SatPerKWeight) int64 {
// ppmToSat takes an amount and a measure of parts per million for the amount
// and returns the amount that the ppm represents.
func ppmToSat(amount btcutil.Amount, ppm int) btcutil.Amount {
return btcutil.Amount(uint64(amount) * uint64(ppm) / FeeBase)
func ppmToSat(amount btcutil.Amount, ppm uint64) btcutil.Amount {
return btcutil.Amount(uint64(amount) * ppm / FeeBase)
}
func mSatToSatoshis(amount lnwire.MilliSatoshi) btcutil.Amount {

@ -50,15 +50,12 @@ var (
chanRule = NewThresholdRule(50, 0)
testQuote = &loop.LoopOutQuote{
SwapFee: btcutil.Amount(1),
PrepayAmount: btcutil.Amount(500),
MinerFee: btcutil.Amount(50),
SwapFee: btcutil.Amount(5),
PrepayAmount: btcutil.Amount(50),
MinerFee: btcutil.Amount(1),
}
prepayFee = ppmToSat(
testQuote.PrepayAmount, defaultPrepayRoutingFeePPM,
)
routingFee = ppmToSat(7500, defaultRoutingFeePPM)
prepayFee, routingFee = testPPMFees(defaultFeePPM, testQuote, 7500)
// chan1Rec is the suggested swap for channel 1 when we use chanRule.
chan1Rec = loop.OutRequest{
@ -66,7 +63,7 @@ var (
OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()},
MaxPrepayRoutingFee: prepayFee,
MaxSwapRoutingFee: routingFee,
MaxMinerFee: defaultMaximumMinerFee,
MaxMinerFee: scaleMinerFee(testQuote.MinerFee),
MaxSwapFee: testQuote.SwapFee,
MaxPrepayAmount: testQuote.PrepayAmount,
SweepConfTarget: loop.DefaultSweepConfTarget,
@ -79,7 +76,7 @@ var (
OutgoingChanSet: loopdb.ChannelSet{chanID2.ToUint64()},
MaxPrepayRoutingFee: prepayFee,
MaxSwapRoutingFee: routingFee,
MaxMinerFee: defaultMaximumMinerFee,
MaxMinerFee: scaleMinerFee(testQuote.MinerFee),
MaxPrepayAmount: testQuote.PrepayAmount,
MaxSwapFee: testQuote.SwapFee,
SweepConfTarget: loop.DefaultSweepConfTarget,
@ -125,8 +122,7 @@ func newTestConfig() (*Config, *test.LndMockServices) {
// Set our fee estimate for the default number of confirmations to our
// limit so that our fees will be ok by default.
lnd.SetFeeEstimate(
defaultParameters.SweepConfTarget,
defaultParameters.SweepFeeRateLimit,
defaultParameters.SweepConfTarget, defaultSweepFeeRateLimit,
)
return &Config{
@ -152,6 +148,34 @@ func newTestConfig() (*Config, *test.LndMockServices) {
}, lnd
}
// testPPMFees calculates the split of fees between prepay and swap invoice
// for the swap amount and ppm, relying on the test quote.
func testPPMFees(ppm uint64, quote *loop.LoopOutQuote,
swapAmount btcutil.Amount) (btcutil.Amount, btcutil.Amount) {
feeTotal := ppmToSat(swapAmount, ppm)
feeAvailable := feeTotal - scaleMinerFee(quote.MinerFee) - quote.SwapFee
return splitOffChain(
feeAvailable, quote.PrepayAmount, swapAmount,
)
}
// applyFeeCategoryQuote returns a copy of the loop out request provided with
// fee categories updated to the quote and routing settings provided.
// nolint:unparam
func applyFeeCategoryQuote(req loop.OutRequest, minerFee btcutil.Amount,
prepayPPM, routingPPM uint64, quote loop.LoopOutQuote) loop.OutRequest {
req.MaxPrepayRoutingFee = ppmToSat(quote.PrepayAmount, prepayPPM)
req.MaxSwapRoutingFee = ppmToSat(req.Amount, routingPPM)
req.MaxSwapFee = quote.SwapFee
req.MaxPrepayAmount = quote.PrepayAmount
req.MaxMinerFee = minerFee
return req
}
// TestParameters tests getting and setting of parameters for our manager.
func TestParameters(t *testing.T) {
cfg, _ := newTestConfig()
@ -535,6 +559,12 @@ func TestRestrictedSuggestions(t *testing.T) {
// TestSweepFeeLimit tests getting of swap suggestions when our estimated sweep
// fee is above and below the configured limit.
func TestSweepFeeLimit(t *testing.T) {
quote := &loop.LoopOutQuote{
SwapFee: btcutil.Amount(1),
PrepayAmount: btcutil.Amount(500),
MinerFee: btcutil.Amount(50),
}
tests := []struct {
name string
feeRate chainfee.SatPerKWeight
@ -545,7 +575,11 @@ func TestSweepFeeLimit(t *testing.T) {
feeRate: defaultSweepFeeRateLimit,
suggestions: &Suggestions{
OutSwaps: []loop.OutRequest{
chan1Rec,
applyFeeCategoryQuote(
chan1Rec, defaultMaximumMinerFee,
defaultPrepayRoutingFeePPM,
defaultRoutingFeePPM, *quote,
),
},
DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
@ -569,6 +603,13 @@ func TestSweepFeeLimit(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) {
cfg, lnd := newTestConfig()
cfg.LoopOutQuote = func(_ context.Context,
_ *loop.LoopOutQuoteRequest) (*loop.LoopOutQuote,
error) {
return quote, nil
}
// Set our test case's fee rate for our mock lnd.
lnd.SetFeeEstimate(
loop.DefaultSweepConfTarget, testCase.feeRate,
@ -579,6 +620,8 @@ func TestSweepFeeLimit(t *testing.T) {
}
params := defaultParameters
params.FeeLimit = defaultFeeCategoryLimit()
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule,
}
@ -598,6 +641,9 @@ func TestSuggestSwaps(t *testing.T) {
channel1,
}
expectedAmt := btcutil.Amount(10000)
prepay, routing := testPPMFees(defaultFeePPM, testQuote, expectedAmt)
tests := []struct {
name string
channels []lndclient.ChannelInfo
@ -669,24 +715,18 @@ func TestSuggestSwaps(t *testing.T) {
suggestions: &Suggestions{
OutSwaps: []loop.OutRequest{
{
Amount: 10000,
Amount: expectedAmt,
OutgoingChanSet: loopdb.ChannelSet{
chanID1.ToUint64(),
chanID2.ToUint64(),
},
MaxPrepayRoutingFee: ppmToSat(
testQuote.PrepayAmount,
defaultPrepayRoutingFeePPM,
),
MaxSwapRoutingFee: ppmToSat(
10000,
defaultRoutingFeePPM,
),
MaxMinerFee: defaultMaximumMinerFee,
MaxSwapFee: testQuote.SwapFee,
MaxPrepayAmount: testQuote.PrepayAmount,
SweepConfTarget: loop.DefaultSweepConfTarget,
Initiator: autoloopSwapInitiator,
MaxPrepayRoutingFee: prepay,
MaxSwapRoutingFee: routing,
MaxMinerFee: scaleMinerFee(testQuote.MinerFee),
MaxSwapFee: testQuote.SwapFee,
MaxPrepayAmount: testQuote.PrepayAmount,
SweepConfTarget: loop.DefaultSweepConfTarget,
Initiator: autoloopSwapInitiator,
},
},
DisqualifiedChans: noneDisqualified,
@ -724,6 +764,12 @@ func TestSuggestSwaps(t *testing.T) {
// TestFeeLimits tests limiting of swap suggestions by fees.
func TestFeeLimits(t *testing.T) {
quote := &loop.LoopOutQuote{
SwapFee: btcutil.Amount(1),
PrepayAmount: btcutil.Amount(500),
MinerFee: btcutil.Amount(50),
}
tests := []struct {
name string
quote *loop.LoopOutQuote
@ -731,10 +777,14 @@ func TestFeeLimits(t *testing.T) {
}{
{
name: "fees ok",
quote: testQuote,
quote: quote,
suggestions: &Suggestions{
OutSwaps: []loop.OutRequest{
chan1Rec,
applyFeeCategoryQuote(
chan1Rec, defaultMaximumMinerFee,
defaultPrepayRoutingFeePPM,
defaultRoutingFeePPM, *quote,
),
},
DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
@ -801,7 +851,10 @@ func TestFeeLimits(t *testing.T) {
channel1,
}
// Set our params to use individual fee limits.
params := defaultParameters
params.FeeLimit = defaultFeeCategoryLimit()
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule,
}
@ -826,6 +879,21 @@ func TestFeeLimits(t *testing.T) {
// 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) {
quote := &loop.LoopOutQuote{
SwapFee: btcutil.Amount(1),
PrepayAmount: btcutil.Amount(500),
MinerFee: btcutil.Amount(50),
}
chan1 := applyFeeCategoryQuote(
chan1Rec, 5000, defaultPrepayRoutingFeePPM,
defaultRoutingFeePPM, *quote,
)
chan2 := applyFeeCategoryQuote(
chan2Rec, 5000, defaultPrepayRoutingFeePPM,
defaultRoutingFeePPM, *quote,
)
tests := []struct {
name string
@ -850,7 +918,7 @@ func TestFeeBudget(t *testing.T) {
maxMinerFee: 5000,
suggestions: &Suggestions{
OutSwaps: []loop.OutRequest{
chan1Rec, chan2Rec,
chan1, chan2,
},
DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
@ -864,7 +932,7 @@ func TestFeeBudget(t *testing.T) {
maxMinerFee: 5000,
suggestions: &Suggestions{
OutSwaps: []loop.OutRequest{
chan1Rec,
chan1,
},
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID2: ReasonBudgetInsufficient,
@ -883,7 +951,7 @@ func TestFeeBudget(t *testing.T) {
},
suggestions: &Suggestions{
OutSwaps: []loop.OutRequest{
chan1Rec, chan2Rec,
chan1, chan2,
},
DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
@ -900,7 +968,7 @@ func TestFeeBudget(t *testing.T) {
},
suggestions: &Suggestions{
OutSwaps: []loop.OutRequest{
chan1Rec,
chan1,
},
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID2: ReasonBudgetInsufficient,
@ -965,6 +1033,13 @@ func TestFeeBudget(t *testing.T) {
return swaps, nil
}
cfg.LoopOutQuote = func(_ context.Context,
_ *loop.LoopOutQuoteRequest) (*loop.LoopOutQuote,
error) {
return quote, nil
}
// Set two channels that need swaps.
lnd.Channels = []lndclient.ChannelInfo{
channel1,
@ -978,8 +1053,13 @@ func TestFeeBudget(t *testing.T) {
}
params.AutoFeeStartDate = testBudgetStart
params.AutoFeeBudget = testCase.budget
params.MaximumMinerFee = testCase.maxMinerFee
params.MaxAutoInFlight = 2
params.FeeLimit = NewFeeCategoryLimit(
defaultSwapFeePPM, defaultRoutingFeePPM,
defaultPrepayRoutingFeePPM,
testCase.maxMinerFee, defaultMaximumPrepay,
defaultSweepFeeRateLimit,
)
// Set our custom max miner fee on each expected swap,
// rather than having to create multiple vars for
@ -1124,19 +1204,17 @@ func TestSizeRestrictions(t *testing.T) {
Maximum: 10000,
}
outSwap = loop.OutRequest{
prepay, routing = testPPMFees(defaultFeePPM, testQuote, 7000)
outSwap = loop.OutRequest{
Amount: 7000,
OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()},
MaxPrepayRoutingFee: prepayFee,
MaxSwapRoutingFee: ppmToSat(
7000,
defaultRoutingFeePPM,
),
MaxMinerFee: defaultMaximumMinerFee,
MaxSwapFee: testQuote.SwapFee,
MaxPrepayAmount: testQuote.PrepayAmount,
SweepConfTarget: loop.DefaultSweepConfTarget,
Initiator: autoloopSwapInitiator,
MaxPrepayRoutingFee: prepay,
MaxSwapRoutingFee: routing,
MaxMinerFee: scaleMinerFee(testQuote.MinerFee),
MaxSwapFee: testQuote.SwapFee,
MaxPrepayAmount: testQuote.PrepayAmount,
SweepConfTarget: loop.DefaultSweepConfTarget,
Initiator: autoloopSwapInitiator,
}
)
@ -1266,6 +1344,147 @@ func TestSizeRestrictions(t *testing.T) {
}
}
// TestFeePercentage tests use of a flat fee percentage to limit the fees we
// pay for swaps. Our test is setup to require a 7500 sat swap, and we test
// this amount against various fee percentages and server quotes.
func TestFeePercentage(t *testing.T) {
var (
okPPM uint64 = 30000
okQuote = &loop.LoopOutQuote{
SwapFee: 15,
PrepayAmount: 30,
MinerFee: 1,
}
rec = loop.OutRequest{
Amount: 7500,
OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()},
MaxMinerFee: scaleMinerFee(okQuote.MinerFee),
MaxSwapFee: okQuote.SwapFee,
MaxPrepayAmount: okQuote.PrepayAmount,
SweepConfTarget: loop.DefaultSweepConfTarget,
Initiator: autoloopSwapInitiator,
}
)
rec.MaxPrepayRoutingFee, rec.MaxSwapRoutingFee = testPPMFees(
okPPM, okQuote, 7500,
)
tests := []struct {
name string
feePPM uint64
quote *loop.LoopOutQuote
suggestions *Suggestions
}{
{
// With our limit set to 3% of swap amount 7500, we
// have a total budget of 225 sat.
name: "fees ok",
feePPM: okPPM,
quote: okQuote,
suggestions: &Suggestions{
OutSwaps: []loop.OutRequest{
rec,
},
DisqualifiedChans: noneDisqualified,
DisqualifiedPeers: noPeersDisqualified,
},
},
{
name: "swap fee too high",
feePPM: 20000,
quote: &loop.LoopOutQuote{
SwapFee: 300,
PrepayAmount: 30,
MinerFee: 1,
},
suggestions: &Suggestions{
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonSwapFee,
},
DisqualifiedPeers: noPeersDisqualified,
},
},
{
name: "miner fee too high",
feePPM: 20000,
quote: &loop.LoopOutQuote{
SwapFee: 80,
PrepayAmount: 30,
MinerFee: 300,
},
suggestions: &Suggestions{
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonMinerFee,
},
DisqualifiedPeers: noPeersDisqualified,
},
},
{
name: "miner and swap too high",
feePPM: 20000,
quote: &loop.LoopOutQuote{
SwapFee: 60,
PrepayAmount: 30,
MinerFee: 1,
},
suggestions: &Suggestions{
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonFeePPMInsufficient,
},
DisqualifiedPeers: noPeersDisqualified,
},
},
{
name: "prepay too high",
feePPM: 30000,
quote: &loop.LoopOutQuote{
SwapFee: 75,
PrepayAmount: 300,
MinerFee: 1,
},
suggestions: &Suggestions{
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonPrepay,
},
DisqualifiedPeers: noPeersDisqualified,
},
},
}
for _, testCase := range tests {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
cfg, lnd := newTestConfig()
cfg.LoopOutQuote = func(_ context.Context,
_ *loop.LoopOutQuoteRequest) (*loop.LoopOutQuote,
error) {
return testCase.quote, nil
}
lnd.Channels = []lndclient.ChannelInfo{
channel1,
}
params := defaultParameters
params.FeeLimit = NewFeePortion(testCase.feePPM)
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule,
}
testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.suggestions, nil,
)
})
}
}
// testSuggestSwapsSetup contains the elements that are used to create a
// suggest swaps test.
type testSuggestSwapsSetup struct {

@ -61,6 +61,10 @@ const (
// from budget elapsed, because we still have some budget available,
// but we have allocated it to other swaps.
ReasonBudgetInsufficient
// ReasonFeePPMInsufficient indicates that the fees a swap would require
// are greater than the portion of swap amount allocated to fees.
ReasonFeePPMInsufficient
)
// String returns a string representation of a reason.
@ -105,6 +109,9 @@ func (r Reason) String() string {
case ReasonBudgetInsufficient:
return "budget insufficient"
case ReasonFeePPMInsufficient:
return "fee portion insufficient"
default:
return "unknown"
}

@ -572,22 +572,14 @@ func (s *swapClientServer) GetLiquidityParams(_ context.Context,
cfg := s.liquidityMgr.GetParameters()
satPerByte := cfg.SweepFeeRateLimit.FeePerKVByte() / 1000
totalRules := len(cfg.ChannelRules) + len(cfg.PeerRules)
rpcCfg := &looprpc.LiquidityParameters{
MaxMinerFeeSat: uint64(cfg.MaximumMinerFee),
MaxSwapFeePpm: uint64(cfg.MaximumSwapFeePPM),
MaxRoutingFeePpm: uint64(cfg.MaximumRoutingFeePPM),
MaxPrepayRoutingFeePpm: uint64(cfg.MaximumPrepayRoutingFeePPM),
MaxPrepaySat: uint64(cfg.MaximumPrepay),
SweepFeeRateSatPerVbyte: uint64(satPerByte),
SweepConfTarget: cfg.SweepConfTarget,
FailureBackoffSec: uint64(cfg.FailureBackOff.Seconds()),
Autoloop: cfg.Autoloop,
AutoloopBudgetSat: uint64(cfg.AutoFeeBudget),
AutoMaxInFlight: uint64(cfg.MaxAutoInFlight),
SweepConfTarget: cfg.SweepConfTarget,
FailureBackoffSec: uint64(cfg.FailureBackOff.Seconds()),
Autoloop: cfg.Autoloop,
AutoloopBudgetSat: uint64(cfg.AutoFeeBudget),
AutoMaxInFlight: uint64(cfg.MaxAutoInFlight),
Rules: make(
[]*looprpc.LiquidityRule, 0, totalRules,
),
@ -595,6 +587,25 @@ func (s *swapClientServer) GetLiquidityParams(_ context.Context,
MaxSwapAmount: uint64(cfg.ClientRestrictions.Maximum),
}
switch f := cfg.FeeLimit.(type) {
case *liquidity.FeeCategoryLimit:
satPerByte := f.SweepFeeRateLimit.FeePerKVByte() / 1000
rpcCfg.SweepFeeRateSatPerVbyte = uint64(satPerByte)
rpcCfg.MaxMinerFeeSat = uint64(f.MaximumMinerFee)
rpcCfg.MaxSwapFeePpm = f.MaximumSwapFeePPM
rpcCfg.MaxRoutingFeePpm = f.MaximumRoutingFeePPM
rpcCfg.MaxPrepayRoutingFeePpm = f.MaximumPrepayRoutingFeePPM
rpcCfg.MaxPrepaySat = uint64(f.MaximumPrepay)
case *liquidity.FeePortion:
rpcCfg.FeePpm = f.PartsPerMillion
default:
return nil, fmt.Errorf("unknown fee limit: %T", cfg.FeeLimit)
}
// 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() {
@ -635,18 +646,14 @@ func (s *swapClientServer) SetLiquidityParams(ctx context.Context,
in *looprpc.SetLiquidityParamsRequest) (*looprpc.SetLiquidityParamsResponse,
error) {
satPerVbyte := chainfee.SatPerKVByte(
in.Parameters.SweepFeeRateSatPerVbyte * 1000,
)
feeLimit, err := rpcToFee(in.Parameters)
if err != nil {
return nil, err
}
params := liquidity.Parameters{
MaximumMinerFee: btcutil.Amount(in.Parameters.MaxMinerFeeSat),
MaximumSwapFeePPM: int(in.Parameters.MaxSwapFeePpm),
MaximumRoutingFeePPM: int(in.Parameters.MaxRoutingFeePpm),
MaximumPrepayRoutingFeePPM: int(in.Parameters.MaxPrepayRoutingFeePpm),
MaximumPrepay: btcutil.Amount(in.Parameters.MaxPrepaySat),
SweepFeeRateLimit: satPerVbyte.FeePerKWeight(),
SweepConfTarget: in.Parameters.SweepConfTarget,
FeeLimit: feeLimit,
SweepConfTarget: in.Parameters.SweepConfTarget,
FailureBackOff: time.Duration(in.Parameters.FailureBackoffSec) *
time.Second,
Autoloop: in.Parameters.Autoloop,
@ -722,6 +729,45 @@ func (s *swapClientServer) SetLiquidityParams(ctx context.Context,
return &looprpc.SetLiquidityParamsResponse{}, nil
}
// rpcToFee converts the values provided over rpc to a fee limit interface,
// failing if an inconsistent set of fields are set.
func rpcToFee(req *looprpc.LiquidityParameters) (liquidity.FeeLimit,
error) {
// Check which fee limit type we have values set for. If any fields
// relevant to our individual categories are set, we count that type
// as set.
isFeePPM := req.FeePpm != 0
isCategories := req.MaxSwapFeePpm != 0 || req.MaxRoutingFeePpm != 0 ||
req.MaxPrepayRoutingFeePpm != 0 || req.MaxMinerFeeSat != 0 ||
req.MaxPrepaySat != 0 || req.SweepFeeRateSatPerVbyte != 0
switch {
case isFeePPM && isCategories:
return nil, errors.New("set either fee ppm, or individual " +
"fee categories")
case isFeePPM:
return liquidity.NewFeePortion(req.FeePpm), nil
case isCategories:
satPerVbyte := chainfee.SatPerKVByte(
req.SweepFeeRateSatPerVbyte * 1000,
)
return liquidity.NewFeeCategoryLimit(
req.MaxSwapFeePpm,
req.MaxRoutingFeePpm,
req.MaxPrepayRoutingFeePpm,
btcutil.Amount(req.MaxMinerFeeSat),
btcutil.Amount(req.MaxPrepaySat),
satPerVbyte.FeePerKWeight(),
), nil
default:
return nil, errors.New("no fee categories set")
}
}
// rpcToRule switches on rpc rule type to convert to our rule interface.
func rpcToRule(rule *looprpc.LiquidityRule) (*liquidity.ThresholdRule, error) {
switch rule.Type {
@ -849,6 +895,9 @@ func rpcAutoloopReason(reason liquidity.Reason) (looprpc.AutoReason, error) {
case liquidity.ReasonBudgetInsufficient:
return looprpc.AutoReason_AUTO_REASON_BUDGET_INSUFFICIENT, nil
case liquidity.ReasonFeePPMInsufficient:
return looprpc.AutoReason_AUTO_REASON_SWAP_FEE, nil
default:
return 0, fmt.Errorf("unknown autoloop reason: %v", reason)
}

@ -251,6 +251,10 @@ const (
//because we still have some budget available, but we have allocated it to
//other swaps.
AutoReason_AUTO_REASON_BUDGET_INSUFFICIENT AutoReason = 12
//
//Fee insufficient indicates that the fee estimate for a swap is higher than
//the portion of total swap amount that we allow fees to consume.
AutoReason_AUTO_REASON_FEE_INSUFFICIENT AutoReason = 13
)
var AutoReason_name = map[int32]string{
@ -267,6 +271,7 @@ var AutoReason_name = map[int32]string{
10: "AUTO_REASON_LOOP_IN",
11: "AUTO_REASON_LIQUIDITY_OK",
12: "AUTO_REASON_BUDGET_INSUFFICIENT",
13: "AUTO_REASON_FEE_INSUFFICIENT",
}
var AutoReason_value = map[string]int32{
@ -283,6 +288,7 @@ var AutoReason_value = map[string]int32{
"AUTO_REASON_LOOP_IN": 10,
"AUTO_REASON_LIQUIDITY_OK": 11,
"AUTO_REASON_BUDGET_INSUFFICIENT": 12,
"AUTO_REASON_FEE_INSUFFICIENT": 13,
}
func (x AutoReason) String() string {
@ -1666,6 +1672,12 @@ type LiquidityParameters struct {
//A set of liquidity rules that describe the desired liquidity balance.
Rules []*LiquidityRule `protobuf:"bytes,1,rep,name=rules,proto3" json:"rules,omitempty"`
//
//The parts per million of swap amount that is allowed to be allocated to swap
//fees. This value is applied across swap categories and may not be set in
//conjunction with sweep fee rate, swap fee ppm, routing fee ppm, prepay
//routing, max prepay and max miner fee.
FeePpm uint64 `protobuf:"varint,16,opt,name=fee_ppm,json=feePpm,proto3" json:"fee_ppm,omitempty"`
//
//The limit we place on our estimated sweep cost for a swap in sat/vByte. If
//the estimated fee for our sweep transaction within the specified
//confirmation target is above this value, we will not suggest any swaps.
@ -1767,6 +1779,13 @@ func (m *LiquidityParameters) GetRules() []*LiquidityRule {
return nil
}
func (m *LiquidityParameters) GetFeePpm() uint64 {
if m != nil {
return m.FeePpm
}
return 0
}
func (m *LiquidityParameters) GetSweepFeeRateSatPerVbyte() uint64 {
if m != nil {
return m.SweepFeeRateSatPerVbyte
@ -2208,171 +2227,173 @@ func init() {
func init() { proto.RegisterFile("client.proto", fileDescriptor_014de31d7ac8c57c) }
var fileDescriptor_014de31d7ac8c57c = []byte{
// 2624 bytes of a gzipped FileDescriptorProto
// 2647 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x59, 0x4f, 0x73, 0xe3, 0xc6,
0xb1, 0x5f, 0xfe, 0x13, 0xc9, 0x26, 0x48, 0x42, 0xa3, 0x5d, 0x89, 0xa2, 0x65, 0xaf, 0x16, 0xf6,
0x3e, 0xcb, 0xb2, 0xbd, 0x7a, 0x96, 0x4f, 0x76, 0xd9, 0xaf, 0x8a, 0xa2, 0xa0, 0x15, 0xd7, 0x12,
0x49, 0x83, 0xe4, 0xba, 0xf6, 0xd5, 0xab, 0x42, 0x8d, 0xc8, 0x91, 0x84, 0x32, 0xf1, 0x67, 0x81,
0xe1, 0xae, 0x54, 0xae, 0x97, 0x54, 0xa5, 0xe2, 0x73, 0x0e, 0xf9, 0x06, 0xb9, 0xe7, 0x96, 0x5b,
0x3e, 0x40, 0x2e, 0x39, 0x25, 0xb9, 0xe5, 0x9a, 0x4b, 0x0e, 0xf9, 0x0e, 0xa9, 0xe9, 0x01, 0x40,
0x80, 0x22, 0xe5, 0xe4, 0x90, 0x9b, 0xd8, 0xfd, 0x9b, 0x9e, 0xe9, 0xff, 0xdd, 0x10, 0x28, 0xe3,
0xa9, 0xc5, 0x1c, 0xfe, 0xcc, 0xf3, 0x5d, 0xee, 0x92, 0xe2, 0xd4, 0x75, 0x3d, 0xdf, 0x1b, 0x37,
0x77, 0xae, 0x5c, 0xf7, 0x6a, 0xca, 0x0e, 0xa8, 0x67, 0x1d, 0x50, 0xc7, 0x71, 0x39, 0xe5, 0x96,
0xeb, 0x04, 0x12, 0xa6, 0xfd, 0x36, 0x0f, 0xb5, 0x33, 0xd7, 0xf5, 0x7a, 0x33, 0x6e, 0xb0, 0xd7,
0x33, 0x16, 0x70, 0xa2, 0x42, 0x8e, 0xda, 0xbc, 0x91, 0xd9, 0xcd, 0xec, 0xe5, 0x0c, 0xf1, 0x27,
0x21, 0x90, 0x9f, 0xb0, 0x80, 0x37, 0xb2, 0xbb, 0x99, 0xbd, 0xb2, 0x81, 0x7f, 0x93, 0x03, 0x78,
0x68, 0xd3, 0x1b, 0x33, 0x78, 0x4b, 0x3d, 0xd3, 0x77, 0x67, 0xdc, 0x72, 0xae, 0xcc, 0x4b, 0xc6,
0x1a, 0x39, 0x3c, 0xb6, 0x6e, 0xd3, 0x9b, 0xc1, 0x5b, 0xea, 0x19, 0x92, 0x73, 0xc2, 0x18, 0xf9,
0x1c, 0x36, 0xc5, 0x01, 0xcf, 0x67, 0x1e, 0xbd, 0x4d, 0x1d, 0xc9, 0xe3, 0x91, 0x0d, 0x9b, 0xde,
0xf4, 0x91, 0x99, 0x38, 0xb4, 0x0b, 0x4a, 0x7c, 0x8b, 0x80, 0x16, 0x10, 0x0a, 0xa1, 0x74, 0x81,
0xf8, 0x00, 0x6a, 0x09, 0xb1, 0xe2, 0xe1, 0x6b, 0x88, 0x51, 0x62, 0x71, 0x2d, 0x9b, 0x13, 0x0d,
0xaa, 0x02, 0x65, 0x5b, 0x0e, 0xf3, 0x51, 0x50, 0x11, 0x41, 0x15, 0x9b, 0xde, 0x9c, 0x0b, 0x9a,
0x90, 0xf4, 0x09, 0xa8, 0xc2, 0x66, 0xa6, 0x3b, 0xe3, 0xe6, 0xf8, 0x9a, 0x3a, 0x0e, 0x9b, 0x36,
0x4a, 0xbb, 0x99, 0xbd, 0xfc, 0x51, 0xb6, 0x91, 0x31, 0x6a, 0x53, 0x69, 0xa5, 0xb6, 0xe4, 0x90,
0x7d, 0x58, 0x77, 0x67, 0xfc, 0xca, 0x15, 0x4a, 0x08, 0xb4, 0x19, 0x30, 0xde, 0xa8, 0xec, 0xe6,
0xf6, 0xf2, 0x46, 0x3d, 0x62, 0x08, 0xec, 0x80, 0x71, 0x81, 0x0d, 0xde, 0x32, 0xe6, 0x99, 0x63,
0xd7, 0xb9, 0x34, 0x39, 0xf5, 0xaf, 0x18, 0x6f, 0x94, 0x77, 0x33, 0x7b, 0x05, 0xa3, 0x8e, 0x8c,
0xb6, 0xeb, 0x5c, 0x0e, 0x91, 0x4c, 0x3e, 0x05, 0x72, 0xcd, 0xa7, 0x63, 0x84, 0x5a, 0xbe, 0x2d,
0x9d, 0xd5, 0xa8, 0x22, 0x78, 0x5d, 0x70, 0xda, 0x49, 0x06, 0xf9, 0x12, 0xb6, 0xd1, 0x38, 0xde,
0xec, 0x62, 0x6a, 0x8d, 0x91, 0x68, 0x4e, 0x18, 0x9d, 0x4c, 0x2d, 0x87, 0x35, 0x40, 0xbc, 0xde,
0xd8, 0x12, 0x80, 0xfe, 0x9c, 0x7f, 0x1c, 0xb2, 0xc9, 0x43, 0x28, 0x4c, 0xe9, 0x05, 0x9b, 0x36,
0x14, 0xf4, 0xab, 0xfc, 0x41, 0x76, 0xa0, 0x6c, 0x39, 0x16, 0xb7, 0x28, 0x77, 0xfd, 0x46, 0x0d,
0x39, 0x73, 0x82, 0xf6, 0x63, 0x16, 0xaa, 0x22, 0x5e, 0x3a, 0xce, 0xea, 0x70, 0x59, 0x74, 0x5a,
0xf6, 0x8e, 0xd3, 0xee, 0xb8, 0x23, 0x77, 0xd7, 0x1d, 0xdb, 0x50, 0x9a, 0xd2, 0x80, 0x9b, 0xd7,
0xae, 0x87, 0x11, 0xa2, 0x18, 0x45, 0xf1, 0xfb, 0xd4, 0xf5, 0xc8, 0xfb, 0x50, 0x65, 0x37, 0x9c,
0xf9, 0x0e, 0x9d, 0x9a, 0xc2, 0x24, 0x18, 0x16, 0x25, 0x43, 0x89, 0x88, 0xa7, 0x7c, 0x3a, 0x26,
0x7b, 0xa0, 0xc6, 0x86, 0x8c, 0x6c, 0xbe, 0x86, 0x66, 0xac, 0x45, 0x66, 0x0c, 0x4d, 0x1e, 0xdb,
0xa1, 0xb8, 0xd2, 0x0e, 0xa5, 0x45, 0x3b, 0xfc, 0x3d, 0x03, 0x0a, 0x06, 0x38, 0x0b, 0x3c, 0xd7,
0x09, 0x18, 0x21, 0x90, 0xb5, 0x26, 0x68, 0x85, 0x32, 0xc6, 0x4b, 0xd6, 0x9a, 0x08, 0x15, 0xac,
0x89, 0x79, 0x71, 0xcb, 0x59, 0x80, 0x1a, 0x2a, 0x46, 0xd1, 0x9a, 0x1c, 0x89, 0x9f, 0xe4, 0x29,
0x28, 0xf8, 0x3a, 0x3a, 0x99, 0xf8, 0x2c, 0x08, 0x64, 0x6a, 0xe1, 0xc1, 0x8a, 0xa0, 0xb7, 0x24,
0x99, 0x3c, 0x83, 0x8d, 0x24, 0xcc, 0x74, 0xbc, 0xc3, 0xb7, 0xc1, 0x35, 0xda, 0xa3, 0x2c, 0xc3,
0x21, 0x44, 0x76, 0x91, 0x41, 0x3e, 0x09, 0xa3, 0x27, 0xc2, 0x4b, 0x78, 0x01, 0xe1, 0x6a, 0x02,
0xde, 0x47, 0xf4, 0x53, 0xa8, 0x05, 0xcc, 0x7f, 0xc3, 0x7c, 0xd3, 0x66, 0x41, 0x40, 0xaf, 0x18,
0x1a, 0xa8, 0x6c, 0x54, 0x25, 0xf5, 0x5c, 0x12, 0x35, 0x15, 0x6a, 0xe7, 0xae, 0x63, 0x71, 0xd7,
0x0f, 0x7d, 0xae, 0xfd, 0x2e, 0x0f, 0x20, 0xb4, 0x1f, 0x70, 0xca, 0x67, 0xc1, 0xd2, 0x8a, 0x21,
0xac, 0x91, 0x5d, 0x69, 0x8d, 0xca, 0xa2, 0x35, 0xf2, 0xfc, 0xd6, 0x93, 0x61, 0x50, 0x3b, 0x5c,
0x7f, 0x16, 0xd6, 0xae, 0x67, 0xe2, 0x8e, 0xe1, 0xad, 0xc7, 0x0c, 0x64, 0x93, 0x3d, 0x28, 0x04,
0x9c, 0x72, 0x59, 0x31, 0x6a, 0x87, 0x24, 0x85, 0x13, 0x6f, 0x61, 0x86, 0x04, 0x90, 0xaf, 0xa1,
0x76, 0x49, 0xad, 0xe9, 0xcc, 0x67, 0xa6, 0xcf, 0x68, 0xe0, 0x3a, 0x18, 0xc9, 0xb5, 0xc3, 0xcd,
0xf8, 0xc8, 0x89, 0x64, 0x1b, 0xc8, 0x35, 0xaa, 0x97, 0xc9, 0x9f, 0xe4, 0x43, 0xa8, 0x87, 0xae,
0x16, 0xf9, 0xc4, 0x2d, 0x3b, 0xaa, 0x3c, 0xb5, 0x39, 0x79, 0x68, 0xd9, 0xe2, 0x45, 0x2a, 0x06,
0xe9, 0xcc, 0x9b, 0x50, 0xce, 0x24, 0x52, 0xd6, 0x9f, 0x9a, 0xa0, 0x8f, 0x90, 0x8c, 0xc8, 0x45,
0x87, 0x17, 0x97, 0x3b, 0x7c, 0xb9, 0x03, 0x95, 0x15, 0x0e, 0x5c, 0x11, 0x1e, 0xd5, 0x55, 0xe1,
0xf1, 0x18, 0x2a, 0x63, 0x37, 0xe0, 0xa6, 0xf4, 0x2f, 0x46, 0x75, 0xce, 0x00, 0x41, 0x1a, 0x20,
0x85, 0x3c, 0x01, 0x05, 0x01, 0xae, 0x33, 0xbe, 0xa6, 0x96, 0x83, 0x45, 0x2a, 0x67, 0xe0, 0xa1,
0x9e, 0x24, 0x89, 0xe4, 0x93, 0x90, 0xcb, 0x4b, 0x89, 0x01, 0x59, 0x6f, 0x11, 0x13, 0xd2, 0xe6,
0x29, 0x55, 0x4f, 0xa4, 0x94, 0x46, 0x40, 0x3d, 0xb3, 0x02, 0x2e, 0xbc, 0x15, 0x44, 0xa1, 0xf4,
0x3f, 0xb0, 0x9e, 0xa0, 0x85, 0xc9, 0xf4, 0x11, 0x14, 0x44, 0xf5, 0x08, 0x1a, 0x99, 0xdd, 0xdc,
0x5e, 0xe5, 0x70, 0xe3, 0x8e, 0xa3, 0x67, 0x81, 0x21, 0x11, 0xda, 0x13, 0xa8, 0x0b, 0x62, 0xc7,
0xb9, 0x74, 0xa3, 0x8a, 0x54, 0x8b, 0x53, 0x51, 0x11, 0x81, 0xa7, 0xd5, 0x40, 0x19, 0x32, 0xdf,
0x8e, 0xaf, 0xfc, 0x39, 0xd4, 0x3b, 0x4e, 0x48, 0x09, 0x2f, 0xfc, 0x2f, 0xa8, 0xdb, 0x96, 0x23,
0x4b, 0x16, 0xb5, 0xdd, 0x99, 0xc3, 0x43, 0x87, 0x57, 0x6d, 0xcb, 0x11, 0xf2, 0x5b, 0x48, 0x44,
0x5c, 0x54, 0xda, 0x42, 0xdc, 0x5a, 0x88, 0x93, 0xd5, 0x4d, 0xe2, 0x5e, 0xe4, 0x4b, 0x19, 0x35,
0xfb, 0x22, 0x5f, 0xca, 0xaa, 0xb9, 0x17, 0xf9, 0x52, 0x4e, 0xcd, 0xbf, 0xc8, 0x97, 0xf2, 0x6a,
0xe1, 0x45, 0xbe, 0x54, 0x54, 0x4b, 0xda, 0x1f, 0x33, 0xa0, 0xf6, 0x66, 0xfc, 0x3f, 0xfa, 0x04,
0x6c, 0x8c, 0x96, 0x63, 0x8e, 0xa7, 0xfc, 0x8d, 0x39, 0x61, 0x53, 0x4e, 0xd1, 0xdd, 0x05, 0x43,
0xb1, 0x2d, 0xa7, 0x3d, 0xe5, 0x6f, 0x8e, 0x05, 0x2d, 0x6a, 0x9f, 0x09, 0x54, 0x39, 0x44, 0xd1,
0x9b, 0x18, 0xf5, 0x13, 0xea, 0xfc, 0x26, 0x03, 0xca, 0xb7, 0x33, 0x97, 0xb3, 0xd5, 0x2d, 0x01,
0x03, 0x6f, 0x5e, 0x87, 0xb3, 0x78, 0x07, 0x8c, 0xe7, 0x35, 0xf8, 0x4e, 0x49, 0xcf, 0x2d, 0x29,
0xe9, 0xf7, 0x36, 0xbb, 0xfc, 0xbd, 0xcd, 0x4e, 0xfb, 0x55, 0x46, 0x78, 0x3d, 0x7c, 0x66, 0x68,
0xf2, 0x5d, 0x50, 0xa2, 0x26, 0x65, 0x06, 0x34, 0x7a, 0x30, 0x04, 0xb2, 0x4b, 0x0d, 0x28, 0x4e,
0x39, 0x98, 0x60, 0x78, 0x63, 0x70, 0x1d, 0x23, 0xc3, 0x29, 0x47, 0xf0, 0xfa, 0x92, 0x15, 0x1e,
0x78, 0x17, 0x20, 0x61, 0xcb, 0x02, 0xea, 0x59, 0x1e, 0x27, 0x0c, 0x29, 0x4d, 0x98, 0x57, 0x0b,
0xda, 0x9f, 0x64, 0x14, 0xfc, 0xbb, 0x4f, 0xfa, 0x00, 0x6a, 0xf3, 0x61, 0x07, 0x31, 0xb2, 0xbf,
0x2a, 0x5e, 0x34, 0xed, 0x08, 0xd4, 0xc7, 0x61, 0x1d, 0x91, 0x73, 0x47, 0xfa, 0xd9, 0x75, 0xc1,
0x19, 0x08, 0x46, 0x28, 0x12, 0xe7, 0x13, 0x61, 0x57, 0x7a, 0x6b, 0x33, 0x87, 0x9b, 0x38, 0xec,
0xc9, 0x9e, 0x5b, 0x47, 0x7b, 0x4a, 0xfa, 0xb1, 0xf0, 0xed, 0xfd, 0x0a, 0x6a, 0x75, 0xa8, 0x0e,
0xdd, 0xef, 0x99, 0x13, 0x27, 0xdb, 0x57, 0x50, 0x8b, 0x08, 0xa1, 0x8a, 0xfb, 0xb0, 0xc6, 0x91,
0x12, 0x66, 0xf7, 0xbc, 0x8c, 0x9f, 0x05, 0x94, 0x23, 0xd8, 0x08, 0x11, 0xda, 0xef, 0xb3, 0x50,
0x8e, 0xa9, 0x22, 0x48, 0x2e, 0x68, 0xc0, 0x4c, 0x9b, 0x8e, 0xa9, 0xef, 0xba, 0x4e, 0x98, 0xe3,
0x8a, 0x20, 0x9e, 0x87, 0x34, 0x51, 0xc2, 0x22, 0x3d, 0xae, 0x69, 0x70, 0x8d, 0xd6, 0x51, 0x8c,
0x4a, 0x48, 0x3b, 0xa5, 0xc1, 0x35, 0xf9, 0x08, 0xd4, 0x08, 0xe2, 0xf9, 0xcc, 0xb2, 0x45, 0xe7,
0x93, 0xfd, 0xb9, 0x1e, 0xd2, 0xfb, 0x21, 0x59, 0x14, 0x78, 0x99, 0x64, 0xa6, 0x47, 0xad, 0x89,
0x69, 0x0b, 0x2b, 0xca, 0x79, 0xb5, 0x26, 0xe9, 0x7d, 0x6a, 0x4d, 0xce, 0x03, 0xca, 0xc9, 0x67,
0xf0, 0x28, 0x31, 0xd4, 0x26, 0xe0, 0x32, 0x8b, 0x89, 0x1f, 0x4f, 0xb5, 0xf1, 0x91, 0x27, 0xa0,
0x88, 0x8e, 0x61, 0x8e, 0x7d, 0x46, 0x39, 0x9b, 0x84, 0x79, 0x5c, 0x11, 0xb4, 0xb6, 0x24, 0x91,
0x06, 0x14, 0xd9, 0x8d, 0x67, 0xf9, 0x6c, 0x82, 0x1d, 0xa3, 0x64, 0x44, 0x3f, 0xc5, 0xe1, 0x80,
0xbb, 0x3e, 0xbd, 0x62, 0xa6, 0x43, 0x6d, 0x16, 0x8e, 0x28, 0x95, 0x90, 0xd6, 0xa5, 0x36, 0xd3,
0xde, 0x81, 0xed, 0xe7, 0x8c, 0x9f, 0x59, 0xaf, 0x67, 0xd6, 0xc4, 0xe2, 0xb7, 0x7d, 0xea, 0xd3,
0x79, 0x15, 0xfc, 0x43, 0x01, 0x36, 0xd2, 0x2c, 0xc6, 0x99, 0x2f, 0x3a, 0x50, 0xc1, 0x9f, 0x4d,
0x59, 0xe4, 0x9d, 0x79, 0xc7, 0x8c, 0xc1, 0xc6, 0x6c, 0xca, 0x0c, 0x09, 0x22, 0x5f, 0xc3, 0xce,
0x3c, 0xc4, 0x7c, 0xd1, 0x03, 0x03, 0xca, 0x4d, 0x8f, 0xf9, 0xe6, 0x1b, 0xd1, 0xe9, 0xd1, 0xfa,
0x98, 0x95, 0x32, 0xda, 0x0c, 0xca, 0x45, 0xc4, 0xf5, 0x99, 0xff, 0x52, 0xb0, 0xc9, 0x87, 0xa0,
0x26, 0x47, 0x45, 0xd3, 0xf3, 0x6c, 0xf4, 0x44, 0x3e, 0xae, 0x66, 0xc2, 0x5e, 0x9e, 0x4d, 0x3e,
0x05, 0xb1, 0x1f, 0x98, 0x29, 0x0b, 0x7b, 0x76, 0x98, 0xf4, 0x42, 0xc6, 0x7c, 0x69, 0x10, 0xf0,
0x2f, 0xa1, 0xb9, 0x7c, 0xd9, 0xc0, 0x53, 0x05, 0x3c, 0xb5, 0xb9, 0x64, 0xe1, 0x10, 0x67, 0xd3,
0x1b, 0x85, 0xf0, 0xe0, 0x1a, 0xe2, 0xe7, 0x1b, 0x85, 0xc8, 0x99, 0x8f, 0x60, 0x3d, 0x35, 0xc2,
0x22, 0xb0, 0x88, 0xc0, 0x5a, 0x62, 0x8c, 0x8d, 0xd3, 0x6b, 0x71, 0xfc, 0x2f, 0x2d, 0x1f, 0xff,
0x9f, 0xc1, 0x46, 0x34, 0xb8, 0x5c, 0xd0, 0xf1, 0xf7, 0xee, 0xe5, 0xa5, 0x19, 0xb0, 0x31, 0x16,
0xe5, 0xbc, 0xb1, 0x1e, 0xb2, 0x8e, 0x24, 0x67, 0xc0, 0xc6, 0xa4, 0x09, 0x25, 0x3a, 0xe3, 0xae,
0xf0, 0x11, 0x36, 0xe2, 0x92, 0x11, 0xff, 0x16, 0xb2, 0xa2, 0xbf, 0xcd, 0x8b, 0xd9, 0xe4, 0x8a,
0xc9, 0x72, 0x51, 0x91, 0xb2, 0x22, 0xd6, 0x11, 0x72, 0xc4, 0x3b, 0xbf, 0x80, 0xed, 0x3b, 0x78,
0x4e, 0x7d, 0x8e, 0x2f, 0x50, 0xa4, 0xcd, 0x16, 0x4e, 0x09, 0xb6, 0x78, 0xc6, 0xc7, 0x40, 0x04,
0xc7, 0x14, 0x26, 0xb1, 0x1c, 0xf3, 0x72, 0x6a, 0x5d, 0x5d, 0x73, 0x9c, 0x43, 0xf2, 0x46, 0x5d,
0x70, 0xce, 0xe9, 0x4d, 0xc7, 0x39, 0x41, 0xf2, 0xb2, 0x4e, 0x57, 0x0b, 0x7d, 0xfe, 0x53, 0x9d,
0xae, 0x9e, 0x8a, 0x0d, 0x89, 0xd3, 0xfe, 0x92, 0x81, 0x6a, 0x2a, 0x38, 0xb1, 0x48, 0xc9, 0x3d,
0xcd, 0x0c, 0x27, 0x81, 0xbc, 0x51, 0x0e, 0x29, 0x9d, 0x09, 0xd9, 0x84, 0x35, 0x6f, 0x76, 0xf1,
0x3d, 0xbb, 0xc5, 0x48, 0x50, 0x8c, 0xf0, 0x17, 0x79, 0x16, 0x8e, 0xa1, 0x59, 0x9c, 0x15, 0x9b,
0xcb, 0x23, 0x3f, 0x31, 0x8f, 0x7e, 0x0a, 0xc4, 0x72, 0xc6, 0xae, 0x2d, 0x62, 0x8b, 0x5f, 0xfb,
0x2c, 0xb8, 0x76, 0xa7, 0x13, 0x8c, 0xdf, 0xaa, 0xb1, 0x1e, 0x71, 0x86, 0x11, 0x43, 0xc0, 0xe3,
0x95, 0x71, 0x0e, 0xcf, 0x4b, 0x78, 0xc4, 0x89, 0xe1, 0xda, 0x2b, 0xd8, 0x1e, 0xac, 0xca, 0x5e,
0xf2, 0x15, 0x80, 0x17, 0xe7, 0x2c, 0x6a, 0x58, 0x39, 0xdc, 0xb9, 0xfb, 0xe0, 0x79, 0x5e, 0x1b,
0x09, 0xbc, 0xb6, 0x03, 0xcd, 0x65, 0xa2, 0x65, 0x81, 0xd6, 0x1e, 0xc1, 0xc6, 0x60, 0x76, 0x75,
0xc5, 0x16, 0x26, 0x35, 0x1f, 0x94, 0x63, 0x2b, 0x78, 0x3d, 0xa3, 0x53, 0xeb, 0xd2, 0x62, 0x93,
0x7f, 0xdd, 0xc8, 0xb9, 0x94, 0x91, 0x3f, 0x86, 0xb5, 0x70, 0x24, 0x97, 0x66, 0x9e, 0x0f, 0x77,
0xad, 0x19, 0x77, 0xc3, 0x79, 0x3c, 0x84, 0x68, 0x3f, 0x66, 0xe0, 0x61, 0xfa, 0x2d, 0x61, 0x13,
0x39, 0x84, 0x52, 0xb4, 0xac, 0x87, 0x85, 0x6a, 0x6b, 0xae, 0x7d, 0xea, 0x7b, 0x86, 0x51, 0x0c,
0x37, 0x77, 0xf2, 0x05, 0x28, 0x93, 0x84, 0x02, 0x8d, 0x2c, 0x9e, 0x7b, 0x14, 0x9f, 0x4b, 0x6a,
0x67, 0xa4, 0xa0, 0xfb, 0x4f, 0xa1, 0x14, 0xed, 0x22, 0x44, 0x81, 0xd2, 0x59, 0xaf, 0xd7, 0x37,
0x7b, 0xa3, 0xa1, 0xfa, 0x80, 0x54, 0xa0, 0x88, 0xbf, 0x3a, 0x5d, 0x35, 0xb3, 0x1f, 0x40, 0x39,
0x5e, 0x45, 0x48, 0x15, 0xca, 0x9d, 0x6e, 0x67, 0xd8, 0x69, 0x0d, 0xf5, 0x63, 0xf5, 0x01, 0x79,
0x04, 0xeb, 0x7d, 0x43, 0xef, 0x9c, 0xb7, 0x9e, 0xeb, 0xa6, 0xa1, 0xbf, 0xd4, 0x5b, 0x67, 0xfa,
0xb1, 0x9a, 0x21, 0x04, 0x6a, 0xa7, 0xc3, 0xb3, 0xb6, 0xd9, 0x1f, 0x1d, 0x9d, 0x75, 0x06, 0xa7,
0xfa, 0xb1, 0x9a, 0x15, 0x32, 0x07, 0xa3, 0x76, 0x5b, 0x1f, 0x0c, 0xd4, 0x1c, 0x01, 0x58, 0x3b,
0x69, 0x75, 0x04, 0x38, 0x4f, 0x36, 0xa0, 0xde, 0xe9, 0xbe, 0xec, 0x75, 0xda, 0xba, 0x39, 0xd0,
0x87, 0x43, 0x41, 0x2c, 0xec, 0xff, 0x23, 0x03, 0xd5, 0xd4, 0x36, 0x43, 0xb6, 0x60, 0x43, 0x1c,
0x19, 0x19, 0xe2, 0xa6, 0xd6, 0xa0, 0xd7, 0x35, 0xbb, 0xbd, 0xae, 0xae, 0x3e, 0x20, 0xef, 0xc0,
0xd6, 0x02, 0xa3, 0x77, 0x72, 0xd2, 0x3e, 0x6d, 0x89, 0xc7, 0x93, 0x26, 0x6c, 0x2e, 0x30, 0x87,
0x9d, 0x73, 0x5d, 0x68, 0x99, 0x25, 0xbb, 0xb0, 0xb3, 0xc0, 0x1b, 0x7c, 0xa7, 0xeb, 0xfd, 0x18,
0x91, 0x23, 0x4f, 0xe1, 0xc9, 0x02, 0xa2, 0xd3, 0x1d, 0x8c, 0x4e, 0x4e, 0x3a, 0xed, 0x8e, 0xde,
0x1d, 0x9a, 0x2f, 0x5b, 0x67, 0x23, 0x5d, 0xcd, 0x93, 0x1d, 0x68, 0x2c, 0x5e, 0xa2, 0x9f, 0xf7,
0x7b, 0x46, 0xcb, 0x78, 0xa5, 0x16, 0xc8, 0xfb, 0xf0, 0xf8, 0x8e, 0x90, 0x76, 0xcf, 0x30, 0xf4,
0xf6, 0xd0, 0x6c, 0x9d, 0xf7, 0x46, 0xdd, 0xa1, 0xba, 0xb6, 0x7f, 0x20, 0x36, 0x86, 0x85, 0x84,
0x14, 0x26, 0x1b, 0x75, 0xbf, 0xe9, 0xf6, 0xbe, 0xeb, 0xaa, 0x0f, 0x84, 0xe5, 0x87, 0xa7, 0x86,
0x3e, 0x38, 0xed, 0x9d, 0x1d, 0xab, 0x99, 0xfd, 0x5f, 0xe6, 0x00, 0xe6, 0xb1, 0x25, 0xac, 0xd3,
0x1a, 0x0d, 0x7b, 0xd1, 0x0d, 0xf3, 0x63, 0x1a, 0xbc, 0x97, 0x64, 0x1c, 0x8d, 0x8e, 0x9f, 0xeb,
0x43, 0xb3, 0xdb, 0x1b, 0x9a, 0x83, 0x61, 0xcb, 0x18, 0xa2, 0xbb, 0x9a, 0xb0, 0x99, 0xc4, 0x48,
0x2b, 0x9c, 0xe8, 0xfa, 0x40, 0xcd, 0x92, 0xf7, 0xa0, 0xb9, 0xe4, 0xbc, 0x7e, 0xd6, 0xea, 0x0f,
0xf4, 0x63, 0x35, 0x47, 0xb6, 0xe1, 0x51, 0x92, 0xdf, 0xe9, 0x9a, 0x27, 0x67, 0x9d, 0xe7, 0xa7,
0x43, 0x35, 0x4f, 0x1a, 0xf0, 0x30, 0x2d, 0xb6, 0x85, 0x52, 0xd5, 0xc2, 0xe2, 0xa1, 0xf3, 0x4e,
0x57, 0x37, 0x90, 0xb5, 0x46, 0x36, 0x81, 0x24, 0x59, 0x7d, 0x43, 0xef, 0xb7, 0x5e, 0xa9, 0x45,
0xf2, 0x18, 0xde, 0x49, 0xd2, 0x23, 0x8b, 0x1e, 0xb5, 0xda, 0xdf, 0xf4, 0x4e, 0x4e, 0xd4, 0xd2,
0xe2, 0x6d, 0x71, 0x34, 0x97, 0x17, 0x6d, 0x13, 0x45, 0x36, 0x08, 0xbf, 0xa5, 0x18, 0x9d, 0x6f,
0x47, 0x9d, 0xe3, 0xce, 0xf0, 0x95, 0xd9, 0xfb, 0x46, 0xad, 0x08, 0xbf, 0x2d, 0xd1, 0x3c, 0x19,
0x00, 0xaa, 0x72, 0xf8, 0xd7, 0xb2, 0xfc, 0x68, 0xd0, 0xc6, 0xcf, 0x94, 0xc4, 0x80, 0x62, 0x98,
0xa8, 0x64, 0x55, 0xea, 0x36, 0x1f, 0xa5, 0x16, 0xbf, 0xb8, 0x40, 0x6d, 0xfd, 0xe2, 0xcf, 0x7f,
0xfb, 0x75, 0x76, 0x5d, 0x53, 0x0e, 0xde, 0x7c, 0x76, 0x20, 0x10, 0x07, 0xee, 0x8c, 0x7f, 0x99,
0xd9, 0x27, 0x3d, 0x58, 0x93, 0x1f, 0xa7, 0xc8, 0x66, 0x4a, 0x64, 0xfc, 0xb5, 0x6a, 0x95, 0xc4,
0x4d, 0x94, 0xa8, 0x6a, 0x95, 0x58, 0xa2, 0xe5, 0x08, 0x81, 0x5f, 0x40, 0x31, 0xfc, 0xf4, 0x91,
0x78, 0x64, 0xfa, 0x63, 0x48, 0x73, 0xd9, 0x76, 0xfa, 0xdf, 0x19, 0xf2, 0xbf, 0x50, 0x8e, 0x17,
0x5b, 0xb2, 0x9d, 0x28, 0xcd, 0xe9, 0xb2, 0xda, 0x6c, 0x2e, 0x63, 0xa5, 0x9f, 0x45, 0x6a, 0xf1,
0xb3, 0x70, 0xe9, 0x25, 0x23, 0x59, 0x8e, 0xc4, 0xd2, 0x4b, 0x1a, 0xa9, 0xeb, 0x13, 0x7b, 0xf0,
0xd2, 0x87, 0x69, 0x4d, 0x14, 0xf9, 0x90, 0x90, 0x94, 0xc8, 0x83, 0x1f, 0xac, 0xc9, 0xff, 0x93,
0xff, 0x03, 0x25, 0x74, 0x00, 0xae, 0xa6, 0x64, 0x6e, 0xac, 0xe4, 0xfe, 0xdc, 0x9c, 0x2b, 0xb3,
0xb8, 0xc4, 0x2e, 0x91, 0xee, 0xce, 0xf8, 0x01, 0x47, 0x69, 0x17, 0xb1, 0x74, 0x5c, 0x79, 0x12,
0xd2, 0x93, 0xcb, 0x63, 0x5a, 0x7a, 0x6a, 0x39, 0xd2, 0x76, 0x51, 0x7a, 0x93, 0x34, 0x52, 0xd2,
0x5f, 0x0b, 0xcc, 0xc1, 0x0f, 0xd4, 0xe6, 0x42, 0x83, 0x9a, 0x98, 0x78, 0xd1, 0xe5, 0xf7, 0xea,
0x30, 0xb7, 0xda, 0xc2, 0xa7, 0x00, 0x6d, 0x1b, 0x2f, 0xd9, 0x20, 0xeb, 0x89, 0x50, 0x88, 0x35,
0x98, 0x4b, 0xbf, 0x57, 0x87, 0xa4, 0xf4, 0xb4, 0x0a, 0x8f, 0x51, 0xfa, 0x36, 0xd9, 0x4a, 0x4a,
0x4f, 0x6a, 0xf0, 0x0a, 0xaa, 0xe2, 0x8e, 0x68, 0xe7, 0x09, 0x12, 0x91, 0x9c, 0x5a, 0xac, 0x9a,
0x5b, 0x77, 0xe8, 0xe9, 0xec, 0x20, 0x75, 0xbc, 0x22, 0xa0, 0xfc, 0x40, 0x2e, 0x53, 0x84, 0x03,
0xb9, 0xbb, 0x0e, 0x10, 0x2d, 0x96, 0xb3, 0x72, 0x57, 0x68, 0xde, 0x3b, 0x59, 0x68, 0x3b, 0x78,
0xe1, 0x26, 0x79, 0x88, 0x17, 0x46, 0x80, 0x03, 0x4f, 0xca, 0xff, 0x19, 0x90, 0xc1, 0x7d, 0xb7,
0xae, 0x9c, 0x71, 0x9a, 0xef, 0xdf, 0x8b, 0x49, 0x1b, 0x54, 0x5b, 0x7a, 0xb9, 0x48, 0x61, 0x06,
0x4a, 0x72, 0x82, 0x20, 0x73, 0x5d, 0x96, 0x0c, 0x39, 0xcd, 0x77, 0x57, 0x70, 0xc3, 0xdb, 0x1a,
0x78, 0x1b, 0x21, 0xaa, 0xb8, 0x4d, 0xcc, 0xb5, 0x07, 0x81, 0x84, 0x5d, 0xac, 0xe1, 0xff, 0x53,
0x3e, 0xff, 0x67, 0x00, 0x00, 0x00, 0xff, 0xff, 0xe6, 0x11, 0x1a, 0x17, 0x86, 0x19, 0x00, 0x00,
0x49, 0x83, 0xd4, 0xba, 0xf6, 0xd5, 0xab, 0x42, 0x8d, 0xc8, 0xa1, 0x84, 0x32, 0xf1, 0x67, 0x81,
0xe1, 0xae, 0x54, 0xae, 0x97, 0x54, 0xa5, 0xca, 0xe7, 0x1c, 0xf2, 0x0d, 0x72, 0xc8, 0x2d, 0xb7,
0xdc, 0xf2, 0x15, 0x72, 0x4a, 0x72, 0xcb, 0x25, 0x87, 0x5c, 0x72, 0xc8, 0x77, 0x48, 0x4d, 0x0f,
0x00, 0x02, 0x14, 0x29, 0x27, 0x87, 0xdc, 0xc4, 0xee, 0xdf, 0x74, 0x4f, 0xff, 0x9d, 0x6e, 0x08,
0x94, 0xd1, 0xd4, 0x62, 0x0e, 0x7f, 0xe6, 0xf9, 0x2e, 0x77, 0x49, 0x71, 0xea, 0xba, 0x9e, 0xef,
0x8d, 0x9a, 0x3b, 0x57, 0xae, 0x7b, 0x35, 0x65, 0x07, 0xd4, 0xb3, 0x0e, 0xa8, 0xe3, 0xb8, 0x9c,
0x72, 0xcb, 0x75, 0x02, 0x09, 0xd3, 0x7e, 0x9b, 0x87, 0xda, 0x99, 0xeb, 0x7a, 0xbd, 0x19, 0x37,
0xd8, 0xeb, 0x19, 0x0b, 0x38, 0x51, 0x21, 0x47, 0x6d, 0xde, 0xc8, 0xec, 0x66, 0xf6, 0x72, 0x86,
0xf8, 0x93, 0x10, 0xc8, 0x8f, 0x59, 0xc0, 0x1b, 0xd9, 0xdd, 0xcc, 0x5e, 0xd9, 0xc0, 0xbf, 0xc9,
0x01, 0x3c, 0xb4, 0xe9, 0x8d, 0x19, 0xbc, 0xa5, 0x9e, 0xe9, 0xbb, 0x33, 0x6e, 0x39, 0x57, 0xe6,
0x84, 0xb1, 0x46, 0x0e, 0x8f, 0xad, 0xdb, 0xf4, 0x66, 0xf0, 0x96, 0x7a, 0x86, 0xe4, 0x9c, 0x30,
0x46, 0x3e, 0x87, 0x4d, 0x71, 0xc0, 0xf3, 0x99, 0x47, 0x6f, 0x53, 0x47, 0xf2, 0x78, 0x64, 0xc3,
0xa6, 0x37, 0x7d, 0x64, 0x26, 0x0e, 0xed, 0x82, 0x12, 0x6b, 0x11, 0xd0, 0x02, 0x42, 0x21, 0x94,
0x2e, 0x10, 0x1f, 0x40, 0x2d, 0x21, 0x56, 0x5c, 0x7c, 0x0d, 0x31, 0x4a, 0x2c, 0xae, 0x65, 0x73,
0xa2, 0x41, 0x55, 0xa0, 0x6c, 0xcb, 0x61, 0x3e, 0x0a, 0x2a, 0x22, 0xa8, 0x62, 0xd3, 0x9b, 0x73,
0x41, 0x13, 0x92, 0x3e, 0x01, 0x55, 0xf8, 0xcc, 0x74, 0x67, 0xdc, 0x1c, 0x5d, 0x53, 0xc7, 0x61,
0xd3, 0x46, 0x69, 0x37, 0xb3, 0x97, 0x3f, 0xca, 0x36, 0x32, 0x46, 0x6d, 0x2a, 0xbd, 0xd4, 0x96,
0x1c, 0xb2, 0x0f, 0xeb, 0xee, 0x8c, 0x5f, 0xb9, 0xc2, 0x08, 0x81, 0x36, 0x03, 0xc6, 0x1b, 0x95,
0xdd, 0xdc, 0x5e, 0xde, 0xa8, 0x47, 0x0c, 0x81, 0x1d, 0x30, 0x2e, 0xb0, 0xc1, 0x5b, 0xc6, 0x3c,
0x73, 0xe4, 0x3a, 0x13, 0x93, 0x53, 0xff, 0x8a, 0xf1, 0x46, 0x79, 0x37, 0xb3, 0x57, 0x30, 0xea,
0xc8, 0x68, 0xbb, 0xce, 0x64, 0x88, 0x64, 0xf2, 0x29, 0x90, 0x6b, 0x3e, 0x1d, 0x21, 0xd4, 0xf2,
0x6d, 0x19, 0xac, 0x46, 0x15, 0xc1, 0xeb, 0x82, 0xd3, 0x4e, 0x32, 0xc8, 0x97, 0xb0, 0x8d, 0xce,
0xf1, 0x66, 0x97, 0x53, 0x6b, 0x84, 0x44, 0x73, 0xcc, 0xe8, 0x78, 0x6a, 0x39, 0xac, 0x01, 0xe2,
0xf6, 0xc6, 0x96, 0x00, 0xf4, 0xe7, 0xfc, 0xe3, 0x90, 0x4d, 0x1e, 0x42, 0x61, 0x4a, 0x2f, 0xd9,
0xb4, 0xa1, 0x60, 0x5c, 0xe5, 0x0f, 0xb2, 0x03, 0x65, 0xcb, 0xb1, 0xb8, 0x45, 0xb9, 0xeb, 0x37,
0x6a, 0xc8, 0x99, 0x13, 0xb4, 0x1f, 0xb3, 0x50, 0x15, 0xf9, 0xd2, 0x71, 0x56, 0xa7, 0xcb, 0x62,
0xd0, 0xb2, 0x77, 0x82, 0x76, 0x27, 0x1c, 0xb9, 0xbb, 0xe1, 0xd8, 0x86, 0xd2, 0x94, 0x06, 0xdc,
0xbc, 0x76, 0x3d, 0xcc, 0x10, 0xc5, 0x28, 0x8a, 0xdf, 0xa7, 0xae, 0x47, 0xde, 0x87, 0x2a, 0xbb,
0xe1, 0xcc, 0x77, 0xe8, 0xd4, 0x14, 0x2e, 0xc1, 0xb4, 0x28, 0x19, 0x4a, 0x44, 0x3c, 0xe5, 0xd3,
0x11, 0xd9, 0x03, 0x35, 0x76, 0x64, 0xe4, 0xf3, 0x35, 0x74, 0x63, 0x2d, 0x72, 0x63, 0xe8, 0xf2,
0xd8, 0x0f, 0xc5, 0x95, 0x7e, 0x28, 0x2d, 0xfa, 0xe1, 0xef, 0x19, 0x50, 0x30, 0xc1, 0x59, 0xe0,
0xb9, 0x4e, 0xc0, 0x08, 0x81, 0xac, 0x35, 0x46, 0x2f, 0x94, 0x31, 0x5f, 0xb2, 0xd6, 0x58, 0x98,
0x60, 0x8d, 0xcd, 0xcb, 0x5b, 0xce, 0x02, 0xb4, 0x50, 0x31, 0x8a, 0xd6, 0xf8, 0x48, 0xfc, 0x24,
0x4f, 0x41, 0xc1, 0xdb, 0xd1, 0xf1, 0xd8, 0x67, 0x41, 0x20, 0x4b, 0x0b, 0x0f, 0x56, 0x04, 0xbd,
0x25, 0xc9, 0xe4, 0x19, 0x6c, 0x24, 0x61, 0xa6, 0xe3, 0x1d, 0xbe, 0x0d, 0xae, 0xd1, 0x1f, 0x65,
0x99, 0x0e, 0x21, 0xb2, 0x8b, 0x0c, 0xf2, 0x49, 0x98, 0x3d, 0x11, 0x5e, 0xc2, 0x0b, 0x08, 0x57,
0x13, 0xf0, 0x3e, 0xa2, 0x9f, 0x42, 0x2d, 0x60, 0xfe, 0x1b, 0xe6, 0x9b, 0x36, 0x0b, 0x02, 0x7a,
0xc5, 0xd0, 0x41, 0x65, 0xa3, 0x2a, 0xa9, 0xe7, 0x92, 0xa8, 0xa9, 0x50, 0x3b, 0x77, 0x1d, 0x8b,
0xbb, 0x7e, 0x18, 0x73, 0xed, 0x77, 0x79, 0x00, 0x61, 0xfd, 0x80, 0x53, 0x3e, 0x0b, 0x96, 0x76,
0x0c, 0xe1, 0x8d, 0xec, 0x4a, 0x6f, 0x54, 0x16, 0xbd, 0x91, 0xe7, 0xb7, 0x9e, 0x4c, 0x83, 0xda,
0xe1, 0xfa, 0xb3, 0xb0, 0x77, 0x3d, 0x13, 0x3a, 0x86, 0xb7, 0x1e, 0x33, 0x90, 0x4d, 0xf6, 0xa0,
0x10, 0x70, 0xca, 0x65, 0xc7, 0xa8, 0x1d, 0x92, 0x14, 0x4e, 0xdc, 0x85, 0x19, 0x12, 0x40, 0xbe,
0x86, 0xda, 0x84, 0x5a, 0xd3, 0x99, 0xcf, 0x4c, 0x9f, 0xd1, 0xc0, 0x75, 0x30, 0x93, 0x6b, 0x87,
0x9b, 0xf1, 0x91, 0x13, 0xc9, 0x36, 0x90, 0x6b, 0x54, 0x27, 0xc9, 0x9f, 0xe4, 0x43, 0xa8, 0x87,
0xa1, 0x16, 0xf5, 0xc4, 0x2d, 0x3b, 0xea, 0x3c, 0xb5, 0x39, 0x79, 0x68, 0xd9, 0xe2, 0x46, 0x2a,
0x26, 0xe9, 0xcc, 0x1b, 0x53, 0xce, 0x24, 0x52, 0xf6, 0x9f, 0x9a, 0xa0, 0x5f, 0x20, 0x19, 0x91,
0x8b, 0x01, 0x2f, 0x2e, 0x0f, 0xf8, 0xf2, 0x00, 0x2a, 0x2b, 0x02, 0xb8, 0x22, 0x3d, 0xaa, 0xab,
0xd2, 0xe3, 0x31, 0x54, 0x46, 0x6e, 0xc0, 0x4d, 0x19, 0x5f, 0xcc, 0xea, 0x9c, 0x01, 0x82, 0x34,
0x40, 0x0a, 0x79, 0x02, 0x0a, 0x02, 0x5c, 0x67, 0x74, 0x4d, 0x2d, 0x07, 0x9b, 0x54, 0xce, 0xc0,
0x43, 0x3d, 0x49, 0x12, 0xc5, 0x27, 0x21, 0x93, 0x89, 0xc4, 0x80, 0xec, 0xb7, 0x88, 0x09, 0x69,
0xf3, 0x92, 0xaa, 0x27, 0x4a, 0x4a, 0x23, 0xa0, 0x9e, 0x59, 0x01, 0x17, 0xd1, 0x0a, 0xa2, 0x54,
0xfa, 0x1f, 0x58, 0x4f, 0xd0, 0xc2, 0x62, 0xfa, 0x08, 0x0a, 0xa2, 0x7b, 0x04, 0x8d, 0xcc, 0x6e,
0x6e, 0xaf, 0x72, 0xb8, 0x71, 0x27, 0xd0, 0xb3, 0xc0, 0x90, 0x08, 0xed, 0x09, 0xd4, 0x05, 0xb1,
0xe3, 0x4c, 0xdc, 0xa8, 0x23, 0xd5, 0xe2, 0x52, 0x54, 0x44, 0xe2, 0x69, 0x35, 0x50, 0x86, 0xcc,
0xb7, 0x63, 0x95, 0x3f, 0x87, 0x7a, 0xc7, 0x09, 0x29, 0xa1, 0xc2, 0xff, 0x82, 0xba, 0x6d, 0x39,
0xb2, 0x65, 0x51, 0xdb, 0x9d, 0x39, 0x3c, 0x0c, 0x78, 0xd5, 0xb6, 0x1c, 0x21, 0xbf, 0x85, 0x44,
0xc4, 0x45, 0xad, 0x2d, 0xc4, 0xad, 0x85, 0x38, 0xd9, 0xdd, 0x24, 0xee, 0x45, 0xbe, 0x94, 0x51,
0xb3, 0x2f, 0xf2, 0xa5, 0xac, 0x9a, 0x7b, 0x91, 0x2f, 0xe5, 0xd4, 0xfc, 0x8b, 0x7c, 0x29, 0xaf,
0x16, 0x5e, 0xe4, 0x4b, 0x45, 0xb5, 0xa4, 0xfd, 0x21, 0x03, 0x6a, 0x6f, 0xc6, 0xff, 0xa3, 0x57,
0xc0, 0x87, 0xd1, 0x72, 0xcc, 0xd1, 0x94, 0xbf, 0x31, 0xc7, 0x6c, 0xca, 0x29, 0x86, 0xbb, 0x60,
0x28, 0xb6, 0xe5, 0xb4, 0xa7, 0xfc, 0xcd, 0xb1, 0xa0, 0x45, 0xcf, 0x67, 0x02, 0x55, 0x0e, 0x51,
0xf4, 0x26, 0x46, 0xfd, 0x84, 0x39, 0xbf, 0xce, 0x80, 0xf2, 0xed, 0xcc, 0xe5, 0x6c, 0xf5, 0x93,
0x80, 0x89, 0x37, 0xef, 0xc3, 0x59, 0xd4, 0x01, 0xa3, 0x79, 0x0f, 0xbe, 0xd3, 0xd2, 0x73, 0x4b,
0x5a, 0xfa, 0xbd, 0x8f, 0x5d, 0xfe, 0xde, 0xc7, 0x4e, 0xfb, 0x65, 0x46, 0x44, 0x3d, 0xbc, 0x66,
0xe8, 0xf2, 0x5d, 0x50, 0xa2, 0x47, 0xca, 0x0c, 0x68, 0x74, 0x61, 0x08, 0xe4, 0x2b, 0x35, 0xa0,
0x38, 0xe5, 0x60, 0x81, 0xa1, 0xc6, 0xe0, 0x3a, 0x46, 0x86, 0x53, 0x8e, 0xe0, 0xf5, 0x25, 0x2b,
0x3c, 0xf0, 0x2e, 0x40, 0xc2, 0x97, 0x05, 0xb4, 0xb3, 0x3c, 0x4a, 0x38, 0x52, 0xba, 0x30, 0xaf,
0x16, 0xb4, 0x3f, 0xca, 0x2c, 0xf8, 0x77, 0xaf, 0xf4, 0x01, 0xd4, 0xe6, 0xc3, 0x0e, 0x62, 0xe4,
0xfb, 0xaa, 0x78, 0xd1, 0xb4, 0x23, 0x50, 0x1f, 0x87, 0x7d, 0x44, 0xce, 0x1d, 0xe9, 0x6b, 0xd7,
0x05, 0x67, 0x20, 0x18, 0xa1, 0x48, 0x9c, 0x4f, 0x84, 0x5f, 0xe9, 0xad, 0xcd, 0x1c, 0x6e, 0xe2,
0xb0, 0x27, 0xdf, 0xdc, 0x3a, 0xfa, 0x53, 0xd2, 0x8f, 0x45, 0x6c, 0xef, 0x37, 0x50, 0xab, 0x43,
0x75, 0xe8, 0x7e, 0xcf, 0x9c, 0xb8, 0xd8, 0xbe, 0x82, 0x5a, 0x44, 0x08, 0x4d, 0xdc, 0x87, 0x35,
0x8e, 0x94, 0xb0, 0xba, 0xe7, 0x6d, 0xfc, 0x2c, 0xa0, 0x1c, 0xc1, 0x46, 0x88, 0xd0, 0x7e, 0x9f,
0x85, 0x72, 0x4c, 0x15, 0x49, 0x72, 0x49, 0x03, 0x66, 0xda, 0x74, 0x44, 0x7d, 0xd7, 0x75, 0xc2,
0x1a, 0x57, 0x04, 0xf1, 0x3c, 0xa4, 0x89, 0x16, 0x16, 0xd9, 0x71, 0x4d, 0x83, 0x6b, 0xf4, 0x8e,
0x62, 0x54, 0x42, 0xda, 0x29, 0x0d, 0xae, 0xc9, 0x47, 0xa0, 0x46, 0x10, 0xcf, 0x67, 0x96, 0x2d,
0x5e, 0x3e, 0xf9, 0x3e, 0xd7, 0x43, 0x7a, 0x3f, 0x24, 0x8b, 0x06, 0x2f, 0x8b, 0xcc, 0xf4, 0xa8,
0x35, 0x36, 0x6d, 0xe1, 0x45, 0x39, 0xaf, 0xd6, 0x24, 0xbd, 0x4f, 0xad, 0xf1, 0x79, 0x40, 0x39,
0xf9, 0x0c, 0x1e, 0x25, 0x86, 0xda, 0x04, 0x5c, 0x56, 0x31, 0xf1, 0xe3, 0xa9, 0x36, 0x3e, 0xf2,
0x04, 0x14, 0xf1, 0x62, 0x98, 0x23, 0x9f, 0x51, 0xce, 0xc6, 0x61, 0x1d, 0x57, 0x04, 0xad, 0x2d,
0x49, 0xa4, 0x01, 0x45, 0x76, 0xe3, 0x59, 0x3e, 0x1b, 0xe3, 0x8b, 0x51, 0x32, 0xa2, 0x9f, 0xe2,
0x70, 0xc0, 0x5d, 0x9f, 0x5e, 0x31, 0xd3, 0xa1, 0x36, 0x0b, 0x47, 0x94, 0x4a, 0x48, 0xeb, 0x52,
0x9b, 0x69, 0xef, 0xc0, 0xf6, 0x73, 0xc6, 0xcf, 0xac, 0xd7, 0x33, 0x6b, 0x6c, 0xf1, 0xdb, 0x3e,
0xf5, 0xe9, 0xbc, 0x0b, 0xfe, 0xb5, 0x00, 0x1b, 0x69, 0x16, 0xe3, 0xcc, 0x17, 0x2f, 0x50, 0xc1,
0x9f, 0x4d, 0x59, 0x14, 0x9d, 0xf9, 0x8b, 0x19, 0x83, 0x8d, 0xd9, 0x94, 0x19, 0x12, 0x44, 0xb6,
0xa0, 0x88, 0xd6, 0x7a, 0x76, 0x43, 0xc5, 0x02, 0x5c, 0x9b, 0x30, 0xd6, 0xf7, 0x6c, 0xf2, 0x35,
0xec, 0xcc, 0x73, 0xcf, 0x17, 0x8f, 0x63, 0x40, 0xb9, 0xe9, 0x31, 0xdf, 0x7c, 0x23, 0x46, 0x00,
0x0c, 0x0b, 0x96, 0xab, 0x4c, 0x43, 0x83, 0x72, 0x91, 0x8a, 0x7d, 0xe6, 0xbf, 0x14, 0x6c, 0xf2,
0x21, 0xa8, 0xc9, 0x19, 0x12, 0x15, 0xe4, 0xf0, 0x48, 0x75, 0x3e, 0x47, 0x0a, 0x3d, 0x9f, 0x82,
0x58, 0x1c, 0xcc, 0x94, 0xeb, 0x3d, 0x3b, 0xec, 0x06, 0x42, 0xc6, 0x7c, 0x9b, 0x10, 0xf0, 0x2f,
0xa1, 0xb9, 0x7c, 0x0b, 0xc1, 0x53, 0x05, 0x3c, 0xb5, 0xb9, 0x64, 0x13, 0x11, 0x67, 0xd3, 0xab,
0x86, 0x08, 0xed, 0x1a, 0xe2, 0xe7, 0xab, 0x86, 0x28, 0xa6, 0x8f, 0x60, 0x3d, 0x35, 0xdb, 0x22,
0xb0, 0x88, 0xc0, 0x5a, 0x62, 0xbe, 0x8d, 0xeb, 0x6e, 0x71, 0x2f, 0x28, 0x2d, 0xdf, 0x0b, 0x9e,
0xc1, 0x46, 0x34, 0xd1, 0x5c, 0xd2, 0xd1, 0xf7, 0xee, 0x64, 0x62, 0x06, 0x6c, 0x84, 0xdd, 0x3a,
0x6f, 0xac, 0x87, 0xac, 0x23, 0xc9, 0x19, 0xb0, 0x11, 0x69, 0x42, 0x89, 0xce, 0xb8, 0x2b, 0x82,
0x87, 0x2f, 0x74, 0xc9, 0x88, 0x7f, 0x0b, 0x59, 0xd1, 0xdf, 0xe6, 0xe5, 0x6c, 0x7c, 0xc5, 0x64,
0x1f, 0xa9, 0x48, 0x59, 0x11, 0xeb, 0x08, 0x39, 0xe2, 0x9e, 0x5f, 0xc0, 0xf6, 0x1d, 0x3c, 0xa7,
0x3e, 0xc7, 0x1b, 0x28, 0xd2, 0x67, 0x0b, 0xa7, 0x04, 0x5b, 0x5c, 0xe3, 0x63, 0x20, 0x82, 0x63,
0x0a, 0x97, 0x58, 0x8e, 0x39, 0x99, 0x5a, 0x57, 0xd7, 0x1c, 0x07, 0x94, 0xbc, 0x51, 0x17, 0x9c,
0x73, 0x7a, 0xd3, 0x71, 0x4e, 0x90, 0xbc, 0xec, 0x09, 0xac, 0x85, 0x31, 0xff, 0xa9, 0x27, 0xb0,
0x9e, 0xca, 0x0d, 0x89, 0xd3, 0xfe, 0x9c, 0x81, 0x6a, 0x2a, 0x6b, 0xb1, 0x7b, 0xc9, 0x05, 0xce,
0x0c, 0x47, 0x84, 0xbc, 0x51, 0x0e, 0x29, 0x9d, 0x31, 0xd9, 0x84, 0x35, 0x6f, 0x76, 0xf9, 0x3d,
0xbb, 0xc5, 0x4c, 0x50, 0x8c, 0xf0, 0x17, 0x79, 0x16, 0xce, 0xa7, 0x59, 0x1c, 0x22, 0x9b, 0xcb,
0x4b, 0x22, 0x31, 0xa8, 0x7e, 0x0a, 0xc4, 0x72, 0x46, 0xae, 0x2d, 0x72, 0x8b, 0x5f, 0xfb, 0x2c,
0xb8, 0x76, 0xa7, 0x63, 0xcc, 0xdf, 0xaa, 0xb1, 0x1e, 0x71, 0x86, 0x11, 0x43, 0xc0, 0xe3, 0x5d,
0x72, 0x0e, 0xcf, 0x4b, 0x78, 0xc4, 0x89, 0xe1, 0xda, 0x2b, 0xd8, 0x1e, 0xac, 0x2a, 0x6b, 0xf2,
0x15, 0x80, 0x17, 0x17, 0x33, 0x5a, 0x58, 0x39, 0xdc, 0xb9, 0x7b, 0xe1, 0x79, 0xc1, 0x1b, 0x09,
0xbc, 0xb6, 0x03, 0xcd, 0x65, 0xa2, 0x65, 0xe7, 0xd6, 0x1e, 0xc1, 0xc6, 0x60, 0x76, 0x75, 0xc5,
0x16, 0x46, 0x38, 0x1f, 0x94, 0x63, 0x2b, 0x78, 0x3d, 0xa3, 0x53, 0x6b, 0x62, 0xb1, 0xf1, 0xbf,
0xee, 0xe4, 0x5c, 0xca, 0xc9, 0x1f, 0xc3, 0x5a, 0x38, 0xab, 0x4b, 0x37, 0xcf, 0xa7, 0xbe, 0xd6,
0x8c, 0xbb, 0xe1, 0xa0, 0x1e, 0x42, 0xb4, 0x1f, 0x33, 0xf0, 0x30, 0x7d, 0x97, 0xf0, 0x75, 0x39,
0x84, 0x52, 0xb4, 0xc5, 0x87, 0x1d, 0x6c, 0x6b, 0x6e, 0x7d, 0xea, 0x43, 0x87, 0x51, 0x0c, 0x57,
0x7a, 0xf2, 0x05, 0x28, 0xe3, 0x84, 0x01, 0x8d, 0x2c, 0x9e, 0x7b, 0x14, 0x9f, 0x4b, 0x5a, 0x67,
0xa4, 0xa0, 0xfb, 0x4f, 0xa1, 0x14, 0x2d, 0x29, 0x44, 0x81, 0xd2, 0x59, 0xaf, 0xd7, 0x37, 0x7b,
0x17, 0x43, 0xf5, 0x01, 0xa9, 0x40, 0x11, 0x7f, 0x75, 0xba, 0x6a, 0x66, 0x3f, 0x80, 0x72, 0xbc,
0xa3, 0x90, 0x2a, 0x94, 0x3b, 0xdd, 0xce, 0xb0, 0xd3, 0x1a, 0xea, 0xc7, 0xea, 0x03, 0xf2, 0x08,
0xd6, 0xfb, 0x86, 0xde, 0x39, 0x6f, 0x3d, 0xd7, 0x4d, 0x43, 0x7f, 0xa9, 0xb7, 0xce, 0xf4, 0x63,
0x35, 0x43, 0x08, 0xd4, 0x4e, 0x87, 0x67, 0x6d, 0xb3, 0x7f, 0x71, 0x74, 0xd6, 0x19, 0x9c, 0xea,
0xc7, 0x6a, 0x56, 0xc8, 0x1c, 0x5c, 0xb4, 0xdb, 0xfa, 0x60, 0xa0, 0xe6, 0x08, 0xc0, 0xda, 0x49,
0xab, 0x23, 0xc0, 0x79, 0xb2, 0x01, 0xf5, 0x4e, 0xf7, 0x65, 0xaf, 0xd3, 0xd6, 0xcd, 0x81, 0x3e,
0x1c, 0x0a, 0x62, 0x61, 0xff, 0x1f, 0x19, 0xa8, 0xa6, 0xd6, 0x1c, 0xb2, 0x05, 0x1b, 0xe2, 0xc8,
0x85, 0x21, 0x34, 0xb5, 0x06, 0xbd, 0xae, 0xd9, 0xed, 0x75, 0x75, 0xf5, 0x01, 0x79, 0x07, 0xb6,
0x16, 0x18, 0xbd, 0x93, 0x93, 0xf6, 0x69, 0x4b, 0x5c, 0x9e, 0x34, 0x61, 0x73, 0x81, 0x39, 0xec,
0x9c, 0xeb, 0xc2, 0xca, 0x2c, 0xd9, 0x85, 0x9d, 0x05, 0xde, 0xe0, 0x3b, 0x5d, 0xef, 0xc7, 0x88,
0x1c, 0x79, 0x0a, 0x4f, 0x16, 0x10, 0x9d, 0xee, 0xe0, 0xe2, 0xe4, 0xa4, 0xd3, 0xee, 0xe8, 0xdd,
0xa1, 0xf9, 0xb2, 0x75, 0x76, 0xa1, 0xab, 0x79, 0xb2, 0x03, 0x8d, 0x45, 0x25, 0xfa, 0x79, 0xbf,
0x67, 0xb4, 0x8c, 0x57, 0x6a, 0x81, 0xbc, 0x0f, 0x8f, 0xef, 0x08, 0x69, 0xf7, 0x0c, 0x43, 0x6f,
0x0f, 0xcd, 0xd6, 0x79, 0xef, 0xa2, 0x3b, 0x54, 0xd7, 0xf6, 0x0f, 0xc4, 0x2a, 0xb1, 0x50, 0x90,
0xc2, 0x65, 0x17, 0xdd, 0x6f, 0xba, 0xbd, 0xef, 0xba, 0xea, 0x03, 0xe1, 0xf9, 0xe1, 0xa9, 0xa1,
0x0f, 0x4e, 0x7b, 0x67, 0xc7, 0x6a, 0x66, 0xff, 0x37, 0x39, 0x80, 0x79, 0x6e, 0x09, 0xef, 0xb4,
0x2e, 0x86, 0xbd, 0x48, 0xc3, 0xfc, 0x98, 0x06, 0xef, 0x25, 0x19, 0x47, 0x17, 0xc7, 0xcf, 0xf5,
0xa1, 0xd9, 0xed, 0x0d, 0xcd, 0xc1, 0xb0, 0x65, 0x0c, 0x31, 0x5c, 0x4d, 0xd8, 0x4c, 0x62, 0xa4,
0x17, 0x4e, 0x74, 0x7d, 0xa0, 0x66, 0xc9, 0x7b, 0xd0, 0x5c, 0x72, 0x5e, 0x3f, 0x6b, 0xf5, 0x07,
0xfa, 0xb1, 0x9a, 0x23, 0xdb, 0xf0, 0x28, 0xc9, 0xef, 0x74, 0xcd, 0x93, 0xb3, 0xce, 0xf3, 0xd3,
0xa1, 0x9a, 0x27, 0x0d, 0x78, 0x98, 0x16, 0xdb, 0x42, 0xa9, 0x6a, 0x61, 0xf1, 0xd0, 0x79, 0xa7,
0xab, 0x1b, 0xc8, 0x5a, 0x23, 0x9b, 0x40, 0x92, 0xac, 0xbe, 0xa1, 0xf7, 0x5b, 0xaf, 0xd4, 0x22,
0x79, 0x0c, 0xef, 0x24, 0xe9, 0x91, 0x47, 0x8f, 0x5a, 0xed, 0x6f, 0x7a, 0x27, 0x27, 0x6a, 0x69,
0x51, 0x5b, 0x9c, 0xcd, 0xe5, 0x45, 0xdf, 0x44, 0x99, 0x0d, 0x22, 0x6e, 0x29, 0x46, 0xe7, 0xdb,
0x8b, 0xce, 0x71, 0x67, 0xf8, 0xca, 0xec, 0x7d, 0xa3, 0x56, 0x44, 0xdc, 0x96, 0x58, 0x9e, 0x4c,
0x00, 0x55, 0x11, 0x39, 0x94, 0xba, 0x96, 0xae, 0xa7, 0x11, 0xd5, 0xc3, 0xbf, 0x94, 0xe5, 0xf7,
0x86, 0x36, 0x7e, 0xe1, 0x24, 0x06, 0x14, 0xc3, 0x52, 0x26, 0xab, 0x8a, 0xbb, 0xf9, 0x28, 0xb5,
0x33, 0xc6, 0x2d, 0x6c, 0xeb, 0x17, 0x7f, 0xfa, 0xdb, 0xaf, 0xb2, 0xeb, 0x9a, 0x72, 0xf0, 0xe6,
0xb3, 0x03, 0x81, 0x38, 0x70, 0x67, 0xfc, 0xcb, 0xcc, 0x3e, 0xe9, 0xc1, 0x9a, 0xfc, 0xae, 0x45,
0x36, 0x53, 0x22, 0xe3, 0x0f, 0x5d, 0xab, 0x24, 0x6e, 0xa2, 0x44, 0x55, 0xab, 0xc4, 0x12, 0x2d,
0x47, 0x08, 0xfc, 0x02, 0x8a, 0xe1, 0x57, 0x93, 0xc4, 0x25, 0xd3, 0xdf, 0x51, 0x9a, 0xcb, 0x16,
0xdb, 0xff, 0xce, 0x90, 0xff, 0x85, 0x72, 0xbc, 0x13, 0x93, 0xed, 0x44, 0xf3, 0x4e, 0x37, 0xde,
0x66, 0x73, 0x19, 0x2b, 0x7d, 0x2d, 0x52, 0x8b, 0xaf, 0x85, 0xfb, 0x32, 0xb9, 0x90, 0x0d, 0x4b,
0xec, 0xcb, 0xa4, 0x91, 0x52, 0x9f, 0x58, 0xa1, 0x97, 0x5e, 0x4c, 0x6b, 0xa2, 0xc8, 0x87, 0x84,
0xa4, 0x44, 0x1e, 0xfc, 0x60, 0x8d, 0xff, 0x9f, 0xfc, 0x1f, 0x28, 0x61, 0x00, 0x70, 0xab, 0x25,
0x73, 0x67, 0x25, 0x57, 0xef, 0xe6, 0xdc, 0x98, 0xc5, 0xfd, 0x77, 0x89, 0x74, 0x77, 0xc6, 0x0f,
0x38, 0x4a, 0xbb, 0x8c, 0xa5, 0xe3, 0xb6, 0x94, 0x90, 0x9e, 0xdc, 0x3b, 0xd3, 0xd2, 0x53, 0x7b,
0x95, 0xb6, 0x8b, 0xd2, 0x9b, 0xa4, 0x91, 0x92, 0xfe, 0x5a, 0x60, 0x0e, 0x7e, 0xa0, 0x36, 0x17,
0x16, 0xd4, 0xc4, 0xb0, 0x8c, 0x21, 0xbf, 0xd7, 0x86, 0xb9, 0xd7, 0x16, 0xbe, 0x22, 0x68, 0xdb,
0xa8, 0x64, 0x83, 0xac, 0x27, 0x52, 0x21, 0xb6, 0x60, 0x2e, 0xfd, 0x5e, 0x1b, 0x92, 0xd2, 0xd3,
0x26, 0x3c, 0x46, 0xe9, 0xdb, 0x64, 0x2b, 0x29, 0x3d, 0x69, 0xc1, 0x2b, 0xa8, 0x0a, 0x1d, 0xd1,
0xba, 0x14, 0x24, 0x32, 0x39, 0xb5, 0x93, 0x35, 0xb7, 0xee, 0xd0, 0xd3, 0xd5, 0x41, 0xea, 0xa8,
0x22, 0xa0, 0xfc, 0x40, 0xee, 0x61, 0x84, 0x03, 0xb9, 0xbb, 0x49, 0x10, 0x2d, 0x96, 0xb3, 0x72,
0xcd, 0x68, 0xde, 0x3b, 0x7b, 0x68, 0x3b, 0xa8, 0x70, 0x93, 0x3c, 0x44, 0x85, 0x11, 0xe0, 0xc0,
0x93, 0xf2, 0x7f, 0x06, 0x64, 0x70, 0x9f, 0xd6, 0x95, 0x53, 0x50, 0xf3, 0xfd, 0x7b, 0x31, 0x69,
0x87, 0x6a, 0x4b, 0x95, 0x8b, 0x12, 0x66, 0xa0, 0x24, 0x67, 0x0c, 0x32, 0xb7, 0x65, 0xc9, 0x18,
0xd4, 0x7c, 0x77, 0x05, 0x37, 0xd4, 0xd6, 0x40, 0x6d, 0x84, 0xa8, 0x42, 0x9b, 0x98, 0x7c, 0x0f,
0x02, 0x09, 0xbb, 0x5c, 0xc3, 0x7f, 0xc5, 0x7c, 0xfe, 0xcf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xdc,
0x41, 0x13, 0xa1, 0xc1, 0x19, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.

@ -730,6 +730,14 @@ message LiquidityParameters {
*/
repeated LiquidityRule rules = 1;
/*
The parts per million of swap amount that is allowed to be allocated to swap
fees. This value is applied across swap categories and may not be set in
conjunction with sweep fee rate, swap fee ppm, routing fee ppm, prepay
routing, max prepay and max miner fee.
*/
uint64 fee_ppm = 16;
/*
The limit we place on our estimated sweep cost for a swap in sat/vByte. If
the estimated fee for our sweep transaction within the specified
@ -952,6 +960,12 @@ enum AutoReason {
other swaps.
*/
AUTO_REASON_BUDGET_INSUFFICIENT = 12;
/*
Fee insufficient indicates that the fee estimate for a swap is higher than
the portion of total swap amount that we allow fees to consume.
*/
AUTO_REASON_FEE_INSUFFICIENT = 13;
}
message Disqualified {

@ -410,10 +410,11 @@
"AUTO_REASON_LOOP_OUT",
"AUTO_REASON_LOOP_IN",
"AUTO_REASON_LIQUIDITY_OK",
"AUTO_REASON_BUDGET_INSUFFICIENT"
"AUTO_REASON_BUDGET_INSUFFICIENT",
"AUTO_REASON_FEE_INSUFFICIENT"
],
"default": "AUTO_REASON_UNKNOWN",
"description": " - AUTO_REASON_BUDGET_NOT_STARTED: Budget not started indicates that we do not recommend any swaps because \nthe start time for our budget has not arrived yet.\n - AUTO_REASON_SWEEP_FEES: Sweep fees indicates that the estimated fees to sweep swaps are too high \nright now.\n - AUTO_REASON_BUDGET_ELAPSED: Budget elapsed indicates that the autoloop budget for the period has been \nelapsed.\n - AUTO_REASON_IN_FLIGHT: In flight indicates that the limit on in-flight automatically dispatched \nswaps has already been reached.\n - AUTO_REASON_SWAP_FEE: Swap fee indicates that the server fee for a specific swap is too high.\n - AUTO_REASON_MINER_FEE: Miner fee indicates that the miner fee for a specific swap is to high.\n - AUTO_REASON_PREPAY: Prepay indicates that the prepay fee for a specific swap is too high.\n - AUTO_REASON_FAILURE_BACKOFF: Failure backoff indicates that a swap has recently failed for this target,\nand the backoff period has not yet passed.\n - AUTO_REASON_LOOP_OUT: Loop out indicates that a loop out swap is currently utilizing the channel,\nso it is not eligible.\n - AUTO_REASON_LOOP_IN: Loop In indicates that a loop in swap is currently in flight for the peer, \nso it is not eligible.\n - AUTO_REASON_LIQUIDITY_OK: Liquidity ok indicates that a target meets the liquidity balance expressed \nin its rule, so no swap is needed.\n - AUTO_REASON_BUDGET_INSUFFICIENT: Budget insufficient indicates that we cannot perform a swap because we do \nnot have enough pending budget available. This differs from budget elapsed, \nbecause we still have some budget available, but we have allocated it to \nother swaps."
"description": " - AUTO_REASON_BUDGET_NOT_STARTED: Budget not started indicates that we do not recommend any swaps because \nthe start time for our budget has not arrived yet.\n - AUTO_REASON_SWEEP_FEES: Sweep fees indicates that the estimated fees to sweep swaps are too high \nright now.\n - AUTO_REASON_BUDGET_ELAPSED: Budget elapsed indicates that the autoloop budget for the period has been \nelapsed.\n - AUTO_REASON_IN_FLIGHT: In flight indicates that the limit on in-flight automatically dispatched \nswaps has already been reached.\n - AUTO_REASON_SWAP_FEE: Swap fee indicates that the server fee for a specific swap is too high.\n - AUTO_REASON_MINER_FEE: Miner fee indicates that the miner fee for a specific swap is to high.\n - AUTO_REASON_PREPAY: Prepay indicates that the prepay fee for a specific swap is too high.\n - AUTO_REASON_FAILURE_BACKOFF: Failure backoff indicates that a swap has recently failed for this target,\nand the backoff period has not yet passed.\n - AUTO_REASON_LOOP_OUT: Loop out indicates that a loop out swap is currently utilizing the channel,\nso it is not eligible.\n - AUTO_REASON_LOOP_IN: Loop In indicates that a loop in swap is currently in flight for the peer, \nso it is not eligible.\n - AUTO_REASON_LIQUIDITY_OK: Liquidity ok indicates that a target meets the liquidity balance expressed \nin its rule, so no swap is needed.\n - AUTO_REASON_BUDGET_INSUFFICIENT: Budget insufficient indicates that we cannot perform a swap because we do \nnot have enough pending budget available. This differs from budget elapsed, \nbecause we still have some budget available, but we have allocated it to \nother swaps.\n - AUTO_REASON_FEE_INSUFFICIENT: Fee insufficient indicates that the fee estimate for a swap is higher than\nthe portion of total swap amount that we allow fees to consume."
},
"looprpcDisqualified": {
"type": "object",
@ -493,6 +494,11 @@
},
"description": "A set of liquidity rules that describe the desired liquidity balance."
},
"fee_ppm": {
"type": "string",
"format": "uint64",
"description": "The parts per million of swap amount that is allowed to be allocated to swap\nfees. This value is applied across swap categories and may not be set in \nconjunction with sweep fee rate, swap fee ppm, routing fee ppm, prepay\nrouting, max prepay and max miner fee."
},
"sweep_fee_rate_sat_per_vbyte": {
"type": "string",
"format": "uint64",

@ -20,6 +20,10 @@ This file tracks release notes for the loop client.
to be set for an individual peer, rather than a specific channel, and
leverages multi-loop-out to more efficiently manage liquidity. To configure
peer-level rules, provide the 'setrule' command with the peer's pubkey.
* Autoloop's fee API has been simplified to allow setting a single percentage
which will be used to limit total swap fees to a percentage of the amount
being swapped. Use `loop setparams --feepercent={percentage}` to update
this value. This fee setting has been updated to the default for autoloop.
#### Breaking Changes

Loading…
Cancel
Save