Merge pull request #332 from carlaKC/autoloop-1-disqualified

autoloop: add reasons to explain no action
pull/335/head
Carla Kirk-Cohen 3 years ago committed by GitHub
commit fe42664e16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2,12 +2,15 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strconv" "strconv"
"github.com/lightninglabs/loop/liquidity" "github.com/lightninglabs/loop/liquidity"
"github.com/lightninglabs/loop/looprpc" "github.com/lightninglabs/loop/looprpc"
"github.com/urfave/cli" "github.com/urfave/cli"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
) )
var getLiquidityParamsCommand = cli.Command{ var getLiquidityParamsCommand = cli.Command{
@ -411,11 +414,22 @@ func suggestSwap(ctx *cli.Context) error {
resp, err := client.SuggestSwaps( resp, err := client.SuggestSwaps(
context.Background(), &looprpc.SuggestSwapsRequest{}, context.Background(), &looprpc.SuggestSwapsRequest{},
) )
if err != nil { if err == nil {
printRespJSON(resp)
return nil
}
// If we got an error because no rules are set, we want to display a
// friendly message.
rpcErr, ok := status.FromError(err)
if !ok {
return err return err
} }
printJSON(resp) if rpcErr.Code() != codes.FailedPrecondition {
return err
}
return nil return errors.New("no rules set for autolooper, please set rules " +
"using the setrule command")
} }

@ -224,8 +224,51 @@ in manually dispatched swaps - for loop out, this would mean the channel is
specified in the outgoing channel swap, and for loop in the channel's peer is specified in the outgoing channel swap, and for loop in the channel's peer is
specified as the last hop for an ongoing swap. This check is put in place to specified as the last hop for an ongoing swap. This check is put in place to
prevent the autolooper from interfering with swaps you have created yourself. prevent the autolooper from interfering with swaps you have created yourself.
If there is an ongoing swap that does not have a restriction placed on it (no
outgoing channel set, or last hop), then the autolooper will take no action
until it has resolved, because it does not know how that swap will affect
liquidity balances.
## Disqualified Swaps
There are various restrictions placed on the client's autoloop functionality.
If a channel is not eligible for a swap at present, or it does not need one
based on the current set of liquidity rules, it will be listed in the
`Disqualified` section of the output of the `SuggestSwaps` API. One of the
following reasons will be displayed:
* Budget not started: if the start date for your budget is in the future,
no swaps will be executed until the start date is reached. See [budget](#budget) to
update.
* Budget elapsed: if the autolooper has elapsed the budget assigned to it for
fees, this reason will be returned. See [budget](#budget) to update.
* Sweep fees: this reason will be displayed if the estimated chain fee rate for
sweeping a loop out swap is higher than the current limit. See [sweep fees](#fee-market-awareness)
to update.
* In flight: there is a limit to the number of automatically dispatched swaps
that the client allows. If this limit has been reached, no further swaps
will be automatically dispatched until the in-flight swaps complete. See
[in flight limit](#in-flight-limit) to update.
* Budget insufficient: if there is not enough remaining budget for a swap,
including the amount currently reserved for in flight swaps, an insufficient
reason will be displayed. This differs from budget elapsed because there is
still budget remaining, just not enough to execute a specific swap.
* Swap fee: there is a limit placed on the fee that the client will pay to the
server for automatically dispatched swaps. The swap fee reason will be shown
if the fees advertised by the server are too high. See [swap fee](#swap-fee)
to update.
* Miner fee: if the estimated on-chain fees for a swap are too high, autoloop
will display a miner fee reason. See [miner fee](#miner-fee) to update.
* Prepay: if the no-show fee that the server will pay in the unlikely event
that the client fails to complete a swap is too high, a prepay reason will
be returned. See [no show fees](#no-show-fee) to update.
* Backoff: if an automatically dispatched swap has recently failed for a channel,
autoloop will backoff for a period before retrying. See [failure backoff](#failure-backoff)
to update.
* Loop out: if there is currently a loop out swap in-flight on a channel, it
will not be used for automated swaps. This issue will resolve itself once the
in-flight swap completes.
* Loop in: if there is currently a loop in swap in-flight for a peer, it will
not be used for automated swaps. This will resolve itself once the swap is
completed.
* 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.
Further details for all of these reasons can be found in loopd's debug level
logs.

@ -178,6 +178,9 @@ var (
// less than the server minimum. // less than the server minimum.
ErrMinLessThanServer = errors.New("minimum swap amount is less than " + ErrMinLessThanServer = errors.New("minimum swap amount is less than " +
"server minimum") "server minimum")
// ErrNoRules is returned when no rules are set for swap suggestions.
ErrNoRules = errors.New("no rules set for autoloop")
) )
// Config contains the external functionality required to run the // Config contains the external functionality required to run the
@ -439,7 +442,14 @@ func (m *Manager) Run(ctx context.Context) error {
for { for {
select { select {
case <-m.cfg.AutoloopTicker.Ticks(): case <-m.cfg.AutoloopTicker.Ticks():
if err := m.autoloop(ctx); err != nil { err := m.autoloop(ctx)
switch err {
case ErrNoRules:
log.Debugf("No rules configured for autoloop")
case nil:
default:
log.Errorf("autoloop failed: %v", err) log.Errorf("autoloop failed: %v", err)
} }
@ -506,12 +516,12 @@ func cloneParameters(params Parameters) Parameters {
// autoloop gets a set of suggested swaps and dispatches them automatically if // autoloop gets a set of suggested swaps and dispatches them automatically if
// we have automated looping enabled. // we have automated looping enabled.
func (m *Manager) autoloop(ctx context.Context) error { func (m *Manager) autoloop(ctx context.Context) error {
swaps, err := m.SuggestSwaps(ctx, true) suggestion, err := m.SuggestSwaps(ctx, true)
if err != nil { if err != nil {
return err return err
} }
for _, swap := range swaps { for _, swap := range suggestion.OutSwaps {
// If we don't actually have dispatch of swaps enabled, log // If we don't actually have dispatch of swaps enabled, log
// suggestions. // suggestions.
if !m.params.Autoloop { if !m.params.Autoloop {
@ -547,6 +557,36 @@ func (m *Manager) ForceAutoLoop(ctx context.Context) error {
} }
} }
// Suggestions provides a set of suggested swaps, and the set of channels that
// were excluded from consideration.
type Suggestions struct {
// OutSwaps is the set of loop out swaps that we suggest executing.
OutSwaps []loop.OutRequest
// DisqualifiedChans maps the set of channels that we do not recommend
// swaps on to the reason that we did not recommend a swap.
DisqualifiedChans map[lnwire.ShortChannelID]Reason
}
func newSuggestions() *Suggestions {
return &Suggestions{
DisqualifiedChans: make(map[lnwire.ShortChannelID]Reason),
}
}
// singleReasonSuggestion is a helper function which returns a set of
// suggestions where all of our rules are disqualified due to a reason that
// applies to all of them (such as being out of budget).
func (m *Manager) singleReasonSuggestion(reason Reason) *Suggestions {
resp := newSuggestions()
for id := range m.params.ChannelRules {
resp.DisqualifiedChans[id] = reason
}
return resp
}
// SuggestSwaps returns a set of swap suggestions based on our current liquidity // SuggestSwaps returns a set of swap suggestions based on our current liquidity
// balance for the set of rules configured for the manager, failing if there are // balance for the set of rules configured for the manager, failing if there are
// no rules set. It takes an autoloop boolean that indicates whether the // no rules set. It takes an autoloop boolean that indicates whether the
@ -554,7 +594,7 @@ func (m *Manager) ForceAutoLoop(ctx context.Context) error {
// to determine the information we add to our swap suggestion and whether we // to determine the information we add to our swap suggestion and whether we
// return any suggestions. // return any suggestions.
func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
[]loop.OutRequest, error) { *Suggestions, error) {
m.paramsLock.Lock() m.paramsLock.Lock()
defer m.paramsLock.Unlock() defer m.paramsLock.Unlock()
@ -562,7 +602,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
// If we have no rules set, exit early to avoid unnecessary calls to // If we have no rules set, exit early to avoid unnecessary calls to
// lnd and the server. // lnd and the server.
if len(m.params.ChannelRules) == 0 { if len(m.params.ChannelRules) == 0 {
return nil, nil return nil, ErrNoRules
} }
// If our start date is in the future, we interpret this as meaning that // If our start date is in the future, we interpret this as meaning that
@ -572,7 +612,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
log.Debugf("autoloop fee budget start time: %v is in "+ log.Debugf("autoloop fee budget start time: %v is in "+
"the future", m.params.AutoFeeStartDate) "the future", m.params.AutoFeeStartDate)
return nil, nil return m.singleReasonSuggestion(ReasonBudgetNotStarted), nil
} }
// Before we get any swap suggestions, we check what the current fee // Before we get any swap suggestions, we check what the current fee
@ -593,7 +633,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
satPerKwToSatPerVByte(estimate), satPerKwToSatPerVByte(estimate),
satPerKwToSatPerVByte(m.params.SweepFeeRateLimit)) satPerKwToSatPerVByte(m.params.SweepFeeRateLimit))
return nil, nil return m.singleReasonSuggestion(ReasonSweepFees), nil
} }
// Get the current server side restrictions, combined with the client // Get the current server side restrictions, combined with the client
@ -630,7 +670,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
m.params.AutoFeeBudget, summary.spentFees, m.params.AutoFeeBudget, summary.spentFees,
summary.pendingFees) summary.pendingFees)
return nil, nil return m.singleReasonSuggestion(ReasonBudgetElapsed), nil
} }
// If we have already reached our total allowed number of in flight // If we have already reached our total allowed number of in flight
@ -639,29 +679,45 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
if allowedSwaps <= 0 { if allowedSwaps <= 0 {
log.Debugf("%v autoloops allowed, %v in flight", log.Debugf("%v autoloops allowed, %v in flight",
m.params.MaxAutoInFlight, summary.inFlightCount) m.params.MaxAutoInFlight, summary.inFlightCount)
return nil, nil
return m.singleReasonSuggestion(ReasonInFlight), nil
} }
eligible, err := m.getEligibleChannels(ctx, loopOut, loopIn) channels, err := m.cfg.Lnd.Client.ListChannels(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var suggestions []loop.OutRequest // Get a summary of the channels and peers that are not eligible due
for _, channel := range eligible { // to ongoing swaps.
channelID := lnwire.NewShortChanIDFromInt(channel.ChannelID) traffic := m.currentSwapTraffic(loopOut, loopIn)
rule, ok := m.params.ChannelRules[channelID]
var (
suggestions []loop.OutRequest
disqualified = make(map[lnwire.ShortChannelID]Reason)
)
for _, channel := range channels {
balance := newBalances(channel)
rule, ok := m.params.ChannelRules[balance.channelID]
if !ok { if !ok {
continue continue
} }
balance := newBalances(channel) // Check whether we can perform a swap, adding the channel to
// our set of disqualified swaps if it is not eligible.
suggestion := rule.suggestSwap(balance, restrictions) reason := traffic.maySwap(channel.PubKeyBytes, balance.channelID)
if reason != ReasonNone {
disqualified[balance.channelID] = reason
continue
}
// We can have nil suggestions in the case where no action is // We can have nil suggestions in the case where no action is
// required, so we skip over them. // required, so we skip over them.
suggestion := rule.suggestSwap(balance, restrictions)
if suggestion == nil { if suggestion == nil {
disqualified[balance.channelID] = ReasonLiquidityOk
continue continue
} }
@ -683,11 +739,9 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
// Check that the estimated fees for the suggested swap are // Check that the estimated fees for the suggested swap are
// below the fee limits configured by the manager. // below the fee limits configured by the manager.
err = m.checkFeeLimits(quote, suggestion.Amount) feeReason := m.checkFeeLimits(quote, suggestion.Amount)
if err != nil { if feeReason != ReasonNone {
log.Infof("suggestion: %v expected fees too high: %v", disqualified[balance.channelID] = feeReason
suggestion, err)
continue continue
} }
@ -700,10 +754,16 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
suggestions = append(suggestions, outRequest) suggestions = append(suggestions, outRequest)
} }
// If we have no suggestions after we have applied all of our limits, // Finally, run through all possible swaps, excluding swaps that are
// just return. // not feasible due to fee or budget restrictions.
resp := &Suggestions{
DisqualifiedChans: disqualified,
}
// If we have no swaps to execute after we have applied all of our
// limits, just return our set of disqualified swaps.
if len(suggestions) == 0 { if len(suggestions) == 0 {
return nil, nil return resp, nil
} }
// Sort suggestions by amount in descending order. // Sort suggestions by amount in descending order.
@ -713,12 +773,38 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
// Run through our suggested swaps in descending order of amount and // Run through our suggested swaps in descending order of amount and
// return all of the swaps which will fit within our remaining budget. // return all of the swaps which will fit within our remaining budget.
var ( available := m.params.AutoFeeBudget - summary.totalFees()
available = m.params.AutoFeeBudget - summary.totalFees()
inBudget []loop.OutRequest // setReason is a helper that adds a swap's channels to our disqualified
) // list with the reason provided.
setReason := func(reason Reason, swap loop.OutRequest) {
for _, id := range swap.OutgoingChanSet {
chanID := lnwire.NewShortChanIDFromInt(id)
resp.DisqualifiedChans[chanID] = reason
}
}
for _, swap := range suggestions { for _, swap := range suggestions {
swap := swap
// If we do not have enough funds available, or we hit our
// in flight limit, we record this value for the rest of the
// swaps.
var reason Reason
switch {
case available == 0:
reason = ReasonBudgetInsufficient
case len(resp.OutSwaps) == allowedSwaps:
reason = ReasonInFlight
}
if reason != ReasonNone {
setReason(reason, swap)
continue
}
fees := worstCaseOutFees( fees := worstCaseOutFees(
swap.MaxPrepayRoutingFee, swap.MaxSwapRoutingFee, swap.MaxPrepayRoutingFee, swap.MaxSwapRoutingFee,
swap.MaxSwapFee, swap.MaxMinerFee, swap.MaxPrepayAmount, swap.MaxSwapFee, swap.MaxMinerFee, swap.MaxPrepayAmount,
@ -729,17 +815,13 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
// fall within the budget and decrement our available amount. // fall within the budget and decrement our available amount.
if fees <= available { if fees <= available {
available -= fees available -= fees
inBudget = append(inBudget, swap) resp.OutSwaps = append(resp.OutSwaps, swap)
} } else {
setReason(ReasonBudgetInsufficient, swap)
// If we're out of budget, or we have hit the max number of
// swaps that we want to dispatch at one time, exit early.
if available == 0 || allowedSwaps == len(inBudget) {
break
} }
} }
return inBudget, nil return resp, nil
} }
// getSwapRestrictions queries the server for its latest swap size restrictions, // getSwapRestrictions queries the server for its latest swap size restrictions,
@ -918,19 +1000,13 @@ func (m *Manager) checkExistingAutoLoops(ctx context.Context,
return &summary, nil return &summary, nil
} }
// getEligibleChannels takes lists of our existing loop out and in swaps, and // currentSwapTraffic examines our existing swaps and returns a summary of the
// gets a list of channels that are not currently being utilized for a swap. // current activity which can be used to determine whether we should perform
// If an unrestricted swap is ongoing, we return an empty set of channels // any swaps.
// because we don't know which channels balances it will affect. func (m *Manager) currentSwapTraffic(loopOut []*loopdb.LoopOut,
func (m *Manager) getEligibleChannels(ctx context.Context, loopIn []*loopdb.LoopIn) *swapTraffic {
loopOut []*loopdb.LoopOut, loopIn []*loopdb.LoopIn) (
[]lndclient.ChannelInfo, error) {
var ( traffic := newSwapTraffic()
existingOut = make(map[lnwire.ShortChannelID]bool)
existingIn = make(map[route.Vertex]bool)
failedOut = make(map[lnwire.ShortChannelID]time.Time)
)
// Failure cutoff is the most recent failure timestamp we will still // Failure cutoff is the most recent failure timestamp we will still
// consider a channel eligible. Any channels involved in swaps that have // consider a channel eligible. Any channels involved in swaps that have
@ -963,7 +1039,7 @@ func (m *Manager) getEligibleChannels(ctx context.Context,
id, id,
) )
failedOut[chanID] = failedAt traffic.failedLoopOut[chanID] = failedAt
} }
} }
} }
@ -978,16 +1054,9 @@ func (m *Manager) getEligibleChannels(ctx context.Context,
continue continue
} }
if len(chanSet) == 0 {
log.Debugf("Ongoing unrestricted loop out: "+
"%v, no suggestions at present", out.Hash)
return nil, nil
}
for _, id := range chanSet { for _, id := range chanSet {
chanID := lnwire.NewShortChanIDFromInt(id) chanID := lnwire.NewShortChanIDFromInt(id)
existingOut[chanID] = true traffic.ongoingLoopOut[chanID] = true
} }
} }
@ -997,83 +1066,91 @@ func (m *Manager) getEligibleChannels(ctx context.Context,
continue continue
} }
// Skip over swaps that may come through any peer.
if in.Contract.LastHop == nil { if in.Contract.LastHop == nil {
log.Debugf("Ongoing unrestricted loop in: "+ continue
"%v, no suggestions at present", in.Hash)
return nil, nil
} }
existingIn[*in.Contract.LastHop] = true traffic.ongoingLoopIn[*in.Contract.LastHop] = true
} }
channels, err := m.cfg.Lnd.Client.ListChannels(ctx) return traffic
if err != nil { }
return nil, err
}
// Run through our set of channels and skip over any channels that // swapTraffic contains a summary of our current and previously failed swaps.
// are currently being utilized by a restricted swap (where restricted type swapTraffic struct {
// means that a loop out limited channels, or a loop in limited last ongoingLoopOut map[lnwire.ShortChannelID]bool
// hop). ongoingLoopIn map[route.Vertex]bool
var eligible []lndclient.ChannelInfo failedLoopOut map[lnwire.ShortChannelID]time.Time
for _, channel := range channels { }
shortID := lnwire.NewShortChanIDFromInt(channel.ChannelID)
lastFail, recentFail := failedOut[shortID] func newSwapTraffic() *swapTraffic {
if recentFail { return &swapTraffic{
log.Debugf("Channel: %v not eligible for "+ ongoingLoopOut: make(map[lnwire.ShortChannelID]bool),
"suggestions, was part of a failed swap at: %v", ongoingLoopIn: make(map[route.Vertex]bool),
channel.ChannelID, lastFail) failedLoopOut: make(map[lnwire.ShortChannelID]time.Time),
}
}
continue // maySwap returns a boolean that indicates whether we may perform a swap for a
} // peer and its set of channels.
func (s *swapTraffic) maySwap(peer route.Vertex,
chanID lnwire.ShortChannelID) Reason {
if existingOut[shortID] { lastFail, recentFail := s.failedLoopOut[chanID]
log.Debugf("Channel: %v not eligible for "+ if recentFail {
"suggestions, ongoing loop out utilizing "+ log.Debugf("Channel: %v not eligible for suggestions, was "+
"channel", channel.ChannelID) "part of a failed swap at: %v", chanID, lastFail)
continue return ReasonFailureBackoff
} }
if existingIn[channel.PubKeyBytes] { if s.ongoingLoopOut[chanID] {
log.Debugf("Channel: %v not eligible for "+ log.Debugf("Channel: %v not eligible for suggestions, "+
"suggestions, ongoing loop in utilizing "+ "ongoing loop out utilizing channel", chanID)
"peer", channel.ChannelID)
continue return ReasonLoopOut
} }
if s.ongoingLoopIn[peer] {
log.Debugf("Peer: %x not eligible for suggestions ongoing "+
"loop in utilizing peer", peer)
eligible = append(eligible, channel) return ReasonLoopIn
} }
return eligible, nil return ReasonNone
} }
// checkFeeLimits takes a set of fees for a swap and checks whether they exceed // checkFeeLimits takes a set of fees for a swap and checks whether they exceed
// our swap limits. // our swap limits.
func (m *Manager) checkFeeLimits(quote *loop.LoopOutQuote, func (m *Manager) checkFeeLimits(quote *loop.LoopOutQuote,
swapAmt btcutil.Amount) error { swapAmt btcutil.Amount) Reason {
maxFee := ppmToSat(swapAmt, m.params.MaximumSwapFeePPM) maxFee := ppmToSat(swapAmt, m.params.MaximumSwapFeePPM)
if quote.SwapFee > maxFee { if quote.SwapFee > maxFee {
return fmt.Errorf("quoted swap fee: %v > maximum swap fee: %v", log.Debugf("quoted swap fee: %v > maximum swap fee: %v",
quote.SwapFee, maxFee) quote.SwapFee, maxFee)
return ReasonSwapFee
} }
if quote.MinerFee > m.params.MaximumMinerFee { if quote.MinerFee > m.params.MaximumMinerFee {
return fmt.Errorf("quoted miner fee: %v > maximum miner "+ log.Debugf("quoted miner fee: %v > maximum miner "+
"fee: %v", quote.MinerFee, m.params.MaximumMinerFee) "fee: %v", quote.MinerFee, m.params.MaximumMinerFee)
return ReasonMinerFee
} }
if quote.PrepayAmount > m.params.MaximumPrepay { if quote.PrepayAmount > m.params.MaximumPrepay {
return fmt.Errorf("quoted prepay: %v > maximum prepay: %v", log.Debugf("quoted prepay: %v > maximum prepay: %v",
quote.PrepayAmount, m.params.MaximumPrepay) quote.PrepayAmount, m.params.MaximumPrepay)
return ReasonPrepay
} }
return nil return ReasonNone
} }
// satPerKwToSatPerVByte converts sat per kWeight to sat per vByte. // satPerKwToSatPerVByte converts sat per kWeight to sat per vByte.

