diff --git a/cmd/loop/liquidity.go b/cmd/loop/liquidity.go index 5413b21..79e46aa 100644 --- a/cmd/loop/liquidity.go +++ b/cmd/loop/liquidity.go @@ -48,6 +48,14 @@ var setLiquidityRuleCommand = cli.Command{ Description: "Update or remove the liquidity rule for a channel/peer.", ArgsUsage: "{shortchanid | peerpubkey}", Flags: []cli.Flag{ + cli.StringFlag{ + Name: "type", + Usage: "the type of swap to perform, set to 'out' " + + "for acquiring inbound liquidity or 'in' for " + + "acquiring outbound liquidity.", + Value: "out", + }, + cli.IntFlag{ Name: "incoming_threshold", Usage: "the minimum percentage of incoming liquidity " + @@ -168,7 +176,18 @@ func setRule(ctx *cli.Context) error { newRule := &looprpc.LiquidityRule{ ChannelId: chanID, Type: looprpc.LiquidityRuleType_THRESHOLD, - SwapType: looprpc.SwapType_LOOP_OUT, + } + if ctx.IsSet("type") { + switch ctx.String("type") { + case "in": + newRule.SwapType = looprpc.SwapType_LOOP_IN + + case "out": + newRule.SwapType = looprpc.SwapType_LOOP_OUT + + default: + return errors.New("please set type to in or out") + } } if pubkeyRule { @@ -292,6 +311,11 @@ var setParamsCommand = cli.Command{ Usage: "the maximum amount in satoshis that the " + "autoloop client will dispatch per-swap", }, + cli.IntFlag{ + Name: "htlc_conf", + Usage: "the confirmation target for loop in on-chain " + + "htlcs", + }, }, Action: setParams, } @@ -422,6 +446,11 @@ func setParams(ctx *cli.Context) error { flagSet = true } + if ctx.IsSet("htlc_conf") { + params.HtlcConfTarget = int32(ctx.Int("htlc_conf")) + flagSet = true + } + if !flagSet { return fmt.Errorf("at least one flag required to set params") } diff --git a/docs/autoloop.md b/docs/autoloop.md index 16ca6e2..42c9668 100644 --- a/docs/autoloop.md +++ b/docs/autoloop.md @@ -9,6 +9,16 @@ following command: loop setparams --autoloop=true ``` +At present, autoloop can be configured to either acquire incoming liquidity +using loop out, or acquire outgoing liquidity using loop in. It cannot support +automated swaps in both directions. To set the type of swaps you would like +to automatically dispatch, use: +``` +loop setparams --type={in|out} +``` + +Autoloop will perform loop out swaps *by default*. + Swaps that are dispatched by the autolooper can be identified in the output of `ListSwaps` by their label field, which will contain: `[reserved]: autoloop-out`. @@ -286,7 +296,9 @@ following reasons will be displayed: * Fee insufficient: if the fees that a swap will cost are more than the percentage of total swap amount that we allow, this reason will be displayed. See [fees](#fees) to update this value. - +* Loop in unreachable: if the client node is unreachable by the server + off-chain, this reason will be displayed. Try improving the connectivity of + your node so that it is reachable by the loop server. Further details for all of these reasons can be found in loopd's debug level logs. diff --git a/liquidity/autoloop_test.go b/liquidity/autoloop_test.go index 72e0e5a..7b8b64c 100644 --- a/liquidity/autoloop_test.go +++ b/liquidity/autoloop_test.go @@ -50,13 +50,22 @@ func TestAutoLoopDisabled(t *testing.T) { // loop in/out swaps. We expect a swap for our channel to be suggested, // but do not expect any swaps to be executed, since autoloop is // disabled by default. - c.autoloop(1, chan1Rec.Amount+1, nil, quotes, nil) + step := &autoloopStep{ + minAmt: 1, + maxAmt: chan1Rec.Amount + 1, + quotesOut: quotes, + } + c.autoloop(step) // Trigger another autoloop, this time setting our server restrictions // to have a minimum swap amount greater than the amount that we need // to swap. In this case we don't even expect to get a quote, because // our suggested swap is beneath the minimum swap size. - c.autoloop(chan1Rec.Amount+1, chan1Rec.Amount+2, nil, nil, nil) + step = &autoloopStep{ + minAmt: chan1Rec.Amount + 1, + maxAmt: chan1Rec.Amount + 2, + } + c.autoloop(step) c.stop() } @@ -99,6 +108,7 @@ func TestAutoLoopEnabled(t *testing.T) { chanID1: chanRule, chanID2: chanRule, }, + HtlcConfTarget: defaultHtlcConfTarget, } ) c := newAutoloopTestCtx(t, params, channels, testRestrictions) @@ -191,7 +201,13 @@ func TestAutoLoopEnabled(t *testing.T) { // Tick our autolooper with no existing swaps, we expect a loop out // swap to be dispatched for each channel. - c.autoloop(1, amt+1, nil, quotes, loopOuts) + step := &autoloopStep{ + minAmt: 1, + maxAmt: amt + 1, + quotesOut: quotes, + expectedOut: loopOuts, + } + c.autoloop(step) // Tick again with both of our swaps in progress. We haven't shifted our // channel balances at all, so swaps should still be suggested, but we @@ -201,7 +217,12 @@ func TestAutoLoopEnabled(t *testing.T) { existingSwapFromRequest(chan2Swap, testTime, nil), } - c.autoloop(1, amt+1, existing, nil, nil) + step = &autoloopStep{ + minAmt: 1, + maxAmt: amt + 1, + existingOut: existing, + } + c.autoloop(step) // Now, we update our channel 2 swap to have failed due to off chain // failure and our first swap to have succeeded. @@ -254,7 +275,14 @@ func TestAutoLoopEnabled(t *testing.T) { // We tick again, this time we expect another swap on channel 1 (which // still has balances which reflect that we need to swap), but nothing // for channel 2, since it has had a failure. - c.autoloop(1, amt+1, existing, quotes, loopOuts) + step = &autoloopStep{ + minAmt: 1, + maxAmt: amt + 1, + existingOut: existing, + quotesOut: quotes, + expectedOut: loopOuts, + } + c.autoloop(step) // Now, we progress our time so that we have sufficiently backed off // for channel 2, and could perform another swap. @@ -268,7 +296,13 @@ func TestAutoLoopEnabled(t *testing.T) { existingSwapFromRequest(chan2Swap, testTime, failedOffChain), } - c.autoloop(1, amt+1, existing, quotes, nil) + step = &autoloopStep{ + minAmt: 1, + maxAmt: amt + 1, + existingOut: existing, + quotesOut: quotes, + } + c.autoloop(step) c.stop() } @@ -318,6 +352,7 @@ func TestCompositeRules(t *testing.T) { PeerRules: map[route.Vertex]*SwapRule{ peer2: chanRule, }, + HtlcConfTarget: defaultHtlcConfTarget, } ) @@ -425,8 +460,357 @@ func TestCompositeRules(t *testing.T) { // swap to be dispatched for each of our rules. We set our server side // maximum to be greater than the swap amount for our peer swap (which // is the larger of the two swaps). - c.autoloop(1, peerAmount+1, nil, quotes, loopOuts) + step := &autoloopStep{ + minAmt: 1, + maxAmt: peerAmount + 1, + quotesOut: quotes, + expectedOut: loopOuts, + } + c.autoloop(step) + + c.stop() +} + +// TestAutoLoopInEnabled tests dispatch of autoloop in swaps. +func TestAutoLoopInEnabled(t *testing.T) { + defer test.Guard(t)() + + var ( + chan1 = lndclient.ChannelInfo{ + ChannelID: chanID1.ToUint64(), + PubKeyBytes: peer1, + Capacity: 100000, + RemoteBalance: 100000, + LocalBalance: 0, + } + + chan2 = lndclient.ChannelInfo{ + ChannelID: chanID2.ToUint64(), + PubKeyBytes: peer2, + Capacity: 200000, + RemoteBalance: 200000, + LocalBalance: 0, + } + + channels = []lndclient.ChannelInfo{ + chan1, chan2, + } + + // Create a rule which will loop in, with no inbound liquidity + // reserve. + rule = &SwapRule{ + ThresholdRule: NewThresholdRule(0, 60), + Type: swap.TypeIn, + } + + // Under these rules, we'll have the following recommended + // swaps: + peer1ExpectedAmt btcutil.Amount = 80000 + peer2ExpectedAmt btcutil.Amount = 160000 + + // Set our per-swap budget to 5% of swap amount. + swapFeePPM uint64 = 50000 + + htlcConfTarget int32 = 10 + + // Calculate the maximum amount we'll pay for each swap and + // set our budget to be able to accommodate both. + peer1MaxFee = ppmToSat(peer1ExpectedAmt, swapFeePPM) + peer2MaxFee = ppmToSat(peer2ExpectedAmt, swapFeePPM) + + params = Parameters{ + Autoloop: true, + AutoFeeBudget: peer1MaxFee + peer2MaxFee + 1, + AutoFeeStartDate: testTime, + MaxAutoInFlight: 2, + FailureBackOff: time.Hour, + FeeLimit: NewFeePortion(swapFeePPM), + ChannelRules: make(map[lnwire.ShortChannelID]*SwapRule), + PeerRules: map[route.Vertex]*SwapRule{ + peer1: rule, + peer2: rule, + }, + HtlcConfTarget: htlcConfTarget, + SweepConfTarget: loop.DefaultSweepConfTarget, + } + ) + c := newAutoloopTestCtx(t, params, channels, testRestrictions) + c.start() + + // Calculate our maximum allowed fees and create quotes that fall within + // our budget. + var ( + quote1 = &loop.LoopInQuote{ + SwapFee: peer1MaxFee / 4, + MinerFee: peer1MaxFee / 8, + } + + quote2Unaffordable = &loop.LoopInQuote{ + SwapFee: peer2MaxFee * 2, + MinerFee: peer2MaxFee * 2, + } + + quoteRequest1 = &loop.LoopInQuoteRequest{ + Amount: peer1ExpectedAmt, + HtlcConfTarget: htlcConfTarget, + LastHop: &peer1, + } + + quoteRequest2 = &loop.LoopInQuoteRequest{ + Amount: peer2ExpectedAmt, + HtlcConfTarget: htlcConfTarget, + LastHop: &peer2, + } + + peer1Swap = &loop.LoopInRequest{ + Amount: peer1ExpectedAmt, + MaxSwapFee: quote1.SwapFee, + MaxMinerFee: quote1.MinerFee, + HtlcConfTarget: htlcConfTarget, + LastHop: &peer1, + ExternalHtlc: false, + Label: labels.AutoloopLabel(swap.TypeIn), + Initiator: autoloopSwapInitiator, + } + ) + + // Tick our autolooper with no existing swaps. Both of our peers + // require swaps, but one of our peer's quotes is too expensive. + step := &autoloopStep{ + minAmt: 1, + maxAmt: peer2ExpectedAmt + 1, + quotesIn: []quoteInRequestResp{ + { + request: quoteRequest1, + quote: quote1, + }, + { + request: quoteRequest2, + quote: quote2Unaffordable, + }, + }, + expectedIn: []loopInRequestResp{ + { + request: peer1Swap, + response: &loop.LoopInSwapInfo{ + SwapHash: lntypes.Hash{1}, + }, + }, + }, + } + c.autoloop(step) + + // Now, we tick again with our first swap in progress. This time, we + // provide a quote for our second swap which is more affordable, so we + // expect it to be dispatched. + + var ( + quote2Affordable = &loop.LoopInQuote{ + SwapFee: peer2MaxFee / 8, + MinerFee: peer2MaxFee / 2, + } + + peer2Swap = &loop.LoopInRequest{ + Amount: peer2ExpectedAmt, + MaxSwapFee: quote2Affordable.SwapFee, + MaxMinerFee: quote2Affordable.MinerFee, + HtlcConfTarget: htlcConfTarget, + LastHop: &peer2, + ExternalHtlc: false, + Label: labels.AutoloopLabel(swap.TypeIn), + Initiator: autoloopSwapInitiator, + } + + existing = []*loopdb.LoopIn{ + existingInFromRequest(peer1Swap, testTime, nil), + } + ) + + step = &autoloopStep{ + minAmt: 1, + maxAmt: peer2ExpectedAmt + 1, + quotesIn: []quoteInRequestResp{ + { + request: quoteRequest2, + quote: quote2Affordable, + }, + }, + existingIn: existing, + expectedIn: []loopInRequestResp{ + { + request: peer2Swap, + response: &loop.LoopInSwapInfo{ + SwapHash: lntypes.Hash{2}, + }, + }, + }, + } + c.autoloop(step) + + c.stop() +} + +// TestAutoloopBothTypes tests dispatching of a loop out and loop in swap at the +// same time. +func TestAutoloopBothTypes(t *testing.T) { + defer test.Guard(t)() + + var ( + chan1 = lndclient.ChannelInfo{ + ChannelID: chanID1.ToUint64(), + PubKeyBytes: peer1, + Capacity: 1000000, + LocalBalance: 1000000, + } + chan2 = lndclient.ChannelInfo{ + ChannelID: chanID2.ToUint64(), + PubKeyBytes: peer2, + Capacity: 200000, + RemoteBalance: 200000, + LocalBalance: 0, + } + + channels = []lndclient.ChannelInfo{ + chan1, chan2, + } + + // Create a rule which will loop out, with no outbound liquidity + // reserve. + outRule = &SwapRule{ + ThresholdRule: NewThresholdRule(40, 0), + Type: swap.TypeOut, + } + + // Create a rule which will loop in, with no inbound liquidity + // reserve. + inRule = &SwapRule{ + ThresholdRule: NewThresholdRule(0, 60), + Type: swap.TypeIn, + } + + // Under this rule, we expect a loop in swap. + loopOutAmt btcutil.Amount = 700000 + loopInAmount btcutil.Amount = 160000 + + // Set our per-swap budget to 5% of swap amount. + swapFeePPM uint64 = 50000 + + htlcConfTarget int32 = 10 + + // Calculate the maximum amount we'll pay for our loop in. + loopOutMaxFee = ppmToSat(loopOutAmt, swapFeePPM) + loopInMaxFee = ppmToSat(loopInAmount, swapFeePPM) + + params = Parameters{ + Autoloop: true, + AutoFeeBudget: loopOutMaxFee + loopInMaxFee + 1, + AutoFeeStartDate: testTime, + MaxAutoInFlight: 2, + FailureBackOff: time.Hour, + FeeLimit: NewFeePortion(swapFeePPM), + ChannelRules: map[lnwire.ShortChannelID]*SwapRule{ + chanID1: outRule, + }, + PeerRules: map[route.Vertex]*SwapRule{ + peer2: inRule, + }, + HtlcConfTarget: htlcConfTarget, + SweepConfTarget: loop.DefaultSweepConfTarget, + } + ) + c := newAutoloopTestCtx(t, params, channels, testRestrictions) + c.start() + + // Calculate our maximum allowed fees and create quotes that fall within + // our budget. + var ( + loopOutQuote = &loop.LoopOutQuote{ + SwapFee: loopOutMaxFee / 4, + PrepayAmount: loopOutMaxFee / 4, + } + + loopOutQuoteReq = &loop.LoopOutQuoteRequest{ + Amount: loopOutAmt, + SweepConfTarget: params.SweepConfTarget, + SwapPublicationDeadline: testTime, + } + + prepayMaxFee, routeMaxFee, + minerFee = params.FeeLimit.loopOutFees( + loopOutAmt, loopOutQuote, + ) + + loopOutSwap = &loop.OutRequest{ + Amount: loopOutAmt, + MaxSwapRoutingFee: routeMaxFee, + MaxPrepayRoutingFee: prepayMaxFee, + MaxSwapFee: loopOutQuote.SwapFee, + MaxPrepayAmount: loopOutQuote.PrepayAmount, + MaxMinerFee: minerFee, + SweepConfTarget: params.SweepConfTarget, + OutgoingChanSet: loopdb.ChannelSet{ + chanID1.ToUint64(), + }, + Label: labels.AutoloopLabel(swap.TypeOut), + Initiator: autoloopSwapInitiator, + } + + loopinQuote = &loop.LoopInQuote{ + SwapFee: loopInMaxFee / 4, + MinerFee: loopInMaxFee / 8, + } + + loopInQuoteReq = &loop.LoopInQuoteRequest{ + Amount: loopInAmount, + HtlcConfTarget: htlcConfTarget, + LastHop: &peer2, + } + loopInSwap = &loop.LoopInRequest{ + Amount: loopInAmount, + MaxSwapFee: loopinQuote.SwapFee, + MaxMinerFee: loopinQuote.MinerFee, + HtlcConfTarget: htlcConfTarget, + LastHop: &peer2, + ExternalHtlc: false, + Label: labels.AutoloopLabel(swap.TypeIn), + Initiator: autoloopSwapInitiator, + } + ) + + step := &autoloopStep{ + minAmt: 1, + maxAmt: loopOutAmt + 1, + quotesOut: []quoteRequestResp{ + { + request: loopOutQuoteReq, + quote: loopOutQuote, + }, + }, + quotesIn: []quoteInRequestResp{ + { + request: loopInQuoteReq, + quote: loopinQuote, + }, + }, + expectedOut: []loopOutRequestResp{ + { + request: loopOutSwap, + response: &loop.LoopOutSwapInfo{ + SwapHash: lntypes.Hash{1}, + }, + }, + }, + expectedIn: []loopInRequestResp{ + { + request: loopInSwap, + response: &loop.LoopInSwapInfo{ + SwapHash: lntypes.Hash{2}, + }, + }, + }, + } + c.autoloop(step) c.stop() } @@ -455,3 +839,24 @@ func existingSwapFromRequest(request *loop.OutRequest, initTime time.Time, }, } } + +func existingInFromRequest(in *loop.LoopInRequest, initTime time.Time, + events []*loopdb.LoopEvent) *loopdb.LoopIn { + + return &loopdb.LoopIn{ + Loop: loopdb.Loop{ + Events: events, + }, + Contract: &loopdb.LoopInContract{ + SwapContract: loopdb.SwapContract{ + MaxSwapFee: in.MaxSwapFee, + MaxMinerFee: in.MaxMinerFee, + InitiationTime: initTime, + Label: in.Label, + }, + HtlcConfTarget: in.HtlcConfTarget, + LastHop: in.LastHop, + ExternalHtlc: in.ExternalHtlc, + }, + } +} diff --git a/liquidity/autoloop_testcontext_test.go b/liquidity/autoloop_testcontext_test.go index a7f9a77..7b59435 100644 --- a/liquidity/autoloop_testcontext_test.go +++ b/liquidity/autoloop_testcontext_test.go @@ -27,10 +27,21 @@ type autoloopTestCtx struct { // quotes is a channel that we get loop out quote requests on. quotes chan *loop.LoopOutQuote + // quoteRequestIn is a channel that requests for loop in quotes are + // pushed into. + quoteRequestIn chan *loop.LoopInQuoteRequest + + // quotesIn is a channel that we get loop in quote responses on. + quotesIn chan *loop.LoopInQuote + // loopOutRestrictions is a channel that we get the server's // restrictions on. loopOutRestrictions chan *Restrictions + // loopInRestrictions is a channel that we get the server's + // loop in restrictions on. + loopInRestrictions chan *Restrictions + // loopOuts is a channel that we get existing loop out swaps on. loopOuts chan []*loopdb.LoopOut @@ -47,6 +58,13 @@ type autoloopTestCtx struct { // loopOut is a channel that we return loop out responses on. loopOut chan *loop.LoopOutSwapInfo + // inRequest is a channel that requests to dispatch loop in swaps are + // pushed into. + inRequest chan *loop.LoopInRequest + + // loopIn is a channel that we return loop in responses on. + loopIn chan *loop.LoopInSwapInfo + // errChan is a channel that we send run errors into. errChan chan error @@ -80,14 +98,18 @@ func newAutoloopTestCtx(t *testing.T, parameters Parameters, quoteRequest: make(chan *loop.LoopOutQuoteRequest), quotes: make(chan *loop.LoopOutQuote), + quoteRequestIn: make(chan *loop.LoopInQuoteRequest), + quotesIn: make(chan *loop.LoopInQuote), loopOutRestrictions: make(chan *Restrictions), + loopInRestrictions: make(chan *Restrictions), loopOuts: make(chan []*loopdb.LoopOut), loopIns: make(chan []*loopdb.LoopIn), restrictions: make(chan *Restrictions), outRequest: make(chan *loop.OutRequest), loopOut: make(chan *loop.LoopOutSwapInfo), - - errChan: make(chan error, 1), + inRequest: make(chan *loop.LoopInRequest), + loopIn: make(chan *loop.LoopInSwapInfo), + errChan: make(chan error, 1), } // Set lnd's channels to equal the set of channels we want for our @@ -96,10 +118,14 @@ func newAutoloopTestCtx(t *testing.T, parameters Parameters, cfg := &Config{ AutoloopTicker: ticker.NewForce(DefaultAutoloopTicker), - Restrictions: func(context.Context, swap.Type) (*Restrictions, + Restrictions: func(_ context.Context, swapType swap.Type) (*Restrictions, error) { - return <-testCtx.loopOutRestrictions, nil + if swapType == swap.TypeOut { + return <-testCtx.loopOutRestrictions, nil + } + + return <-testCtx.loopInRestrictions, nil }, ListLoopOut: func() ([]*loopdb.LoopOut, error) { return <-testCtx.loopOuts, nil @@ -123,6 +149,20 @@ func newAutoloopTestCtx(t *testing.T, parameters Parameters, return <-testCtx.loopOut, nil }, + LoopInQuote: func(_ context.Context, + req *loop.LoopInQuoteRequest) (*loop.LoopInQuote, error) { + + testCtx.quoteRequestIn <- req + + return <-testCtx.quotesIn, nil + }, + LoopIn: func(_ context.Context, + req *loop.LoopInRequest) (*loop.LoopInSwapInfo, error) { + + testCtx.inRequest <- req + + return <-testCtx.loopIn, nil + }, MinimumConfirmations: loop.DefaultSweepConfTarget, Lnd: &testCtx.lnd.LndServices, Clock: testCtx.testClock, @@ -177,31 +217,70 @@ type loopOutRequestResp struct { response *loop.LoopOutSwapInfo } +// quoteInRequestResp pairs an expected loop in quote request with the response +// we would like to provide the manager with. +type quoteInRequestResp struct { + request *loop.LoopInQuoteRequest + quote *loop.LoopInQuote +} + +// loopInRequestResp pairs and expected loop in request with the response we +// would like the mocked server to respond with. +type loopInRequestResp struct { + request *loop.LoopInRequest + response *loop.LoopInSwapInfo +} + +// autoloopStep contains all of the information to required to step +// through an autoloop tick. +type autoloopStep struct { + minAmt btcutil.Amount + maxAmt btcutil.Amount + existingOut []*loopdb.LoopOut + existingIn []*loopdb.LoopIn + quotesOut []quoteRequestResp + quotesIn []quoteInRequestResp + expectedOut []loopOutRequestResp + expectedIn []loopInRequestResp +} + // autoloop walks our test context through the process of triggering our // autoloop functionality, providing mocked values as required. The set of // quotes provided indicates that we expect swap suggestions to be made (since // we will query for a quote for each suggested swap). The set of expected // swaps indicates whether we expect any of these swap suggestions to actually // be dispatched by the autolooper. -func (c *autoloopTestCtx) autoloop(minAmt, maxAmt btcutil.Amount, - existingOut []*loopdb.LoopOut, quotes []quoteRequestResp, - expectedSwaps []loopOutRequestResp) { - +func (c *autoloopTestCtx) autoloop(step *autoloopStep) { // Tick our autoloop ticker to force assessing whether we want to loop. c.manager.cfg.AutoloopTicker.Force <- testTime // Send a mocked response from the server with the swap size limits. - c.loopOutRestrictions <- NewRestrictions(minAmt, maxAmt) + c.loopOutRestrictions <- NewRestrictions(step.minAmt, step.maxAmt) + c.loopInRestrictions <- NewRestrictions(step.minAmt, step.maxAmt) // Provide the liquidity manager with our desired existing set of swaps. - c.loopOuts <- existingOut - c.loopIns <- nil + c.loopOuts <- step.existingOut + c.loopIns <- step.existingIn // Assert that we query the server for a quote for each of our // recommended swaps. Note that this differs from our set of expected // swaps because we may get quotes for suggested swaps but then just // log them. - for _, expected := range quotes { + for _, expected := range step.quotesIn { + request := <-c.quoteRequestIn + assert.Equal( + c.t, expected.request.Amount, request.Amount, + ) + + assert.Equal( + c.t, expected.request.HtlcConfTarget, + request.HtlcConfTarget, + ) + + c.quotesIn <- expected.quote + } + + for _, expected := range step.quotesOut { request := <-c.quoteRequest assert.Equal( c.t, expected.request.Amount, request.Amount, @@ -214,7 +293,7 @@ func (c *autoloopTestCtx) autoloop(minAmt, maxAmt btcutil.Amount, } // Assert that we dispatch the expected set of swaps. - for _, expected := range expectedSwaps { + for _, expected := range step.expectedOut { actual := <-c.outRequest // Set our destination address to nil so that we do not need to @@ -224,4 +303,12 @@ func (c *autoloopTestCtx) autoloop(minAmt, maxAmt btcutil.Amount, assert.Equal(c.t, expected.request, actual) c.loopOut <- expected.response } + + for _, expected := range step.expectedIn { + actual := <-c.inRequest + + assert.Equal(c.t, expected.request, actual) + + c.loopIn <- expected.response + } } diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index 97548b9..87b06c1 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -90,6 +90,10 @@ const ( ) var ( + // defaultHtlcConfTarget is the default confirmation target we use for + // loop in swap htlcs, set to the same default at the client. + defaultHtlcConfTarget = loop.DefaultHtlcConfTarget + // defaultBudget is the default autoloop budget we set. This budget will // only be used for automatically dispatched swaps if autoloop is // explicitly enabled, so we are happy to set a non-zero value here. The @@ -107,6 +111,7 @@ var ( PeerRules: make(map[route.Vertex]*SwapRule), FailureBackOff: defaultFailureBackoff, SweepConfTarget: defaultConfTarget, + HtlcConfTarget: defaultHtlcConfTarget, FeeLimit: defaultFeePortion(), } @@ -170,10 +175,18 @@ type Config struct { LoopOutQuote func(ctx context.Context, request *loop.LoopOutQuoteRequest) (*loop.LoopOutQuote, error) + // LoopInQuote provides a quote for a loop in swap. + LoopInQuote func(ctx context.Context, + request *loop.LoopInQuoteRequest) (*loop.LoopInQuote, error) + // LoopOut dispatches a loop out. LoopOut func(ctx context.Context, request *loop.OutRequest) ( *loop.LoopOutSwapInfo, error) + // LoopIn dispatches a loop in swap. + LoopIn func(ctx context.Context, + request *loop.LoopInRequest) (*loop.LoopInSwapInfo, error) + // Clock allows easy mocking of time in unit tests. Clock clock.Clock @@ -212,6 +225,10 @@ type Parameters struct { // transaction in. This value affects the on chain fees we will pay. SweepConfTarget int32 + // HtlcConfTarget is the confirmation target that we use for publishing + // loop in swap htlcs on chain. + HtlcConfTarget int32 + // FeeLimit controls the fee limit we place on swaps. FeeLimit FeeLimit @@ -249,10 +266,11 @@ func (p Parameters) String() string { } return fmt.Sprintf("rules: %v, failure backoff: %v, sweep "+ - "sweep conf target: %v, fees: %v, auto budget: %v, budget "+ - "start: %v, max auto in flight: %v, minimum swap size=%v, "+ - "maximum swap size=%v", strings.Join(ruleList, ","), - p.FailureBackOff, p.SweepConfTarget, p.FeeLimit, + "sweep conf target: %v, htlc conf target: %v,fees: %v, "+ + "auto budget: %v, budget start: %v, max auto in flight: %v, "+ + "minimum swap size=%v, maximum swap size=%v", + strings.Join(ruleList, ","), p.FailureBackOff, + p.SweepConfTarget, p.HtlcConfTarget, p.FeeLimit, p.AutoFeeBudget, p.AutoFeeStartDate, p.MaxAutoInFlight, p.ClientRestrictions.Minimum, p.ClientRestrictions.Maximum) } @@ -310,6 +328,11 @@ func (p Parameters) validate(minConfs int32, openChans []lndclient.ChannelInfo, return ErrZeroChannelID } + if rule.Type == swap.TypeIn { + return errors.New("channel level rules not supported for " + + "loop in swaps, only peer-level rules allowed") + } + if err := rule.validate(); err != nil { return fmt.Errorf("channel: %v has invalid rule: %v", channel.ToUint64(), err) @@ -329,6 +352,10 @@ func (p Parameters) validate(minConfs int32, openChans []lndclient.ChannelInfo, minConfs) } + if p.HtlcConfTarget < 1 { + return fmt.Errorf("htlc confirmation target must be > 0") + } + if err := p.FeeLimit.validate(); err != nil { return err } @@ -508,7 +535,7 @@ func (m *Manager) autoloop(ctx context.Context) error { // If we don't actually have dispatch of swaps enabled, log // suggestions. if !m.params.Autoloop { - log.Debugf("recommended autoloop: %v sats over "+ + log.Debugf("recommended autoloop out: %v sats over "+ "%v", swap.Amount, swap.OutgoingChanSet) continue @@ -526,6 +553,27 @@ func (m *Manager) autoloop(ctx context.Context) error { loopOut.HtlcAddressP2WSH) } + for _, in := range suggestion.InSwaps { + // If we don't actually have dispatch of swaps enabled, log + // suggestions. + if !m.params.Autoloop { + log.Debugf("recommended autoloop in: %v sats over "+ + "%v", in.Amount, in.LastHop) + + continue + } + + in := in + loopIn, err := m.cfg.LoopIn(ctx, &in) + if err != nil { + return err + } + + log.Infof("loop in automatically dispatched: hash: %v, "+ + "address: %v", loopIn.SwapHash, + loopIn.HtlcAddressNP2WSH) + } + return nil } @@ -546,6 +594,9 @@ type Suggestions struct { // OutSwaps is the set of loop out swaps that we suggest executing. OutSwaps []loop.OutRequest + // InSwaps is the set of loop in swaps that we suggest executing. + InSwaps []loop.LoopInRequest + // 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 @@ -563,13 +614,17 @@ func newSuggestions() *Suggestions { } func (s *Suggestions) addSwap(swap swapSuggestion) error { - out, ok := swap.(*loopOutSwapSuggestion) - if !ok { + switch t := swap.(type) { + case *loopOutSwapSuggestion: + s.OutSwaps = append(s.OutSwaps, t.OutRequest) + + case *loopInSwapSuggestion: + s.InSwaps = append(s.InSwaps, t.LoopInRequest) + + default: return fmt.Errorf("unexpected swap type: %T", swap) } - s.OutSwaps = append(s.OutSwaps, out.OutRequest) - return nil } @@ -624,6 +679,11 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( return nil, err } + inRestrictions, err := m.getSwapRestrictions(ctx, swap.TypeIn) + if err != nil { + return nil, err + } + // List our current set of swaps so that we can determine which channels // are already being utilized by swaps. Note that these calls may race // with manual initiation of swaps. @@ -708,7 +768,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( suggestion, err := m.suggestSwap( ctx, traffic, balances, rule, outRestrictions, - autoloop, + inRestrictions, autoloop, ) var reasonErr *reasonError if errors.As(err, &reasonErr) { @@ -734,7 +794,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( suggestion, err := m.suggestSwap( ctx, traffic, balance, rule, outRestrictions, - autoloop, + inRestrictions, autoloop, ) var reasonErr *reasonError @@ -830,18 +890,24 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // swap request for the rule provided. func (m *Manager) suggestSwap(ctx context.Context, traffic *swapTraffic, balance *balances, rule *SwapRule, outRestrictions *Restrictions, - autoloop bool) (swapSuggestion, error) { + inRestrictions *Restrictions, autoloop bool) (swapSuggestion, error) { var ( builder swapBuilder restrictions *Restrictions ) + // Get an appropriate builder and set of restrictions based on our swap + // type. switch rule.Type { case swap.TypeOut: builder = newLoopOutBuilder(m.cfg) restrictions = outRestrictions + case swap.TypeIn: + builder = newLoopInBuilder(m.cfg) + restrictions = inRestrictions + default: return nil, fmt.Errorf("unsupported swap type: %v", rule.Type) } @@ -863,7 +929,7 @@ func (m *Manager) suggestSwap(ctx context.Context, traffic *swapTraffic, // Next, get the amount that we need to swap for this entity, skipping // over it if no change in liquidity is required. - amount := rule.swapAmount(balance, restrictions) + amount := rule.swapAmount(balance, restrictions, rule.Type) if amount == 0 { return nil, newReasonError(ReasonLiquidityOk) } diff --git a/liquidity/liquidity_test.go b/liquidity/liquidity_test.go index 398aba4..c86f6a0 100644 --- a/liquidity/liquidity_test.go +++ b/liquidity/liquidity_test.go @@ -16,6 +16,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -1292,6 +1293,19 @@ func TestInFlightLimit(t *testing.T) { } } +type mockServer struct { + mock.Mock +} + +// Restrictions mocks a call to the server to get swap size restrictions. +func (m *mockServer) Restrictions(ctx context.Context, swapType swap.Type) ( + *Restrictions, error) { + + args := m.Called(ctx, swapType) + + return args.Get(0).(*Restrictions), args.Error(1) +} + // TestSizeRestrictions tests the use of client-set size restrictions on swaps. func TestSizeRestrictions(t *testing.T) { var ( @@ -1321,9 +1335,7 @@ func TestSizeRestrictions(t *testing.T) { // has configured. clientRestrictions Restrictions - // server holds the server's mocked responses to our terms - // endpoint. - serverRestrictions []Restrictions + prepareMock func(m *mockServer) // suggestions is the set of suggestions we expect. suggestions *Suggestions @@ -1336,9 +1348,6 @@ func TestSizeRestrictions(t *testing.T) { clientRestrictions: Restrictions{ Minimum: 7000, }, - serverRestrictions: []Restrictions{ - serverRestrictions, serverRestrictions, - }, suggestions: &Suggestions{ OutSwaps: []loop.OutRequest{ chan1Rec, @@ -1352,9 +1361,6 @@ func TestSizeRestrictions(t *testing.T) { clientRestrictions: Restrictions{ Minimum: 8000, }, - serverRestrictions: []Restrictions{ - serverRestrictions, serverRestrictions, - }, suggestions: &Suggestions{ DisqualifiedChans: map[lnwire.ShortChannelID]Reason{ chanID1: ReasonLiquidityOk, @@ -1367,9 +1373,6 @@ func TestSizeRestrictions(t *testing.T) { clientRestrictions: Restrictions{ Maximum: 7000, }, - serverRestrictions: []Restrictions{ - serverRestrictions, serverRestrictions, - }, suggestions: &Suggestions{ OutSwaps: []loop.OutRequest{ outSwap, @@ -1387,12 +1390,26 @@ func TestSizeRestrictions(t *testing.T) { Minimum: 6500, Maximum: 9000, }, - serverRestrictions: []Restrictions{ - serverRestrictions, - { - Minimum: 5000, - Maximum: 6000, - }, + prepareMock: func(m *mockServer) { + restrictions := serverRestrictions + + m.On( + "Restrictions", mock.Anything, + swap.TypeOut, + ).Return( + &restrictions, nil, + ).Once() + + m.On( + "Restrictions", mock.Anything, + swap.TypeOut, + ).Return( + &Restrictions{ + Minimum: 5000, + Maximum: 6000, + }, nil, + ).Once() + }, suggestions: nil, expectedError: ErrMaxExceedsServer, @@ -1415,27 +1432,32 @@ func TestSizeRestrictions(t *testing.T) { chanID1: chanRule, } - // callCount tracks the number of calls we make to - // our restrictions endpoint. - var callCount int - - cfg.Restrictions = func(_ context.Context, _ swap.Type) ( - *Restrictions, error) { + // Use a mock that has our expected calls for the test + // case set to provide server restrictions. + mockServer := &mockServer{} - restrictions := testCase.serverRestrictions[callCount] - callCount++ + // If the test wants us to prime the mock, use its + // function, otherwise just return our default + // restrictions. + if testCase.prepareMock != nil { + testCase.prepareMock(mockServer) + } else { + restrictions := serverRestrictions - return &restrictions, nil + mockServer.On( + "Restrictions", mock.Anything, + mock.Anything, + ).Return(&restrictions, nil) } + + cfg.Restrictions = mockServer.Restrictions + testSuggestSwaps( t, newSuggestSwapsSetup(cfg, lnd, params), testCase.suggestions, testCase.expectedError, ) - require.Equal( - t, callCount, len(testCase.serverRestrictions), - "too many restrictions provided by mock", - ) + mockServer.AssertExpectations(t) }) } } diff --git a/liquidity/loopin_builder.go b/liquidity/loopin_builder.go new file mode 100644 index 0000000..ff475f3 --- /dev/null +++ b/liquidity/loopin_builder.go @@ -0,0 +1,126 @@ +package liquidity + +import ( + "context" + + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/loop" + "github.com/lightninglabs/loop/labels" + "github.com/lightninglabs/loop/swap" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Compile-time assertion that loopInBuilder satisfies the swapBuilder +// interface. +var _ swapBuilder = (*loopInBuilder)(nil) + +func newLoopInBuilder(cfg *Config) *loopInBuilder { + return &loopInBuilder{ + cfg: cfg, + } +} + +type loopInBuilder struct { + // cfg contains all the external functionality we require to create + // swaps. + cfg *Config +} + +// swapType returns the swap type that the builder is responsible for creating. +func (b *loopInBuilder) swapType() swap.Type { + return swap.TypeIn +} + +// maySwap checks whether we can currently execute a swap, examining the +// current on-chain fee conditions against relevant to our swap type against +// our fee restrictions. +// +// For loop in, we cannot check any upfront costs because we do not know how +// many inputs will be used for our on-chain htlc before it is made, so we can't +// make nay estimations. +func (b *loopInBuilder) maySwap(_ context.Context, _ Parameters) error { + return nil +} + +// inUse examines our current swap traffic to determine whether we should +// suggest the builder's type of swap for the peer and channels suggested. +func (b *loopInBuilder) inUse(traffic *swapTraffic, peer route.Vertex, + channels []lnwire.ShortChannelID) error { + + for _, chanID := range channels { + if traffic.ongoingLoopOut[chanID] { + log.Debugf("Channel: %v not eligible for suggestions, "+ + "ongoing loop out utilizing channel", chanID) + + return newReasonError(ReasonLoopOut) + } + } + + if traffic.ongoingLoopIn[peer] { + log.Debugf("Peer: %x not eligible for suggestions ongoing "+ + "loop in utilizing peer", peer) + + return newReasonError(ReasonLoopIn) + } + + lastFail, recentFail := traffic.failedLoopIn[peer] + if recentFail { + log.Debugf("Peer: %v not eligible for suggestions, "+ + "was part of a failed swap at: %v", peer, + lastFail) + + return newReasonError(ReasonFailureBackoff) + } + + return nil +} + +// buildSwap creates a swap for the target peer/channels provided. The autoloop +// boolean indicates whether this swap will actually be executed. +// +// For loop in, we do not add the autoloop label for dry runs. +func (b *loopInBuilder) buildSwap(ctx context.Context, pubkey route.Vertex, + _ []lnwire.ShortChannelID, amount btcutil.Amount, + autoloop bool, params Parameters) (swapSuggestion, error) { + + quote, err := b.cfg.LoopInQuote(ctx, &loop.LoopInQuoteRequest{ + Amount: amount, + LastHop: &pubkey, + HtlcConfTarget: params.HtlcConfTarget, + }) + if err != nil { + // If the server fails our quote, we're not reachable right + // now, so we want to catch this error and fail with a + // structured error so that we know why we can't swap. + status, ok := status.FromError(err) + if ok && status.Code() == codes.FailedPrecondition { + return nil, newReasonError(ReasonLoopInUnreachable) + } + + return nil, err + } + + if err := params.FeeLimit.loopInLimits(amount, quote); err != nil { + return nil, err + } + + request := loop.LoopInRequest{ + Amount: amount, + MaxSwapFee: quote.SwapFee, + MaxMinerFee: quote.MinerFee, + HtlcConfTarget: params.HtlcConfTarget, + LastHop: &pubkey, + Initiator: autoloopSwapInitiator, + } + + if autoloop { + request.Label = labels.AutoloopLabel(swap.TypeIn) + } + + return &loopInSwapSuggestion{ + LoopInRequest: request, + }, nil +} diff --git a/liquidity/loopin_builder_test.go b/liquidity/loopin_builder_test.go new file mode 100644 index 0000000..faef546 --- /dev/null +++ b/liquidity/loopin_builder_test.go @@ -0,0 +1,193 @@ +package liquidity + +import ( + "context" + "errors" + "testing" + + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/loop" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// TestLoopinInUse tests that the loop in swap builder prevents dispatching +// swaps for peers when there is already a swap running for that peer. +func TestLoopinInUse(t *testing.T) { + var ( + peer1 = route.Vertex{1} + chan1 = lnwire.NewShortChanIDFromInt(1) + + peer2 = route.Vertex{2} + chan2 = lnwire.NewShortChanIDFromInt(2) + ) + + tests := []struct { + name string + ongoingLoopOut *lnwire.ShortChannelID + ongoingLoopIn *route.Vertex + failedLoopIn *route.Vertex + expectedErr error + }{ + { + name: "swap allowed", + ongoingLoopIn: &peer2, + ongoingLoopOut: &chan2, + failedLoopIn: &peer2, + expectedErr: nil, + }, + { + name: "conflicts with loop out", + ongoingLoopOut: &chan1, + expectedErr: newReasonError(ReasonLoopOut), + }, + { + name: "conflicts with loop in", + ongoingLoopIn: &peer1, + expectedErr: newReasonError(ReasonLoopIn), + }, + { + name: "previous failed loopin", + failedLoopIn: &peer1, + expectedErr: newReasonError(ReasonFailureBackoff), + }, + } + + for _, testCase := range tests { + traffic := newSwapTraffic() + + if testCase.ongoingLoopOut != nil { + traffic.ongoingLoopOut[*testCase.ongoingLoopOut] = true + } + + if testCase.ongoingLoopIn != nil { + traffic.ongoingLoopIn[*testCase.ongoingLoopIn] = true + } + + if testCase.failedLoopIn != nil { + traffic.failedLoopIn[*testCase.failedLoopIn] = testTime + } + + builder := newLoopInBuilder(nil) + err := builder.inUse(traffic, peer1, []lnwire.ShortChannelID{ + chan1, + }) + + require.Equal(t, testCase.expectedErr, err) + } +} + +// TestLoopinBuildSwap tests construction of loop in swaps for autoloop, +// including the case where the client cannot get a quote because it is not +// reachable from the server. +func TestLoopinBuildSwap(t *testing.T) { + var ( + peer1 = route.Vertex{1} + chan1 = lnwire.NewShortChanIDFromInt(1) + + htlcConfTarget int32 = 6 + swapAmt btcutil.Amount = 100000 + + quote = &loop.LoopInQuote{ + SwapFee: 1, + MinerFee: 2, + } + + expectedSwap = &loopInSwapSuggestion{ + loop.LoopInRequest{ + Amount: swapAmt, + MaxSwapFee: quote.SwapFee, + MaxMinerFee: quote.MinerFee, + HtlcConfTarget: htlcConfTarget, + LastHop: &peer1, + Initiator: autoloopSwapInitiator, + }, + } + + quoteRequest = &loop.LoopInQuoteRequest{ + Amount: swapAmt, + LastHop: &peer1, + HtlcConfTarget: htlcConfTarget, + } + + errPrecondition = status.Error(codes.FailedPrecondition, "failed") + errOtherCode = status.Error(codes.DeadlineExceeded, "timeout") + errNoCode = errors.New("failure") + ) + + tests := []struct { + name string + prepareMock func(*mockCfg) + expectedSwap swapSuggestion + expectedErr error + }{ + { + name: "quote successful", + prepareMock: func(m *mockCfg) { + m.On( + "LoopInQuote", mock.Anything, + quoteRequest, + ).Return(quote, nil) + }, + expectedSwap: expectedSwap, + }, + { + name: "client unreachable", + prepareMock: func(m *mockCfg) { + m.On( + "LoopInQuote", mock.Anything, + quoteRequest, + ).Return(quote, errPrecondition) + }, + expectedSwap: nil, + expectedErr: newReasonError(ReasonLoopInUnreachable), + }, + { + name: "other error code", + prepareMock: func(m *mockCfg) { + m.On( + "LoopInQuote", mock.Anything, + quoteRequest, + ).Return(quote, errOtherCode) + }, + expectedSwap: nil, + expectedErr: errOtherCode, + }, + { + name: "no error code", + prepareMock: func(m *mockCfg) { + m.On( + "LoopInQuote", mock.Anything, + quoteRequest, + ).Return(quote, errNoCode) + }, + expectedSwap: nil, + expectedErr: errNoCode, + }, + } + + for _, testCase := range tests { + mock, cfg := newMockConfig() + params := defaultParameters + params.HtlcConfTarget = htlcConfTarget + params.AutoFeeBudget = 100000 + + testCase.prepareMock(mock) + + builder := newLoopInBuilder(cfg) + swap, err := builder.buildSwap( + context.Background(), peer1, []lnwire.ShortChannelID{ + chan1, + }, swapAmt, false, params, + ) + assert.Equal(t, testCase.expectedSwap, swap) + assert.Equal(t, testCase.expectedErr, err) + + mock.AssertExpectations(t) + } +} diff --git a/liquidity/mock.go b/liquidity/mock.go new file mode 100644 index 0000000..a23de57 --- /dev/null +++ b/liquidity/mock.go @@ -0,0 +1,33 @@ +package liquidity + +import ( + "context" + + "github.com/lightninglabs/loop" + "github.com/stretchr/testify/mock" +) + +// newMockConfig returns a liquidity config with mocked calls. Note that +// functions that are not implemented by the mock will panic if called. +func newMockConfig() (*mockCfg, *Config) { + mockCfg := &mockCfg{} + + // Create a liquidity config which calls our mock. + config := &Config{ + LoopInQuote: mockCfg.LoopInQuote, + } + + return mockCfg, config +} + +type mockCfg struct { + mock.Mock +} + +// LoopInQuote mocks a call to get a loop in quote from the server. +func (m *mockCfg) LoopInQuote(ctx context.Context, + request *loop.LoopInQuoteRequest) (*loop.LoopInQuote, error) { + + args := m.Called(ctx, request) + return args.Get(0).(*loop.LoopInQuote), args.Error(1) +} diff --git a/liquidity/reasons.go b/liquidity/reasons.go index 00601e8..aee4a4c 100644 --- a/liquidity/reasons.go +++ b/liquidity/reasons.go @@ -65,6 +65,10 @@ const ( // ReasonFeePPMInsufficient indicates that the fees a swap would require // are greater than the portion of swap amount allocated to fees. ReasonFeePPMInsufficient + + // ReasonLoopInUnreachable indicates that the server does not have a + // path to the client, so cannot perform a loop in swap at this time. + ReasonLoopInUnreachable ) // String returns a string representation of a reason. @@ -112,6 +116,9 @@ func (r Reason) String() string { case ReasonFeePPMInsufficient: return "fee portion insufficient" + case ReasonLoopInUnreachable: + return "loop in unreachable" + default: return "unknown" } diff --git a/liquidity/threshold_rule.go b/liquidity/threshold_rule.go index 8c09fe3..27ab92b 100644 --- a/liquidity/threshold_rule.go +++ b/liquidity/threshold_rule.go @@ -72,7 +72,7 @@ func (r *ThresholdRule) validate() error { // swapAmount suggests a swap based on the liquidity thresholds configured, // returning zero if no swap is recommended. func (r *ThresholdRule) swapAmount(channel *balances, - restrictions *Restrictions) btcutil.Amount { + restrictions *Restrictions, swapType swap.Type) btcutil.Amount { var ( // For loop out swaps, we want to adjust our incoming liquidity @@ -95,6 +95,14 @@ func (r *ThresholdRule) swapAmount(channel *balances, reservePercentage = uint64(r.MinimumOutgoing) ) + // For loop in swaps, we reverse our target and reserve values. + if swapType == swap.TypeIn { + targetBalance = channel.outgoing + targetPercentage = uint64(r.MinimumOutgoing) + reserveBalance = channel.incoming + reservePercentage = uint64(r.MinimumIncoming) + } + // Examine our total balance and required ratios to decide whether we // need to swap. amount := calculateSwapAmount( diff --git a/liquidity/threshold_rule_test.go b/liquidity/threshold_rule_test.go index a460def..11d164e 100644 --- a/liquidity/threshold_rule_test.go +++ b/liquidity/threshold_rule_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/btcsuite/btcutil" + "github.com/lightninglabs/loop/swap" "github.com/stretchr/testify/require" ) @@ -249,6 +250,7 @@ func TestSuggestSwap(t *testing.T) { t.Run(test.name, func(t *testing.T) { swap := test.rule.swapAmount( test.channel, test.outRestrictions, + swap.TypeOut, ) require.Equal(t, test.swap, swap) }) diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index 23a4bb6..bcd9e98 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -719,8 +719,9 @@ func (s *swapClientServer) GetLiquidityParams(_ context.Context, Rules: make( []*clientrpc.LiquidityRule, 0, totalRules, ), - MinSwapAmount: uint64(cfg.ClientRestrictions.Minimum), - MaxSwapAmount: uint64(cfg.ClientRestrictions.Maximum), + MinSwapAmount: uint64(cfg.ClientRestrictions.Minimum), + MaxSwapAmount: uint64(cfg.ClientRestrictions.Maximum), + HtlcConfTarget: cfg.HtlcConfTarget, } switch f := cfg.FeeLimit.(type) { @@ -812,6 +813,7 @@ func (s *swapClientServer) SetLiquidityParams(ctx context.Context, Minimum: btcutil.Amount(in.Parameters.MinSwapAmount), Maximum: btcutil.Amount(in.Parameters.MaxSwapAmount), }, + HtlcConfTarget: in.Parameters.HtlcConfTarget, } // Zero unix time is different to zero golang time. @@ -953,13 +955,17 @@ func (s *swapClientServer) SuggestSwaps(ctx context.Context, return nil, err } - var ( - loopOut []*clientrpc.LoopOutRequest - disqualified []*clientrpc.Disqualified - ) + resp := &clientrpc.SuggestSwapsResponse{ + LoopOut: make( + []*clientrpc.LoopOutRequest, len(suggestions.OutSwaps), + ), + LoopIn: make( + []*clientrpc.LoopInRequest, len(suggestions.InSwaps), + ), + } - for _, swap := range suggestions.OutSwaps { - loopOut = append(loopOut, &clientrpc.LoopOutRequest{ + for i, swap := range suggestions.OutSwaps { + resp.LoopOut[i] = &clientrpc.LoopOutRequest{ Amt: int64(swap.Amount), OutgoingChanSet: swap.OutgoingChanSet, MaxSwapFee: int64(swap.MaxSwapFee), @@ -968,7 +974,22 @@ func (s *swapClientServer) SuggestSwaps(ctx context.Context, MaxSwapRoutingFee: int64(swap.MaxSwapRoutingFee), MaxPrepayRoutingFee: int64(swap.MaxPrepayRoutingFee), SweepConfTarget: swap.SweepConfTarget, - }) + } + } + + for i, swap := range suggestions.InSwaps { + loopIn := &clientrpc.LoopInRequest{ + Amt: int64(swap.Amount), + MaxSwapFee: int64(swap.MaxSwapFee), + MaxMinerFee: int64(swap.MaxMinerFee), + HtlcConfTarget: swap.HtlcConfTarget, + } + + if swap.LastHop != nil { + loopIn.LastHop = swap.LastHop[:] + } + + resp.LoopIn[i] = loopIn } for id, reason := range suggestions.DisqualifiedChans { @@ -982,7 +1003,7 @@ func (s *swapClientServer) SuggestSwaps(ctx context.Context, ChannelId: id.ToUint64(), } - disqualified = append(disqualified, exclChan) + resp.Disqualified = append(resp.Disqualified, exclChan) } for pubkey, reason := range suggestions.DisqualifiedPeers { @@ -996,13 +1017,10 @@ func (s *swapClientServer) SuggestSwaps(ctx context.Context, Pubkey: pubkey[:], } - disqualified = append(disqualified, exclChan) + resp.Disqualified = append(resp.Disqualified, exclChan) } - return &clientrpc.SuggestSwapsResponse{ - LoopOut: loopOut, - Disqualified: disqualified, - }, nil + return resp, nil } func rpcAutoloopReason(reason liquidity.Reason) (clientrpc.AutoReason, error) { diff --git a/loopd/utils.go b/loopd/utils.go index 59e8cca..753f6f5 100644 --- a/loopd/utils.go +++ b/loopd/utils.go @@ -39,6 +39,7 @@ func getLiquidityManager(client *loop.Client) *liquidity.Manager { mngrCfg := &liquidity.Config{ AutoloopTicker: ticker.NewForce(liquidity.DefaultAutoloopTicker), LoopOut: client.LoopOut, + LoopIn: client.LoopIn, Restrictions: func(ctx context.Context, swapType swap.Type) (*liquidity.Restrictions, error) { @@ -65,6 +66,7 @@ func getLiquidityManager(client *loop.Client) *liquidity.Manager { Lnd: client.LndServices, Clock: clock.NewDefaultClock(), LoopOutQuote: client.LoopOutQuote, + LoopInQuote: client.LoopInQuote, ListLoopOut: client.Store.FetchLoopOutSwaps, ListLoopIn: client.Store.FetchLoopInSwaps, MinimumConfirmations: minConfTarget, diff --git a/looprpc/client.pb.go b/looprpc/client.pb.go index c2260b4..647e4aa 100644 --- a/looprpc/client.pb.go +++ b/looprpc/client.pb.go @@ -2179,6 +2179,9 @@ type LiquidityParameters struct { //dispatch a swap for. This value is subject to the server-side limits //specified by the LoopOutTerms endpoint. MaxSwapAmount uint64 `protobuf:"varint,15,opt,name=max_swap_amount,json=maxSwapAmount,proto3" json:"max_swap_amount,omitempty"` + // + //The confirmation target for loop in on-chain htlcs. + HtlcConfTarget int32 `protobuf:"varint,17,opt,name=htlc_conf_target,json=htlcConfTarget,proto3" json:"htlc_conf_target,omitempty"` } func (x *LiquidityParameters) Reset() { @@ -2325,6 +2328,13 @@ func (x *LiquidityParameters) GetMaxSwapAmount() uint64 { return 0 } +func (x *LiquidityParameters) GetHtlcConfTarget() int32 { + if x != nil { + return x.HtlcConfTarget + } + return 0 +} + type LiquidityRule struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2636,6 +2646,9 @@ type SuggestSwapsResponse struct { //The set of recommended loop outs. LoopOut []*LoopOutRequest `protobuf:"bytes,1,rep,name=loop_out,json=loopOut,proto3" json:"loop_out,omitempty"` // + //The set of recommended loop in swaps + LoopIn []*LoopInRequest `protobuf:"bytes,3,rep,name=loop_in,json=loopIn,proto3" json:"loop_in,omitempty"` + // //Disqualified contains the set of channels that swaps are not recommended //for. Disqualified []*Disqualified `protobuf:"bytes,2,rep,name=disqualified,proto3" json:"disqualified,omitempty"` @@ -2680,6 +2693,13 @@ func (x *SuggestSwapsResponse) GetLoopOut() []*LoopOutRequest { return nil } +func (x *SuggestSwapsResponse) GetLoopIn() []*LoopInRequest { + if x != nil { + return x.LoopIn + } + return nil +} + func (x *SuggestSwapsResponse) GetDisqualified() []*Disqualified { if x != nil { return x.Disqualified @@ -2910,7 +2930,7 @@ var file_client_proto_rawDesc = []byte{ 0x67, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x1b, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xe0, 0x05, 0x0a, 0x13, 0x4c, 0x69, 0x71, 0x75, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x8a, 0x06, 0x0a, 0x13, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2c, 0x0a, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, @@ -2956,166 +2976,172 @@ var file_client_proto_rawDesc = []byte{ 0x01, 0x28, 0x04, 0x52, 0x0d, 0x6d, 0x69, 0x6e, 0x53, 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x6d, 0x61, 0x78, - 0x53, 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x84, 0x02, 0x0a, 0x0d, 0x4c, - 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x1d, 0x0a, 0x0a, - 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x09, 0x73, - 0x77, 0x61, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, - 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, - 0x65, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, - 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, - 0x6b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x1a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x71, 0x75, - 0x69, 0x64, 0x69, 0x74, 0x79, 0x52, 0x75, 0x6c, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x5f, - 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x11, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, - 0x6c, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x5f, 0x74, - 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x11, - 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, - 0x64, 0x22, 0x59, 0x0a, 0x19, 0x53, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, - 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3c, - 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x71, - 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, - 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x22, 0x1c, 0x0a, 0x1a, - 0x53, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, - 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x75, - 0x67, 0x67, 0x65, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x22, 0x72, 0x0a, 0x0c, 0x44, 0x69, 0x73, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, - 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, - 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12, 0x2b, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, - 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, - 0x70, 0x63, 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, - 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x85, 0x01, 0x0a, 0x14, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, - 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, - 0x0a, 0x08, 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x6f, 0x75, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, - 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x07, 0x6c, 0x6f, 0x6f, 0x70, 0x4f, - 0x75, 0x74, 0x12, 0x39, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, - 0x65, 0x64, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, - 0x70, 0x63, 0x2e, 0x44, 0x69, 0x73, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x64, 0x52, - 0x0c, 0x64, 0x69, 0x73, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2a, 0x25, 0x0a, - 0x08, 0x53, 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x4c, 0x4f, 0x4f, - 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, - 0x49, 0x4e, 0x10, 0x01, 0x2a, 0x73, 0x0a, 0x09, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x54, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x15, 0x0a, 0x11, 0x50, 0x52, 0x45, 0x49, 0x4d, 0x41, 0x47, 0x45, 0x5f, 0x52, 0x45, 0x56, - 0x45, 0x41, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x48, 0x54, 0x4c, 0x43, 0x5f, - 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x53, - 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, - 0x45, 0x44, 0x10, 0x04, 0x12, 0x13, 0x0a, 0x0f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, - 0x53, 0x45, 0x54, 0x54, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x2a, 0xed, 0x01, 0x0a, 0x0d, 0x46, 0x61, - 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x17, 0x0a, 0x13, 0x46, - 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, - 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, - 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4f, 0x46, 0x46, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x10, - 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, - 0x53, 0x4f, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x02, 0x12, 0x20, 0x0a, - 0x1c, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, - 0x53, 0x57, 0x45, 0x45, 0x50, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x03, 0x12, - 0x25, 0x0a, 0x21, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, - 0x4e, 0x5f, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x56, - 0x41, 0x4c, 0x55, 0x45, 0x10, 0x04, 0x12, 0x1c, 0x0a, 0x18, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, - 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x54, 0x45, 0x4d, 0x50, 0x4f, 0x52, 0x41, - 0x52, 0x59, 0x10, 0x05, 0x12, 0x23, 0x0a, 0x1f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, - 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x43, 0x4f, 0x52, 0x52, 0x45, 0x43, 0x54, - 0x5f, 0x41, 0x4d, 0x4f, 0x55, 0x4e, 0x54, 0x10, 0x06, 0x2a, 0x2f, 0x0a, 0x11, 0x4c, 0x69, 0x71, - 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x52, 0x75, 0x6c, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, - 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x54, - 0x48, 0x52, 0x45, 0x53, 0x48, 0x4f, 0x4c, 0x44, 0x10, 0x01, 0x2a, 0xa6, 0x03, 0x0a, 0x0a, 0x41, - 0x75, 0x74, 0x6f, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x17, 0x0a, 0x13, 0x41, 0x55, 0x54, - 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, - 0x10, 0x00, 0x12, 0x22, 0x0a, 0x1e, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, - 0x4e, 0x5f, 0x42, 0x55, 0x44, 0x47, 0x45, 0x54, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x53, 0x54, 0x41, - 0x52, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, - 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x53, 0x57, 0x45, 0x45, 0x50, 0x5f, 0x46, 0x45, 0x45, 0x53, - 0x10, 0x02, 0x12, 0x1e, 0x0a, 0x1a, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, - 0x4e, 0x5f, 0x42, 0x55, 0x44, 0x47, 0x45, 0x54, 0x5f, 0x45, 0x4c, 0x41, 0x50, 0x53, 0x45, 0x44, - 0x10, 0x03, 0x12, 0x19, 0x0a, 0x15, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, - 0x4e, 0x5f, 0x49, 0x4e, 0x5f, 0x46, 0x4c, 0x49, 0x47, 0x48, 0x54, 0x10, 0x04, 0x12, 0x18, 0x0a, - 0x14, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x53, 0x57, 0x41, - 0x50, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x05, 0x12, 0x19, 0x0a, 0x15, 0x41, 0x55, 0x54, 0x4f, 0x5f, - 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4d, 0x49, 0x4e, 0x45, 0x52, 0x5f, 0x46, 0x45, 0x45, - 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, - 0x4e, 0x5f, 0x50, 0x52, 0x45, 0x50, 0x41, 0x59, 0x10, 0x07, 0x12, 0x1f, 0x0a, 0x1b, 0x41, 0x55, - 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, - 0x45, 0x5f, 0x42, 0x41, 0x43, 0x4b, 0x4f, 0x46, 0x46, 0x10, 0x08, 0x12, 0x18, 0x0a, 0x14, 0x41, - 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, - 0x4f, 0x55, 0x54, 0x10, 0x09, 0x12, 0x17, 0x0a, 0x13, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, - 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x0a, 0x12, 0x1c, - 0x0a, 0x18, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4c, 0x49, - 0x51, 0x55, 0x49, 0x44, 0x49, 0x54, 0x59, 0x5f, 0x4f, 0x4b, 0x10, 0x0b, 0x12, 0x23, 0x0a, 0x1f, - 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x42, 0x55, 0x44, 0x47, - 0x45, 0x54, 0x5f, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x10, - 0x0c, 0x12, 0x20, 0x0a, 0x1c, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, - 0x5f, 0x46, 0x45, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, - 0x54, 0x10, 0x0d, 0x32, 0xc2, 0x07, 0x0a, 0x0a, 0x53, 0x77, 0x61, 0x70, 0x43, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x12, 0x39, 0x0a, 0x07, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x12, 0x17, 0x2e, - 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, - 0x2e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, - 0x06, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x12, 0x16, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, - 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x07, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, - 0x72, 0x12, 0x17, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x6f, 0x6e, 0x69, - 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6c, 0x6f, 0x6f, - 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x30, - 0x01, 0x12, 0x42, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x12, 0x19, - 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, - 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, - 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x08, 0x53, 0x77, 0x61, 0x70, 0x49, 0x6e, 0x66, - 0x6f, 0x12, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, - 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6c, 0x6f, - 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x40, 0x0a, 0x0c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x54, 0x65, 0x72, 0x6d, 0x73, - 0x12, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, - 0x63, 0x2e, 0x4f, 0x75, 0x74, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x51, 0x75, 0x6f, - 0x74, 0x65, 0x12, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x6f, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, - 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x75, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x6f, 0x70, 0x49, - 0x6e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x12, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, - 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, - 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4c, 0x6f, - 0x6f, 0x70, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, - 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x51, 0x75, 0x6f, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x50, 0x72, - 0x6f, 0x62, 0x65, 0x12, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, - 0x6f, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x6c, 0x6f, 0x6f, - 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4c, 0x73, 0x61, 0x74, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x73, 0x12, 0x16, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x6c, 0x6f, - 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, - 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, - 0x70, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, - 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, - 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, - 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x5d, 0x0a, 0x12, - 0x53, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, - 0x6d, 0x73, 0x12, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, - 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, - 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, - 0x61, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0c, 0x53, - 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x12, 0x1c, 0x2e, 0x6c, 0x6f, - 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x53, 0x77, 0x61, - 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, - 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, - 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, - 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x53, 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x68, 0x74, + 0x6c, 0x63, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x11, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x68, 0x74, 0x6c, 0x63, 0x43, 0x6f, 0x6e, 0x66, 0x54, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x22, 0x84, 0x02, 0x0a, 0x0d, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, + 0x74, 0x79, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, + 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, + 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, 0x65, 0x52, 0x08, 0x73, 0x77, 0x61, + 0x70, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12, 0x2e, 0x0a, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x6c, 0x6f, + 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x52, + 0x75, 0x6c, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2d, 0x0a, + 0x12, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, + 0x6f, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x11, 0x69, 0x6e, 0x63, 0x6f, 0x6d, + 0x69, 0x6e, 0x67, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12, 0x2d, 0x0a, 0x12, + 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, + 0x6c, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x11, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, + 0x6e, 0x67, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x22, 0x59, 0x0a, 0x19, 0x53, + 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3c, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6c, + 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, + 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x22, 0x1c, 0x0a, 0x1a, 0x53, 0x65, 0x74, 0x4c, 0x69, 0x71, + 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x53, + 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x72, 0x0a, 0x0c, 0x44, + 0x69, 0x73, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, + 0x62, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b, + 0x65, 0x79, 0x12, 0x2b, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x75, 0x74, + 0x6f, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, + 0xb6, 0x01, 0x0a, 0x14, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x08, 0x6c, 0x6f, 0x6f, 0x70, + 0x5f, 0x6f, 0x75, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6c, 0x6f, 0x6f, + 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x52, 0x07, 0x6c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x12, 0x2f, 0x0a, 0x07, + 0x6c, 0x6f, 0x6f, 0x70, 0x5f, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, + 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x06, 0x6c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x12, 0x39, 0x0a, + 0x0c, 0x64, 0x69, 0x73, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, + 0x73, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x64, 0x52, 0x0c, 0x64, 0x69, 0x73, 0x71, + 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2a, 0x25, 0x0a, 0x08, 0x53, 0x77, 0x61, 0x70, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, + 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x01, 0x2a, + 0x73, 0x0a, 0x09, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0d, 0x0a, 0x09, + 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x50, + 0x52, 0x45, 0x49, 0x4d, 0x41, 0x47, 0x45, 0x5f, 0x52, 0x45, 0x56, 0x45, 0x41, 0x4c, 0x45, 0x44, + 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, + 0x53, 0x48, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, + 0x53, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x12, + 0x13, 0x0a, 0x0f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x45, 0x54, 0x54, 0x4c, + 0x45, 0x44, 0x10, 0x05, 0x2a, 0xed, 0x01, 0x0a, 0x0d, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, + 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x17, 0x0a, 0x13, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, + 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, + 0x1b, 0x0a, 0x17, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, + 0x4e, 0x5f, 0x4f, 0x46, 0x46, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, + 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x54, + 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x02, 0x12, 0x20, 0x0a, 0x1c, 0x46, 0x41, 0x49, 0x4c, + 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x53, 0x57, 0x45, 0x45, 0x50, + 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x03, 0x12, 0x25, 0x0a, 0x21, 0x46, 0x41, + 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x53, + 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x10, + 0x04, 0x12, 0x1c, 0x0a, 0x18, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, + 0x53, 0x4f, 0x4e, 0x5f, 0x54, 0x45, 0x4d, 0x50, 0x4f, 0x52, 0x41, 0x52, 0x59, 0x10, 0x05, 0x12, + 0x23, 0x0a, 0x1f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, + 0x4e, 0x5f, 0x49, 0x4e, 0x43, 0x4f, 0x52, 0x52, 0x45, 0x43, 0x54, 0x5f, 0x41, 0x4d, 0x4f, 0x55, + 0x4e, 0x54, 0x10, 0x06, 0x2a, 0x2f, 0x0a, 0x11, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, + 0x79, 0x52, 0x75, 0x6c, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, + 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x54, 0x48, 0x52, 0x45, 0x53, 0x48, + 0x4f, 0x4c, 0x44, 0x10, 0x01, 0x2a, 0xa6, 0x03, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x6f, 0x52, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x17, 0x0a, 0x13, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, + 0x53, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x22, 0x0a, + 0x1e, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x42, 0x55, 0x44, + 0x47, 0x45, 0x54, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x45, 0x44, 0x10, + 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, + 0x5f, 0x53, 0x57, 0x45, 0x45, 0x50, 0x5f, 0x46, 0x45, 0x45, 0x53, 0x10, 0x02, 0x12, 0x1e, 0x0a, + 0x1a, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x42, 0x55, 0x44, + 0x47, 0x45, 0x54, 0x5f, 0x45, 0x4c, 0x41, 0x50, 0x53, 0x45, 0x44, 0x10, 0x03, 0x12, 0x19, 0x0a, + 0x15, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x5f, + 0x46, 0x4c, 0x49, 0x47, 0x48, 0x54, 0x10, 0x04, 0x12, 0x18, 0x0a, 0x14, 0x41, 0x55, 0x54, 0x4f, + 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x53, 0x57, 0x41, 0x50, 0x5f, 0x46, 0x45, 0x45, + 0x10, 0x05, 0x12, 0x19, 0x0a, 0x15, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, + 0x4e, 0x5f, 0x4d, 0x49, 0x4e, 0x45, 0x52, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, + 0x12, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x45, + 0x50, 0x41, 0x59, 0x10, 0x07, 0x12, 0x1f, 0x0a, 0x1b, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, + 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x42, 0x41, 0x43, + 0x4b, 0x4f, 0x46, 0x46, 0x10, 0x08, 0x12, 0x18, 0x0a, 0x14, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, + 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x09, + 0x12, 0x17, 0x0a, 0x13, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, + 0x4c, 0x4f, 0x4f, 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x0a, 0x12, 0x1c, 0x0a, 0x18, 0x41, 0x55, 0x54, + 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4c, 0x49, 0x51, 0x55, 0x49, 0x44, 0x49, + 0x54, 0x59, 0x5f, 0x4f, 0x4b, 0x10, 0x0b, 0x12, 0x23, 0x0a, 0x1f, 0x41, 0x55, 0x54, 0x4f, 0x5f, + 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x42, 0x55, 0x44, 0x47, 0x45, 0x54, 0x5f, 0x49, 0x4e, + 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x10, 0x0c, 0x12, 0x20, 0x0a, 0x1c, + 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x46, 0x45, 0x45, 0x5f, + 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x10, 0x0d, 0x32, 0xc2, + 0x07, 0x0a, 0x0a, 0x53, 0x77, 0x61, 0x70, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x39, 0x0a, + 0x07, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x12, 0x17, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, + 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x4c, 0x6f, 0x6f, 0x70, + 0x49, 0x6e, 0x12, 0x16, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, + 0x70, 0x49, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, + 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x39, 0x0a, 0x07, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x12, 0x17, 0x2e, 0x6c, + 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, + 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, + 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x12, 0x19, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, + 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x39, 0x0a, 0x08, 0x53, 0x77, 0x61, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x18, 0x2e, 0x6c, + 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, + 0x2e, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x40, 0x0a, 0x0c, 0x4c, + 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x12, 0x15, 0x2e, 0x6c, 0x6f, + 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x75, 0x74, + 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, + 0x0c, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x15, 0x2e, + 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4f, + 0x75, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x41, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x54, 0x65, 0x72, 0x6d, + 0x73, 0x12, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x65, 0x72, 0x6d, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, + 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x51, + 0x75, 0x6f, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x51, + 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6c, 0x6f, + 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x12, 0x15, + 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, + 0x50, 0x72, 0x6f, 0x62, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, + 0x0d, 0x47, 0x65, 0x74, 0x4c, 0x73, 0x61, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x16, + 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, + 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x56, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, + 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, + 0x47, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, + 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, + 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, + 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x5d, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x69, + 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x22, 0x2e, + 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x69, 0x71, 0x75, 0x69, + 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x23, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x4c, + 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0c, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, + 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x12, 0x1c, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, + 0x2e, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, + 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, + 0x6c, 0x6f, 0x6f, 0x70, 0x2f, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3182,40 +3208,41 @@ var file_client_proto_depIdxs = []int32{ 25, // 11: looprpc.SetLiquidityParamsRequest.parameters:type_name -> looprpc.LiquidityParameters 4, // 12: looprpc.Disqualified.reason:type_name -> looprpc.AutoReason 5, // 13: looprpc.SuggestSwapsResponse.loop_out:type_name -> looprpc.LoopOutRequest - 30, // 14: looprpc.SuggestSwapsResponse.disqualified:type_name -> looprpc.Disqualified - 5, // 15: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest - 6, // 16: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest - 8, // 17: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest - 10, // 18: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest - 12, // 19: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest - 13, // 20: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest - 16, // 21: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest - 13, // 22: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest - 16, // 23: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest - 19, // 24: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest - 21, // 25: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest - 24, // 26: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest - 27, // 27: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest - 29, // 28: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest - 7, // 29: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse - 7, // 30: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse - 9, // 31: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus - 11, // 32: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse - 9, // 33: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus - 15, // 34: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse - 18, // 35: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse - 14, // 36: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse - 17, // 37: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse - 20, // 38: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse - 22, // 39: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse - 25, // 40: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters - 28, // 41: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse - 31, // 42: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse - 29, // [29:43] is the sub-list for method output_type - 15, // [15:29] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 6, // 14: looprpc.SuggestSwapsResponse.loop_in:type_name -> looprpc.LoopInRequest + 30, // 15: looprpc.SuggestSwapsResponse.disqualified:type_name -> looprpc.Disqualified + 5, // 16: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest + 6, // 17: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest + 8, // 18: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest + 10, // 19: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest + 12, // 20: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest + 13, // 21: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest + 16, // 22: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest + 13, // 23: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest + 16, // 24: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest + 19, // 25: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest + 21, // 26: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest + 24, // 27: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest + 27, // 28: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest + 29, // 29: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest + 7, // 30: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse + 7, // 31: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse + 9, // 32: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus + 11, // 33: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse + 9, // 34: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus + 15, // 35: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse + 18, // 36: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse + 14, // 37: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse + 17, // 38: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse + 20, // 39: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse + 22, // 40: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse + 25, // 41: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters + 28, // 42: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse + 31, // 43: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse + 30, // [30:44] is the sub-list for method output_type + 16, // [16:30] is the sub-list for method input_type + 16, // [16:16] is the sub-list for extension type_name + 16, // [16:16] is the sub-list for extension extendee + 0, // [0:16] is the sub-list for field type_name } func init() { file_client_proto_init() } diff --git a/looprpc/client.proto b/looprpc/client.proto index 8fb641f..aedce02 100644 --- a/looprpc/client.proto +++ b/looprpc/client.proto @@ -848,6 +848,11 @@ message LiquidityParameters { specified by the LoopOutTerms endpoint. */ uint64 max_swap_amount = 15; + + /* + The confirmation target for loop in on-chain htlcs. + */ + int32 htlc_conf_target = 17; } enum LiquidityRuleType { @@ -1012,6 +1017,11 @@ message SuggestSwapsResponse { */ repeated LoopOutRequest loop_out = 1; + /* + The set of recommended loop in swaps + */ + repeated LoopInRequest loop_in = 3; + /* Disqualified contains the set of channels that swaps are not recommended for. diff --git a/looprpc/client.swagger.json b/looprpc/client.swagger.json index b697247..ccbbe0f 100644 --- a/looprpc/client.swagger.json +++ b/looprpc/client.swagger.json @@ -675,6 +675,11 @@ "type": "string", "format": "uint64", "description": "The maximum amount, expressed in satoshis, that the autoloop client will\ndispatch a swap for. This value is subject to the server-side limits\nspecified by the LoopOutTerms endpoint." + }, + "htlc_conf_target": { + "type": "integer", + "format": "int32", + "description": "The confirmation target for loop in on-chain htlcs." } } }, @@ -999,6 +1004,13 @@ }, "description": "The set of recommended loop outs." }, + "loop_in": { + "type": "array", + "items": { + "$ref": "#/definitions/looprpcLoopInRequest" + }, + "title": "The set of recommended loop in swaps" + }, "disqualified": { "type": "array", "items": { diff --git a/release_notes.md b/release_notes.md index c71d135..19c13c8 100644 --- a/release_notes.md +++ b/release_notes.md @@ -15,6 +15,10 @@ This file tracks release notes for the loop client. ## Next release #### New Features +* Loop in functionality has been added to AutoLoop. This feature can be enabled + to acquire outgoing capacity on your node automatically, using + `loop setrule --type=in`. At present, autoloop can only be set to loop out + *or* loop in, and cannot manage liquidity in both directions. #### Breaking Changes