@ -107,6 +107,10 @@ var (
} }
testRestrictions = NewRestrictions(1, 10000) testRestrictions = NewRestrictions(1, 10000)
// noneDisqualified can be used in tests where we don't have any
// disqualified channels so that we can use require.Equal.
noneDisqualified = make(map[lnwire.ShortChannelID]Reason)
) )
// newTestConfig creates a default test config. // newTestConfig creates a default test config.
@ -283,7 +287,7 @@ func TestRestrictedSuggestions(t *testing.T) {
channels []lndclient.ChannelInfo channels []lndclient.ChannelInfo
loopOut []*loopdb.LoopOut loopOut []*loopdb.LoopOut
loopIn []*loopdb.LoopIn loopIn []*loopdb.LoopIn
expected []loop.OutRequest expected *Suggestions
}{ }{
{ {
name: "no existing swaps", name: "no existing swaps",
@ -292,14 +296,17 @@ func TestRestrictedSuggestions(t *testing.T) {
}, },
loopOut: nil, loopOut: nil,
loopIn: nil, loopIn: nil,
expected: []loop.OutRequest{ expected: &Suggestions{
chan1Rec, OutSwaps: []loop.OutRequest{
chan1Rec,
},
DisqualifiedChans: noneDisqualified,
}, },
}, },
{ {
name: "unrestricted loop out", name: "unrestricted loop out",
channels: []lndclient.ChannelInfo{ channels: []lndclient.ChannelInfo{
channel1, channel2, channel1,
}, },
loopOut: []*loopdb.LoopOut{ loopOut: []*loopdb.LoopOut{
{ {
@ -308,12 +315,17 @@ func TestRestrictedSuggestions(t *testing.T) {
}, },
}, },
}, },
expected: nil, expected: &Suggestions{
OutSwaps: []loop.OutRequest{
chan1Rec,
},
DisqualifiedChans: noneDisqualified,
},
}, },
{ {
name: "unrestricted loop in", name: "unrestricted loop in",
channels: []lndclient.ChannelInfo{ channels: []lndclient.ChannelInfo{
channel1, channel2, channel1,
}, },
loopIn: []*loopdb.LoopIn{ loopIn: []*loopdb.LoopIn{
{ {
@ -322,7 +334,12 @@ func TestRestrictedSuggestions(t *testing.T) {
}, },
}, },
}, },
expected: nil, expected: &Suggestions{
OutSwaps: []loop.OutRequest{
chan1Rec,
},
DisqualifiedChans: noneDisqualified,
},
}, },
{ {
name: "restricted loop out", name: "restricted loop out",
@ -334,8 +351,13 @@ func TestRestrictedSuggestions(t *testing.T) {
Contract: chan1Out, Contract: chan1Out,
}, },
}, },
expected: []loop.OutRequest{ expected: &Suggestions{
chan2Rec, OutSwaps: []loop.OutRequest{
chan2Rec,
},
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonLoopOut,
},
}, },
}, },
{ {
@ -350,8 +372,13 @@ func TestRestrictedSuggestions(t *testing.T) {
}, },
}, },
}, },
expected: []loop.OutRequest{ expected: &Suggestions{
chan1Rec, OutSwaps: []loop.OutRequest{
chan1Rec,
},
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID2: ReasonLoopIn,
},
}, },
}, },
{ {
@ -369,7 +396,11 @@ func TestRestrictedSuggestions(t *testing.T) {
}, },
}, },
}, },
expected: nil, expected: &Suggestions{
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonFailureBackoff,
},
},
}, },
{ {
name: "swap failed before cutoff", name: "swap failed before cutoff",
@ -386,8 +417,11 @@ func TestRestrictedSuggestions(t *testing.T) {
}, },
}, },
}, },
expected: []loop.OutRequest{ expected: &Suggestions{
chan1Rec, OutSwaps: []loop.OutRequest{
chan1Rec,
},
DisqualifiedChans: noneDisqualified,
}, },
}, },
{ {
@ -405,7 +439,11 @@ func TestRestrictedSuggestions(t *testing.T) {
}, },
}, },
}, },
expected: nil, expected: &Suggestions{
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonLoopOut,
},
},
}, },
} }
@ -443,21 +481,28 @@ func TestRestrictedSuggestions(t *testing.T) {
// fee is above and below the configured limit. // fee is above and below the configured limit.
func TestSweepFeeLimit(t *testing.T) { func TestSweepFeeLimit(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
feeRate chainfee.SatPerKWeight feeRate chainfee.SatPerKWeight
swaps []loop.OutRequest suggestions *Suggestions
}{ }{
{ {
name: "fee estimate ok", name: "fee estimate ok",
feeRate: defaultSweepFeeRateLimit, feeRate: defaultSweepFeeRateLimit,
swaps: []loop.OutRequest{ suggestions: &Suggestions{
chan1Rec, OutSwaps: []loop.OutRequest{
chan1Rec,
},
DisqualifiedChans: noneDisqualified,
}, },
}, },
{ {
name: "fee estimate above limit", name: "fee estimate above limit",
feeRate: defaultSweepFeeRateLimit + 1, feeRate: defaultSweepFeeRateLimit + 1,
swaps: nil, suggestions: &Suggestions{
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonSweepFees,
},
},
}, },
} }
@ -483,7 +528,7 @@ func TestSweepFeeLimit(t *testing.T) {
testSuggestSwaps( testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params), t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.swaps, nil, testCase.suggestions, nil,
) )
}) })
} }
@ -493,21 +538,26 @@ func TestSweepFeeLimit(t *testing.T) {
// the liquidity manager and the current set of channel balances. // the liquidity manager and the current set of channel balances.
func TestSuggestSwaps(t *testing.T) { func TestSuggestSwaps(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
rules map[lnwire.ShortChannelID]*ThresholdRule rules map[lnwire.ShortChannelID]*ThresholdRule
swaps []loop.OutRequest suggestions *Suggestions
err error
}{ }{
{ {
name: "no rules", name: "no rules",
rules: map[lnwire.ShortChannelID]*ThresholdRule{}, rules: map[lnwire.ShortChannelID]*ThresholdRule{},
err: ErrNoRules,
}, },
{ {
name: "loop out", name: "loop out",
rules: map[lnwire.ShortChannelID]*ThresholdRule{ rules: map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule, chanID1: chanRule,
}, },
swaps: []loop.OutRequest{ suggestions: &Suggestions{
chan1Rec, OutSwaps: []loop.OutRequest{
chan1Rec,
},
DisqualifiedChans: noneDisqualified,
}, },
}, },
{ {
@ -515,7 +565,9 @@ func TestSuggestSwaps(t *testing.T) {
rules: map[lnwire.ShortChannelID]*ThresholdRule{ rules: map[lnwire.ShortChannelID]*ThresholdRule{
chanID2: NewThresholdRule(10, 10), chanID2: NewThresholdRule(10, 10),
}, },
swaps: nil, suggestions: &Suggestions{
DisqualifiedChans: noneDisqualified,
},
}, },
} }
@ -534,7 +586,7 @@ func TestSuggestSwaps(t *testing.T) {
testSuggestSwaps( testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params), t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.swaps, nil, testCase.suggestions, testCase.err,
) )
}) })
} }
@ -543,15 +595,18 @@ func TestSuggestSwaps(t *testing.T) {
// TestFeeLimits tests limiting of swap suggestions by fees. // TestFeeLimits tests limiting of swap suggestions by fees.
func TestFeeLimits(t *testing.T) { func TestFeeLimits(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
quote *loop.LoopOutQuote quote *loop.LoopOutQuote
expected []loop.OutRequest suggestions *Suggestions
}{ }{
{ {
name: "fees ok", name: "fees ok",
quote: testQuote, quote: testQuote,
expected: []loop.OutRequest{ suggestions: &Suggestions{
chan1Rec, OutSwaps: []loop.OutRequest{
chan1Rec,
},
DisqualifiedChans: noneDisqualified,
}, },
}, },
{ {
@ -561,6 +616,11 @@ func TestFeeLimits(t *testing.T) {
PrepayAmount: defaultMaximumPrepay + 1, PrepayAmount: defaultMaximumPrepay + 1,
MinerFee: 50, MinerFee: 50,
}, },
suggestions: &Suggestions{
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonPrepay,
},
},
}, },
{ {
name: "insufficient miner fee", name: "insufficient miner fee",
@ -569,6 +629,11 @@ func TestFeeLimits(t *testing.T) {
PrepayAmount: 100, PrepayAmount: 100,
MinerFee: defaultMaximumMinerFee + 1, MinerFee: defaultMaximumMinerFee + 1,
}, },
suggestions: &Suggestions{
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonMinerFee,
},
},
}, },
{ {
// Swap fee limited to 0.5% of 7500 = 37,5. // Swap fee limited to 0.5% of 7500 = 37,5.
@ -578,6 +643,11 @@ func TestFeeLimits(t *testing.T) {
PrepayAmount: 100, PrepayAmount: 100,
MinerFee: 500, MinerFee: 500,
}, },
suggestions: &Suggestions{
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonSwapFee,
},
},
}, },
} }
@ -604,7 +674,7 @@ func TestFeeLimits(t *testing.T) {
testSuggestSwaps( testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params), t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.expected, nil, testCase.suggestions, nil,
) )
}) })
} }
@ -635,8 +705,8 @@ func TestFeeBudget(t *testing.T) {
// last update time to their total cost. // last update time to their total cost.
existingSwaps map[time.Time]btcutil.Amount existingSwaps map[time.Time]btcutil.Amount
// expectedSwaps is the set of swaps we expect to be suggested. // suggestions is the set of swaps we expect to be suggested.
expectedSwaps []loop.OutRequest suggestions *Suggestions
}{ }{
{ {
// Two swaps will cost (78+5000)*2, set exactly 10156 // Two swaps will cost (78+5000)*2, set exactly 10156
@ -644,8 +714,11 @@ func TestFeeBudget(t *testing.T) {
name: "budget for 2 swaps, no existing", name: "budget for 2 swaps, no existing",
budget: 10156, budget: 10156,
maxMinerFee: 5000, maxMinerFee: 5000,
expectedSwaps: []loop.OutRequest{ suggestions: &Suggestions{
chan1Rec, chan2Rec, OutSwaps: []loop.OutRequest{
chan1Rec, chan2Rec,
},
DisqualifiedChans: noneDisqualified,
}, },
}, },
{ {
@ -654,8 +727,13 @@ func TestFeeBudget(t *testing.T) {
name: "budget for 1 swaps, no existing", name: "budget for 1 swaps, no existing",
budget: 10155, budget: 10155,
maxMinerFee: 5000, maxMinerFee: 5000,
expectedSwaps: []loop.OutRequest{ suggestions: &Suggestions{
chan1Rec, OutSwaps: []loop.OutRequest{
chan1Rec,
},
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID2: ReasonBudgetInsufficient,
},
}, },
}, },
{ {
@ -667,8 +745,11 @@ func TestFeeBudget(t *testing.T) {
existingSwaps: map[time.Time]btcutil.Amount{ existingSwaps: map[time.Time]btcutil.Amount{
testBudgetStart.Add(time.Hour * -1): 200, testBudgetStart.Add(time.Hour * -1): 200,
}, },
expectedSwaps: []loop.OutRequest{ suggestions: &Suggestions{
chan1Rec, chan2Rec, OutSwaps: []loop.OutRequest{
chan1Rec, chan2Rec,
},
DisqualifiedChans: noneDisqualified,
}, },
}, },
{ {
@ -680,8 +761,13 @@ func TestFeeBudget(t *testing.T) {
existingSwaps: map[time.Time]btcutil.Amount{ existingSwaps: map[time.Time]btcutil.Amount{
testBudgetStart.Add(time.Hour): 500, testBudgetStart.Add(time.Hour): 500,
}, },
expectedSwaps: []loop.OutRequest{ suggestions: &Suggestions{
chan1Rec, OutSwaps: []loop.OutRequest{
chan1Rec,
},
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID2: ReasonBudgetInsufficient,
},
}, },
}, },
{ {
@ -691,7 +777,12 @@ func TestFeeBudget(t *testing.T) {
existingSwaps: map[time.Time]btcutil.Amount{ existingSwaps: map[time.Time]btcutil.Amount{
testBudgetStart.Add(time.Hour): 500, testBudgetStart.Add(time.Hour): 500,
}, },
expectedSwaps: nil, suggestions: &Suggestions{
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonBudgetElapsed,
chanID2: ReasonBudgetElapsed,
},
},
}, },
} }
@ -754,14 +845,14 @@ func TestFeeBudget(t *testing.T) {
// Set our custom max miner fee on each expected swap, // Set our custom max miner fee on each expected swap,
// rather than having to create multiple vars for // rather than having to create multiple vars for
// different rates. // different rates.
for i := range testCase.expectedSwaps { for i := range testCase.suggestions.OutSwaps {
testCase.expectedSwaps[i].MaxMinerFee = testCase.suggestions.OutSwaps[i].MaxMinerFee =
testCase.maxMinerFee testCase.maxMinerFee
} }
testSuggestSwaps( testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params), t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.expectedSwaps, nil, testCase.suggestions, nil,
) )
}) })
} }
@ -774,20 +865,26 @@ func TestInFlightLimit(t *testing.T) {
name string name string
maxInFlight int maxInFlight int
existingSwaps []*loopdb.LoopOut existingSwaps []*loopdb.LoopOut
expectedSwaps []loop.OutRequest suggestions *Suggestions
}{ }{
{ {
name: "none in flight, extra space", name: "none in flight, extra space",
maxInFlight: 3, maxInFlight: 3,
expectedSwaps: []loop.OutRequest{ suggestions: &Suggestions{
chan1Rec, chan2Rec, OutSwaps: []loop.OutRequest{
chan1Rec, chan2Rec,
},
DisqualifiedChans: noneDisqualified,
}, },
}, },
{ {
name: "none in flight, exact match", name: "none in flight, exact match",
maxInFlight: 2, maxInFlight: 2,
expectedSwaps: []loop.OutRequest{ suggestions: &Suggestions{
chan1Rec, chan2Rec, OutSwaps: []loop.OutRequest{
chan1Rec, chan2Rec,
},
DisqualifiedChans: noneDisqualified,
}, },
}, },
{ {
@ -798,8 +895,13 @@ func TestInFlightLimit(t *testing.T) {
Contract: autoOutContract, Contract: autoOutContract,
}, },
}, },
expectedSwaps: []loop.OutRequest{ suggestions: &Suggestions{
chan1Rec, OutSwaps: []loop.OutRequest{
chan1Rec,
},
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID2: ReasonInFlight,
},
}, },
}, },
{ {
@ -810,7 +912,12 @@ func TestInFlightLimit(t *testing.T) {
Contract: autoOutContract, Contract: autoOutContract,
}, },
}, },
expectedSwaps: nil, suggestions: &Suggestions{
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonInFlight,
chanID2: ReasonInFlight,
},
},
}, },
{ {
name: "max swaps exceeded", name: "max swaps exceeded",
@ -823,7 +930,12 @@ func TestInFlightLimit(t *testing.T) {
Contract: autoOutContract, Contract: autoOutContract,
}, },
}, },
expectedSwaps: nil, suggestions: &Suggestions{
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonInFlight,
chanID2: ReasonInFlight,
},
},
}, },
} }
@ -854,7 +966,7 @@ func TestInFlightLimit(t *testing.T) {
testSuggestSwaps( testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params), t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.expectedSwaps, nil, testCase.suggestions, nil,
) )
}) })
} }
@ -869,13 +981,18 @@ func TestSizeRestrictions(t *testing.T) {
} }
outSwap = loop.OutRequest{ outSwap = loop.OutRequest{
Amount: 7000,
OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()}, OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()},
MaxPrepayRoutingFee: prepayFee, MaxPrepayRoutingFee: prepayFee,
MaxMinerFee: defaultMaximumMinerFee, MaxSwapRoutingFee: ppmToSat(
MaxSwapFee: testQuote.SwapFee, 7000,
MaxPrepayAmount: testQuote.PrepayAmount, defaultRoutingFeePPM,
SweepConfTarget: loop.DefaultSweepConfTarget, ),
Initiator: autoloopSwapInitiator, MaxMinerFee: defaultMaximumMinerFee,
MaxSwapFee: testQuote.SwapFee,
MaxPrepayAmount: testQuote.PrepayAmount,
SweepConfTarget: loop.DefaultSweepConfTarget,
Initiator: autoloopSwapInitiator,
} }
) )
@ -890,8 +1007,8 @@ func TestSizeRestrictions(t *testing.T) {
// endpoint. // endpoint.
serverRestrictions []Restrictions serverRestrictions []Restrictions
// expectedAmount is the amount that we expect for our swap. // suggestions is the set of suggestions we expect.
expectedAmount btcutil.Amount suggestions *Suggestions
// expectedError is the error we expect. // expectedError is the error we expect.
expectedError error expectedError error
@ -904,7 +1021,12 @@ func TestSizeRestrictions(t *testing.T) {
serverRestrictions: []Restrictions{ serverRestrictions: []Restrictions{
serverRestrictions, serverRestrictions, serverRestrictions, serverRestrictions,
}, },
expectedAmount: 7500, suggestions: &Suggestions{
OutSwaps: []loop.OutRequest{
chan1Rec,
},
DisqualifiedChans: noneDisqualified,
},
}, },
{ {
name: "minimum more than server, no swap", name: "minimum more than server, no swap",
@ -914,7 +1036,11 @@ func TestSizeRestrictions(t *testing.T) {
serverRestrictions: []Restrictions{ serverRestrictions: []Restrictions{
serverRestrictions, serverRestrictions, serverRestrictions, serverRestrictions,
}, },
expectedAmount: 0, suggestions: &Suggestions{
DisqualifiedChans: map[lnwire.ShortChannelID]Reason{
chanID1: ReasonLiquidityOk,
},
},
}, },
{ {
name: "maximum less than server, swap happens", name: "maximum less than server, swap happens",
@ -924,7 +1050,12 @@ func TestSizeRestrictions(t *testing.T) {
serverRestrictions: []Restrictions{ serverRestrictions: []Restrictions{
serverRestrictions, serverRestrictions, serverRestrictions, serverRestrictions,
}, },
expectedAmount: 7000, suggestions: &Suggestions{
OutSwaps: []loop.OutRequest{
outSwap,
},
DisqualifiedChans: noneDisqualified,
},
}, },
{ {
// Originally, our client params are ok. But then the // Originally, our client params are ok. But then the
@ -942,8 +1073,8 @@ func TestSizeRestrictions(t *testing.T) {
Maximum: 6000, Maximum: 6000,
}, },
}, },
expectedAmount: 0, suggestions: nil,
expectedError: ErrMaxExceedsServer, expectedError: ErrMaxExceedsServer,
}, },
} }
@ -975,25 +1106,9 @@ func TestSizeRestrictions(t *testing.T) {
return &restrictions, nil return &restrictions, nil
} }
// If we expect a swap (non-zero amount), we add a
// swap to our set of expected swaps, and update amount
// and fee accordingly.
var expectedSwaps []loop.OutRequest
if testCase.expectedAmount != 0 {
outSwap.Amount = testCase.expectedAmount
outSwap.MaxSwapRoutingFee = ppmToSat(
testCase.expectedAmount,
defaultRoutingFeePPM,
)
expectedSwaps = append(expectedSwaps, outSwap)
}
testSuggestSwaps( testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params), t, newSuggestSwapsSetup(cfg, lnd, params),
expectedSwaps, testCase.expectedError, testCase.suggestions, testCase.expectedError,
) )
require.Equal( require.Equal(
@ -1028,7 +1143,7 @@ func newSuggestSwapsSetup(cfg *Config, lnd *test.LndMockServices,
// use the default parameters and setup two channels (channel1 + channel2) with // use the default parameters and setup two channels (channel1 + channel2) with
// chanRule set for each. // chanRule set for each.
func testSuggestSwaps(t *testing.T, setup *testSuggestSwapsSetup, func testSuggestSwaps(t *testing.T, setup *testSuggestSwapsSetup,
expected []loop.OutRequest, expectedErr error) { expected *Suggestions, expectedErr error) {
t.Parallel() t.Parallel()

@ -0,0 +1,62 @@
package liquidity
// Reason is an enum which represents the various reasons we have for not
// executing a swap.
type Reason uint8
const (
// ReasonNone is the zero value reason, added so that this enum can
// align with the numeric values used in our protobufs and avoid
// ambiguity around default zero values.
ReasonNone Reason = iota
// ReasonBudgetNotStarted indicates that we do not recommend any swaps
// because the start time for our budget has not arrived yet.
ReasonBudgetNotStarted
// ReasonSweepFees indicates that the estimated fees to sweep swaps
// are too high right now.
ReasonSweepFees
// ReasonBudgetElapsed indicates that the autoloop budget for the
// period has been elapsed.
ReasonBudgetElapsed
// ReasonInFlight indicates that the limit on in-flight automatically
// dispatched swaps has already been reached.
ReasonInFlight
// ReasonSwapFee indicates that the server fee for a specific swap is
// too high.
ReasonSwapFee
// ReasonMinerFee indicates that the miner fee for a specific swap is
// to high.
ReasonMinerFee
// ReasonPrepay indicates that the prepay fee for a specific swap is
// too high.
ReasonPrepay
// ReasonFailureBackoff indicates that a swap has recently failed for
// this target, and the backoff period has not yet passed.
ReasonFailureBackoff
// ReasonLoopOut indicates that a loop out swap is currently utilizing
// the channel, so it is not eligible.
ReasonLoopOut
// ReasonLoopIn indicates that a loop in swap is currently in flight
// for the peer, so it is not eligible.
ReasonLoopIn
// ReasonLiquidityOk indicates that a target meets the liquidity
// balance expressed in its rule, so no swap is needed.
ReasonLiquidityOk
// ReasonBudgetInsufficient indicates that we cannot perform a swap
// because we do not have enough pending budget available. This differs
// from budget elapsed, because we still have some budget available,
// but we have allocated it to other swaps.
ReasonBudgetInsufficient
)

@ -21,6 +21,8 @@ import (
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/queue" "github.com/lightningnetwork/lnd/queue"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
) )
const ( const (
@ -702,14 +704,23 @@ func rpcToRule(rule *looprpc.LiquidityRule) (*liquidity.ThresholdRule, error) {
func (s *swapClientServer) SuggestSwaps(ctx context.Context, func (s *swapClientServer) SuggestSwaps(ctx context.Context,
_ *looprpc.SuggestSwapsRequest) (*looprpc.SuggestSwapsResponse, error) { _ *looprpc.SuggestSwapsRequest) (*looprpc.SuggestSwapsResponse, error) {
swaps, err := s.liquidityMgr.SuggestSwaps(ctx, false) suggestions, err := s.liquidityMgr.SuggestSwaps(ctx, false)
if err != nil { switch err {
case liquidity.ErrNoRules:
return nil, status.Error(codes.FailedPrecondition, err.Error())
case nil:
default:
return nil, err return nil, err
} }
var loopOut []*looprpc.LoopOutRequest var (
loopOut []*looprpc.LoopOutRequest
disqualified []*looprpc.Disqualified
)
for _, swap := range swaps { for _, swap := range suggestions.OutSwaps {
loopOut = append(loopOut, &looprpc.LoopOutRequest{ loopOut = append(loopOut, &looprpc.LoopOutRequest{
Amt: int64(swap.Amount), Amt: int64(swap.Amount),
OutgoingChanSet: swap.OutgoingChanSet, OutgoingChanSet: swap.OutgoingChanSet,
@ -722,11 +733,71 @@ func (s *swapClientServer) SuggestSwaps(ctx context.Context,
}) })
} }
for id, reason := range suggestions.DisqualifiedChans {
autoloopReason, err := rpcAutoloopReason(reason)
if err != nil {
return nil, err
}
exclChan := &looprpc.Disqualified{
Reason: autoloopReason,
ChannelId: id.ToUint64(),
}
disqualified = append(disqualified, exclChan)
}
return &looprpc.SuggestSwapsResponse{ return &looprpc.SuggestSwapsResponse{
LoopOut: loopOut, LoopOut: loopOut,
Disqualified: disqualified,
}, nil }, nil
} }
func rpcAutoloopReason(reason liquidity.Reason) (looprpc.AutoReason, error) {
switch reason {
case liquidity.ReasonNone:
return looprpc.AutoReason_AUTO_REASON_UNKNOWN, nil
case liquidity.ReasonBudgetNotStarted:
return looprpc.AutoReason_AUTO_REASON_BUDGET_NOT_STARTED, nil
case liquidity.ReasonSweepFees:
return looprpc.AutoReason_AUTO_REASON_SWEEP_FEES, nil
case liquidity.ReasonBudgetElapsed:
return looprpc.AutoReason_AUTO_REASON_BUDGET_ELAPSED, nil
case liquidity.ReasonInFlight:
return looprpc.AutoReason_AUTO_REASON_IN_FLIGHT, nil
case liquidity.ReasonSwapFee:
return looprpc.AutoReason_AUTO_REASON_SWAP_FEE, nil
case liquidity.ReasonMinerFee:
return looprpc.AutoReason_AUTO_REASON_MINER_FEE, nil
case liquidity.ReasonPrepay:
return looprpc.AutoReason_AUTO_REASON_PREPAY, nil
case liquidity.ReasonFailureBackoff:
return looprpc.AutoReason_AUTO_REASON_FAILURE_BACKOFF, nil
case liquidity.ReasonLoopOut:
return looprpc.AutoReason_AUTO_REASON_LOOP_OUT, nil
case liquidity.ReasonLoopIn:
return looprpc.AutoReason_AUTO_REASON_LOOP_IN, nil
case liquidity.ReasonLiquidityOk:
return looprpc.AutoReason_AUTO_REASON_LIQUIDITY_OK, nil
case liquidity.ReasonBudgetInsufficient:
return looprpc.AutoReason_AUTO_REASON_BUDGET_INSUFFICIENT, nil
default:
return 0, fmt.Errorf("unknown autoloop reason: %v", reason)
}
}
// processStatusUpdates reads updates on the status channel and processes them. // processStatusUpdates reads updates on the status channel and processes them.
// //
// NOTE: This must run inside a goroutine as it blocks until the main context // NOTE: This must run inside a goroutine as it blocks until the main context

@ -200,6 +200,99 @@ func (LiquidityRuleType) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_014de31d7ac8c57c, []int{3} return fileDescriptor_014de31d7ac8c57c, []int{3}
} }
type AutoReason int32
const (
AutoReason_AUTO_REASON_UNKNOWN AutoReason = 0
//
//Budget not started indicates that we do not recommend any swaps because
//the start time for our budget has not arrived yet.
AutoReason_AUTO_REASON_BUDGET_NOT_STARTED AutoReason = 1
//
//Sweep fees indicates that the estimated fees to sweep swaps are too high
//right now.
AutoReason_AUTO_REASON_SWEEP_FEES AutoReason = 2
//
//Budget elapsed indicates that the autoloop budget for the period has been
//elapsed.
AutoReason_AUTO_REASON_BUDGET_ELAPSED AutoReason = 3
//
//In flight indicates that the limit on in-flight automatically dispatched
//swaps has already been reached.
AutoReason_AUTO_REASON_IN_FLIGHT AutoReason = 4
//
//Swap fee indicates that the server fee for a specific swap is too high.
AutoReason_AUTO_REASON_SWAP_FEE AutoReason = 5
//
//Miner fee indicates that the miner fee for a specific swap is to high.
AutoReason_AUTO_REASON_MINER_FEE AutoReason = 6
//
//Prepay indicates that the prepay fee for a specific swap is too high.
AutoReason_AUTO_REASON_PREPAY AutoReason = 7
//
//Failure backoff indicates that a swap has recently failed for this target,
//and the backoff period has not yet passed.
AutoReason_AUTO_REASON_FAILURE_BACKOFF AutoReason = 8
//
//Loop out indicates that a loop out swap is currently utilizing the channel,
//so it is not eligible.
AutoReason_AUTO_REASON_LOOP_OUT AutoReason = 9
//
//Loop In indicates that a loop in swap is currently in flight for the peer,
//so it is not eligible.
AutoReason_AUTO_REASON_LOOP_IN AutoReason = 10
//
//Liquidity ok indicates that a target meets the liquidity balance expressed
//in its rule, so no swap is needed.
AutoReason_AUTO_REASON_LIQUIDITY_OK AutoReason = 11
//
//Budget insufficient indicates that we cannot perform a swap because we do
//not have enough pending budget available. This differs from budget elapsed,
//because we still have some budget available, but we have allocated it to
//other swaps.
AutoReason_AUTO_REASON_BUDGET_INSUFFICIENT AutoReason = 12
)
var AutoReason_name = map[int32]string{
0: "AUTO_REASON_UNKNOWN",
1: "AUTO_REASON_BUDGET_NOT_STARTED",
2: "AUTO_REASON_SWEEP_FEES",
3: "AUTO_REASON_BUDGET_ELAPSED",
4: "AUTO_REASON_IN_FLIGHT",
5: "AUTO_REASON_SWAP_FEE",
6: "AUTO_REASON_MINER_FEE",
7: "AUTO_REASON_PREPAY",
8: "AUTO_REASON_FAILURE_BACKOFF",
9: "AUTO_REASON_LOOP_OUT",
10: "AUTO_REASON_LOOP_IN",
11: "AUTO_REASON_LIQUIDITY_OK",
12: "AUTO_REASON_BUDGET_INSUFFICIENT",
}
var AutoReason_value = map[string]int32{
"AUTO_REASON_UNKNOWN": 0,
"AUTO_REASON_BUDGET_NOT_STARTED": 1,
"AUTO_REASON_SWEEP_FEES": 2,
"AUTO_REASON_BUDGET_ELAPSED": 3,
"AUTO_REASON_IN_FLIGHT": 4,
"AUTO_REASON_SWAP_FEE": 5,
"AUTO_REASON_MINER_FEE": 6,
"AUTO_REASON_PREPAY": 7,
"AUTO_REASON_FAILURE_BACKOFF": 8,
"AUTO_REASON_LOOP_OUT": 9,
"AUTO_REASON_LOOP_IN": 10,
"AUTO_REASON_LIQUIDITY_OK": 11,
"AUTO_REASON_BUDGET_INSUFFICIENT": 12,
}
func (x AutoReason) String() string {
return proto.EnumName(AutoReason_name, int32(x))
}
func (AutoReason) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_014de31d7ac8c57c, []int{4}
}
type LoopOutRequest struct { type LoopOutRequest struct {
// //
//Requested swap amount in sat. This does not include the swap and miner fee. //Requested swap amount in sat. This does not include the swap and miner fee.
@ -1954,20 +2047,75 @@ func (m *SuggestSwapsRequest) XXX_DiscardUnknown() {
var xxx_messageInfo_SuggestSwapsRequest proto.InternalMessageInfo var xxx_messageInfo_SuggestSwapsRequest proto.InternalMessageInfo
type Disqualified struct {
//
//The short channel ID of the channel that was excluded from our suggestions.
ChannelId uint64 `protobuf:"varint,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"`
//
//The reason that we excluded the channel from the our suggestions.
Reason AutoReason `protobuf:"varint,2,opt,name=reason,proto3,enum=looprpc.AutoReason" json:"reason,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Disqualified) Reset() { *m = Disqualified{} }
func (m *Disqualified) String() string { return proto.CompactTextString(m) }
func (*Disqualified) ProtoMessage() {}
func (*Disqualified) Descriptor() ([]byte, []int) {
return fileDescriptor_014de31d7ac8c57c, []int{23}
}
func (m *Disqualified) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Disqualified.Unmarshal(m, b)
}
func (m *Disqualified) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Disqualified.Marshal(b, m, deterministic)
}
func (m *Disqualified) XXX_Merge(src proto.Message) {
xxx_messageInfo_Disqualified.Merge(m, src)
}
func (m *Disqualified) XXX_Size() int {
return xxx_messageInfo_Disqualified.Size(m)
}
func (m *Disqualified) XXX_DiscardUnknown() {
xxx_messageInfo_Disqualified.DiscardUnknown(m)
}
var xxx_messageInfo_Disqualified proto.InternalMessageInfo
func (m *Disqualified) GetChannelId() uint64 {
if m != nil {
return m.ChannelId
}
return 0
}
func (m *Disqualified) GetReason() AutoReason {
if m != nil {
return m.Reason
}
return AutoReason_AUTO_REASON_UNKNOWN
}
type SuggestSwapsResponse struct { type SuggestSwapsResponse struct {
// //
//The set of recommended loop outs. //The set of recommended loop outs.
LoopOut []*LoopOutRequest `protobuf:"bytes,1,rep,name=loop_out,json=loopOut,proto3" json:"loop_out,omitempty"` LoopOut []*LoopOutRequest `protobuf:"bytes,1,rep,name=loop_out,json=loopOut,proto3" json:"loop_out,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"` //
XXX_unrecognized []byte `json:"-"` //Disqualified contains the set of channels that swaps are not recommended
XXX_sizecache int32 `json:"-"` //for.
Disqualified []*Disqualified `protobuf:"bytes,2,rep,name=disqualified,proto3" json:"disqualified,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
} }
func (m *SuggestSwapsResponse) Reset() { *m = SuggestSwapsResponse{} } func (m *SuggestSwapsResponse) Reset() { *m = SuggestSwapsResponse{} }
func (m *SuggestSwapsResponse) String() string { return proto.CompactTextString(m) } func (m *SuggestSwapsResponse) String() string { return proto.CompactTextString(m) }
func (*SuggestSwapsResponse) ProtoMessage() {} func (*SuggestSwapsResponse) ProtoMessage() {}
func (*SuggestSwapsResponse) Descriptor() ([]byte, []int) { func (*SuggestSwapsResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_014de31d7ac8c57c, []int{23} return fileDescriptor_014de31d7ac8c57c, []int{24}
} }
func (m *SuggestSwapsResponse) XXX_Unmarshal(b []byte) error { func (m *SuggestSwapsResponse) XXX_Unmarshal(b []byte) error {
@ -1995,11 +2143,19 @@ func (m *SuggestSwapsResponse) GetLoopOut() []*LoopOutRequest {
return nil return nil
} }
func (m *SuggestSwapsResponse) GetDisqualified() []*Disqualified {
if m != nil {
return m.Disqualified
}
return nil
}
func init() { func init() {
proto.RegisterEnum("looprpc.SwapType", SwapType_name, SwapType_value) proto.RegisterEnum("looprpc.SwapType", SwapType_name, SwapType_value)
proto.RegisterEnum("looprpc.SwapState", SwapState_name, SwapState_value) proto.RegisterEnum("looprpc.SwapState", SwapState_name, SwapState_value)
proto.RegisterEnum("looprpc.FailureReason", FailureReason_name, FailureReason_value) proto.RegisterEnum("looprpc.FailureReason", FailureReason_name, FailureReason_value)
proto.RegisterEnum("looprpc.LiquidityRuleType", LiquidityRuleType_name, LiquidityRuleType_value) proto.RegisterEnum("looprpc.LiquidityRuleType", LiquidityRuleType_name, LiquidityRuleType_value)
proto.RegisterEnum("looprpc.AutoReason", AutoReason_name, AutoReason_value)
proto.RegisterType((*LoopOutRequest)(nil), "looprpc.LoopOutRequest") proto.RegisterType((*LoopOutRequest)(nil), "looprpc.LoopOutRequest")
proto.RegisterType((*LoopInRequest)(nil), "looprpc.LoopInRequest") proto.RegisterType((*LoopInRequest)(nil), "looprpc.LoopInRequest")
proto.RegisterType((*SwapResponse)(nil), "looprpc.SwapResponse") proto.RegisterType((*SwapResponse)(nil), "looprpc.SwapResponse")
@ -2023,163 +2179,177 @@ func init() {
proto.RegisterType((*SetLiquidityParamsRequest)(nil), "looprpc.SetLiquidityParamsRequest") proto.RegisterType((*SetLiquidityParamsRequest)(nil), "looprpc.SetLiquidityParamsRequest")
proto.RegisterType((*SetLiquidityParamsResponse)(nil), "looprpc.SetLiquidityParamsResponse") proto.RegisterType((*SetLiquidityParamsResponse)(nil), "looprpc.SetLiquidityParamsResponse")
proto.RegisterType((*SuggestSwapsRequest)(nil), "looprpc.SuggestSwapsRequest") proto.RegisterType((*SuggestSwapsRequest)(nil), "looprpc.SuggestSwapsRequest")
proto.RegisterType((*Disqualified)(nil), "looprpc.Disqualified")
proto.RegisterType((*SuggestSwapsResponse)(nil), "looprpc.SuggestSwapsResponse") proto.RegisterType((*SuggestSwapsResponse)(nil), "looprpc.SuggestSwapsResponse")
} }
func init() { proto.RegisterFile("client.proto", fileDescriptor_014de31d7ac8c57c) } func init() { proto.RegisterFile("client.proto", fileDescriptor_014de31d7ac8c57c) }
var fileDescriptor_014de31d7ac8c57c = []byte{ var fileDescriptor_014de31d7ac8c57c = []byte{
// 2389 bytes of a gzipped FileDescriptorProto // 2605 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x58, 0xcd, 0x6f, 0xe3, 0xc6, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x59, 0x4b, 0x73, 0x1b, 0xc7,
0x15, 0x5f, 0x7d, 0x59, 0xd2, 0x13, 0x45, 0xd1, 0xe3, 0x5d, 0x5b, 0x56, 0x1c, 0xc4, 0xcb, 0x64, 0xf1, 0x17, 0x5e, 0x04, 0xd0, 0x58, 0x00, 0xcb, 0xa1, 0x44, 0x82, 0x30, 0x6d, 0x51, 0x6b, 0xeb,
0x1b, 0xc7, 0x49, 0xac, 0xc6, 0x39, 0x25, 0x48, 0x0a, 0x68, 0x65, 0x39, 0x96, 0x6b, 0x4b, 0x2a, 0x6f, 0x9a, 0xb6, 0xc5, 0xbf, 0xe9, 0x93, 0x5d, 0x76, 0xaa, 0x40, 0x70, 0x29, 0x42, 0x26, 0x01,
0x25, 0x6f, 0x90, 0xa2, 0x00, 0x31, 0x96, 0xc6, 0x16, 0x11, 0xf1, 0x23, 0xe4, 0x68, 0xd7, 0x46, 0x78, 0x01, 0xc8, 0x25, 0x57, 0xaa, 0xb6, 0x86, 0xc0, 0x90, 0xdc, 0x32, 0xf6, 0xa1, 0xdd, 0x81,
0xd0, 0x16, 0x28, 0xd0, 0x73, 0x0f, 0xfd, 0x0f, 0x7a, 0xef, 0xad, 0xb7, 0xf6, 0xde, 0x4b, 0x4f, 0x44, 0x95, 0x2b, 0x49, 0x55, 0x2a, 0x3e, 0xe7, 0x90, 0x6f, 0x90, 0x7b, 0x6e, 0xb9, 0x25, 0xf7,
0xed, 0xb1, 0xd7, 0x5e, 0x7a, 0xe8, 0xff, 0x50, 0xcc, 0x1b, 0x92, 0x22, 0x65, 0xc9, 0x41, 0x0f, 0x5c, 0x72, 0x4a, 0x8e, 0xb9, 0xe6, 0x92, 0x43, 0xbe, 0x43, 0x6a, 0x7a, 0x76, 0x17, 0xbb, 0x20,
0xbd, 0x89, 0xef, 0xfd, 0xe6, 0xcd, 0x9b, 0xf7, 0xfd, 0x04, 0xca, 0x78, 0x66, 0x31, 0x87, 0x1f, 0x40, 0x55, 0x0e, 0xb9, 0x11, 0xdd, 0xbf, 0xe9, 0x9e, 0x7e, 0x4e, 0xf7, 0x12, 0x94, 0xf1, 0xd4,
0x79, 0xbe, 0xcb, 0x5d, 0x52, 0x9c, 0xb9, 0xae, 0xe7, 0x7b, 0xe3, 0xc6, 0xde, 0xad, 0xeb, 0xde, 0x62, 0x0e, 0x7f, 0xe2, 0xf9, 0x2e, 0x77, 0x49, 0x71, 0xea, 0xba, 0x9e, 0xef, 0x8d, 0x9b, 0x3b,
0xce, 0x58, 0x93, 0x7a, 0x56, 0x93, 0x3a, 0x8e, 0xcb, 0x29, 0xb7, 0x5c, 0x27, 0x90, 0x30, 0xfd, 0x57, 0xae, 0x7b, 0x35, 0x65, 0x07, 0xd4, 0xb3, 0x0e, 0xa8, 0xe3, 0xb8, 0x9c, 0x72, 0xcb, 0x75,
0x8f, 0x79, 0x50, 0x2f, 0x5c, 0xd7, 0xeb, 0xcf, 0xb9, 0xc1, 0xbe, 0x9b, 0xb3, 0x80, 0x13, 0x0d, 0x02, 0x09, 0xd3, 0xfe, 0x90, 0x87, 0xda, 0x99, 0xeb, 0x7a, 0xbd, 0x19, 0x37, 0xd8, 0xcb, 0x19,
0x72, 0xd4, 0xe6, 0xf5, 0xcc, 0x7e, 0xe6, 0x20, 0x67, 0x88, 0x9f, 0x84, 0x40, 0x7e, 0xc2, 0x02, 0x0b, 0x38, 0x51, 0x21, 0x47, 0x6d, 0xde, 0xc8, 0xec, 0x66, 0xf6, 0x72, 0x86, 0xf8, 0x93, 0x10,
0x5e, 0xcf, 0xee, 0x67, 0x0e, 0xca, 0x06, 0xfe, 0x26, 0x4d, 0x78, 0x6a, 0xd3, 0x3b, 0x33, 0x78, 0xc8, 0x4f, 0x58, 0xc0, 0x1b, 0xd9, 0xdd, 0xcc, 0x5e, 0xd9, 0xc0, 0xbf, 0xc9, 0x01, 0xdc, 0xb7,
0x43, 0x3d, 0xd3, 0x77, 0xe7, 0xdc, 0x72, 0x6e, 0xcd, 0x1b, 0xc6, 0xea, 0x39, 0x3c, 0xb6, 0x69, 0xe9, 0x8d, 0x19, 0xbc, 0xa6, 0x9e, 0xe9, 0xbb, 0x33, 0x6e, 0x39, 0x57, 0xe6, 0x25, 0x63, 0x8d,
0xd3, 0xbb, 0xe1, 0x1b, 0xea, 0x19, 0x92, 0x73, 0xca, 0x18, 0xf9, 0x14, 0xb6, 0xc5, 0x01, 0xcf, 0x1c, 0x1e, 0x5b, 0xb7, 0xe9, 0xcd, 0xe0, 0x35, 0xf5, 0x0c, 0xc9, 0x39, 0x61, 0x8c, 0x7c, 0x0e,
0x67, 0x1e, 0xbd, 0x4f, 0x1d, 0xc9, 0xe3, 0x91, 0x2d, 0x9b, 0xde, 0x0d, 0x90, 0x99, 0x38, 0xb4, 0x9b, 0xe2, 0x80, 0xe7, 0x33, 0x8f, 0xbe, 0x49, 0x1d, 0xc9, 0xe3, 0x91, 0x0d, 0x9b, 0xde, 0xf4,
0x0f, 0x4a, 0x7c, 0x8b, 0x80, 0x16, 0x10, 0x0a, 0xa1, 0x74, 0x81, 0x78, 0x0f, 0xd4, 0x84, 0x58, 0x91, 0x99, 0x38, 0xb4, 0x0b, 0x4a, 0xac, 0x45, 0x40, 0x0b, 0x08, 0x85, 0x50, 0xba, 0x40, 0x7c,
0xa1, 0xf8, 0x06, 0x62, 0x94, 0x58, 0x5c, 0xcb, 0xe6, 0x44, 0x87, 0xaa, 0x40, 0xd9, 0x96, 0xc3, 0x00, 0xb5, 0x84, 0x58, 0x71, 0xf1, 0x35, 0xc4, 0x28, 0xb1, 0xb8, 0x96, 0xcd, 0x89, 0x06, 0x55,
0x7c, 0x14, 0x54, 0x44, 0x50, 0xc5, 0xa6, 0x77, 0x97, 0x82, 0x26, 0x24, 0x7d, 0x04, 0x9a, 0xb0, 0x81, 0xb2, 0x2d, 0x87, 0xf9, 0x28, 0xa8, 0x88, 0xa0, 0x8a, 0x4d, 0x6f, 0xce, 0x05, 0x4d, 0x48,
0x99, 0xe9, 0xce, 0xb9, 0x39, 0x9e, 0x52, 0xc7, 0x61, 0xb3, 0x7a, 0x69, 0x3f, 0x73, 0x90, 0x7f, 0xfa, 0x04, 0x54, 0xe1, 0x33, 0xd3, 0x9d, 0x71, 0x73, 0x7c, 0x4d, 0x1d, 0x87, 0x4d, 0x1b, 0xa5,
0x99, 0xad, 0x67, 0x0c, 0x75, 0x26, 0xad, 0xd4, 0x96, 0x1c, 0x72, 0x08, 0x9b, 0xee, 0x9c, 0xdf, 0xdd, 0xcc, 0x5e, 0xfe, 0x28, 0xdb, 0xc8, 0x18, 0xb5, 0xa9, 0xf4, 0x52, 0x5b, 0x72, 0xc8, 0x3e,
0xba, 0xe2, 0x11, 0x02, 0x6d, 0x06, 0x8c, 0xd7, 0x2b, 0xfb, 0xb9, 0x83, 0xbc, 0x51, 0x8b, 0x18, 0xac, 0xbb, 0x33, 0x7e, 0xe5, 0x0a, 0x23, 0x04, 0xda, 0x0c, 0x18, 0x6f, 0x54, 0x76, 0x73, 0x7b,
0x02, 0x3b, 0x64, 0x5c, 0x60, 0x83, 0x37, 0x8c, 0x79, 0xe6, 0xd8, 0x75, 0x6e, 0x4c, 0x4e, 0xfd, 0x79, 0xa3, 0x1e, 0x31, 0x04, 0x76, 0xc0, 0xb8, 0xc0, 0x06, 0xaf, 0x19, 0xf3, 0xcc, 0xb1, 0xeb,
0x5b, 0xc6, 0xeb, 0xe5, 0xfd, 0xcc, 0x41, 0xc1, 0xa8, 0x21, 0xa3, 0xed, 0x3a, 0x37, 0x23, 0x24, 0x5c, 0x9a, 0x9c, 0xfa, 0x57, 0x8c, 0x37, 0xca, 0xbb, 0x99, 0xbd, 0x82, 0x51, 0x47, 0x46, 0xdb,
0x93, 0x8f, 0x81, 0x4c, 0xf9, 0x6c, 0x8c, 0x50, 0xcb, 0xb7, 0xa5, 0xb3, 0xea, 0x55, 0x04, 0x6f, 0x75, 0x2e, 0x87, 0x48, 0x26, 0x9f, 0x02, 0xb9, 0xe6, 0xd3, 0x31, 0x42, 0x2d, 0xdf, 0x96, 0xc1,
0x0a, 0x4e, 0x3b, 0xc9, 0x20, 0x9f, 0xc3, 0x2e, 0x1a, 0xc7, 0x9b, 0x5f, 0xcf, 0xac, 0x31, 0x12, 0x6a, 0x54, 0x11, 0xbc, 0x2e, 0x38, 0xed, 0x24, 0x83, 0x7c, 0x09, 0xdb, 0xe8, 0x1c, 0x6f, 0x76,
0xcd, 0x09, 0xa3, 0x93, 0x99, 0xe5, 0xb0, 0x3a, 0x08, 0xed, 0x8d, 0x1d, 0x01, 0x18, 0x2c, 0xf8, 0x31, 0xb5, 0xc6, 0x48, 0x34, 0x27, 0x8c, 0x4e, 0xa6, 0x96, 0xc3, 0x1a, 0x20, 0x6e, 0x6f, 0x6c,
0x27, 0x21, 0x9b, 0x3c, 0x85, 0xc2, 0x8c, 0x5e, 0xb3, 0x59, 0x5d, 0x41, 0xbf, 0xca, 0x0f, 0xb2, 0x09, 0x40, 0x7f, 0xce, 0x3f, 0x0e, 0xd9, 0xe4, 0x3e, 0x14, 0xa6, 0xf4, 0x82, 0x4d, 0x1b, 0x0a,
0x07, 0x65, 0xcb, 0xb1, 0xb8, 0x45, 0xb9, 0xeb, 0xd7, 0x55, 0xe4, 0x2c, 0x08, 0xfa, 0x6f, 0xb3, 0xc6, 0x55, 0xfe, 0x20, 0x3b, 0x50, 0xb6, 0x1c, 0x8b, 0x5b, 0x94, 0xbb, 0x7e, 0xa3, 0x86, 0x9c,
0x50, 0x15, 0xf1, 0xd2, 0x75, 0xd6, 0x87, 0xcb, 0xb2, 0xd3, 0xb2, 0x0f, 0x9c, 0xf6, 0xc0, 0x1d, 0x39, 0x41, 0xfb, 0x29, 0x0b, 0x55, 0x91, 0x2f, 0x1d, 0x67, 0x75, 0xba, 0x2c, 0x06, 0x2d, 0x7b,
0xb9, 0x87, 0xee, 0xd8, 0x85, 0xd2, 0x8c, 0x06, 0xdc, 0x9c, 0xba, 0x1e, 0x46, 0x88, 0x62, 0x14, 0x2b, 0x68, 0xb7, 0xc2, 0x91, 0xbb, 0x1d, 0x8e, 0x6d, 0x28, 0x4d, 0x69, 0xc0, 0xcd, 0x6b, 0xd7,
0xc5, 0xf7, 0x99, 0xeb, 0x91, 0x77, 0xa1, 0xca, 0xee, 0x38, 0xf3, 0x1d, 0x3a, 0x33, 0x85, 0x49, 0xc3, 0x0c, 0x51, 0x8c, 0xa2, 0xf8, 0x7d, 0xea, 0x7a, 0xe4, 0x7d, 0xa8, 0xb2, 0x1b, 0xce, 0x7c,
0x30, 0x2c, 0x4a, 0x86, 0x12, 0x11, 0xcf, 0xf8, 0x6c, 0x4c, 0x0e, 0x40, 0x8b, 0x0d, 0x19, 0xd9, 0x87, 0x4e, 0x4d, 0xe1, 0x12, 0x4c, 0x8b, 0x92, 0xa1, 0x44, 0xc4, 0x53, 0x3e, 0x1d, 0x93, 0x3d,
0x7c, 0x03, 0xcd, 0xa8, 0x46, 0x66, 0x0c, 0x4d, 0x1e, 0xdb, 0xa1, 0xb8, 0xd6, 0x0e, 0xa5, 0x65, 0x50, 0x63, 0x47, 0x46, 0x3e, 0x5f, 0x43, 0x37, 0xd6, 0x22, 0x37, 0x86, 0x2e, 0x8f, 0xfd, 0x50,
0x3b, 0xfc, 0x3b, 0x03, 0x0a, 0x06, 0x38, 0x0b, 0x3c, 0xd7, 0x09, 0x18, 0x21, 0x90, 0xb5, 0x26, 0x5c, 0xe9, 0x87, 0xd2, 0xa2, 0x1f, 0xfe, 0x95, 0x01, 0x05, 0x13, 0x9c, 0x05, 0x9e, 0xeb, 0x04,
0x68, 0x85, 0x32, 0xc6, 0x4b, 0xd6, 0x9a, 0x88, 0x27, 0x58, 0x13, 0xf3, 0xfa, 0x9e, 0xb3, 0x00, 0x8c, 0x10, 0xc8, 0x5a, 0x13, 0xf4, 0x42, 0x19, 0xf3, 0x25, 0x6b, 0x4d, 0x84, 0x09, 0xd6, 0xc4,
0x5f, 0xa8, 0x18, 0x45, 0x6b, 0xf2, 0x52, 0x7c, 0x92, 0x17, 0xa0, 0xa0, 0x76, 0x74, 0x32, 0xf1, 0xbc, 0x78, 0xc3, 0x59, 0x80, 0x16, 0x2a, 0x46, 0xd1, 0x9a, 0x1c, 0x89, 0x9f, 0xe4, 0x31, 0x28,
0x59, 0x10, 0xc8, 0xd4, 0xc2, 0x83, 0x15, 0x41, 0x6f, 0x49, 0x32, 0x39, 0x82, 0xad, 0x24, 0xcc, 0x78, 0x3b, 0x3a, 0x99, 0xf8, 0x2c, 0x08, 0x64, 0x69, 0xe1, 0xc1, 0x8a, 0xa0, 0xb7, 0x24, 0x99,
0x74, 0xbc, 0xe3, 0x37, 0xc1, 0x14, 0xed, 0x51, 0x96, 0xe1, 0x10, 0x22, 0x7b, 0xc8, 0x20, 0x1f, 0x3c, 0x81, 0x8d, 0x24, 0xcc, 0x74, 0xbc, 0xc3, 0xd7, 0xc1, 0x35, 0xfa, 0xa3, 0x2c, 0xd3, 0x21,
0x85, 0xd1, 0x13, 0xe1, 0x25, 0xbc, 0x80, 0x70, 0x2d, 0x01, 0x1f, 0x20, 0xfa, 0x05, 0xa8, 0x01, 0x44, 0x76, 0x91, 0x41, 0x3e, 0x09, 0xb3, 0x27, 0xc2, 0x4b, 0x78, 0x01, 0xe1, 0x6a, 0x02, 0xde,
0xf3, 0x5f, 0x33, 0xdf, 0xb4, 0x59, 0x10, 0xd0, 0x5b, 0x86, 0x06, 0x2a, 0x1b, 0x55, 0x49, 0xbd, 0x47, 0xf4, 0x63, 0xa8, 0x05, 0xcc, 0x7f, 0xc5, 0x7c, 0xd3, 0x66, 0x41, 0x40, 0xaf, 0x18, 0x3a,
0x94, 0x44, 0x5d, 0x03, 0xf5, 0xd2, 0x75, 0x2c, 0xee, 0xfa, 0xa1, 0xcf, 0xf5, 0x3f, 0xe5, 0x01, 0xa8, 0x6c, 0x54, 0x25, 0xf5, 0x5c, 0x12, 0x35, 0x15, 0x6a, 0xe7, 0xae, 0x63, 0x71, 0xd7, 0x0f,
0xc4, 0xeb, 0x87, 0x9c, 0xf2, 0x79, 0xb0, 0xb2, 0x62, 0x08, 0x6b, 0x64, 0xd7, 0x5a, 0xa3, 0xb2, 0x63, 0xae, 0xfd, 0x31, 0x0f, 0x20, 0xac, 0x1f, 0x70, 0xca, 0x67, 0xc1, 0xd2, 0x8e, 0x21, 0xbc,
0x6c, 0x8d, 0x3c, 0xbf, 0xf7, 0x64, 0x18, 0xa8, 0xc7, 0x9b, 0x47, 0x61, 0xed, 0x3a, 0x12, 0x77, 0x91, 0x5d, 0xe9, 0x8d, 0xca, 0xa2, 0x37, 0xf2, 0xfc, 0x8d, 0x27, 0xd3, 0xa0, 0x76, 0xb8, 0xfe,
0x8c, 0xee, 0x3d, 0x66, 0x20, 0x9b, 0x1c, 0x40, 0x21, 0xe0, 0x94, 0xcb, 0x8a, 0xa1, 0x1e, 0x93, 0x24, 0xec, 0x5d, 0x4f, 0x84, 0x8e, 0xe1, 0x1b, 0x8f, 0x19, 0xc8, 0x26, 0x7b, 0x50, 0x08, 0x38,
0x14, 0x4e, 0xe8, 0xc2, 0x0c, 0x09, 0x20, 0x5f, 0x82, 0x7a, 0x43, 0xad, 0xd9, 0xdc, 0x67, 0xa6, 0xe5, 0xb2, 0x63, 0xd4, 0x0e, 0x49, 0x0a, 0x27, 0xee, 0xc2, 0x0c, 0x09, 0x20, 0x5f, 0x43, 0xed,
0xcf, 0x68, 0xe0, 0x3a, 0x18, 0xc9, 0xea, 0xf1, 0x76, 0x7c, 0xe4, 0x54, 0xb2, 0x0d, 0xe4, 0x1a, 0x92, 0x5a, 0xd3, 0x99, 0xcf, 0x4c, 0x9f, 0xd1, 0xc0, 0x75, 0x30, 0x93, 0x6b, 0x87, 0x9b, 0xf1,
0xd5, 0x9b, 0xe4, 0x27, 0x79, 0x1f, 0x6a, 0xa1, 0xab, 0x45, 0x3e, 0x71, 0xcb, 0x8e, 0x2a, 0x8f, 0x91, 0x13, 0xc9, 0x36, 0x90, 0x6b, 0x54, 0x2f, 0x93, 0x3f, 0xc9, 0x87, 0x50, 0x0f, 0x43, 0x2d,
0xba, 0x20, 0x8f, 0x2c, 0x5b, 0x68, 0xa4, 0x61, 0x90, 0xce, 0xbd, 0x09, 0xe5, 0x4c, 0x22, 0x65, 0xea, 0x89, 0x5b, 0x76, 0xd4, 0x79, 0x6a, 0x73, 0xf2, 0xd0, 0xb2, 0xc5, 0x8d, 0x54, 0x4c, 0xd2,
0xfd, 0x51, 0x05, 0xfd, 0x0a, 0xc9, 0x88, 0x5c, 0x76, 0x78, 0x71, 0xb5, 0xc3, 0x57, 0x3b, 0x50, 0x99, 0x37, 0xa1, 0x9c, 0x49, 0xa4, 0xec, 0x3f, 0x35, 0x41, 0x1f, 0x21, 0x19, 0x91, 0x8b, 0x01,
0x59, 0xe3, 0xc0, 0x35, 0xe1, 0x51, 0x5d, 0x17, 0x1e, 0xef, 0x40, 0x65, 0xec, 0x06, 0xdc, 0x94, 0x2f, 0x2e, 0x0f, 0xf8, 0xf2, 0x00, 0x2a, 0x2b, 0x02, 0xb8, 0x22, 0x3d, 0xaa, 0xab, 0xd2, 0xe3,
0xfe, 0xc5, 0xa8, 0xce, 0x19, 0x20, 0x48, 0x43, 0xa4, 0x90, 0xe7, 0xa0, 0x20, 0xc0, 0x75, 0xc6, 0x21, 0x54, 0xc6, 0x6e, 0xc0, 0x4d, 0x19, 0x5f, 0xcc, 0xea, 0x9c, 0x01, 0x82, 0x34, 0x40, 0x0a,
0x53, 0x6a, 0x39, 0x58, 0xa4, 0x72, 0x06, 0x1e, 0xea, 0x4b, 0x92, 0x48, 0x3e, 0x09, 0xb9, 0xb9, 0x79, 0x04, 0x0a, 0x02, 0x5c, 0x67, 0x7c, 0x4d, 0x2d, 0x07, 0x9b, 0x54, 0xce, 0xc0, 0x43, 0x3d,
0x91, 0x18, 0x90, 0xf5, 0x16, 0x31, 0x21, 0x6d, 0x91, 0x52, 0xb5, 0x44, 0x4a, 0xe9, 0x04, 0xb4, 0x49, 0x12, 0xc5, 0x27, 0x21, 0x97, 0x97, 0x12, 0x03, 0xb2, 0xdf, 0x22, 0x26, 0xa4, 0xcd, 0x4b,
0x0b, 0x2b, 0xe0, 0xc2, 0x5b, 0x41, 0x14, 0x4a, 0x3f, 0x81, 0xcd, 0x04, 0x2d, 0x4c, 0xa6, 0x0f, 0xaa, 0x9e, 0x28, 0x29, 0x8d, 0x80, 0x7a, 0x66, 0x05, 0x5c, 0x44, 0x2b, 0x88, 0x52, 0xe9, 0x67,
0xa0, 0x20, 0xaa, 0x47, 0x50, 0xcf, 0xec, 0xe7, 0x0e, 0x2a, 0xc7, 0x5b, 0x0f, 0x1c, 0x3d, 0x0f, 0xb0, 0x9e, 0xa0, 0x85, 0xc5, 0xf4, 0x11, 0x14, 0x44, 0xf7, 0x08, 0x1a, 0x99, 0xdd, 0xdc, 0x5e,
0x0c, 0x89, 0xd0, 0x9f, 0x43, 0x4d, 0x10, 0xbb, 0xce, 0x8d, 0x1b, 0x55, 0x24, 0x35, 0x4e, 0x45, 0xe5, 0x70, 0xe3, 0x56, 0xa0, 0x67, 0x81, 0x21, 0x11, 0xda, 0x23, 0xa8, 0x0b, 0x62, 0xc7, 0xb9,
0x45, 0x04, 0x9e, 0xae, 0x82, 0x32, 0x62, 0xbe, 0x1d, 0x5f, 0xf9, 0x6b, 0xa8, 0x75, 0x9d, 0x90, 0x74, 0xa3, 0x8e, 0x54, 0x8b, 0x4b, 0x51, 0x11, 0x89, 0xa7, 0xd5, 0x40, 0x19, 0x32, 0xdf, 0x8e,
0x12, 0x5e, 0xf8, 0x23, 0xa8, 0xd9, 0x96, 0x23, 0x4b, 0x16, 0xb5, 0xdd, 0xb9, 0xc3, 0x43, 0x87, 0x55, 0xfe, 0x0a, 0xea, 0x1d, 0x27, 0xa4, 0x84, 0x0a, 0xff, 0x0f, 0xea, 0xb6, 0xe5, 0xc8, 0x96,
0x57, 0x6d, 0xcb, 0x11, 0xf2, 0x5b, 0x48, 0x44, 0x5c, 0x54, 0xda, 0x42, 0xdc, 0x46, 0x88, 0x93, 0x45, 0x6d, 0x77, 0xe6, 0xf0, 0x30, 0xe0, 0x55, 0xdb, 0x72, 0x84, 0xfc, 0x16, 0x12, 0x11, 0x17,
0xd5, 0x4d, 0xe2, 0xce, 0xf3, 0xa5, 0x8c, 0x96, 0x3d, 0xcf, 0x97, 0xb2, 0x5a, 0xee, 0x3c, 0x5f, 0xb5, 0xb6, 0x10, 0xb7, 0x16, 0xe2, 0x64, 0x77, 0x93, 0xb8, 0x67, 0xf9, 0x52, 0x46, 0xcd, 0x3e,
0xca, 0x69, 0xf9, 0xf3, 0x7c, 0x29, 0xaf, 0x15, 0xce, 0xf3, 0xa5, 0xa2, 0x56, 0xd2, 0xff, 0x96, 0xcb, 0x97, 0xb2, 0x6a, 0xee, 0x59, 0xbe, 0x94, 0x53, 0xf3, 0xcf, 0xf2, 0xa5, 0xbc, 0x5a, 0x78,
0x01, 0xad, 0x3f, 0xe7, 0xff, 0x57, 0x15, 0xb0, 0x31, 0x5a, 0x8e, 0x39, 0x9e, 0xf1, 0xd7, 0xe6, 0x96, 0x2f, 0x15, 0xd5, 0x92, 0xf6, 0xd7, 0x0c, 0xa8, 0xbd, 0x19, 0xff, 0x9f, 0x5e, 0x01, 0x1f,
0x84, 0xcd, 0x38, 0x45, 0x77, 0x17, 0x0c, 0xc5, 0xb6, 0x9c, 0xf6, 0x8c, 0xbf, 0x3e, 0x11, 0xb4, 0x46, 0xcb, 0x31, 0xc7, 0x53, 0xfe, 0xca, 0x9c, 0xb0, 0x29, 0xa7, 0x18, 0xee, 0x82, 0xa1, 0xd8,
0xa8, 0x7d, 0x26, 0x50, 0xe5, 0x10, 0x45, 0xef, 0x62, 0xd4, 0x0f, 0x3c, 0xe7, 0x0f, 0x19, 0x50, 0x96, 0xd3, 0x9e, 0xf2, 0x57, 0xc7, 0x82, 0x16, 0x3d, 0x9f, 0x09, 0x54, 0x39, 0x44, 0xd1, 0x9b,
0x7e, 0x36, 0x77, 0x39, 0x5b, 0xdf, 0x12, 0x30, 0xf0, 0x16, 0x75, 0x38, 0x8b, 0x77, 0xc0, 0x78, 0x18, 0xf5, 0x16, 0x73, 0x7e, 0x9f, 0x01, 0xe5, 0xdb, 0x99, 0xcb, 0xd9, 0xea, 0x27, 0x01, 0x13,
0x51, 0x83, 0x1f, 0x94, 0xf4, 0xdc, 0x8a, 0x92, 0xfe, 0x68, 0xb3, 0xcb, 0x3f, 0xda, 0xec, 0xf4, 0x6f, 0xde, 0x87, 0xb3, 0xa8, 0x03, 0xc6, 0xf3, 0x1e, 0x7c, 0xab, 0xa5, 0xe7, 0x96, 0xb4, 0xf4,
0xdf, 0x65, 0x84, 0xd7, 0x43, 0x35, 0x43, 0x93, 0xef, 0x83, 0x12, 0x35, 0x29, 0x33, 0xa0, 0x91, 0x3b, 0x1f, 0xbb, 0xfc, 0x9d, 0x8f, 0x9d, 0xf6, 0xdb, 0x8c, 0x88, 0x7a, 0x78, 0xcd, 0xd0, 0xe5,
0xc2, 0x10, 0xc8, 0x2e, 0x35, 0xa4, 0x38, 0xe5, 0x60, 0x82, 0xe1, 0x8d, 0xc1, 0x34, 0x46, 0x86, 0xbb, 0xa0, 0x44, 0x8f, 0x94, 0x19, 0xd0, 0xe8, 0xc2, 0x10, 0xc8, 0x57, 0x6a, 0x40, 0x71, 0xca,
0x53, 0x8e, 0xe0, 0x0d, 0x24, 0x2b, 0x3c, 0xf0, 0x36, 0x40, 0xc2, 0x96, 0x05, 0x7c, 0x67, 0x79, 0xc1, 0x02, 0x43, 0x8d, 0xc1, 0x75, 0x8c, 0x0c, 0xa7, 0x1c, 0xc1, 0xeb, 0x4b, 0x56, 0x78, 0xe0,
0x9c, 0x30, 0xa4, 0x34, 0x61, 0x5e, 0x2b, 0xe8, 0x7f, 0x97, 0x51, 0xf0, 0xbf, 0xaa, 0xf4, 0x1e, 0x5d, 0x80, 0x84, 0x2f, 0x0b, 0x68, 0x67, 0x79, 0x9c, 0x70, 0xa4, 0x74, 0x61, 0x5e, 0x2d, 0x68,
0xa8, 0x8b, 0x61, 0x07, 0x31, 0xb2, 0xbf, 0x2a, 0x5e, 0x34, 0xed, 0x08, 0xd4, 0x87, 0x61, 0x1d, 0x7f, 0x93, 0x59, 0xf0, 0xdf, 0x5e, 0xe9, 0x03, 0xa8, 0xcd, 0x87, 0x1d, 0xc4, 0xc8, 0xf7, 0x55,
0x91, 0x73, 0x47, 0x5a, 0xed, 0x9a, 0xe0, 0x0c, 0x05, 0x23, 0x14, 0x89, 0xf3, 0x89, 0xb0, 0x2b, 0xf1, 0xa2, 0x69, 0x47, 0xa0, 0x3e, 0x0e, 0xfb, 0x88, 0x9c, 0x3b, 0xd2, 0xd7, 0xae, 0x0b, 0xce,
0xbd, 0xb7, 0x99, 0xc3, 0x4d, 0x1c, 0xf6, 0x64, 0xcf, 0xad, 0xa1, 0x3d, 0x25, 0xfd, 0x44, 0xf8, 0x40, 0x30, 0x42, 0x91, 0x38, 0x9f, 0x08, 0xbf, 0xd2, 0x37, 0x36, 0x73, 0xb8, 0x89, 0xc3, 0x9e,
0xf6, 0xf1, 0x07, 0xea, 0x35, 0xa8, 0x8e, 0xdc, 0x6f, 0x99, 0x13, 0x27, 0xdb, 0x17, 0xa0, 0x46, 0x7c, 0x73, 0xeb, 0xe8, 0x4f, 0x49, 0x3f, 0x16, 0xb1, 0xbd, 0xdb, 0x40, 0xad, 0x0e, 0xd5, 0xa1,
0x84, 0xf0, 0x89, 0x87, 0xb0, 0xc1, 0x91, 0x12, 0x66, 0xf7, 0xa2, 0x8c, 0x5f, 0x04, 0x94, 0x23, 0xfb, 0x03, 0x73, 0xe2, 0x62, 0xfb, 0x0a, 0x6a, 0x11, 0x21, 0x34, 0x71, 0x1f, 0xd6, 0x38, 0x52,
0xd8, 0x08, 0x11, 0xfa, 0x9f, 0xb3, 0x50, 0x8e, 0xa9, 0x22, 0x48, 0xae, 0x69, 0xc0, 0x4c, 0x9b, 0xc2, 0xea, 0x9e, 0xb7, 0xf1, 0xb3, 0x80, 0x72, 0x04, 0x1b, 0x21, 0x42, 0xfb, 0x53, 0x16, 0xca,
0x8e, 0xa9, 0xef, 0xba, 0x4e, 0x98, 0xe3, 0x8a, 0x20, 0x5e, 0x86, 0x34, 0x51, 0xc2, 0xa2, 0x77, 0x31, 0x55, 0x24, 0xc9, 0x05, 0x0d, 0x98, 0x69, 0xd3, 0x31, 0xf5, 0x5d, 0xd7, 0x09, 0x6b, 0x5c,
0x4c, 0x69, 0x30, 0x45, 0xeb, 0x28, 0x46, 0x25, 0xa4, 0x9d, 0xd1, 0x60, 0x4a, 0x3e, 0x00, 0x2d, 0x11, 0xc4, 0xf3, 0x90, 0x26, 0x5a, 0x58, 0x64, 0xc7, 0x35, 0x0d, 0xae, 0xd1, 0x3b, 0x8a, 0x51,
0x82, 0x78, 0x3e, 0xb3, 0x6c, 0xd1, 0xf9, 0x64, 0x7f, 0xae, 0x85, 0xf4, 0x41, 0x48, 0x16, 0x05, 0x09, 0x69, 0xa7, 0x34, 0xb8, 0x26, 0x1f, 0x81, 0x1a, 0x41, 0x3c, 0x9f, 0x59, 0xb6, 0x78, 0xf9,
0x5e, 0x26, 0x99, 0xe9, 0x51, 0x6b, 0x62, 0xda, 0xc2, 0x8a, 0x72, 0x5e, 0x55, 0x25, 0x7d, 0x40, 0xe4, 0xfb, 0x5c, 0x0f, 0xe9, 0xfd, 0x90, 0x2c, 0x1a, 0xbc, 0x2c, 0x32, 0xd3, 0xa3, 0xd6, 0xc4,
0xad, 0xc9, 0x65, 0x40, 0x39, 0xf9, 0x04, 0x9e, 0x25, 0x86, 0xda, 0x04, 0x5c, 0x66, 0x31, 0xf1, 0xb4, 0x85, 0x17, 0xe5, 0xbc, 0x5a, 0x93, 0xf4, 0x3e, 0xb5, 0x26, 0xe7, 0x01, 0xe5, 0xe4, 0x33,
0xe3, 0xa9, 0x36, 0x3e, 0xf2, 0x1c, 0x14, 0xd1, 0x31, 0xcc, 0xb1, 0xcf, 0x28, 0x67, 0x93, 0x30, 0x78, 0x90, 0x18, 0x6a, 0x13, 0x70, 0x59, 0xc5, 0xc4, 0x8f, 0xa7, 0xda, 0xf8, 0xc8, 0x23, 0x50,
0x8f, 0x2b, 0x82, 0xd6, 0x96, 0x24, 0x52, 0x87, 0x22, 0xbb, 0xf3, 0x2c, 0x9f, 0x4d, 0xb0, 0x63, 0xc4, 0x8b, 0x61, 0x8e, 0x7d, 0x46, 0x39, 0x9b, 0x84, 0x75, 0x5c, 0x11, 0xb4, 0xb6, 0x24, 0x91,
0x94, 0x8c, 0xe8, 0x53, 0x1c, 0x0e, 0xb8, 0xeb, 0xd3, 0x5b, 0x66, 0x3a, 0xd4, 0x66, 0xe1, 0x88, 0x06, 0x14, 0xd9, 0x8d, 0x67, 0xf9, 0x6c, 0x82, 0x2f, 0x46, 0xc9, 0x88, 0x7e, 0x8a, 0xc3, 0x01,
0x52, 0x09, 0x69, 0x3d, 0x6a, 0x33, 0xfd, 0x2d, 0xd8, 0xfd, 0x8a, 0xf1, 0x0b, 0xeb, 0xbb, 0xb9, 0x77, 0x7d, 0x7a, 0xc5, 0x4c, 0x87, 0xda, 0x2c, 0x1c, 0x51, 0x2a, 0x21, 0xad, 0x4b, 0x6d, 0xa6,
0x35, 0xb1, 0xf8, 0xfd, 0x80, 0xfa, 0x74, 0x51, 0x05, 0xff, 0x5a, 0x80, 0xad, 0x34, 0x8b, 0x71, 0xbd, 0x03, 0xdb, 0x4f, 0x19, 0x3f, 0xb3, 0x5e, 0xce, 0xac, 0x89, 0xc5, 0xdf, 0xf4, 0xa9, 0x4f,
0xe6, 0x8b, 0x0e, 0x54, 0xf0, 0xe7, 0x33, 0x16, 0x79, 0x67, 0xd1, 0x31, 0x63, 0xb0, 0x31, 0x9f, 0xe7, 0x5d, 0xf0, 0x2f, 0x05, 0xd8, 0x48, 0xb3, 0x18, 0x67, 0xbe, 0x78, 0x81, 0x0a, 0xfe, 0x6c,
0x31, 0x43, 0x82, 0xc8, 0x97, 0xb0, 0xb7, 0x08, 0x31, 0x5f, 0xf4, 0xc0, 0x80, 0x72, 0xd3, 0x63, 0xca, 0xa2, 0xe8, 0xcc, 0x5f, 0xcc, 0x18, 0x6c, 0xcc, 0xa6, 0xcc, 0x90, 0x20, 0xf2, 0x35, 0xec,
0xbe, 0xf9, 0x5a, 0x74, 0x7a, 0xb4, 0x3e, 0x66, 0xa5, 0x8c, 0x36, 0x83, 0x72, 0x11, 0x71, 0x03, 0xcc, 0x53, 0xcc, 0x17, 0x6f, 0x60, 0x40, 0xb9, 0xe9, 0x31, 0xdf, 0x7c, 0x25, 0x5e, 0x7a, 0xf4,
0xe6, 0xbf, 0x12, 0x6c, 0xf2, 0x3e, 0x68, 0xc9, 0x51, 0xd1, 0xf4, 0x3c, 0x1b, 0x3d, 0x91, 0x8f, 0x3e, 0x56, 0xa5, 0xcc, 0x36, 0x83, 0x72, 0x91, 0x71, 0x7d, 0xe6, 0x3f, 0x17, 0x6c, 0xf2, 0x21,
0xab, 0x99, 0xb0, 0x97, 0x67, 0x93, 0x8f, 0x41, 0xec, 0x07, 0x66, 0xca, 0xc2, 0x9e, 0x1d, 0x26, 0xa8, 0xc9, 0x51, 0xd1, 0xf4, 0x3c, 0x1b, 0x23, 0x91, 0x8f, 0xbb, 0x99, 0xf0, 0x97, 0x67, 0x93,
0xbd, 0x90, 0xb1, 0x58, 0x1a, 0x04, 0xfc, 0x73, 0x68, 0xac, 0x5e, 0x36, 0xf0, 0x54, 0x01, 0x4f, 0x4f, 0x41, 0xec, 0x07, 0x66, 0xca, 0xc3, 0x9e, 0x1d, 0x16, 0xbd, 0x90, 0x31, 0x5f, 0x1a, 0x04,
0x6d, 0xaf, 0x58, 0x38, 0xc4, 0xd9, 0xf4, 0x46, 0x21, 0x3c, 0xb8, 0x81, 0xf8, 0xc5, 0x46, 0x21, 0xfc, 0x4b, 0x68, 0x2e, 0x5f, 0x36, 0xf0, 0x54, 0x01, 0x4f, 0x6d, 0x2e, 0x59, 0x38, 0xc4, 0xd9,
0x72, 0xe6, 0x03, 0xd8, 0x4c, 0x8d, 0xb0, 0x08, 0x2c, 0x22, 0x50, 0x4d, 0x8c, 0xb1, 0x71, 0x7a, 0xf4, 0x46, 0x21, 0x22, 0xb8, 0x86, 0xf8, 0xf9, 0x46, 0x21, 0x6a, 0xe6, 0x23, 0x58, 0x4f, 0x8d,
0x2d, 0x8f, 0xff, 0xa5, 0xd5, 0xe3, 0xff, 0x11, 0x6c, 0x45, 0x83, 0xcb, 0x35, 0x1d, 0x7f, 0xeb, 0xb0, 0x08, 0x2c, 0x22, 0xb0, 0x96, 0x18, 0x63, 0xe3, 0xf2, 0x5a, 0x1c, 0xff, 0x4b, 0xcb, 0xc7,
0xde, 0xdc, 0x98, 0x01, 0x1b, 0x63, 0x51, 0xce, 0x1b, 0x9b, 0x21, 0xeb, 0xa5, 0xe4, 0x0c, 0xd9, 0xff, 0x27, 0xb0, 0x11, 0x0d, 0x2e, 0x17, 0x74, 0xfc, 0x83, 0x7b, 0x79, 0x69, 0x06, 0x6c, 0x8c,
0x98, 0x34, 0xa0, 0x44, 0xe7, 0xdc, 0x15, 0x3e, 0xc2, 0x46, 0x5c, 0x32, 0xe2, 0x6f, 0x21, 0x2b, 0x4d, 0x39, 0x6f, 0xac, 0x87, 0xac, 0x23, 0xc9, 0x19, 0xb0, 0x31, 0x69, 0x42, 0x89, 0xce, 0xb8,
0xfa, 0x6d, 0x5e, 0xcf, 0x27, 0xb7, 0x4c, 0x96, 0x8b, 0x8a, 0x94, 0x15, 0xb1, 0x5e, 0x22, 0x47, 0x2b, 0x62, 0x84, 0x0f, 0x71, 0xc9, 0x88, 0x7f, 0x0b, 0x59, 0xd1, 0xdf, 0xe6, 0xc5, 0x6c, 0x72,
0xe8, 0xf9, 0x19, 0xec, 0x3e, 0xc0, 0x73, 0xea, 0x73, 0xd4, 0x40, 0x91, 0x36, 0x5b, 0x3a, 0x25, 0xc5, 0x64, 0xbb, 0xa8, 0x48, 0x59, 0x11, 0xeb, 0x08, 0x39, 0xe2, 0x9e, 0x5f, 0xc0, 0xf6, 0x2d,
0xd8, 0x42, 0x8d, 0x0f, 0x81, 0x08, 0x8e, 0x29, 0x4c, 0x62, 0x39, 0xe6, 0xcd, 0xcc, 0xba, 0x9d, 0x3c, 0xa7, 0x3e, 0xc7, 0x1b, 0x28, 0xd2, 0x67, 0x0b, 0xa7, 0x04, 0x5b, 0x5c, 0xe3, 0x63, 0x20,
0x72, 0x9c, 0x43, 0xf2, 0x46, 0x4d, 0x70, 0x2e, 0xe9, 0x5d, 0xd7, 0x39, 0x45, 0xf2, 0xaa, 0x4e, 0x82, 0x63, 0x0a, 0x97, 0x58, 0x8e, 0x79, 0x39, 0xb5, 0xae, 0xae, 0x39, 0xce, 0x21, 0x79, 0xa3,
0xa7, 0x86, 0x3e, 0xff, 0xa1, 0x4e, 0x57, 0x4b, 0xc5, 0x86, 0xc4, 0xe9, 0x7f, 0xc9, 0x40, 0x35, 0x2e, 0x38, 0xe7, 0xf4, 0xa6, 0xe3, 0x9c, 0x20, 0x79, 0xd9, 0x4b, 0x57, 0x0b, 0x63, 0xfe, 0xb6,
0x15, 0x9c, 0x58, 0xa4, 0xe4, 0x9e, 0x66, 0x86, 0x93, 0x40, 0xde, 0x28, 0x87, 0x94, 0xee, 0x84, 0x97, 0xae, 0x9e, 0xca, 0x0d, 0x89, 0xd3, 0xfe, 0x9c, 0x81, 0x6a, 0x2a, 0x39, 0xb1, 0x49, 0xc9,
0x1c, 0x85, 0xe3, 0x66, 0x16, 0x67, 0xc2, 0xc6, 0xea, 0x08, 0x4f, 0xcc, 0x9d, 0x1f, 0x03, 0xb1, 0x3d, 0xcd, 0x0c, 0x27, 0x81, 0xbc, 0x51, 0x0e, 0x29, 0x9d, 0x09, 0x79, 0x12, 0x8e, 0x9b, 0x59,
0x9c, 0xb1, 0x6b, 0x8b, 0x18, 0xe2, 0x53, 0x9f, 0x05, 0x53, 0x77, 0x36, 0xc1, 0x38, 0xad, 0x1a, 0x9c, 0x09, 0x9b, 0xcb, 0x33, 0x3c, 0x31, 0x77, 0x7e, 0x0a, 0xc4, 0x72, 0xc6, 0xae, 0x2d, 0x72,
0x9b, 0x11, 0x67, 0x14, 0x31, 0x04, 0x3c, 0x5e, 0x0d, 0x17, 0xf0, 0xbc, 0x84, 0x47, 0x9c, 0x18, 0x88, 0x5f, 0xfb, 0x2c, 0xb8, 0x76, 0xa7, 0x13, 0xcc, 0xd3, 0xaa, 0xb1, 0x1e, 0x71, 0x86, 0x11,
0xae, 0x7f, 0x03, 0xbb, 0xc3, 0x75, 0x59, 0x4a, 0xbe, 0x00, 0xf0, 0xe2, 0xdc, 0xc4, 0x97, 0x54, 0x43, 0xc0, 0xe3, 0xd5, 0x70, 0x0e, 0xcf, 0x4b, 0x78, 0xc4, 0x89, 0xe1, 0xda, 0x0b, 0xd8, 0x1e,
0x8e, 0xf7, 0x1e, 0x2a, 0xbc, 0xc8, 0x5f, 0x23, 0x81, 0xd7, 0xf7, 0xa0, 0xb1, 0x4a, 0xb4, 0x2c, 0xac, 0xaa, 0x52, 0xf2, 0x15, 0x80, 0x17, 0xd7, 0x26, 0x5a, 0x52, 0x39, 0xdc, 0xb9, 0x7d, 0xe1,
0xc4, 0xfa, 0x33, 0xd8, 0x1a, 0xce, 0x6f, 0x6f, 0xd9, 0xd2, 0x44, 0x76, 0x0e, 0x4f, 0xd3, 0xe4, 0x79, 0xfd, 0x1a, 0x09, 0xbc, 0xb6, 0x03, 0xcd, 0x65, 0xa2, 0x65, 0x23, 0xd6, 0x1e, 0xc0, 0xc6,
0xb0, 0x6e, 0x1f, 0x43, 0x29, 0xda, 0x8f, 0xc3, 0xda, 0xb0, 0xb3, 0x50, 0x24, 0xf5, 0x17, 0x82, 0x60, 0x76, 0x75, 0xc5, 0x16, 0x26, 0xb2, 0xef, 0x41, 0x39, 0xb6, 0x82, 0x97, 0x33, 0x3a, 0xb5,
0x51, 0x0c, 0x97, 0xe5, 0xc3, 0x17, 0x50, 0x8a, 0x66, 0x78, 0xa2, 0x40, 0xe9, 0xa2, 0xdf, 0x1f, 0x2e, 0x2d, 0x36, 0x79, 0x9b, 0x33, 0x3f, 0x86, 0xb5, 0x70, 0xc4, 0x96, 0xee, 0x9c, 0x0f, 0x6b,
0x98, 0xfd, 0xab, 0x91, 0xf6, 0x84, 0x54, 0xa0, 0x88, 0x5f, 0xdd, 0x9e, 0x96, 0x39, 0x0c, 0xa0, 0xad, 0x19, 0x77, 0xc3, 0xf9, 0x3a, 0x84, 0x68, 0x3f, 0x65, 0xe0, 0x7e, 0x5a, 0x67, 0xf8, 0x28,
0x1c, 0x8f, 0xf0, 0xa4, 0x0a, 0xe5, 0x6e, 0xaf, 0x3b, 0xea, 0xb6, 0x46, 0x9d, 0x13, 0xed, 0x09, 0x1c, 0x42, 0x29, 0x5a, 0xbe, 0xc3, 0xc6, 0xb3, 0x35, 0xb7, 0x32, 0xf5, 0x7d, 0xc2, 0x28, 0x86,
0x79, 0x06, 0x9b, 0x03, 0xa3, 0xd3, 0xbd, 0x6c, 0x7d, 0xd5, 0x31, 0x8d, 0xce, 0xab, 0x4e, 0xeb, 0x9b, 0x38, 0xf9, 0x02, 0x94, 0x49, 0xe2, 0xa2, 0x8d, 0x2c, 0x9e, 0x7b, 0x10, 0x9f, 0x4b, 0x5a,
0xa2, 0x73, 0xa2, 0x65, 0x08, 0x01, 0xf5, 0x6c, 0x74, 0xd1, 0x36, 0x07, 0x57, 0x2f, 0x2f, 0xba, 0x61, 0xa4, 0xa0, 0xfb, 0x8f, 0xa1, 0x14, 0xed, 0x16, 0x44, 0x81, 0xd2, 0x59, 0xaf, 0xd7, 0x37,
0xc3, 0xb3, 0xce, 0x89, 0x96, 0x15, 0x32, 0x87, 0x57, 0xed, 0x76, 0x67, 0x38, 0xd4, 0x72, 0x04, 0x7b, 0xa3, 0xa1, 0x7a, 0x8f, 0x54, 0xa0, 0x88, 0xbf, 0x3a, 0x5d, 0x35, 0xb3, 0x1f, 0x40, 0x39,
0x60, 0xe3, 0xb4, 0xd5, 0x15, 0xe0, 0x3c, 0xd9, 0x82, 0x5a, 0xb7, 0xf7, 0xaa, 0xdf, 0x6d, 0x77, 0x5e, 0x2d, 0x48, 0x15, 0xca, 0x9d, 0x6e, 0x67, 0xd8, 0x69, 0x0d, 0xf5, 0x63, 0xf5, 0x1e, 0x79,
0xcc, 0x61, 0x67, 0x34, 0x12, 0xc4, 0xc2, 0xe1, 0x7f, 0x32, 0x50, 0x4d, 0x6d, 0x01, 0x64, 0x07, 0x00, 0xeb, 0x7d, 0x43, 0xef, 0x9c, 0xb7, 0x9e, 0xea, 0xa6, 0xa1, 0x3f, 0xd7, 0x5b, 0x67, 0xfa,
0xb6, 0xc4, 0x91, 0x2b, 0x43, 0xdc, 0xd4, 0x1a, 0xf6, 0x7b, 0x66, 0xaf, 0xdf, 0xeb, 0x68, 0x4f, 0xb1, 0x9a, 0x21, 0x04, 0x6a, 0xa7, 0xc3, 0xb3, 0xb6, 0xd9, 0x1f, 0x1d, 0x9d, 0x75, 0x06, 0xa7,
0xc8, 0x5b, 0xb0, 0xb3, 0xc4, 0xe8, 0x9f, 0x9e, 0xb6, 0xcf, 0x5a, 0x42, 0x79, 0xd2, 0x80, 0xed, 0xfa, 0xb1, 0x9a, 0x15, 0x32, 0x07, 0xa3, 0x76, 0x5b, 0x1f, 0x0c, 0xd4, 0x1c, 0x01, 0x58, 0x3b,
0x25, 0xe6, 0xa8, 0x7b, 0xd9, 0x11, 0xaf, 0xcc, 0x92, 0x7d, 0xd8, 0x5b, 0xe2, 0x0d, 0xbf, 0xee, 0x69, 0x75, 0x04, 0x38, 0x4f, 0x36, 0xa0, 0xde, 0xe9, 0x3e, 0xef, 0x75, 0xda, 0xba, 0x39, 0xd0,
0x74, 0x06, 0x31, 0x22, 0x47, 0x5e, 0xc0, 0xf3, 0x25, 0x44, 0xb7, 0x37, 0xbc, 0x3a, 0x3d, 0xed, 0x87, 0x43, 0x41, 0x2c, 0xec, 0xff, 0x3b, 0x03, 0xd5, 0xd4, 0x76, 0x42, 0xb6, 0x60, 0x43, 0x1c,
0xb6, 0xbb, 0x9d, 0xde, 0xc8, 0x7c, 0xd5, 0xba, 0xb8, 0xea, 0x68, 0x79, 0xb2, 0x07, 0xf5, 0xe5, 0x19, 0x19, 0x42, 0x53, 0x6b, 0xd0, 0xeb, 0x9a, 0xdd, 0x5e, 0x57, 0x57, 0xef, 0x91, 0x77, 0x60,
0x4b, 0x3a, 0x97, 0x83, 0xbe, 0xd1, 0x32, 0xbe, 0xd1, 0x0a, 0xe4, 0x5d, 0x78, 0xe7, 0x81, 0x90, 0x6b, 0x81, 0xd1, 0x3b, 0x39, 0x69, 0x9f, 0xb6, 0xc4, 0xe5, 0x49, 0x13, 0x36, 0x17, 0x98, 0xc3,
0x76, 0xdf, 0x30, 0x3a, 0xed, 0x91, 0xd9, 0xba, 0xec, 0x5f, 0xf5, 0x46, 0xda, 0xc6, 0x61, 0x53, 0xce, 0xb9, 0x2e, 0xac, 0xcc, 0x92, 0x5d, 0xd8, 0x59, 0xe0, 0x0d, 0xbe, 0xd3, 0xf5, 0x7e, 0x8c,
0x4c, 0xda, 0x4b, 0x01, 0x2e, 0x4c, 0x76, 0xd5, 0xfb, 0x69, 0xaf, 0xff, 0x75, 0x4f, 0x7b, 0x22, 0xc8, 0x91, 0xc7, 0xf0, 0x68, 0x01, 0xd1, 0xe9, 0x0e, 0x46, 0x27, 0x27, 0x9d, 0x76, 0x47, 0xef,
0x2c, 0x3f, 0x3a, 0x33, 0x3a, 0xc3, 0xb3, 0xfe, 0xc5, 0x89, 0x96, 0x39, 0xfe, 0x67, 0x59, 0x6e, 0x0e, 0xcd, 0xe7, 0xad, 0xb3, 0x91, 0xae, 0xe6, 0xc9, 0x0e, 0x34, 0x16, 0x95, 0xe8, 0xe7, 0xfd,
0x79, 0x6d, 0xfc, 0x5f, 0x89, 0x18, 0x50, 0x0c, 0xdd, 0x4c, 0xd6, 0x39, 0xbe, 0xf1, 0x2c, 0x35, 0x9e, 0xd1, 0x32, 0x5e, 0xa8, 0x05, 0xf2, 0x3e, 0x3c, 0xbc, 0x25, 0xa4, 0xdd, 0x33, 0x0c, 0xbd,
0xa9, 0xc7, 0x91, 0xb6, 0xf3, 0x9b, 0x7f, 0xfc, 0xeb, 0xf7, 0xd9, 0x4d, 0x5d, 0x69, 0xbe, 0xfe, 0x3d, 0x34, 0x5b, 0xe7, 0xbd, 0x51, 0x77, 0xa8, 0xae, 0xed, 0x1f, 0x88, 0x0d, 0x60, 0xa1, 0xf0,
0xa4, 0x29, 0x10, 0x4d, 0x77, 0xce, 0x3f, 0xcf, 0x1c, 0x92, 0x3e, 0x6c, 0xc8, 0x7f, 0x13, 0xc8, 0x84, 0xcb, 0x46, 0xdd, 0x6f, 0xba, 0xbd, 0xef, 0xba, 0xea, 0x3d, 0xe1, 0xf9, 0xe1, 0xa9, 0xa1,
0x76, 0x4a, 0x64, 0xfc, 0xf7, 0xc2, 0x3a, 0x89, 0xdb, 0x28, 0x51, 0xd3, 0x2b, 0xb1, 0x44, 0xcb, 0x0f, 0x4e, 0x7b, 0x67, 0xc7, 0x6a, 0x66, 0xff, 0x37, 0x39, 0x80, 0x79, 0x6e, 0x09, 0xef, 0xb4,
0x11, 0x02, 0x3f, 0x83, 0x62, 0xb8, 0xab, 0x26, 0x94, 0x4c, 0x6f, 0xaf, 0x8d, 0x55, 0xeb, 0xc4, 0x46, 0xc3, 0x5e, 0xa4, 0x61, 0x7e, 0x4c, 0x83, 0xf7, 0x92, 0x8c, 0xa3, 0xd1, 0xf1, 0x53, 0x7d,
0x8f, 0x33, 0xe4, 0xe7, 0x50, 0x8e, 0x37, 0x11, 0xb2, 0x9b, 0xc8, 0xb1, 0x74, 0x7e, 0x34, 0x1a, 0x68, 0x76, 0x7b, 0x43, 0x73, 0x30, 0x6c, 0x19, 0x43, 0x0c, 0x57, 0x13, 0x36, 0x93, 0x18, 0xe9,
0xab, 0x58, 0x69, 0xb5, 0x88, 0x1a, 0xab, 0x85, 0x5b, 0x0a, 0xb9, 0x92, 0x79, 0x20, 0xb6, 0x14, 0x85, 0x13, 0x5d, 0x1f, 0xa8, 0x59, 0xf2, 0x1e, 0x34, 0x97, 0x9c, 0xd7, 0xcf, 0x5a, 0xfd, 0x81,
0x52, 0x4f, 0x5d, 0x9f, 0x58, 0x5c, 0x56, 0x2a, 0xa6, 0x37, 0x50, 0xe4, 0x53, 0x42, 0x52, 0x22, 0x7e, 0xac, 0xe6, 0xc8, 0x36, 0x3c, 0x48, 0xf2, 0x3b, 0x5d, 0xf3, 0xe4, 0xac, 0xf3, 0xf4, 0x74,
0x9b, 0xdf, 0x5b, 0x93, 0x5f, 0x92, 0x5f, 0x80, 0x12, 0x3a, 0x00, 0x77, 0x09, 0xb2, 0x30, 0x56, 0xa8, 0xe6, 0x49, 0x03, 0xee, 0xa7, 0xc5, 0xb6, 0x50, 0xaa, 0x5a, 0x58, 0x3c, 0x74, 0xde, 0xe9,
0x72, 0xe1, 0x69, 0x2c, 0x1e, 0xb3, 0xbc, 0x75, 0xac, 0x90, 0xee, 0xce, 0x79, 0x93, 0xa3, 0xb4, 0xea, 0x06, 0xb2, 0xd6, 0xc8, 0x26, 0x90, 0x24, 0xab, 0x6f, 0xe8, 0xfd, 0xd6, 0x0b, 0xb5, 0x48,
0xeb, 0x58, 0x3a, 0xce, 0xa8, 0x09, 0xe9, 0xc9, 0x69, 0x3f, 0x2d, 0x3d, 0x35, 0xcd, 0xea, 0xfb, 0x1e, 0xc2, 0x3b, 0x49, 0x7a, 0xe4, 0xd1, 0xa3, 0x56, 0xfb, 0x9b, 0xde, 0xc9, 0x89, 0x5a, 0x5a,
0x28, 0xbd, 0x41, 0xea, 0x29, 0xe9, 0xdf, 0x09, 0x4c, 0xf3, 0x7b, 0x6a, 0x73, 0xf1, 0x02, 0x55, 0xd4, 0x16, 0x67, 0x73, 0x79, 0xd1, 0x37, 0x51, 0x66, 0x83, 0x88, 0x5b, 0x8a, 0xd1, 0xf9, 0x76,
0x8c, 0x28, 0xe8, 0xf2, 0x47, 0xdf, 0xb0, 0xb0, 0xda, 0xd2, 0xee, 0xa6, 0xef, 0xe2, 0x25, 0x5b, 0xd4, 0x39, 0xee, 0x0c, 0x5f, 0x98, 0xbd, 0x6f, 0xd4, 0x8a, 0x88, 0xdb, 0x12, 0xcb, 0x93, 0x09,
0x64, 0x33, 0x11, 0x0a, 0xf1, 0x0b, 0x16, 0xd2, 0x1f, 0x7d, 0x43, 0x52, 0x7a, 0xfa, 0x09, 0xef, 0xa0, 0x2a, 0x87, 0xff, 0x28, 0xcb, 0x8f, 0x00, 0x6d, 0xfc, 0xec, 0x48, 0x0c, 0x28, 0x86, 0x85,
0xa0, 0xf4, 0x5d, 0xb2, 0x93, 0x94, 0x9e, 0x7c, 0xc1, 0x37, 0x50, 0x15, 0x77, 0x44, 0x43, 0x6a, 0x4a, 0x56, 0x95, 0x6e, 0xf3, 0x41, 0x6a, 0x91, 0x8b, 0x1b, 0xd1, 0xd6, 0xaf, 0xff, 0xfe, 0xcf,
0x90, 0x88, 0xe4, 0xd4, 0x24, 0xdc, 0xd8, 0x79, 0x40, 0x4f, 0x67, 0x07, 0xa9, 0xe1, 0x15, 0x01, 0xdf, 0x65, 0xd7, 0x35, 0xe5, 0xe0, 0xd5, 0x67, 0x07, 0x02, 0x71, 0xe0, 0xce, 0xf8, 0x97, 0x99,
0xe5, 0x4d, 0x39, 0xfd, 0x12, 0x0e, 0xe4, 0xe1, 0xfc, 0x46, 0xf4, 0x58, 0xce, 0xda, 0xe1, 0xae, 0x7d, 0xd2, 0x83, 0x35, 0xf9, 0xb1, 0x89, 0x6c, 0xa6, 0x44, 0xc6, 0x5f, 0x9f, 0x56, 0x49, 0xdc,
0xf1, 0x68, 0x8b, 0xd0, 0xf7, 0xf0, 0xc2, 0x6d, 0xf2, 0x14, 0x2f, 0x8c, 0x00, 0x4d, 0x4f, 0xca, 0x44, 0x89, 0xaa, 0x56, 0x89, 0x25, 0x5a, 0x8e, 0x10, 0xf8, 0x05, 0x14, 0xc3, 0x4f, 0x19, 0x89,
0xff, 0x15, 0x90, 0xe1, 0x63, 0xb7, 0xae, 0x6d, 0x56, 0x8d, 0x77, 0x1f, 0xc5, 0xa4, 0x0d, 0xaa, 0x4b, 0xa6, 0x3f, 0x6e, 0x34, 0x97, 0x6d, 0x9b, 0xff, 0x9f, 0x21, 0xdf, 0x43, 0x39, 0x5e, 0x54,
0xaf, 0xbc, 0x5c, 0xa4, 0x30, 0x03, 0x25, 0xd9, 0x7f, 0xc8, 0xe2, 0x2d, 0x2b, 0xba, 0x55, 0xe3, 0xc9, 0x76, 0xa2, 0x05, 0xa7, 0xdb, 0x67, 0xb3, 0xb9, 0x8c, 0x95, 0xbe, 0x16, 0xa9, 0xc5, 0xd7,
0xed, 0x35, 0xdc, 0xf0, 0xb6, 0x3a, 0xde, 0x46, 0x88, 0x26, 0x6e, 0x13, 0x83, 0x48, 0x33, 0x90, 0xc2, 0x25, 0x96, 0x8c, 0x64, 0x3b, 0x12, 0x4b, 0x2c, 0x69, 0xa4, 0xd4, 0x27, 0xf6, 0xda, 0xa5,
0xb0, 0xeb, 0x0d, 0xfc, 0x03, 0xfc, 0xd3, 0xff, 0x06, 0x00, 0x00, 0xff, 0xff, 0xe8, 0x03, 0x91, 0x17, 0xd3, 0x9a, 0x28, 0xf2, 0x3e, 0x21, 0x29, 0x91, 0x07, 0x3f, 0x5a, 0x93, 0x5f, 0x90, 0x9f,
0x2d, 0x37, 0x17, 0x00, 0x00, 0x83, 0x12, 0x06, 0x00, 0x57, 0x4d, 0x32, 0x77, 0x56, 0x72, 0x1f, 0x6e, 0xce, 0x8d, 0x59, 0x5c,
0x4a, 0x97, 0x48, 0x77, 0x67, 0xfc, 0x80, 0xa3, 0xb4, 0x8b, 0x58, 0x3a, 0xae, 0x30, 0x09, 0xe9,
0xc9, 0x65, 0x30, 0x2d, 0x3d, 0xb5, 0xec, 0x68, 0xbb, 0x28, 0xbd, 0x49, 0x1a, 0x29, 0xe9, 0x2f,
0x05, 0xe6, 0xe0, 0x47, 0x6a, 0x73, 0x61, 0x41, 0x4d, 0x4c, 0xb0, 0x18, 0xf2, 0x3b, 0x6d, 0x98,
0x7b, 0x6d, 0x61, 0xb5, 0xd7, 0xb6, 0x51, 0xc9, 0x06, 0x59, 0x4f, 0xa4, 0x42, 0x6c, 0xc1, 0x5c,
0xfa, 0x9d, 0x36, 0x24, 0xa5, 0xa7, 0x4d, 0x78, 0x88, 0xd2, 0xb7, 0xc9, 0x56, 0x52, 0x7a, 0xd2,
0x82, 0x17, 0x50, 0x15, 0x3a, 0xa2, 0x1d, 0x26, 0x48, 0x64, 0x72, 0x6a, 0x51, 0x6a, 0x6e, 0xdd,
0xa2, 0xa7, 0xab, 0x83, 0xd4, 0x51, 0x45, 0x40, 0xf9, 0x81, 0x5c, 0x8e, 0x08, 0x07, 0x72, 0x7b,
0xbc, 0x27, 0x5a, 0x2c, 0x67, 0xe5, 0xec, 0xdf, 0xbc, 0x73, 0x82, 0xd0, 0x76, 0x50, 0xe1, 0x26,
0xb9, 0x8f, 0x0a, 0x23, 0xc0, 0x81, 0x27, 0xe5, 0xff, 0x12, 0xc8, 0xe0, 0x2e, 0xad, 0x2b, 0x67,
0x99, 0xe6, 0xfb, 0x77, 0x62, 0xd2, 0x0e, 0xd5, 0x96, 0x2a, 0x17, 0x25, 0xcc, 0x40, 0x49, 0x4e,
0x10, 0x64, 0x6e, 0xcb, 0x92, 0x61, 0xa6, 0xf9, 0xee, 0x0a, 0x6e, 0xa8, 0xad, 0x81, 0xda, 0x08,
0x51, 0x85, 0x36, 0x31, 0xa7, 0x1e, 0x04, 0x12, 0x76, 0xb1, 0x86, 0xff, 0x1f, 0xf9, 0xfc, 0x3f,
0x01, 0x00, 0x00, 0xff, 0xff, 0xb9, 0xba, 0x11, 0xb0, 0x56, 0x19, 0x00, 0x00,
} }
// Reference imports to suppress errors if they are not otherwise used. // Reference imports to suppress errors if they are not otherwise used.

@ -872,9 +872,102 @@ message SetLiquidityParamsResponse {
message SuggestSwapsRequest { message SuggestSwapsRequest {
} }
enum AutoReason {
AUTO_REASON_UNKNOWN = 0;
/*
Budget not started indicates that we do not recommend any swaps because
the start time for our budget has not arrived yet.
*/
AUTO_REASON_BUDGET_NOT_STARTED = 1;
/*
Sweep fees indicates that the estimated fees to sweep swaps are too high
right now.
*/
AUTO_REASON_SWEEP_FEES = 2;
/*
Budget elapsed indicates that the autoloop budget for the period has been
elapsed.
*/
AUTO_REASON_BUDGET_ELAPSED = 3;
/*
In flight indicates that the limit on in-flight automatically dispatched
swaps has already been reached.
*/
AUTO_REASON_IN_FLIGHT = 4;
/*
Swap fee indicates that the server fee for a specific swap is too high.
*/
AUTO_REASON_SWAP_FEE = 5;
/*
Miner fee indicates that the miner fee for a specific swap is to high.
*/
AUTO_REASON_MINER_FEE = 6;
/*
Prepay indicates that the prepay fee for a specific swap is too high.
*/
AUTO_REASON_PREPAY = 7;
/*
Failure backoff indicates that a swap has recently failed for this target,
and the backoff period has not yet passed.
*/
AUTO_REASON_FAILURE_BACKOFF = 8;
/*
Loop out indicates that a loop out swap is currently utilizing the channel,
so it is not eligible.
*/
AUTO_REASON_LOOP_OUT = 9;
/*
Loop In indicates that a loop in swap is currently in flight for the peer,
so it is not eligible.
*/
AUTO_REASON_LOOP_IN = 10;
/*
Liquidity ok indicates that a target meets the liquidity balance expressed
in its rule, so no swap is needed.
*/
AUTO_REASON_LIQUIDITY_OK = 11;
/*
Budget insufficient indicates that we cannot perform a swap because we do
not have enough pending budget available. This differs from budget elapsed,
because we still have some budget available, but we have allocated it to
other swaps.
*/
AUTO_REASON_BUDGET_INSUFFICIENT = 12;
}
message Disqualified {
/*
The short channel ID of the channel that was excluded from our suggestions.
*/
uint64 channel_id = 1;
/*
The reason that we excluded the channel from the our suggestions.
*/
AutoReason reason = 2;
}
message SuggestSwapsResponse { message SuggestSwapsResponse {
/* /*
The set of recommended loop outs. The set of recommended loop outs.
*/ */
repeated LoopOutRequest loop_out = 1; repeated LoopOutRequest loop_out = 1;
/*
Disqualified contains the set of channels that swaps are not recommended
for.
*/
repeated Disqualified disqualified = 2;
} }

@ -395,6 +395,40 @@
} }
}, },
"definitions": { "definitions": {
"looprpcAutoReason": {
"type": "string",
"enum": [
"AUTO_REASON_UNKNOWN",
"AUTO_REASON_BUDGET_NOT_STARTED",
"AUTO_REASON_SWEEP_FEES",
"AUTO_REASON_BUDGET_ELAPSED",
"AUTO_REASON_IN_FLIGHT",
"AUTO_REASON_SWAP_FEE",
"AUTO_REASON_MINER_FEE",
"AUTO_REASON_PREPAY",
"AUTO_REASON_FAILURE_BACKOFF",
"AUTO_REASON_LOOP_OUT",
"AUTO_REASON_LOOP_IN",
"AUTO_REASON_LIQUIDITY_OK",
"AUTO_REASON_BUDGET_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."
},
"looprpcDisqualified": {
"type": "object",
"properties": {
"channel_id": {
"type": "string",
"format": "uint64",
"description": "The short channel ID of the channel that was excluded from our suggestions."
},
"reason": {
"$ref": "#/definitions/looprpcAutoReason",
"description": "The reason that we excluded the channel from the our suggestions."
}
}
},
"looprpcFailureReason": { "looprpcFailureReason": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -808,6 +842,13 @@
"$ref": "#/definitions/looprpcLoopOutRequest" "$ref": "#/definitions/looprpcLoopOutRequest"
}, },
"description": "The set of recommended loop outs." "description": "The set of recommended loop outs."
},
"disqualified": {
"type": "array",
"items": {
"$ref": "#/definitions/looprpcDisqualified"
},
"description": "Disqualified contains the set of channels that swaps are not recommended\nfor."
} }
} }
}, },

@ -24,6 +24,14 @@ This file tracks release notes for the loop client.
baked one with the exact permissions needed for Loop. If the now deprecated baked one with the exact permissions needed for Loop. If the now deprecated
flag/option `--lnd.macaroondir` is used, it will fall back to use only the flag/option `--lnd.macaroondir` is used, it will fall back to use only the
`admin.macaroon` from that directory. `admin.macaroon` from that directory.
* The rules used for autoloop have been relaxed to allow autoloop to dispatch
swaps even if there are manually initiated swaps that are not limited to a
single channel in progress. This change was made to allow autoloop to coexist
with manual swaps.
* The `SuggestSwaps` endpoint has been updated to include reasons that indicate
why the Autolooper is not currently dispatching swaps for the set of rules
that the client is configured with. See the [autoloop documentation](docs/autoloop.md) for a
detailed explanations of these reasons.
#### Breaking Changes #### Breaking Changes
* The `AutoOut`, `AutoOutBudgetSat` and `AutoOutBudgetStartSec` fields in the * The `AutoOut`, `AutoOutBudgetSat` and `AutoOutBudgetStartSec` fields in the
@ -31,5 +39,8 @@ This file tracks release notes for the loop client.
been renamed to `Autoloop`, `AutoloopBudgetSat` and `AutoloopBudgetStartSec`. been renamed to `Autoloop`, `AutoloopBudgetSat` and `AutoloopBudgetStartSec`.
* The `autoout` flag for enabling automatic dispatch of loop out swaps has been * The `autoout` flag for enabling automatic dispatch of loop out swaps has been
renamed to `autoloop` so that it can cover loop out and loop in. renamed to `autoloop` so that it can cover loop out and loop in.
* The `SuggestSwaps` rpc call will now fail with a `FailedPrecondition` grpc
error code if no rules are configured for the autolooper. Previously the rpc
would return an empty response.
#### Bug Fixes #### Bug Fixes

Loading…
Cancel
Save