From c4fd8ce15015563b743b16005155959dacb0a491 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 12 Mar 2019 16:10:37 +0100 Subject: [PATCH] multi: loop in swap --- client.go | 117 +++++- client_test.go | 10 +- cmd/loop/loopin.go | 83 ++++ cmd/loop/loopout.go | 2 +- cmd/loop/main.go | 20 +- cmd/loop/terms.go | 21 +- cmd/loopd/daemon.go | 18 +- cmd/loopd/swapclient_server.go | 89 ++++- interface.go | 111 ++++++ lndclient/chainnotifier_client.go | 1 - lndclient/lightning_client.go | 8 +- loopin.go | 621 ++++++++++++++++++++++++++++++ loopin_test.go | 385 ++++++++++++++++++ loopin_testcontext_test.go | 62 +++ loopout.go | 2 +- looprpc/client.pb.go | 378 ++++++++++++++---- looprpc/client.proto | 74 +++- looprpc/client.swagger.json | 13 +- looprpc/server.pb.go | 378 ++++++++++++++++-- looprpc/server.proto | 29 ++ server_mock_test.go | 48 +++ store_mock_test.go | 98 ++++- swap_server_client.go | 70 ++++ test/context.go | 11 - test/lightning_client_mock.go | 2 +- testcontext_test.go | 4 +- 26 files changed, 2472 insertions(+), 183 deletions(-) create mode 100644 cmd/loop/loopin.go create mode 100644 loopin.go create mode 100644 loopin_test.go create mode 100644 loopin_testcontext_test.go diff --git a/client.go b/client.go index 8489d18..bbd45f4 100644 --- a/client.go +++ b/client.go @@ -118,6 +118,11 @@ func (s *Client) FetchLoopOutSwaps() ([]*loopdb.LoopOut, error) { return s.Store.FetchLoopOutSwaps() } +// FetchLoopInSwaps returns a list of all swaps currently in the database. +func (s *Client) FetchLoopInSwaps() ([]*loopdb.LoopIn, error) { + return s.Store.FetchLoopInSwaps() +} + // Run is a blocking call that executes all swaps. Any pending swaps are // restored from persistent storage and resumed. Subsequent updates will be // sent through the passed in statusChan. The function can be terminated by @@ -144,7 +149,12 @@ func (s *Client) Run(ctx context.Context, // Query store before starting event loop to prevent new swaps from // being treated as swaps that need to be resumed. - pendingSwaps, err := s.Store.FetchLoopOutSwaps() + pendingLoopOutSwaps, err := s.Store.FetchLoopOutSwaps() + if err != nil { + return err + } + + pendingLoopInSwaps, err := s.Store.FetchLoopInSwaps() if err != nil { return err } @@ -154,7 +164,7 @@ func (s *Client) Run(ctx context.Context, go func() { defer s.wg.Done() - s.resumeSwaps(mainCtx, pendingSwaps) + s.resumeSwaps(mainCtx, pendingLoopOutSwaps, pendingLoopInSwaps) // Signal that new requests can be accepted. Otherwise the new // swap could already have been added to the store and read in @@ -194,19 +204,33 @@ func (s *Client) Run(ctx context.Context, // resumeSwaps restarts all pending swaps from the provided list. func (s *Client) resumeSwaps(ctx context.Context, - swaps []*loopdb.LoopOut) { + loopOutSwaps []*loopdb.LoopOut, loopInSwaps []*loopdb.LoopIn) { + + swapCfg := &swapConfig{ + lnd: s.lndServices, + store: s.Store, + } - for _, pend := range swaps { + for _, pend := range loopOutSwaps { if pend.State().Type() != loopdb.StateTypePending { continue } - swapCfg := &swapConfig{ - lnd: s.lndServices, - store: s.Store, - } swap, err := resumeLoopOutSwap(ctx, swapCfg, pend) if err != nil { - logger.Errorf("resuming swap: %v", err) + logger.Errorf("resuming loop out swap: %v", err) + continue + } + + s.executor.initiateSwap(ctx, swap) + } + + for _, pend := range loopInSwaps { + if pend.State().Type() != loopdb.StateTypePending { + continue + } + swap, err := resumeLoopInSwap(ctx, swapCfg, pend) + if err != nil { + logger.Errorf("resuming loop in swap: %v", err) continue } @@ -320,3 +344,78 @@ func (s *Client) waitForInitialized(ctx context.Context) error { return nil } + +// LoopIn initiates a loop in swap. +func (s *Client) LoopIn(globalCtx context.Context, + request *LoopInRequest) (*lntypes.Hash, error) { + + logger.Infof("Loop in %v (channel: %v)", + request.Amount, + request.LoopInChannel, + ) + + if err := s.waitForInitialized(globalCtx); err != nil { + return nil, err + } + + // Create a new swap object for this swap. + initiationHeight := s.executor.height() + swapCfg := swapConfig{ + lnd: s.lndServices, + store: s.Store, + server: s.Server, + } + swap, err := newLoopInSwap( + globalCtx, &swapCfg, initiationHeight, request, + ) + if err != nil { + return nil, err + } + + // Post swap to the main loop. + s.executor.initiateSwap(globalCtx, swap) + + // Return hash so that the caller can identify this swap in the updates + // stream. + return &swap.hash, nil +} + +// LoopInQuote takes an amount and returns a break down of estimated +// costs for the client. Both the swap server and the on-chain fee estimator are +// queried to get to build the quote response. +func (s *Client) LoopInQuote(ctx context.Context, + request *LoopInQuoteRequest) (*LoopInQuote, error) { + + terms, err := s.Server.GetLoopInTerms(ctx) + if err != nil { + return nil, err + } + + if request.Amount < terms.MinSwapAmount { + return nil, ErrSwapAmountTooLow + } + + if request.Amount > terms.MaxSwapAmount { + return nil, ErrSwapAmountTooHigh + } + + swapFee := terms.SwapFeeBase + + request.Amount*btcutil.Amount(terms.SwapFeeRate)/ + btcutil.Amount(swap.FeeRateTotalParts) + + // TODO: Find a way to get a fee estimate. + minerFee := btcutil.Amount(0) + + return &LoopInQuote{ + SwapFee: swapFee, + MinerFee: minerFee, + PrepayAmount: btcutil.Amount(terms.PrepayAmt), + }, nil +} + +// LoopInTerms returns the terms on which the server executes swaps. +func (s *Client) LoopInTerms(ctx context.Context) ( + *LoopInTerms, error) { + + return s.Server.GetLoopInTerms(ctx) +} diff --git a/client_test.go b/client_test.go index d1918b3..dc64311 100644 --- a/client_test.go +++ b/client_test.go @@ -197,12 +197,14 @@ func testResume(t *testing.T, expired, preimageRevealed, expectSuccess bool) { MaxMinerFee: 50000, }, }, - Events: []*loopdb.LoopOutEvent{ - { - State: state, + Loop: loopdb.Loop{ + Events: []*loopdb.LoopEvent{ + { + State: state, + }, }, + Hash: hash, }, - Hash: hash, } if expired { diff --git a/cmd/loop/loopin.go b/cmd/loop/loopin.go new file mode 100644 index 0000000..954e563 --- /dev/null +++ b/cmd/loop/loopin.go @@ -0,0 +1,83 @@ +package main + +import ( + "context" + "fmt" + + "github.com/lightninglabs/loop/looprpc" + "github.com/urfave/cli" +) + +var loopInCommand = cli.Command{ + Name: "in", + Usage: "perform an on-chain to off-chain swap (loop in)", + ArgsUsage: "amt", + Description: ` + Send the amount in satoshis specified by the amt argument off-chain.`, + Flags: []cli.Flag{ + cli.Uint64Flag{ + Name: "channel", + Usage: "the 8-byte compact channel ID of the channel to loop in", + }, + }, + Action: loopIn, +} + +func loopIn(ctx *cli.Context) error { + // Show command help if no arguments and flags were provided. + if ctx.NArg() < 1 { + cli.ShowCommandHelp(ctx, "in") + return nil + } + + args := ctx.Args() + + amt, err := parseAmt(args[0]) + if err != nil { + return err + } + + client, cleanup, err := getClient(ctx) + if err != nil { + return err + } + defer cleanup() + + quote, err := client.GetLoopInQuote( + context.Background(), + &looprpc.QuoteRequest{ + Amt: int64(amt), + }, + ) + if err != nil { + return err + } + + limits := getLimits(amt, quote) + + if err := displayLimits(amt, limits); err != nil { + return err + } + + var loopInChannel uint64 + if ctx.IsSet("channel") { + loopInChannel = ctx.Uint64("channel") + } + + resp, err := client.LoopIn(context.Background(), &looprpc.LoopInRequest{ + Amt: int64(amt), + MaxMinerFee: int64(limits.maxMinerFee), + MaxPrepayAmt: int64(limits.maxPrepayAmt), + MaxSwapFee: int64(limits.maxSwapFee), + MaxPrepayRoutingFee: int64(limits.maxPrepayRoutingFee), + LoopInChannel: loopInChannel, + }) + if err != nil { + return err + } + + fmt.Printf("Swap initiated with id: %v\n", resp.Id[:8]) + fmt.Printf("Run swapcli without a command to monitor progress.\n") + + return nil +} diff --git a/cmd/loop/loopout.go b/cmd/loop/loopout.go index af472d3..657c295 100644 --- a/cmd/loop/loopout.go +++ b/cmd/loop/loopout.go @@ -81,7 +81,7 @@ func loopOut(ctx *cli.Context) error { MaxPrepayAmt: int64(limits.maxPrepayAmt), MaxSwapFee: int64(limits.maxSwapFee), MaxPrepayRoutingFee: int64(limits.maxPrepayRoutingFee), - MaxSwapRoutingFee: int64(limits.maxSwapRoutingFee), + MaxSwapRoutingFee: int64(*limits.maxSwapRoutingFee), LoopOutChannel: unchargeChannel, }) if err != nil { diff --git a/cmd/loop/main.go b/cmd/loop/main.go index 1d91454..00006e6 100644 --- a/cmd/loop/main.go +++ b/cmd/loop/main.go @@ -61,7 +61,8 @@ func main() { }, } app.Commands = []cli.Command{ - loopOutCommand, termsCommand, monitorCommand, quoteCommand, + loopOutCommand, loopInCommand, termsCommand, + monitorCommand, quoteCommand, } err := app.Run(os.Args) @@ -87,7 +88,7 @@ func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount { } type limits struct { - maxSwapRoutingFee btcutil.Amount + maxSwapRoutingFee *btcutil.Amount maxPrepayRoutingFee btcutil.Amount maxMinerFee btcutil.Amount maxSwapFee btcutil.Amount @@ -96,7 +97,6 @@ type limits struct { func getLimits(amt btcutil.Amount, quote *looprpc.QuoteResponse) *limits { return &limits{ - maxSwapRoutingFee: getMaxRoutingFee(btcutil.Amount(amt)), maxPrepayRoutingFee: getMaxRoutingFee(btcutil.Amount( quote.PrepayAmt, )), @@ -111,8 +111,10 @@ func getLimits(amt btcutil.Amount, quote *looprpc.QuoteResponse) *limits { } func displayLimits(amt btcutil.Amount, l *limits) error { - totalSuccessMax := l.maxSwapRoutingFee + l.maxPrepayRoutingFee + - l.maxMinerFee + l.maxSwapFee + totalSuccessMax := l.maxPrepayRoutingFee + l.maxMinerFee + l.maxSwapFee + if l.maxSwapRoutingFee != nil { + totalSuccessMax += *l.maxSwapRoutingFee + } fmt.Printf("Max swap fees for %d loop out: %d\n", btcutil.Amount(amt), totalSuccessMax, @@ -129,8 +131,12 @@ func displayLimits(amt btcutil.Amount, l *limits) error { case "x": fmt.Println() fmt.Printf("Max on-chain fee: %d\n", l.maxMinerFee) - fmt.Printf("Max off-chain swap routing fee: %d\n", - l.maxSwapRoutingFee) + + if l.maxSwapRoutingFee != nil { + fmt.Printf("Max off-chain swap routing fee: %d\n", + *l.maxSwapRoutingFee) + } + fmt.Printf("Max off-chain prepay routing fee: %d\n", l.maxPrepayRoutingFee) fmt.Printf("Max swap fee: %d\n", l.maxSwapFee) diff --git a/cmd/loop/terms.go b/cmd/loop/terms.go index dbf18f2..cc73660 100644 --- a/cmd/loop/terms.go +++ b/cmd/loop/terms.go @@ -4,11 +4,11 @@ import ( "context" "fmt" - "github.com/btcsuite/btcutil" - "github.com/lightninglabs/loop/swap" + "github.com/urfave/cli" + "github.com/btcsuite/btcutil" "github.com/lightninglabs/loop/looprpc" - "github.com/urfave/cli" + "github.com/lightninglabs/loop/swap" ) var termsCommand = cli.Command{ @@ -25,14 +25,13 @@ func terms(ctx *cli.Context) error { defer cleanup() req := &looprpc.TermsRequest{} - terms, err := client.LoopOutTerms(context.Background(), req) + loopOutTerms, err := client.LoopOutTerms(context.Background(), req) if err != nil { return err } - fmt.Printf("Amount: %d - %d\n", - btcutil.Amount(terms.MinSwapAmount), - btcutil.Amount(terms.MaxSwapAmount), + loopInTerms, err := client.GetLoopInTerms( + context.Background(), &looprpc.TermsRequest{}, ) if err != nil { return err @@ -54,7 +53,13 @@ func terms(ctx *cli.Context) error { fmt.Println("Loop Out") fmt.Println("--------") - printTerms(terms) + printTerms(loopOutTerms) + + fmt.Println() + + fmt.Println("Loop In") + fmt.Println("------") + printTerms(loopInTerms) return nil } diff --git a/cmd/loopd/daemon.go b/cmd/loopd/daemon.go index dd5875d..b089cf9 100644 --- a/cmd/loopd/daemon.go +++ b/cmd/loopd/daemon.go @@ -37,11 +37,11 @@ func daemon(config *config) error { // Before starting the client, build an in-memory view of all swaps. // This view is used to update newly connected clients with the most // recent swaps. - storedSwaps, err := swapClient.FetchLoopOutSwaps() + loopOutSwaps, err := swapClient.FetchLoopOutSwaps() if err != nil { return err } - for _, swap := range storedSwaps { + for _, swap := range loopOutSwaps { swaps[swap.Hash] = loop.SwapInfo{ SwapType: loop.TypeOut, SwapContract: swap.Contract.SwapContract, @@ -51,6 +51,20 @@ func daemon(config *config) error { } } + loopInSwaps, err := swapClient.FetchLoopInSwaps() + if err != nil { + return err + } + for _, swap := range loopInSwaps { + swaps[swap.Hash] = loop.SwapInfo{ + SwapType: loop.TypeIn, + SwapContract: swap.Contract.SwapContract, + State: swap.State(), + SwapHash: swap.Hash, + LastUpdate: swap.LastUpdateTime(), + } + } + // Instantiate the loopd gRPC server. server := swapClientServer{ impl: swapClient, diff --git a/cmd/loopd/swapclient_server.go b/cmd/loopd/swapclient_server.go index 6678fb0..edbec3c 100644 --- a/cmd/loopd/swapclient_server.go +++ b/cmd/loopd/swapclient_server.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "fmt" "sort" @@ -32,7 +33,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context, in *looprpc.LoopOutRequest) ( *looprpc.SwapResponse, error) { - logger.Infof("LoopOut request received") + logger.Infof("Loop out request received") var sweepAddr btcutil.Address if in.Dest == "" { @@ -83,6 +84,8 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) ( state = looprpc.SwapState_INITIATED case loopdb.StatePreimageRevealed: state = looprpc.SwapState_PREIMAGE_REVEALED + case loopdb.StateHtlcPublished: + state = looprpc.SwapState_HTLC_PUBLISHED case loopdb.StateSuccess: state = looprpc.SwapState_SUCCESS default: @@ -103,6 +106,16 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) ( return nil, err } + var swapType looprpc.SwapType + switch loopSwap.SwapType { + case loop.TypeIn: + swapType = looprpc.SwapType_LOOP_IN + case loop.TypeOut: + swapType = looprpc.SwapType_LOOP_OUT + default: + return nil, errors.New("unknown swap type") + } + return &looprpc.SwapStatus{ Amt: int64(loopSwap.AmountRequested), Id: loopSwap.SwapHash.String(), @@ -110,7 +123,7 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) ( InitiationTime: loopSwap.InitiationTime.UnixNano(), LastUpdateTime: loopSwap.LastUpdate.UnixNano(), HtlcAddress: address.EncodeAddress(), - Type: looprpc.SwapType_LOOP_OUT, + Type: swapType, }, nil } @@ -212,7 +225,7 @@ func (s *swapClientServer) Monitor(in *looprpc.MonitorRequest, func (s *swapClientServer) LoopOutTerms(ctx context.Context, req *looprpc.TermsRequest) (*looprpc.TermsResponse, error) { - logger.Infof("Terms request received") + logger.Infof("Loop out terms request received") terms, err := s.impl.LoopOutTerms(ctx) if err != nil { @@ -248,3 +261,73 @@ func (s *swapClientServer) LoopOutQuote(ctx context.Context, SwapFee: int64(quote.SwapFee), }, nil } + +// GetTerms returns the terms that the server enforces for swaps. +func (s *swapClientServer) GetLoopInTerms(ctx context.Context, req *looprpc.TermsRequest) ( + *looprpc.TermsResponse, error) { + + logger.Infof("Loop in terms request received") + + terms, err := s.impl.LoopInTerms(ctx) + if err != nil { + logger.Errorf("Terms request: %v", err) + return nil, err + } + + return &looprpc.TermsResponse{ + MinSwapAmount: int64(terms.MinSwapAmount), + MaxSwapAmount: int64(terms.MaxSwapAmount), + PrepayAmt: int64(terms.PrepayAmt), + SwapFeeBase: int64(terms.SwapFeeBase), + SwapFeeRate: int64(terms.SwapFeeRate), + CltvDelta: int32(terms.CltvDelta), + }, nil +} + +// GetQuote returns a quote for a swap with the provided parameters. +func (s *swapClientServer) GetLoopInQuote(ctx context.Context, + req *looprpc.QuoteRequest) (*looprpc.QuoteResponse, error) { + + logger.Infof("Loop in quote request received") + + quote, err := s.impl.LoopInQuote(ctx, &loop.LoopInQuoteRequest{ + Amount: btcutil.Amount(req.Amt), + HtlcConfTarget: defaultConfTarget, + }) + if err != nil { + return nil, err + } + return &looprpc.QuoteResponse{ + MinerFee: int64(quote.MinerFee), + PrepayAmt: int64(quote.PrepayAmount), + SwapFee: int64(quote.SwapFee), + }, nil +} + +func (s *swapClientServer) LoopIn(ctx context.Context, + in *looprpc.LoopInRequest) ( + *looprpc.SwapResponse, error) { + + logger.Infof("Loop in request received") + + req := &loop.LoopInRequest{ + Amount: btcutil.Amount(in.Amt), + MaxMinerFee: btcutil.Amount(in.MaxMinerFee), + MaxPrepayAmount: btcutil.Amount(in.MaxPrepayAmt), + MaxPrepayRoutingFee: btcutil.Amount(in.MaxPrepayRoutingFee), + MaxSwapFee: btcutil.Amount(in.MaxSwapFee), + HtlcConfTarget: defaultConfTarget, + } + if in.LoopInChannel != 0 { + req.LoopInChannel = &in.LoopInChannel + } + hash, err := s.impl.LoopIn(ctx, req) + if err != nil { + logger.Errorf("Loop in: %v", err) + return nil, err + } + + return &looprpc.SwapResponse{ + Id: hash.String(), + }, nil +} diff --git a/interface.go b/interface.go index b6baabe..e6c6d28 100644 --- a/interface.go +++ b/interface.go @@ -158,6 +158,107 @@ type LoopOutTerms struct { SwapPaymentDest [33]byte } +// LoopInRequest contains the required parameters for the swap. +type LoopInRequest struct { + // Amount specifies the requested swap amount in sat. This does not + // include the swap and miner fee. + Amount btcutil.Amount + + // MaxPrepayRoutingFee is the maximum off-chain fee in msat that may be + // paid for payment to the server. This limit is applied during path + // finding. Typically this value is taken from the response of the + // UnchargeQuote call. + MaxPrepayRoutingFee btcutil.Amount + + // MaxSwapFee is the maximum we are willing to pay the server for the + // swap. This value is not disclosed in the swap initiation call, but if + // the server asks for a higher fee, we abort the swap. Typically this + // value is taken from the response of the UnchargeQuote call. It + // includes the prepay amount. + MaxSwapFee btcutil.Amount + + // MaxPrepayAmount is the maximum amount of the swap fee that may be + // charged as a prepayment. + MaxPrepayAmount btcutil.Amount + + // MaxMinerFee is the maximum in on-chain fees that we are willing to + // spent. If we publish the on-chain htlc and the fee estimate turns out + // higher than this value, we cancel the swap. + // + // MaxMinerFee is typically taken from the response of the UnchargeQuote + // call. + MaxMinerFee btcutil.Amount + + // HtlcConfTarget specifies the targeted confirmation target for the + // client htlc tx. + HtlcConfTarget int32 + + // LoopInChannel optionally specifies the short channel id of the + // channel to charge. + LoopInChannel *uint64 +} + +// LoopInTerms are the server terms on which it executes charge swaps. +type LoopInTerms struct { + // SwapFeeBase is the fixed per-swap base fee. + SwapFeeBase btcutil.Amount + + // SwapFeeRate is the variable fee in parts per million. + SwapFeeRate int64 + + // PrepayAmt is the fixed part of the swap fee that needs to be prepaid. + PrepayAmt btcutil.Amount + + // MinSwapAmount is the minimum amount that the server requires for a + // swap. + MinSwapAmount btcutil.Amount + + // MaxSwapAmount is the maximum amount that the server accepts for a + // swap. + MaxSwapAmount btcutil.Amount + + // Time lock delta relative to current block height that swap server + // will accept on the swap initiation call. + CltvDelta int32 +} + +// In contains status information for a loop in swap. +type In struct { + loopdb.LoopInContract + + SwapInfoKit + + // State where the swap is in. + State loopdb.SwapState +} + +// LoopInQuoteRequest specifies the swap parameters for which a quote is +// requested. +type LoopInQuoteRequest struct { + // Amount specifies the requested swap amount in sat. This does not + // include the swap and miner fee. + Amount btcutil.Amount + + // HtlcConfTarget specifies the targeted confirmation target for the + // client sweep tx. + HtlcConfTarget int32 +} + +// LoopInQuote contains estimates for the fees making up the total swap cost +// for the client. +type LoopInQuote struct { + // SwapFee is the fee that the swap server is charging for the swap. + SwapFee btcutil.Amount + + // PrepayAmount is the part of the swap fee that is requested as a + // prepayment. + PrepayAmount btcutil.Amount + + // MinerFee is an estimate of the on-chain fee that needs to be paid to + // sweep the htlc. + MinerFee btcutil.Amount +} + // SwapInfoKit contains common swap info fields. type SwapInfoKit struct { // Hash is the sha256 hash of the preimage that unlocks the htlcs. It @@ -191,3 +292,13 @@ type SwapInfo struct { loopdb.SwapContract } + +// LastUpdate returns the last update time of the swap +func (s *In) LastUpdate() time.Time { + return s.LastUpdateTime +} + +// SwapHash returns the swap hash. +func (s *In) SwapHash() lntypes.Hash { + return s.Hash +} diff --git a/lndclient/chainnotifier_client.go b/lndclient/chainnotifier_client.go index 12e727e..4ff3b5f 100644 --- a/lndclient/chainnotifier_client.go +++ b/lndclient/chainnotifier_client.go @@ -125,7 +125,6 @@ func (s *chainNotifierClient) RegisterConfirmationsNtfn(ctx context.Context, txid *chainhash.Hash, pkScript []byte, numConfs, heightHint int32) ( chan *chainntnfs.TxConfirmation, chan error, error) { - // TODO: Height hint var txidSlice []byte if txid != nil { txidSlice = txid[:] diff --git a/lndclient/lightning_client.go b/lndclient/lightning_client.go index 9067a9d..d4e9c74 100644 --- a/lndclient/lightning_client.go +++ b/lndclient/lightning_client.go @@ -310,13 +310,19 @@ func (s *lightningClient) AddInvoice(ctx context.Context, rpcIn := &lnrpc.Invoice{ Memo: in.Memo, - RHash: in.Hash[:], Value: int64(in.Value), Expiry: in.Expiry, CltvExpiry: in.CltvExpiry, Private: true, } + if in.Preimage != nil { + rpcIn.RPreimage = in.Preimage[:] + } + if in.Hash != nil { + rpcIn.RHash = in.Hash[:] + } + resp, err := s.client.AddInvoice(rpcCtx, rpcIn) if err != nil { return lntypes.Hash{}, "", err diff --git a/loopin.go b/loopin.go new file mode 100644 index 0000000..8b09e23 --- /dev/null +++ b/loopin.go @@ -0,0 +1,621 @@ +package loop + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "fmt" + "time" + + "github.com/lightninglabs/loop/swap" + + "github.com/btcsuite/btcutil" + + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/channeldb" + + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" + + "github.com/lightninglabs/loop/lndclient" + "github.com/lightninglabs/loop/loopdb" + "github.com/lightningnetwork/lnd/lntypes" +) + +var ( + // MaxLoopInAcceptDelta configures the maximum acceptable number of + // remaining blocks until the on-chain htlc expires. This value is used + // to decide whether we want to continue with the swap parameters as + // proposed by the server. It is a protection to prevent the server from + // getting us to lock up our funds to an arbitrary point in the future. + MaxLoopInAcceptDelta = int32(1500) + + // MinLoopInPublishDelta defines the minimum number of remaining blocks + // until on-chain htlc expiry required to proceed to publishing the htlc + // tx. This value isn't critical, as we could even safely publish the + // htlc after expiry. The reason we do implement this check is to + // prevent us from publishing an htlc that the server surely wouldn't + // follow up to. + MinLoopInPublishDelta = int32(10) +) + +// loopInSwap contains all the in-memory state related to a pending loop in +// swap. +type loopInSwap struct { + swapKit + + loopdb.LoopInContract + + prePaymentChan chan lndclient.PaymentResult + + timeoutAddr btcutil.Address +} + +// newLoopInSwap initiates a new loop in swap. +func newLoopInSwap(globalCtx context.Context, cfg *swapConfig, + currentHeight int32, request *LoopInRequest) (*loopInSwap, error) { + + // Request current server loop in terms and use these to calculate the + // swap fee that we should subtract from the swap amount in the payment + // request that we send to the server. + quote, err := cfg.server.GetLoopInTerms(globalCtx) + if err != nil { + return nil, fmt.Errorf("loop in terms: %v", err) + } + + swapFee := swap.CalcFee( + request.Amount, quote.SwapFeeBase, quote.SwapFeeRate, + ) + + if swapFee > request.MaxSwapFee { + logger.Warnf("Swap fee %v exceeding maximum of %v", + swapFee, request.MaxSwapFee) + + return nil, ErrSwapFeeTooHigh + } + + // Calculate the swap invoice amount. The prepay is added which + // effectively forces the server to pay us back our prepayment on a + // successful swap. + swapInvoiceAmt := request.Amount - swapFee + quote.PrepayAmt + + // Generate random preimage. + var swapPreimage lntypes.Preimage + if _, err := rand.Read(swapPreimage[:]); err != nil { + logger.Error("Cannot generate preimage") + } + swapHash := lntypes.Hash(sha256.Sum256(swapPreimage[:])) + + // Derive a sender key for this swap. + keyDesc, err := cfg.lnd.WalletKit.DeriveNextKey( + globalCtx, swap.KeyFamily, + ) + if err != nil { + return nil, err + } + var senderKey [33]byte + copy(senderKey[:], keyDesc.PubKey.SerializeCompressed()) + + // Create the swap invoice in lnd. + _, swapInvoice, err := cfg.lnd.Client.AddInvoice( + globalCtx, &invoicesrpc.AddInvoiceData{ + Preimage: &swapPreimage, + Value: swapInvoiceAmt, + Memo: "swap", + Expiry: 3600 * 24 * 365, + }, + ) + if err != nil { + return nil, err + } + + // Post the swap parameters to the swap server. The response contains + // the server revocation key, the prepay invoice and the expiry height + // of the on-chain swap htlc. + logger.Infof("Initiating swap request at height %v", currentHeight) + swapResp, err := cfg.server.NewLoopInSwap(globalCtx, swapHash, + request.Amount, senderKey, swapInvoice, + ) + if err != nil { + return nil, fmt.Errorf("cannot initiate swap: %v", err) + } + + // Validate the response parameters the prevent us continuing with a + // swap that is based on parameters outside our allowed range. + err = validateLoopInContract(cfg.lnd, currentHeight, request, swapResp) + if err != nil { + return nil, err + } + + // Instantiate a struct that contains all required data to start the + // swap. + initiationTime := time.Now() + + contract := loopdb.LoopInContract{ + HtlcConfTarget: request.HtlcConfTarget, + LoopInChannel: request.LoopInChannel, + SwapContract: loopdb.SwapContract{ + InitiationHeight: currentHeight, + InitiationTime: initiationTime, + PrepayInvoice: swapResp.prepayInvoice, + ReceiverKey: swapResp.receiverKey, + SenderKey: senderKey, + Preimage: swapPreimage, + AmountRequested: request.Amount, + CltvExpiry: swapResp.expiry, + MaxMinerFee: request.MaxMinerFee, + MaxSwapFee: request.MaxSwapFee, + MaxPrepayRoutingFee: request.MaxPrepayRoutingFee, + }, + } + + swapKit, err := newSwapKit( + swapHash, TypeIn, cfg, &contract.SwapContract, + ) + if err != nil { + return nil, err + } + + swapKit.lastUpdateTime = initiationTime + + swap := &loopInSwap{ + LoopInContract: contract, + swapKit: *swapKit, + } + + // Persist the data before exiting this function, so that the caller can + // trust that this swap will be resumed on restart. + err = cfg.store.CreateLoopIn(swapHash, &swap.LoopInContract) + if err != nil { + return nil, fmt.Errorf("cannot store swap: %v", err) + } + + return swap, nil +} + +// resumeLoopInSwap returns a swap object representing a pending swap that has +// been restored from the database. +func resumeLoopInSwap(reqContext context.Context, cfg *swapConfig, + pend *loopdb.LoopIn) (*loopInSwap, error) { + + hash := lntypes.Hash(sha256.Sum256(pend.Contract.Preimage[:])) + + logger.Infof("Resuming loop in swap %v", hash) + + swapKit, err := newSwapKit( + hash, TypeIn, cfg, &pend.Contract.SwapContract, + ) + if err != nil { + return nil, err + } + + swap := &loopInSwap{ + LoopInContract: *pend.Contract, + swapKit: *swapKit, + } + + lastUpdate := pend.LastUpdate() + if lastUpdate == nil { + swap.lastUpdateTime = pend.Contract.InitiationTime + } else { + swap.state = lastUpdate.State + swap.lastUpdateTime = lastUpdate.Time + } + + return swap, nil +} + +// validateLoopInContract validates the contract parameters against our +// request. +func validateLoopInContract(lnd *lndclient.LndServices, + height int32, + request *LoopInRequest, + response *newLoopInResponse) error { + + // Check prepay amount. + chainParams := lnd.ChainParams + + prepayInvoiceAmt, err := swap.GetInvoiceAmt( + chainParams, response.prepayInvoice, + ) + if err != nil { + return err + } + + if prepayInvoiceAmt > request.MaxPrepayAmount { + logger.Warnf("Prepay amount %v exceeding maximum of %v", + prepayInvoiceAmt, request.MaxPrepayAmount) + + return ErrPrepayAmountTooHigh + } + + // Verify that we are not forced to publish an htlc that locks up our + // funds for too long in case the server doesn't follow through. + if response.expiry-height > MaxLoopInAcceptDelta { + return ErrExpiryTooFar + } + + return nil +} + +// execute starts/resumes the swap. It is a thin wrapper around executeSwap to +// conveniently handle the error case. +func (s *loopInSwap) execute(mainCtx context.Context, + cfg *executeConfig, height int32) error { + + s.executeConfig = *cfg + s.height = height + + // Announce swap by sending out an initial update. + err := s.sendUpdate(mainCtx) + if err != nil { + return err + } + + // Execute the swap until it either reaches a final state or a temporary + // error occurs. + err = s.executeSwap(mainCtx) + + // Sanity check. If there is no error, the swap must be in a final + // state. + if err == nil && s.state.Type() == loopdb.StateTypePending { + err = fmt.Errorf("swap in non-final state %v", s.state) + } + + // If an unexpected error happened, report a temporary failure + // but don't persist the error. Otherwise for example a + // connection error could lead to abandoning the swap + // permanently and losing funds. + if err != nil { + s.log.Errorf("Swap error: %v", err) + s.state = loopdb.StateFailTemporary + + // If we cannot send out this update, there is nothing we can do. + _ = s.sendUpdate(mainCtx) + + return err + } + + s.log.Infof("Loop in swap completed: %v "+ + "(final cost: server %v, onchain %v)", + s.state, + s.cost.Server, + s.cost.Onchain, + ) + + return nil +} + +// waitPrepayPaid waits for the prepay payment to complete and adds the amount +// to the balance of payments to the server. +func (s *loopInSwap) waitPrepayPaid(ctx context.Context) error { + s.log.Infof("Wait for prepay outcome") + + select { + case result := <-s.prePaymentChan: + if result.Err != nil { + // Server didn't take the prepayment. + s.log.Infof("Prepayment failed: %v", + result.Err) + } else { + s.cost.Server += result.PaidAmt + } + return nil + + case <-ctx.Done(): + return ctx.Err() + } +} + +// executeSwap executes the swap. +func (s *loopInSwap) executeSwap(globalCtx context.Context) error { + var err error + + // For loop in, the client takes the first step by publishing the + // on-chain htlc. Only do this is we haven't already done so in a + // previous run. + if s.state == loopdb.StateInitiated { + published, err := s.publishOnChainHtlc(globalCtx) + if err != nil { + return err + } + if !published { + return nil + } + } + + // Wait for the htlc to confirm. After a restart this will pick up a + // previously published tx. + conf, err := s.waitForHtlcConf(globalCtx) + if err != nil { + return err + } + + // Determine the htlc outpoint by inspecting the htlc tx. + htlcOutpoint, _, err := swap.GetScriptOutput( + conf.Tx, s.htlc.ScriptHash, + ) + if err != nil { + return err + } + + // Now that the htlc is on the chain, the server needs to take action. + // It requires us to pay the prepay before it will continue. + if err := s.payPrepay(globalCtx); err != nil { + return err + } + + // The server is expected to see the htlc on-chain and knowing that it + // can sweep that htlc with the preimage, it should pay our swap + // invoice, receive the preimage and sweep the htlc. We are waiting for + // this to happen and simultaneously watch the htlc expiry height. When + // the htlc expires, we will publish a timeout tx to reclaim the funds. + spend, err := s.waitForHtlcSpend(globalCtx, htlcOutpoint) + if err != nil { + return err + } + + // Determine the htlc input of the spending tx and inspect the witness + // to findout whether a success or a timeout tx spend the htlc. + htlcInput := spend.SpendingTx.TxIn[spend.SpenderInputIndex] + + if s.htlc.IsSuccessWitness(htlcInput.Witness) { + // The server swept the htlc. Swap invoice should have been + // paid. We are waiting for this event to be sure of this. + err := s.waitForSwapPaid(globalCtx) + if err != nil { + return err + } + + s.state = loopdb.StateSuccess + } else { + s.state = loopdb.StateFailTimeout + } + + // Wait for outcome of prepay payment (for accounting). + if err := s.waitPrepayPaid(globalCtx); err != nil { + return err + } + + // Persist swap outcome. + if err := s.persistState(globalCtx); err != nil { + return err + } + + return nil +} + +// waitForHtlcConf watches the chain until the htlc confirms. +func (s *loopInSwap) waitForHtlcConf(globalCtx context.Context) ( + *chainntnfs.TxConfirmation, error) { + + ctx, cancel := context.WithCancel(globalCtx) + defer cancel() + confChan, confErr, err := s.lnd.ChainNotifier.RegisterConfirmationsNtfn( + ctx, nil, s.htlc.ScriptHash, 1, s.InitiationHeight, + ) + if err != nil { + return nil, err + } + for { + select { + + // Htlc confirmed. + case conf := <-confChan: + return conf, nil + + // Conf ntfn error. + case err := <-confErr: + return nil, err + + // Keep up with block height. + case notification := <-s.blockEpochChan: + s.height = notification.(int32) + + // Cancel. + case <-globalCtx.Done(): + return nil, globalCtx.Err() + } + } +} + +// publishOnChainHtlc checks whether there are still enough blocks left and if +// so, it publishes the htlc and advances the swap state. +func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) { + var err error + + blocksRemaining := s.CltvExpiry - s.height + s.log.Infof("Blocks left until on-chain expiry: %v", blocksRemaining) + + // Verify whether it still makes sense to publish the htlc. + if blocksRemaining < MinLoopInPublishDelta { + s.state = loopdb.StateFailTimeout + return false, s.persistState(ctx) + } + + // Get fee estimate from lnd. + feeRate, err := s.lnd.WalletKit.EstimateFee( + ctx, s.LoopInContract.HtlcConfTarget, + ) + if err != nil { + return false, fmt.Errorf("estimate fee: %v", err) + } + + // Transition to state HtlcPublished before calling SendOutputs to + // prevent us from ever paying multiple times after a crash. + s.state = loopdb.StateHtlcPublished + err = s.persistState(ctx) + if err != nil { + return false, err + } + + s.log.Infof("Publishing on chain HTLC with fee rate %v", feeRate) + tx, err := s.lnd.WalletKit.SendOutputs(ctx, + []*wire.TxOut{{ + PkScript: s.htlc.ScriptHash, + Value: int64(s.LoopInContract.AmountRequested), + }}, + feeRate, + ) + if err != nil { + return false, fmt.Errorf("send outputs: %v", err) + } + s.log.Infof("Published on chain HTLC tx %v", tx.TxHash()) + + return true, nil + +} + +// payPrepay pays the prepay invoice obtained from the server. It stores the +// result channel for later use. +func (s *loopInSwap) payPrepay(ctx context.Context) error { + // Pay the prepay invoice. + s.log.Infof("Sending prepayment %v", s.PrepayInvoice) + s.prePaymentChan = s.lnd.Client.PayInvoice( + ctx, s.PrepayInvoice, s.MaxPrepayRoutingFee, + nil, + ) + + return nil +} + +// waitForHtlcSpend waits until a spending tx of the htlc gets confirmed and +// returns the spend details. +func (s *loopInSwap) waitForHtlcSpend(ctx context.Context, + htlc *wire.OutPoint) (*chainntnfs.SpendDetail, error) { + + // Register the htlc spend notification. + rpcCtx, cancel := context.WithCancel(ctx) + defer cancel() + spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn( + rpcCtx, nil, s.htlc.ScriptHash, s.InitiationHeight, + ) + if err != nil { + return nil, fmt.Errorf("register spend ntfn: %v", err) + } + + for { + select { + // Spend notification error. + case err := <-spendErr: + return nil, err + + case notification := <-s.blockEpochChan: + s.height = notification.(int32) + + if s.height >= s.LoopInContract.CltvExpiry { + err := s.publishTimeoutTx(ctx, htlc) + if err != nil { + return nil, err + } + } + + // Htlc spend, break loop. + case spendDetails := <-spendChan: + s.log.Infof("Htlc spend by tx: %v", + spendDetails.SpenderTxHash) + + return spendDetails, nil + + case <-ctx.Done(): + return nil, ctx.Err() + } + } +} + +// waitForSwapPaid waits until our swap invoice gets paid by the server. +func (s *loopInSwap) waitForSwapPaid(ctx context.Context) error { + // Wait for swap invoice to be paid. + rpcCtx, cancel := context.WithCancel(ctx) + defer cancel() + s.log.Infof("Subscribing to swap invoice %v", s.hash) + swapInvoiceChan, swapInvoiceErr, err := s.lnd.Invoices.SubscribeSingleInvoice( + rpcCtx, s.hash, + ) + if err != nil { + return err + } + + for { + select { + + // Swap invoice ntfn error. + case err := <-swapInvoiceErr: + return err + + case state := <-swapInvoiceChan: + s.log.Infof("Received swap invoice update: %v", state) + if state != channeldb.ContractSettled { + continue + } + + return nil + + case <-ctx.Done(): + return ctx.Err() + } + } +} + +// publishTimeoutTx publishes a timeout tx after the on-chain htlc has expired. +// The swap failed and we are reclaiming our funds. +func (s *loopInSwap) publishTimeoutTx(ctx context.Context, + htlc *wire.OutPoint) error { + + if s.timeoutAddr == nil { + var err error + s.timeoutAddr, err = s.lnd.WalletKit.NextAddr(ctx) + if err != nil { + return err + } + } + + // Calculate sweep tx fee + // + // TODO: Configure timeout conf. + fee, err := s.sweeper.GetSweepFee( + ctx, s.htlc.MaxTimeoutWitnessSize, 2, + ) + if err != nil { + return err + } + + witnessFunc := func(sig []byte) (wire.TxWitness, error) { + return s.htlc.GenTimeoutWitness(sig) + } + + timeoutTx, err := s.sweeper.CreateSweepTx( + ctx, s.height, s.htlc, *htlc, s.SenderKey, witnessFunc, + s.LoopInContract.AmountRequested, fee, s.timeoutAddr, + ) + if err != nil { + return err + } + + timeoutTxHash := timeoutTx.TxHash() + s.log.Infof("Publishing timeout tx %v with fee %v to addr %v", + timeoutTxHash, fee, s.timeoutAddr) + + err = s.lnd.WalletKit.PublishTransaction(ctx, timeoutTx) + if err != nil { + s.log.Warnf("publish timeout: %v", err) + } + + return nil +} + +// persistState updates the swap state and sends out an update notification. +func (s *loopInSwap) persistState(ctx context.Context) error { + updateTime := time.Now() + + s.lastUpdateTime = updateTime + + // Update state in store. + err := s.store.UpdateLoopIn(s.hash, updateTime, s.state) + if err != nil { + return err + } + + // Send out swap update + return s.sendUpdate(ctx) +} diff --git a/loopin_test.go b/loopin_test.go new file mode 100644 index 0000000..434ca18 --- /dev/null +++ b/loopin_test.go @@ -0,0 +1,385 @@ +package loop + +import ( + "context" + "testing" + + "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/swap" + "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/channeldb" + + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" +) + +var ( + testLoopInRequest = LoopInRequest{ + MaxPrepayAmount: btcutil.Amount(100), + Amount: btcutil.Amount(50000), + MaxSwapFee: btcutil.Amount(1000), + HtlcConfTarget: 2, + } +) + +func TestLoopInSuccess(t *testing.T) { + defer test.Guard(t)() + + ctx := newLoopInTestContext(t) + + height := int32(600) + + cfg := &swapConfig{ + lnd: &ctx.lnd.LndServices, + store: ctx.store, + server: ctx.server, + } + + swap, err := newLoopInSwap( + context.Background(), cfg, + height, &testLoopInRequest, + ) + if err != nil { + t.Fatal(err) + } + + ctx.store.assertLoopInStored() + + errChan := make(chan error) + go func() { + err := swap.execute(context.Background(), ctx.cfg, height) + if err != nil { + logger.Error(err) + } + errChan <- err + }() + + ctx.assertState(loopdb.StateInitiated) + + ctx.assertState(loopdb.StateHtlcPublished) + ctx.store.assertLoopInState(loopdb.StateHtlcPublished) + + // Expect htlc to be published. + htlcTx := <-ctx.lnd.SendOutputsChannel + + // Expect register for htlc conf. + <-ctx.lnd.RegisterConfChannel + + // Confirm htlc. + ctx.lnd.ConfChannel <- &chainntnfs.TxConfirmation{ + Tx: &htlcTx, + } + + // Expect prepay to be paid. + prepay := <-ctx.lnd.SendPaymentChannel + + prepayInvoice, err := ctx.lnd.DecodeInvoice(prepay.PaymentRequest) + if err != nil { + t.Fatal(err) + } + + logger.Infof("Prepay hash: %x", *prepayInvoice.PaymentHash) + + // Signal prepay pulled. + prepay.Done <- nil + + // Client starts listening for spend of htlc. + <-ctx.lnd.RegisterSpendChannel + + // Server spends htlc. + successTx := wire.MsgTx{} + successTx.AddTxIn(&wire.TxIn{ + // PreviousOutPoint: wire.OutPoint{ + // Hash: htlcTx.TxHash(), + // Index: uint32(htlcIndex), + // }, + Witness: [][]byte{{}, {}, {}}, + }) + + ctx.lnd.SpendChannel <- &chainntnfs.SpendDetail{ + SpendingTx: &successTx, + SpenderInputIndex: 0, + } + + // Client starts listening for swap invoice updates. + subscription := <-ctx.lnd.SingleInvoiceSubcribeChannel + if subscription.Hash != ctx.server.swapHash { + t.Fatal("client subscribing to wrong invoice") + } + + // Server has already paid invoice before spending the htlc. Signal + // settled. + subscription.Update <- channeldb.ContractSettled + + ctx.assertState(loopdb.StateSuccess) + ctx.store.assertLoopInState(loopdb.StateSuccess) + + err = <-errChan + if err != nil { + t.Fatal(err) + } +} + +func TestLoopInTimeout(t *testing.T) { + defer test.Guard(t)() + + ctx := newLoopInTestContext(t) + + height := int32(600) + + cfg := &swapConfig{ + lnd: &ctx.lnd.LndServices, + store: ctx.store, + server: ctx.server, + } + + swap, err := newLoopInSwap( + context.Background(), cfg, + height, &testLoopInRequest, + ) + if err != nil { + t.Fatal(err) + } + + ctx.store.assertLoopInStored() + + errChan := make(chan error) + go func() { + err := swap.execute(context.Background(), ctx.cfg, height) + if err != nil { + logger.Error(err) + } + errChan <- err + }() + + ctx.assertState(loopdb.StateInitiated) + + ctx.assertState(loopdb.StateHtlcPublished) + ctx.store.assertLoopInState(loopdb.StateHtlcPublished) + + // Expect htlc to be published. + htlcTx := <-ctx.lnd.SendOutputsChannel + + // Expect register for htlc conf. + <-ctx.lnd.RegisterConfChannel + + // Confirm htlc. + ctx.lnd.ConfChannel <- &chainntnfs.TxConfirmation{ + Tx: &htlcTx, + } + + // Expect prepay to be paid. + prepay := <-ctx.lnd.SendPaymentChannel + + prepayInvoice, err := ctx.lnd.DecodeInvoice(prepay.PaymentRequest) + if err != nil { + t.Fatal(err) + } + + logger.Infof("Prepay hash: %x", *prepayInvoice.PaymentHash) + + // Signal prepay pulled. + prepay.Done <- nil + + // Client starts listening for spend of htlc. + <-ctx.lnd.RegisterSpendChannel + + // Let htlc expire. + ctx.blockEpochChan <- swap.LoopInContract.CltvExpiry + + // Expect timeout tx to be published. + timeoutTx := <-ctx.lnd.TxPublishChannel + + // Confirm timeout tx. + ctx.lnd.SpendChannel <- &chainntnfs.SpendDetail{ + SpendingTx: timeoutTx, + SpenderInputIndex: 0, + } + + ctx.assertState(loopdb.StateFailTimeout) + ctx.store.assertLoopInState(loopdb.StateFailTimeout) + + err = <-errChan + if err != nil { + t.Fatal(err) + } +} + +func TestLoopInResume(t *testing.T) { + t.Run("initiated", func(t *testing.T) { + testLoopInResume(t, loopdb.StateInitiated, false) + }) + + t.Run("initiated expired", func(t *testing.T) { + testLoopInResume(t, loopdb.StateInitiated, true) + }) + + t.Run("htlc published", func(t *testing.T) { + testLoopInResume(t, loopdb.StateHtlcPublished, false) + }) +} + +func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool) { + defer test.Guard(t)() + + ctx := newLoopInTestContext(t) + + cfg := &swapConfig{ + lnd: &ctx.lnd.LndServices, + store: ctx.store, + server: ctx.server, + } + + senderKey := [33]byte{4} + receiverKey := [33]byte{5} + + prepayInvoice, err := getInvoice(testPreimage.Hash(), 5000, "prepay") + if err != nil { + t.Fatal(err) + } + contract := &loopdb.LoopInContract{ + HtlcConfTarget: 2, + SwapContract: loopdb.SwapContract{ + Preimage: testPreimage, + AmountRequested: 100000, + CltvExpiry: 744, + ReceiverKey: receiverKey, + SenderKey: senderKey, + MaxSwapFee: 60000, + PrepayInvoice: prepayInvoice, + MaxMinerFee: 50000, + }, + } + pendSwap := &loopdb.LoopIn{ + Contract: contract, + Loop: loopdb.Loop{ + Events: []*loopdb.LoopEvent{ + { + State: state, + }, + }, + Hash: testPreimage.Hash(), + }, + } + + htlc, err := swap.NewHtlc( + contract.CltvExpiry, contract.SenderKey, contract.ReceiverKey, + testPreimage.Hash(), + ) + if err != nil { + t.Fatal(err) + } + + err = ctx.store.CreateLoopIn(testPreimage.Hash(), contract) + if err != nil { + t.Fatal(err) + } + + swap, err := resumeLoopInSwap( + context.Background(), cfg, + pendSwap, + ) + if err != nil { + t.Fatal(err) + } + + var height int32 + if expired { + height = 740 + } else { + height = 600 + } + + errChan := make(chan error) + go func() { + err := swap.execute(context.Background(), ctx.cfg, height) + if err != nil { + logger.Error(err) + } + errChan <- err + }() + + defer func() { + err = <-errChan + if err != nil { + t.Fatal(err) + } + + select { + case <-ctx.lnd.SendPaymentChannel: + t.Fatal("unexpected payment sent") + default: + } + + select { + case <-ctx.lnd.SendOutputsChannel: + t.Fatal("unexpected tx published") + default: + } + }() + + var htlcTx wire.MsgTx + if state == loopdb.StateInitiated { + ctx.assertState(loopdb.StateInitiated) + + if expired { + ctx.assertState(loopdb.StateFailTimeout) + return + } + + ctx.assertState(loopdb.StateHtlcPublished) + ctx.store.assertLoopInState(loopdb.StateHtlcPublished) + + // Expect htlc to be published. + htlcTx = <-ctx.lnd.SendOutputsChannel + } else { + ctx.assertState(loopdb.StateHtlcPublished) + + htlcTx.AddTxOut(&wire.TxOut{ + PkScript: htlc.ScriptHash, + }) + } + + // Expect register for htlc conf. + <-ctx.lnd.RegisterConfChannel + + // Confirm htlc. + ctx.lnd.ConfChannel <- &chainntnfs.TxConfirmation{ + Tx: &htlcTx, + } + + // Expect prepay to be paid. + prepay := <-ctx.lnd.SendPaymentChannel + + // Signal prepay pulled. + prepay.Done <- nil + + // Client starts listening for spend of htlc. + <-ctx.lnd.RegisterSpendChannel + + // Server spends htlc. + successTx := wire.MsgTx{} + successTx.AddTxIn(&wire.TxIn{ + Witness: [][]byte{{}, {}, {}}, + }) + successTxHash := successTx.TxHash() + + ctx.lnd.SpendChannel <- &chainntnfs.SpendDetail{ + SpendingTx: &successTx, + SpenderTxHash: &successTxHash, + SpenderInputIndex: 0, + } + + // Client starts listening for swap invoice updates. + subscription := <-ctx.lnd.SingleInvoiceSubcribeChannel + if subscription.Hash != testPreimage.Hash() { + t.Fatal("client subscribing to wrong invoice") + } + + // Server has already paid invoice before spending the htlc. Signal + // settled. + subscription.Update <- channeldb.ContractSettled + + ctx.assertState(loopdb.StateSuccess) +} diff --git a/loopin_testcontext_test.go b/loopin_testcontext_test.go new file mode 100644 index 0000000..0bc95e4 --- /dev/null +++ b/loopin_testcontext_test.go @@ -0,0 +1,62 @@ +package loop + +import ( + "testing" + "time" + + "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/sweep" + "github.com/lightninglabs/loop/test" +) + +type loopInTestContext struct { + t *testing.T + lnd *test.LndMockServices + server *serverMock + store *storeMock + sweeper *sweep.Sweeper + cfg *executeConfig + statusChan chan SwapInfo + blockEpochChan chan interface{} +} + +func newLoopInTestContext(t *testing.T) *loopInTestContext { + lnd := test.NewMockLnd() + server := newServerMock() + store := newStoreMock(t) + sweeper := sweep.Sweeper{Lnd: &lnd.LndServices} + + blockEpochChan := make(chan interface{}) + statusChan := make(chan SwapInfo) + + expiryChan := make(chan time.Time) + timerFactory := func(expiry time.Duration) <-chan time.Time { + return expiryChan + } + + cfg := executeConfig{ + statusChan: statusChan, + sweeper: &sweeper, + blockEpochChan: blockEpochChan, + timerFactory: timerFactory, + } + + return &loopInTestContext{ + t: t, + lnd: lnd, + server: server, + store: store, + sweeper: &sweeper, + cfg: &cfg, + statusChan: statusChan, + blockEpochChan: blockEpochChan, + } +} + +func (c *loopInTestContext) assertState(expectedState loopdb.SwapState) { + state := <-c.statusChan + if state.State != expectedState { + c.t.Fatalf("expected state %v but got %v", expectedState, + state.State) + } +} diff --git a/loopout.go b/loopout.go index 065eaaf..427995d 100644 --- a/loopout.go +++ b/loopout.go @@ -135,7 +135,7 @@ func resumeLoopOutSwap(reqContext context.Context, cfg *swapConfig, hash := lntypes.Hash(sha256.Sum256(pend.Contract.Preimage[:])) - logger.Infof("Resuming swap %v", hash) + logger.Infof("Resuming loop out swap %v", hash) swapKit, err := newSwapKit( hash, TypeOut, cfg, &pend.Contract.SwapContract, diff --git a/looprpc/client.pb.go b/looprpc/client.pb.go index 5f9a660..36eff70 100644 --- a/looprpc/client.pb.go +++ b/looprpc/client.pb.go @@ -29,20 +29,24 @@ type SwapType int32 const ( // LOOP_OUT indicates an loop out swap (off-chain to on-chain) SwapType_LOOP_OUT SwapType = 0 + // LOOP_IN indicates a loop in swap (on-chain to off-chain) + SwapType_LOOP_IN SwapType = 1 ) var SwapType_name = map[int32]string{ 0: "LOOP_OUT", + 1: "LOOP_IN", } var SwapType_value = map[string]int32{ "LOOP_OUT": 0, + "LOOP_IN": 1, } func (x SwapType) String() string { return proto.EnumName(SwapType_name, int32(x)) } func (SwapType) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_client_ecf1f0d12250b1cb, []int{0} + return fileDescriptor_client_592ed489859e5ea5, []int{0} } type SwapState int32 @@ -61,6 +65,10 @@ const ( // case where we wait for fees to come down before we sweep. SwapState_PREIMAGE_REVEALED SwapState = 1 // * + // HTLC_PUBLISHED is reached when the htlc tx has been published in a loop in + // swap. + SwapState_HTLC_PUBLISHED SwapState = 2 + // * // SUCCESS is the final swap state that is reached when the sweep tx has // the required confirmation depth. SwapState_SUCCESS SwapState = 3 @@ -73,12 +81,14 @@ const ( var SwapState_name = map[int32]string{ 0: "INITIATED", 1: "PREIMAGE_REVEALED", + 2: "HTLC_PUBLISHED", 3: "SUCCESS", 4: "FAILED", } var SwapState_value = map[string]int32{ "INITIATED": 0, "PREIMAGE_REVEALED": 1, + "HTLC_PUBLISHED": 2, "SUCCESS": 3, "FAILED": 4, } @@ -87,7 +97,7 @@ func (x SwapState) String() string { return proto.EnumName(SwapState_name, int32(x)) } func (SwapState) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_client_ecf1f0d12250b1cb, []int{1} + return fileDescriptor_client_592ed489859e5ea5, []int{1} } type LoopOutRequest struct { @@ -145,7 +155,7 @@ func (m *LoopOutRequest) Reset() { *m = LoopOutRequest{} } func (m *LoopOutRequest) String() string { return proto.CompactTextString(m) } func (*LoopOutRequest) ProtoMessage() {} func (*LoopOutRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_client_ecf1f0d12250b1cb, []int{0} + return fileDescriptor_client_592ed489859e5ea5, []int{0} } func (m *LoopOutRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_LoopOutRequest.Unmarshal(m, b) @@ -221,6 +231,107 @@ func (m *LoopOutRequest) GetLoopOutChannel() uint64 { return 0 } +type LoopInRequest struct { + // * + // Requested swap amount in sat. This does not include the swap and miner + // fee. + Amt int64 `protobuf:"varint,1,opt,name=amt,proto3" json:"amt,omitempty"` + // * + // Maximum off-chain fee in msat that may be paid for payment to the server. + // This limit is applied during path finding. Typically this value is taken + // from the response of the GetQuote call. + MaxPrepayRoutingFee int64 `protobuf:"varint,2,opt,name=max_prepay_routing_fee,json=maxPrepayRoutingFee,proto3" json:"max_prepay_routing_fee,omitempty"` + // * + // Maximum we are willing to pay the server for the swap. This value is not + // disclosed in the swap initiation call, but if the server asks for a + // higher fee, we abort the swap. Typically this value is taken from the + // response of the GetQuote call. It includes the prepay amount. + MaxSwapFee int64 `protobuf:"varint,3,opt,name=max_swap_fee,json=maxSwapFee,proto3" json:"max_swap_fee,omitempty"` + // * + // Maximum amount of the swap fee that may be charged as a prepayment. + MaxPrepayAmt int64 `protobuf:"varint,4,opt,name=max_prepay_amt,json=maxPrepayAmt,proto3" json:"max_prepay_amt,omitempty"` + // * + // Maximum in on-chain fees that we are willing to spent. If we want to + // publish the on-chain htlc and the fee estimate turns out higher than this + // value, we cancel the swap. + // + // max_miner_fee is typically taken from the response of the GetQuote call. + MaxMinerFee int64 `protobuf:"varint,5,opt,name=max_miner_fee,json=maxMinerFee,proto3" json:"max_miner_fee,omitempty"` + // * + // The channel to loop in. If zero, the channel to loop in is selected based + // on the lowest routing fee for the swap payment from the server. + LoopInChannel uint64 `protobuf:"varint,6,opt,name=loop_in_channel,json=loopInChannel,proto3" json:"loop_in_channel,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *LoopInRequest) Reset() { *m = LoopInRequest{} } +func (m *LoopInRequest) String() string { return proto.CompactTextString(m) } +func (*LoopInRequest) ProtoMessage() {} +func (*LoopInRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_client_592ed489859e5ea5, []int{1} +} +func (m *LoopInRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_LoopInRequest.Unmarshal(m, b) +} +func (m *LoopInRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_LoopInRequest.Marshal(b, m, deterministic) +} +func (dst *LoopInRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_LoopInRequest.Merge(dst, src) +} +func (m *LoopInRequest) XXX_Size() int { + return xxx_messageInfo_LoopInRequest.Size(m) +} +func (m *LoopInRequest) XXX_DiscardUnknown() { + xxx_messageInfo_LoopInRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_LoopInRequest proto.InternalMessageInfo + +func (m *LoopInRequest) GetAmt() int64 { + if m != nil { + return m.Amt + } + return 0 +} + +func (m *LoopInRequest) GetMaxPrepayRoutingFee() int64 { + if m != nil { + return m.MaxPrepayRoutingFee + } + return 0 +} + +func (m *LoopInRequest) GetMaxSwapFee() int64 { + if m != nil { + return m.MaxSwapFee + } + return 0 +} + +func (m *LoopInRequest) GetMaxPrepayAmt() int64 { + if m != nil { + return m.MaxPrepayAmt + } + return 0 +} + +func (m *LoopInRequest) GetMaxMinerFee() int64 { + if m != nil { + return m.MaxMinerFee + } + return 0 +} + +func (m *LoopInRequest) GetLoopInChannel() uint64 { + if m != nil { + return m.LoopInChannel + } + return 0 +} + type SwapResponse struct { // * // Swap identifier to track status in the update stream that is returned from @@ -235,7 +346,7 @@ func (m *SwapResponse) Reset() { *m = SwapResponse{} } func (m *SwapResponse) String() string { return proto.CompactTextString(m) } func (*SwapResponse) ProtoMessage() {} func (*SwapResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_client_ecf1f0d12250b1cb, []int{1} + return fileDescriptor_client_592ed489859e5ea5, []int{2} } func (m *SwapResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_SwapResponse.Unmarshal(m, b) @@ -272,7 +383,7 @@ func (m *MonitorRequest) Reset() { *m = MonitorRequest{} } func (m *MonitorRequest) String() string { return proto.CompactTextString(m) } func (*MonitorRequest) ProtoMessage() {} func (*MonitorRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_client_ecf1f0d12250b1cb, []int{2} + return fileDescriptor_client_592ed489859e5ea5, []int{3} } func (m *MonitorRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_MonitorRequest.Unmarshal(m, b) @@ -325,7 +436,7 @@ func (m *SwapStatus) Reset() { *m = SwapStatus{} } func (m *SwapStatus) String() string { return proto.CompactTextString(m) } func (*SwapStatus) ProtoMessage() {} func (*SwapStatus) Descriptor() ([]byte, []int) { - return fileDescriptor_client_ecf1f0d12250b1cb, []int{3} + return fileDescriptor_client_592ed489859e5ea5, []int{4} } func (m *SwapStatus) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_SwapStatus.Unmarshal(m, b) @@ -404,7 +515,7 @@ func (m *TermsRequest) Reset() { *m = TermsRequest{} } func (m *TermsRequest) String() string { return proto.CompactTextString(m) } func (*TermsRequest) ProtoMessage() {} func (*TermsRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_client_ecf1f0d12250b1cb, []int{4} + return fileDescriptor_client_592ed489859e5ea5, []int{5} } func (m *TermsRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_TermsRequest.Unmarshal(m, b) @@ -446,10 +557,7 @@ type TermsResponse struct { MaxSwapAmount int64 `protobuf:"varint,6,opt,name=max_swap_amount,json=maxSwapAmount,proto3" json:"max_swap_amount,omitempty"` // * // On-chain cltv expiry delta - CltvDelta int32 `protobuf:"varint,7,opt,name=cltv_delta,json=cltvDelta,proto3" json:"cltv_delta,omitempty"` - // * - // Maximum cltv expiry delta - MaxCltv int32 `protobuf:"varint,8,opt,name=max_cltv,json=maxCltv,proto3" json:"max_cltv,omitempty"` + CltvDelta int32 `protobuf:"varint,7,opt,name=cltv_delta,json=cltvDelta,proto3" json:"cltv_delta,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -459,7 +567,7 @@ func (m *TermsResponse) Reset() { *m = TermsResponse{} } func (m *TermsResponse) String() string { return proto.CompactTextString(m) } func (*TermsResponse) ProtoMessage() {} func (*TermsResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_client_ecf1f0d12250b1cb, []int{5} + return fileDescriptor_client_592ed489859e5ea5, []int{6} } func (m *TermsResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_TermsResponse.Unmarshal(m, b) @@ -528,13 +636,6 @@ func (m *TermsResponse) GetCltvDelta() int32 { return 0 } -func (m *TermsResponse) GetMaxCltv() int32 { - if m != nil { - return m.MaxCltv - } - return 0 -} - type QuoteRequest struct { // * // The amount to swap in satoshis. @@ -548,7 +649,7 @@ func (m *QuoteRequest) Reset() { *m = QuoteRequest{} } func (m *QuoteRequest) String() string { return proto.CompactTextString(m) } func (*QuoteRequest) ProtoMessage() {} func (*QuoteRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_client_ecf1f0d12250b1cb, []int{6} + return fileDescriptor_client_592ed489859e5ea5, []int{7} } func (m *QuoteRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_QuoteRequest.Unmarshal(m, b) @@ -594,7 +695,7 @@ func (m *QuoteResponse) Reset() { *m = QuoteResponse{} } func (m *QuoteResponse) String() string { return proto.CompactTextString(m) } func (*QuoteResponse) ProtoMessage() {} func (*QuoteResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_client_ecf1f0d12250b1cb, []int{7} + return fileDescriptor_client_592ed489859e5ea5, []int{8} } func (m *QuoteResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_QuoteResponse.Unmarshal(m, b) @@ -637,6 +738,7 @@ func (m *QuoteResponse) GetMinerFee() int64 { func init() { proto.RegisterType((*LoopOutRequest)(nil), "looprpc.LoopOutRequest") + proto.RegisterType((*LoopInRequest)(nil), "looprpc.LoopInRequest") proto.RegisterType((*SwapResponse)(nil), "looprpc.SwapResponse") proto.RegisterType((*MonitorRequest)(nil), "looprpc.MonitorRequest") proto.RegisterType((*SwapStatus)(nil), "looprpc.SwapStatus") @@ -666,6 +768,12 @@ type SwapClientClient interface { // point onwards, progress can be tracked via the SwapStatus stream that is // returned from Monitor(). LoopOut(ctx context.Context, in *LoopOutRequest, opts ...grpc.CallOption) (*SwapResponse, error) + // * + // LoopIn initiates a loop in swap with the given parameters. The call + // returns after the swap has been set up with the swap server. From that + // point onwards, progress can be tracked via the SwapStatus stream + // that is returned from Monitor(). + LoopIn(ctx context.Context, in *LoopInRequest, opts ...grpc.CallOption) (*SwapResponse, error) // * loop: `monitor` // Monitor will return a stream of swap updates for currently active swaps. // TODO: add MonitorSync version for REST clients. @@ -677,6 +785,12 @@ type SwapClientClient interface { // LoopOutQuote returns a quote for a loop out swap with the provided // parameters. LoopOutQuote(ctx context.Context, in *QuoteRequest, opts ...grpc.CallOption) (*QuoteResponse, error) + // * + // GetTerms returns the terms that the server enforces for swaps. + GetLoopInTerms(ctx context.Context, in *TermsRequest, opts ...grpc.CallOption) (*TermsResponse, error) + // * + // GetQuote returns a quote for a swap with the provided parameters. + GetLoopInQuote(ctx context.Context, in *QuoteRequest, opts ...grpc.CallOption) (*QuoteResponse, error) } type swapClientClient struct { @@ -696,6 +810,15 @@ func (c *swapClientClient) LoopOut(ctx context.Context, in *LoopOutRequest, opts return out, nil } +func (c *swapClientClient) LoopIn(ctx context.Context, in *LoopInRequest, opts ...grpc.CallOption) (*SwapResponse, error) { + out := new(SwapResponse) + err := c.cc.Invoke(ctx, "/looprpc.SwapClient/LoopIn", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *swapClientClient) Monitor(ctx context.Context, in *MonitorRequest, opts ...grpc.CallOption) (SwapClient_MonitorClient, error) { stream, err := c.cc.NewStream(ctx, &_SwapClient_serviceDesc.Streams[0], "/looprpc.SwapClient/Monitor", opts...) if err != nil { @@ -746,6 +869,24 @@ func (c *swapClientClient) LoopOutQuote(ctx context.Context, in *QuoteRequest, o return out, nil } +func (c *swapClientClient) GetLoopInTerms(ctx context.Context, in *TermsRequest, opts ...grpc.CallOption) (*TermsResponse, error) { + out := new(TermsResponse) + err := c.cc.Invoke(ctx, "/looprpc.SwapClient/GetLoopInTerms", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *swapClientClient) GetLoopInQuote(ctx context.Context, in *QuoteRequest, opts ...grpc.CallOption) (*QuoteResponse, error) { + out := new(QuoteResponse) + err := c.cc.Invoke(ctx, "/looprpc.SwapClient/GetLoopInQuote", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // SwapClientServer is the server API for SwapClient service. type SwapClientServer interface { // * loop: `out` @@ -754,6 +895,12 @@ type SwapClientServer interface { // point onwards, progress can be tracked via the SwapStatus stream that is // returned from Monitor(). LoopOut(context.Context, *LoopOutRequest) (*SwapResponse, error) + // * + // LoopIn initiates a loop in swap with the given parameters. The call + // returns after the swap has been set up with the swap server. From that + // point onwards, progress can be tracked via the SwapStatus stream + // that is returned from Monitor(). + LoopIn(context.Context, *LoopInRequest) (*SwapResponse, error) // * loop: `monitor` // Monitor will return a stream of swap updates for currently active swaps. // TODO: add MonitorSync version for REST clients. @@ -765,6 +912,12 @@ type SwapClientServer interface { // LoopOutQuote returns a quote for a loop out swap with the provided // parameters. LoopOutQuote(context.Context, *QuoteRequest) (*QuoteResponse, error) + // * + // GetTerms returns the terms that the server enforces for swaps. + GetLoopInTerms(context.Context, *TermsRequest) (*TermsResponse, error) + // * + // GetQuote returns a quote for a swap with the provided parameters. + GetLoopInQuote(context.Context, *QuoteRequest) (*QuoteResponse, error) } func RegisterSwapClientServer(s *grpc.Server, srv SwapClientServer) { @@ -789,6 +942,24 @@ func _SwapClient_LoopOut_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _SwapClient_LoopIn_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LoopInRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwapClientServer).LoopIn(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.SwapClient/LoopIn", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwapClientServer).LoopIn(ctx, req.(*LoopInRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _SwapClient_Monitor_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(MonitorRequest) if err := stream.RecvMsg(m); err != nil { @@ -846,6 +1017,42 @@ func _SwapClient_LoopOutQuote_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _SwapClient_GetLoopInTerms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TermsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwapClientServer).GetLoopInTerms(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.SwapClient/GetLoopInTerms", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwapClientServer).GetLoopInTerms(ctx, req.(*TermsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SwapClient_GetLoopInQuote_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QuoteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwapClientServer).GetLoopInQuote(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.SwapClient/GetLoopInQuote", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwapClientServer).GetLoopInQuote(ctx, req.(*QuoteRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _SwapClient_serviceDesc = grpc.ServiceDesc{ ServiceName: "looprpc.SwapClient", HandlerType: (*SwapClientServer)(nil), @@ -854,6 +1061,10 @@ var _SwapClient_serviceDesc = grpc.ServiceDesc{ MethodName: "LoopOut", Handler: _SwapClient_LoopOut_Handler, }, + { + MethodName: "LoopIn", + Handler: _SwapClient_LoopIn_Handler, + }, { MethodName: "LoopOutTerms", Handler: _SwapClient_LoopOutTerms_Handler, @@ -862,6 +1073,14 @@ var _SwapClient_serviceDesc = grpc.ServiceDesc{ MethodName: "LoopOutQuote", Handler: _SwapClient_LoopOutQuote_Handler, }, + { + MethodName: "GetLoopInTerms", + Handler: _SwapClient_GetLoopInTerms_Handler, + }, + { + MethodName: "GetLoopInQuote", + Handler: _SwapClient_GetLoopInQuote_Handler, + }, }, Streams: []grpc.StreamDesc{ { @@ -873,60 +1092,65 @@ var _SwapClient_serviceDesc = grpc.ServiceDesc{ Metadata: "client.proto", } -func init() { proto.RegisterFile("client.proto", fileDescriptor_client_ecf1f0d12250b1cb) } - -var fileDescriptor_client_ecf1f0d12250b1cb = []byte{ - // 825 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x54, 0xcb, 0x72, 0xe3, 0x44, - 0x14, 0x1d, 0xc9, 0x4e, 0x6c, 0xdd, 0xc8, 0x8a, 0xdd, 0xf3, 0xf2, 0x18, 0x86, 0x32, 0x2a, 0x1e, - 0xae, 0x2c, 0x22, 0xc8, 0xac, 0x60, 0x67, 0x1c, 0x0f, 0x65, 0x2a, 0x21, 0xa6, 0xed, 0x50, 0xc5, - 0x4a, 0xd5, 0x63, 0xf7, 0x64, 0x54, 0xa5, 0x56, 0x6b, 0xd4, 0xad, 0x8c, 0x5d, 0x14, 0x1b, 0x7e, - 0x81, 0xdf, 0x60, 0xc5, 0xaf, 0xb0, 0xe0, 0x07, 0xf8, 0x05, 0xf6, 0x54, 0x3f, 0xec, 0x48, 0x81, - 0xd9, 0x75, 0x9d, 0x3e, 0xf7, 0x74, 0xf7, 0xb9, 0xe7, 0x36, 0xf8, 0xab, 0x34, 0xa1, 0x99, 0x3c, - 0xcd, 0x0b, 0x2e, 0x39, 0x6a, 0xa5, 0x9c, 0xe7, 0x45, 0xbe, 0x1a, 0x7c, 0x78, 0xc3, 0xf9, 0x4d, - 0x4a, 0x23, 0x92, 0x27, 0x11, 0xc9, 0x32, 0x2e, 0x89, 0x4c, 0x78, 0x26, 0x0c, 0x2d, 0xfc, 0xc3, - 0x85, 0xe0, 0x82, 0xf3, 0xfc, 0xaa, 0x94, 0x98, 0xbe, 0x2d, 0xa9, 0x90, 0xa8, 0x0b, 0x0d, 0xc2, - 0x64, 0xdf, 0x19, 0x3a, 0xa3, 0x06, 0x56, 0x4b, 0x84, 0xa0, 0xb9, 0xa6, 0x42, 0xf6, 0xdd, 0xa1, - 0x33, 0xf2, 0xb0, 0x5e, 0xa3, 0x08, 0x1e, 0x31, 0xb2, 0x89, 0xc5, 0x3b, 0x92, 0xc7, 0x05, 0x2f, - 0x65, 0x92, 0xdd, 0xc4, 0xaf, 0x29, 0xed, 0x37, 0x74, 0x59, 0x8f, 0x91, 0xcd, 0xe2, 0x1d, 0xc9, - 0xb1, 0xd9, 0x79, 0x49, 0x29, 0x7a, 0x01, 0x4f, 0x54, 0x41, 0x5e, 0xd0, 0x9c, 0x6c, 0x6b, 0x25, - 0x4d, 0x5d, 0xf2, 0x90, 0x91, 0xcd, 0x5c, 0x6f, 0x56, 0x8a, 0x86, 0xe0, 0xef, 0x4f, 0x51, 0xd4, - 0x03, 0x4d, 0x05, 0xab, 0xae, 0x18, 0x9f, 0x40, 0x50, 0x91, 0x55, 0x17, 0x3f, 0xd4, 0x1c, 0x7f, - 0x2f, 0x37, 0x66, 0x12, 0x85, 0xd0, 0x51, 0x2c, 0x96, 0x64, 0xb4, 0xd0, 0x42, 0x2d, 0x4d, 0x3a, - 0x62, 0x64, 0x73, 0xa9, 0x30, 0xa5, 0x34, 0x82, 0xae, 0xf2, 0x2c, 0xe6, 0xa5, 0x8c, 0x57, 0x6f, - 0x48, 0x96, 0xd1, 0xb4, 0xdf, 0x1e, 0x3a, 0xa3, 0x26, 0x0e, 0x52, 0xe3, 0xd0, 0xc4, 0xa0, 0xe1, - 0x47, 0xe0, 0xeb, 0xc7, 0x51, 0x91, 0xf3, 0x4c, 0x50, 0x14, 0x80, 0x9b, 0xac, 0xb5, 0x61, 0x1e, - 0x76, 0x93, 0x75, 0xd8, 0x85, 0xe0, 0x92, 0x67, 0x89, 0xe4, 0x85, 0xf5, 0x34, 0xfc, 0xc7, 0x01, - 0x50, 0x25, 0x0b, 0x49, 0x64, 0x29, 0xfe, 0xc7, 0x62, 0x23, 0xe1, 0xee, 0x24, 0xd0, 0xa7, 0xd0, - 0x94, 0xdb, 0xdc, 0xd8, 0x19, 0x9c, 0xf5, 0x4e, 0x6d, 0x37, 0x4f, 0x95, 0xc8, 0x72, 0x9b, 0x53, - 0xac, 0xb7, 0xd1, 0x08, 0x0e, 0x84, 0x24, 0xd2, 0x78, 0x18, 0x9c, 0xa1, 0x1a, 0x4f, 0x1d, 0x46, - 0xb1, 0x21, 0xa0, 0xcf, 0xe1, 0x38, 0xc9, 0x12, 0x99, 0xe8, 0xee, 0xc7, 0x32, 0x61, 0x3b, 0x33, - 0x83, 0x3b, 0x78, 0x99, 0x30, 0x63, 0x03, 0x11, 0x32, 0x2e, 0xf3, 0x35, 0x91, 0xd4, 0x30, 0x8d, - 0xa5, 0x81, 0xc2, 0xaf, 0x35, 0xac, 0x99, 0x1f, 0x83, 0xff, 0x46, 0xa6, 0xab, 0x98, 0xac, 0xd7, - 0x05, 0x15, 0x42, 0x7b, 0xea, 0xe1, 0x23, 0x85, 0x8d, 0x0d, 0x14, 0x06, 0xe0, 0x2f, 0x69, 0xc1, - 0xc4, 0xce, 0x87, 0xdf, 0x5d, 0xe8, 0x58, 0xc0, 0x7a, 0x77, 0x02, 0x3d, 0xdd, 0xdd, 0x9c, 0x6c, - 0x19, 0xcd, 0x64, 0xac, 0x83, 0x66, 0xac, 0x3c, 0x56, 0x1b, 0x73, 0x83, 0x9f, 0xab, 0xcc, 0x85, - 0xd0, 0xd9, 0x25, 0x21, 0x7e, 0x45, 0x04, 0xd5, 0x7e, 0x35, 0xf0, 0x91, 0x30, 0x59, 0xf8, 0x86, - 0x08, 0x5a, 0xe3, 0x14, 0xca, 0x99, 0x46, 0x8d, 0x83, 0x95, 0x17, 0xcf, 0x01, 0x2a, 0x79, 0x31, - 0xf1, 0xf3, 0xf2, 0x7d, 0x58, 0x3e, 0x83, 0x63, 0x96, 0x64, 0x26, 0x74, 0x84, 0xf1, 0x32, 0x93, - 0xd6, 0xaa, 0x0e, 0x4b, 0x32, 0x65, 0xec, 0x58, 0x83, 0x9a, 0xb7, 0x0b, 0xa7, 0xe5, 0x1d, 0x5a, - 0x9e, 0xc9, 0xa7, 0xe5, 0x3d, 0x07, 0x58, 0xa5, 0xf2, 0x36, 0x5e, 0xd3, 0x54, 0x12, 0xed, 0xd2, - 0x01, 0xf6, 0x14, 0x72, 0xae, 0x00, 0xf4, 0x0c, 0xda, 0x4a, 0x46, 0x01, 0x3a, 0x6f, 0x07, 0xb8, - 0xc5, 0xc8, 0x66, 0x92, 0xca, 0xdb, 0x70, 0x08, 0xfe, 0x0f, 0x25, 0x97, 0xf4, 0xbd, 0xa3, 0x19, - 0xbe, 0x86, 0x8e, 0x65, 0x58, 0x3f, 0x9f, 0x41, 0x7b, 0x3f, 0x2d, 0x86, 0xd7, 0xb2, 0x4f, 0xbf, - 0xf7, 0x6c, 0xf7, 0xfe, 0xb3, 0x3f, 0x00, 0xef, 0x6e, 0x3e, 0x8c, 0x6b, 0x6d, 0x66, 0x87, 0xe3, - 0xa4, 0x0f, 0xed, 0x5d, 0xf4, 0x90, 0x0f, 0xed, 0x8b, 0xab, 0xab, 0x79, 0x7c, 0x75, 0xbd, 0xec, - 0x3e, 0x38, 0xf9, 0x0e, 0xbc, 0x7d, 0xd8, 0x50, 0x07, 0xbc, 0xd9, 0xf7, 0xb3, 0xe5, 0x6c, 0xbc, - 0x9c, 0x9e, 0x77, 0x1f, 0xa0, 0xc7, 0xd0, 0x9b, 0xe3, 0xe9, 0xec, 0x72, 0xfc, 0xed, 0x34, 0xc6, - 0xd3, 0x1f, 0xa7, 0xe3, 0x8b, 0xe9, 0x79, 0xd7, 0x41, 0x47, 0xd0, 0x5a, 0x5c, 0x4f, 0x26, 0xd3, - 0xc5, 0xa2, 0xdb, 0x40, 0x00, 0x87, 0x2f, 0xc7, 0x33, 0xb5, 0xd1, 0x3c, 0xfb, 0xcb, 0x35, 0x63, - 0x32, 0xd1, 0x3f, 0x19, 0xc2, 0xd0, 0xb2, 0x7f, 0x13, 0x7a, 0xba, 0x4f, 0x76, 0xfd, 0xb7, 0x1a, - 0x3c, 0xae, 0x45, 0x7e, 0x67, 0x43, 0xf8, 0xf4, 0xd7, 0x3f, 0xff, 0xfe, 0xcd, 0xed, 0x85, 0x7e, - 0x74, 0xfb, 0x65, 0xa4, 0x18, 0x11, 0x2f, 0xe5, 0xd7, 0xce, 0x09, 0xfa, 0x0a, 0x5a, 0x76, 0x36, - 0x2b, 0x9a, 0xf5, 0x69, 0x1d, 0x3c, 0xfc, 0xcf, 0x18, 0x95, 0xe2, 0x0b, 0x07, 0xfd, 0x04, 0xbe, - 0x3d, 0x5c, 0x47, 0x18, 0xdd, 0x1d, 0x5d, 0xcd, 0xf8, 0xe0, 0xc9, 0x7d, 0xd8, 0x5e, 0x69, 0xa0, - 0xaf, 0xf4, 0x08, 0xa1, 0xea, 0x95, 0x22, 0xa9, 0xa5, 0xe2, 0xbd, 0xb4, 0xee, 0x66, 0x45, 0xba, - 0xda, 0xff, 0x8a, 0x74, 0xad, 0xe9, 0xe1, 0x50, 0x4b, 0x0f, 0x50, 0xbf, 0x26, 0xfd, 0x56, 0x71, - 0xa2, 0x9f, 0x09, 0x93, 0xbf, 0xbc, 0x3a, 0xd4, 0xdf, 0xfd, 0x8b, 0x7f, 0x03, 0x00, 0x00, 0xff, - 0xff, 0x2d, 0xba, 0xce, 0x65, 0x25, 0x06, 0x00, 0x00, +func init() { proto.RegisterFile("client.proto", fileDescriptor_client_592ed489859e5ea5) } + +var fileDescriptor_client_592ed489859e5ea5 = []byte{ + // 906 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x55, 0xcd, 0x6e, 0xdb, 0x46, + 0x18, 0x0c, 0x29, 0x59, 0x3f, 0x9f, 0x29, 0x9a, 0xda, 0x24, 0x8e, 0xaa, 0x36, 0x85, 0x4a, 0x34, + 0xa9, 0xe0, 0x83, 0xd5, 0x3a, 0x87, 0xa2, 0xbd, 0x14, 0x8a, 0xa4, 0x24, 0x02, 0xe4, 0x58, 0xa5, + 0xe4, 0x02, 0xbd, 0x94, 0xd8, 0x48, 0x1b, 0x87, 0x00, 0xb9, 0xcb, 0x90, 0xcb, 0xc4, 0x46, 0xd1, + 0x4b, 0xdf, 0xa0, 0xed, 0x9b, 0xf4, 0x55, 0xfa, 0x0a, 0x3d, 0xf5, 0xde, 0x7b, 0xb1, 0x3f, 0xa4, + 0x49, 0xbb, 0x06, 0x8c, 0xdc, 0xa8, 0xd9, 0xd9, 0xe1, 0xce, 0x70, 0xbe, 0x15, 0x58, 0x9b, 0x30, + 0x20, 0x94, 0x1f, 0xc6, 0x09, 0xe3, 0x0c, 0x35, 0x43, 0xc6, 0xe2, 0x24, 0xde, 0xf4, 0x3f, 0x39, + 0x63, 0xec, 0x2c, 0x24, 0x23, 0x1c, 0x07, 0x23, 0x4c, 0x29, 0xe3, 0x98, 0x07, 0x8c, 0xa6, 0x8a, + 0xe6, 0xfe, 0x69, 0x82, 0xbd, 0x60, 0x2c, 0x3e, 0xc9, 0xb8, 0x47, 0xde, 0x66, 0x24, 0xe5, 0xc8, + 0x81, 0x1a, 0x8e, 0x78, 0xcf, 0x18, 0x18, 0xc3, 0x9a, 0x27, 0x1e, 0x11, 0x82, 0xfa, 0x96, 0xa4, + 0xbc, 0x67, 0x0e, 0x8c, 0x61, 0xdb, 0x93, 0xcf, 0x68, 0x04, 0xf7, 0x22, 0x7c, 0xee, 0xa7, 0xef, + 0x71, 0xec, 0x27, 0x2c, 0xe3, 0x01, 0x3d, 0xf3, 0x5f, 0x13, 0xd2, 0xab, 0xc9, 0x6d, 0xdd, 0x08, + 0x9f, 0xaf, 0xde, 0xe3, 0xd8, 0x53, 0x2b, 0xcf, 0x08, 0x41, 0x4f, 0x60, 0x5f, 0x6c, 0x88, 0x13, + 0x12, 0xe3, 0x8b, 0xca, 0x96, 0xba, 0xdc, 0x72, 0x37, 0xc2, 0xe7, 0x4b, 0xb9, 0x58, 0xda, 0x34, + 0x00, 0xab, 0x78, 0x8b, 0xa0, 0xee, 0x48, 0x2a, 0x68, 0x75, 0xc1, 0xf8, 0x1c, 0xec, 0x92, 0xac, + 0x38, 0x78, 0x43, 0x72, 0xac, 0x42, 0x6e, 0x1c, 0x71, 0xe4, 0x42, 0x47, 0xb0, 0xa2, 0x80, 0x92, + 0x44, 0x0a, 0x35, 0x25, 0x69, 0x37, 0xc2, 0xe7, 0xc7, 0x02, 0x13, 0x4a, 0x43, 0x70, 0x44, 0x66, + 0x3e, 0xcb, 0xb8, 0xbf, 0x79, 0x83, 0x29, 0x25, 0x61, 0xaf, 0x35, 0x30, 0x86, 0x75, 0xcf, 0x0e, + 0x55, 0x42, 0x13, 0x85, 0xba, 0xff, 0x18, 0xd0, 0x11, 0xa1, 0xcd, 0xe9, 0xcd, 0x99, 0xdd, 0x6c, + 0xd7, 0xbc, 0xbd, 0xdd, 0xda, 0x2d, 0xec, 0xd6, 0x6f, 0x63, 0x77, 0xe7, 0xba, 0xdd, 0xc7, 0xb0, + 0x27, 0xed, 0x06, 0xb4, 0x70, 0xdb, 0x90, 0x6e, 0x3b, 0xa1, 0xb4, 0x96, 0x9b, 0xfd, 0x14, 0x2c, + 0xf9, 0x25, 0x49, 0x1a, 0x33, 0x9a, 0x12, 0x64, 0x83, 0x19, 0x6c, 0xa5, 0xd3, 0xb6, 0x67, 0x06, + 0x5b, 0xd7, 0x01, 0xfb, 0x98, 0xd1, 0x80, 0xb3, 0x44, 0x87, 0xe1, 0xfe, 0x6b, 0x00, 0x88, 0x2d, + 0x2b, 0x8e, 0x79, 0x96, 0xfe, 0x4f, 0x36, 0x4a, 0xc2, 0xcc, 0x25, 0xd0, 0x23, 0xa8, 0xf3, 0x8b, + 0x58, 0xd9, 0xb5, 0x8f, 0xba, 0x87, 0xba, 0xba, 0x87, 0x42, 0x64, 0x7d, 0x11, 0x13, 0x4f, 0x2e, + 0xa3, 0x21, 0xec, 0xa4, 0x1c, 0x73, 0x55, 0x18, 0xfb, 0x08, 0x55, 0x78, 0xe2, 0x65, 0xc4, 0x53, + 0x04, 0xf4, 0x05, 0xec, 0x05, 0x34, 0xe0, 0x81, 0xac, 0xba, 0xcf, 0x83, 0x28, 0x4f, 0xc0, 0xbe, + 0x84, 0xd7, 0x41, 0xa4, 0xbe, 0x39, 0x4e, 0xb9, 0x9f, 0xc5, 0x5b, 0xcc, 0x89, 0x62, 0xaa, 0xfe, + 0xd8, 0x02, 0x3f, 0x95, 0xb0, 0x64, 0x7e, 0x06, 0xd6, 0x1b, 0x1e, 0x6e, 0x7c, 0xbc, 0xdd, 0x26, + 0x24, 0x4d, 0x65, 0x81, 0xda, 0xde, 0xae, 0xc0, 0xc6, 0x0a, 0x72, 0x6d, 0xb0, 0xd6, 0x24, 0x89, + 0xd2, 0x3c, 0x87, 0xdf, 0x4c, 0xe8, 0x68, 0x40, 0x67, 0x77, 0x00, 0x5d, 0xf9, 0x6d, 0x63, 0x7c, + 0x11, 0x11, 0xca, 0x7d, 0x39, 0x55, 0x2a, 0xca, 0x3d, 0xb1, 0xb0, 0x54, 0xf8, 0x54, 0x54, 0xca, + 0x85, 0x4e, 0xde, 0x03, 0xff, 0x15, 0x4e, 0xf3, 0xde, 0xec, 0xa6, 0xaa, 0x09, 0x4f, 0x71, 0x4a, + 0x2a, 0x9c, 0x44, 0x24, 0x53, 0xab, 0x70, 0x3c, 0x91, 0xc5, 0x43, 0x80, 0x6b, 0x6d, 0x69, 0xc7, + 0x45, 0x55, 0x1e, 0xc3, 0x5e, 0x14, 0x50, 0x55, 0x39, 0x1c, 0xb1, 0x8c, 0x72, 0x1d, 0x55, 0x27, + 0x0a, 0xa8, 0x08, 0x76, 0x2c, 0x41, 0xc9, 0xcb, 0xab, 0xa9, 0x79, 0x0d, 0xcd, 0x53, 0xed, 0xd4, + 0xbc, 0x87, 0x00, 0x9b, 0x90, 0xbf, 0xf3, 0xb7, 0x24, 0xe4, 0x58, 0xa6, 0xb4, 0xe3, 0xb5, 0x05, + 0x32, 0x15, 0x80, 0x3b, 0x00, 0xeb, 0xfb, 0x8c, 0x71, 0x72, 0xe3, 0xe0, 0xb8, 0xaf, 0xa1, 0xa3, + 0x19, 0x3a, 0xb4, 0x8f, 0xa0, 0x55, 0x0c, 0x84, 0xe2, 0x35, 0xb5, 0xbf, 0x2b, 0xde, 0xcc, 0xab, + 0xde, 0x3e, 0x86, 0xf6, 0xe5, 0x08, 0xa8, 0x68, 0x5a, 0x91, 0xee, 0xff, 0xc1, 0x23, 0x68, 0xe5, + 0xfd, 0x42, 0x16, 0xb4, 0x16, 0x27, 0x27, 0x4b, 0xff, 0xe4, 0x74, 0xed, 0xdc, 0x41, 0xbb, 0xd0, + 0x94, 0xbf, 0xe6, 0x2f, 0x1d, 0xe3, 0xe0, 0x27, 0x68, 0x17, 0xf5, 0x42, 0x1d, 0x68, 0xcf, 0x5f, + 0xce, 0xd7, 0xf3, 0xf1, 0x7a, 0x36, 0x75, 0xee, 0xa0, 0xfb, 0xd0, 0x5d, 0x7a, 0xb3, 0xf9, 0xf1, + 0xf8, 0xf9, 0xcc, 0xf7, 0x66, 0x3f, 0xcc, 0xc6, 0x8b, 0xd9, 0xd4, 0x31, 0x10, 0x02, 0xfb, 0xc5, + 0x7a, 0x31, 0xf1, 0x97, 0xa7, 0x4f, 0x17, 0xf3, 0xd5, 0x8b, 0xd9, 0xd4, 0x31, 0x85, 0xe6, 0xea, + 0x74, 0x32, 0x99, 0xad, 0x56, 0x4e, 0x0d, 0x01, 0x34, 0x9e, 0x8d, 0xe7, 0x82, 0x5c, 0x3f, 0xfa, + 0xbd, 0xae, 0x86, 0x65, 0x22, 0x2f, 0x6f, 0xe4, 0x41, 0x53, 0x5f, 0xc7, 0xe8, 0x41, 0xd1, 0xef, + 0xea, 0x05, 0xdd, 0xbf, 0x5f, 0x29, 0x7e, 0x9e, 0x93, 0xfb, 0xe0, 0xd7, 0xbf, 0xfe, 0xfe, 0xc3, + 0xec, 0xba, 0xd6, 0xe8, 0xdd, 0x57, 0x23, 0xc1, 0x18, 0xb1, 0x8c, 0x7f, 0x6b, 0x1c, 0xa0, 0xaf, + 0xa1, 0xa1, 0x6e, 0x2b, 0xb4, 0x5f, 0x91, 0x2c, 0xae, 0xaf, 0x1b, 0x14, 0xd1, 0x37, 0xd0, 0xd4, + 0xa3, 0x5d, 0x3a, 0x4c, 0x75, 0xd8, 0xfb, 0x77, 0xaf, 0x4d, 0x61, 0x96, 0x7e, 0x69, 0xa0, 0x1f, + 0xc1, 0xd2, 0xa7, 0x96, 0x13, 0x80, 0x2e, 0xdf, 0x50, 0x1e, 0x91, 0xfe, 0xfe, 0x55, 0x58, 0x7b, + 0xe9, 0x4b, 0x2f, 0xf7, 0x10, 0x2a, 0x7b, 0x19, 0x71, 0x29, 0xe5, 0x17, 0xd2, 0xb2, 0x27, 0x25, + 0xe9, 0x72, 0xb3, 0x4a, 0xd2, 0x95, 0x3a, 0xb9, 0x03, 0x29, 0xdd, 0x47, 0xbd, 0x8a, 0xf4, 0x5b, + 0xc1, 0x19, 0xfd, 0x8c, 0x23, 0xfe, 0x0b, 0xfa, 0x0e, 0xec, 0xe7, 0x84, 0xab, 0x84, 0x3e, 0xe4, + 0xf4, 0x15, 0x81, 0x0f, 0x39, 0xe3, 0xab, 0x86, 0xfc, 0x73, 0x7e, 0xf2, 0x5f, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xc7, 0xd4, 0x7e, 0xf6, 0xd3, 0x07, 0x00, 0x00, } diff --git a/looprpc/client.proto b/looprpc/client.proto index 104994b..435b8ea 100644 --- a/looprpc/client.proto +++ b/looprpc/client.proto @@ -22,6 +22,14 @@ service SwapClient { }; } + /** + LoopIn initiates a loop in swap with the given parameters. The call + returns after the swap has been set up with the swap server. From that + point onwards, progress can be tracked via the SwapStatus stream + that is returned from Monitor(). + */ + rpc LoopIn(LoopInRequest) returns (SwapResponse); + /** loop: `monitor` Monitor will return a stream of swap updates for currently active swaps. TODO: add MonitorSync version for REST clients. @@ -46,6 +54,16 @@ service SwapClient { get: "/v1/loop/out/quote/{amt}" }; } + + /** + GetTerms returns the terms that the server enforces for swaps. + */ + rpc GetLoopInTerms(TermsRequest) returns(TermsResponse); + + /** + GetQuote returns a quote for a swap with the provided parameters. + */ + rpc GetLoopInQuote(QuoteRequest) returns(QuoteResponse); } message LoopOutRequest { @@ -111,6 +129,48 @@ message LoopOutRequest { uint64 loop_out_channel = 8; } +message LoopInRequest { + /** + Requested swap amount in sat. This does not include the swap and miner + fee. + */ + int64 amt = 1; + + /** + Maximum off-chain fee in msat that may be paid for payment to the server. + This limit is applied during path finding. Typically this value is taken + from the response of the GetQuote call. + */ + int64 max_prepay_routing_fee = 2; + + /** + Maximum we are willing to pay the server for the swap. This value is not + disclosed in the swap initiation call, but if the server asks for a + higher fee, we abort the swap. Typically this value is taken from the + response of the GetQuote call. It includes the prepay amount. + */ + int64 max_swap_fee = 3; + + /** + Maximum amount of the swap fee that may be charged as a prepayment. + */ + int64 max_prepay_amt = 4; + + /** + Maximum in on-chain fees that we are willing to spent. If we want to + publish the on-chain htlc and the fee estimate turns out higher than this + value, we cancel the swap. + + max_miner_fee is typically taken from the response of the GetQuote call. + */ + int64 max_miner_fee = 5; + + /** + The channel to loop in. If zero, the channel to loop in is selected based + on the lowest routing fee for the swap payment from the server. + */ + uint64 loop_in_channel = 6; +} message SwapResponse { /** @@ -165,6 +225,9 @@ message SwapStatus { enum SwapType { // LOOP_OUT indicates an loop out swap (off-chain to on-chain) LOOP_OUT = 0; + + // LOOP_IN indicates a loop in swap (on-chain to off-chain) + LOOP_IN = 1; } enum SwapState { @@ -184,6 +247,12 @@ enum SwapState { */ PREIMAGE_REVEALED = 1; + /** + HTLC_PUBLISHED is reached when the htlc tx has been published in a loop in + swap. + */ + HTLC_PUBLISHED = 2; + /** SUCCESS is the final swap state that is reached when the sweep tx has the required confirmation depth. @@ -236,11 +305,6 @@ message TermsResponse { On-chain cltv expiry delta */ int32 cltv_delta = 7; - - /** - Maximum cltv expiry delta - */ - int32 max_cltv = 8; } message QuoteRequest { diff --git a/looprpc/client.swagger.json b/looprpc/client.swagger.json index efdaaf6..71fe251 100644 --- a/looprpc/client.swagger.json +++ b/looprpc/client.swagger.json @@ -165,11 +165,12 @@ "enum": [ "INITIATED", "PREIMAGE_REVEALED", + "HTLC_PUBLISHED", "SUCCESS", "FAILED" ], "default": "INITIATED", - "description": " - INITIATED: *\nINITIATED is the initial state of a swap. At that point, the initiation\ncall to the server has been made and the payment process has been started\nfor the swap and prepayment invoices.\n - PREIMAGE_REVEALED: *\nPREIMAGE_REVEALED is reached when the sweep tx publication is first\nattempted. From that point on, we should consider the preimage to no\nlonger be secret and we need to do all we can to get the sweep confirmed.\nThis state will mostly coalesce with StateHtlcConfirmed, except in the\ncase where we wait for fees to come down before we sweep.\n - SUCCESS: *\nSUCCESS is the final swap state that is reached when the sweep tx has\nthe required confirmation depth.\n - FAILED: *\nFAILED is the final swap state for a failed swap with or without loss of\nthe swap amount." + "description": " - INITIATED: *\nINITIATED is the initial state of a swap. At that point, the initiation\ncall to the server has been made and the payment process has been started\nfor the swap and prepayment invoices.\n - PREIMAGE_REVEALED: *\nPREIMAGE_REVEALED is reached when the sweep tx publication is first\nattempted. From that point on, we should consider the preimage to no\nlonger be secret and we need to do all we can to get the sweep confirmed.\nThis state will mostly coalesce with StateHtlcConfirmed, except in the\ncase where we wait for fees to come down before we sweep.\n - HTLC_PUBLISHED: *\nHTLC_PUBLISHED is reached when the htlc tx has been published in a loop in\nswap.\n - SUCCESS: *\nSUCCESS is the final swap state that is reached when the sweep tx has\nthe required confirmation depth.\n - FAILED: *\nFAILED is the final swap state for a failed swap with or without loss of\nthe swap amount." }, "looprpcSwapStatus": { "type": "object", @@ -210,10 +211,11 @@ "looprpcSwapType": { "type": "string", "enum": [ - "LOOP_OUT" + "LOOP_OUT", + "LOOP_IN" ], "default": "LOOP_OUT", - "title": "- LOOP_OUT: LOOP_OUT indicates an loop out swap (off-chain to on-chain)" + "title": "- LOOP_OUT: LOOP_OUT indicates an loop out swap (off-chain to on-chain)\n - LOOP_IN: LOOP_IN indicates a loop in swap (on-chain to off-chain)" }, "looprpcTermsResponse": { "type": "object", @@ -251,11 +253,6 @@ "type": "integer", "format": "int32", "title": "*\nOn-chain cltv expiry delta" - }, - "max_cltv": { - "type": "integer", - "format": "int32", - "title": "*\nMaximum cltv expiry delta" } } } diff --git a/looprpc/server.pb.go b/looprpc/server.pb.go index fcb9eb5..8fd80e9 100644 --- a/looprpc/server.pb.go +++ b/looprpc/server.pb.go @@ -37,7 +37,7 @@ func (m *ServerLoopOutRequest) Reset() { *m = ServerLoopOutRequest{} } func (m *ServerLoopOutRequest) String() string { return proto.CompactTextString(m) } func (*ServerLoopOutRequest) ProtoMessage() {} func (*ServerLoopOutRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_server_76a33eec530f39a2, []int{0} + return fileDescriptor_server_1af9bb28f4c6b777, []int{0} } func (m *ServerLoopOutRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ServerLoopOutRequest.Unmarshal(m, b) @@ -92,7 +92,7 @@ func (m *ServerLoopOutResponse) Reset() { *m = ServerLoopOutResponse{} } func (m *ServerLoopOutResponse) String() string { return proto.CompactTextString(m) } func (*ServerLoopOutResponse) ProtoMessage() {} func (*ServerLoopOutResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_server_76a33eec530f39a2, []int{1} + return fileDescriptor_server_1af9bb28f4c6b777, []int{1} } func (m *ServerLoopOutResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ServerLoopOutResponse.Unmarshal(m, b) @@ -150,7 +150,7 @@ func (m *ServerLoopOutQuoteRequest) Reset() { *m = ServerLoopOutQuoteReq func (m *ServerLoopOutQuoteRequest) String() string { return proto.CompactTextString(m) } func (*ServerLoopOutQuoteRequest) ProtoMessage() {} func (*ServerLoopOutQuoteRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_server_76a33eec530f39a2, []int{2} + return fileDescriptor_server_1af9bb28f4c6b777, []int{2} } func (m *ServerLoopOutQuoteRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ServerLoopOutQuoteRequest.Unmarshal(m, b) @@ -187,7 +187,7 @@ func (m *ServerLoopOutQuote) Reset() { *m = ServerLoopOutQuote{} } func (m *ServerLoopOutQuote) String() string { return proto.CompactTextString(m) } func (*ServerLoopOutQuote) ProtoMessage() {} func (*ServerLoopOutQuote) Descriptor() ([]byte, []int) { - return fileDescriptor_server_76a33eec530f39a2, []int{3} + return fileDescriptor_server_1af9bb28f4c6b777, []int{3} } func (m *ServerLoopOutQuote) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ServerLoopOutQuote.Unmarshal(m, b) @@ -256,11 +256,239 @@ func (m *ServerLoopOutQuote) GetCltvDelta() int32 { return 0 } +type ServerLoopInRequest struct { + SenderKey []byte `protobuf:"bytes,1,opt,name=sender_key,json=senderKey,proto3" json:"sender_key,omitempty"` + SwapHash []byte `protobuf:"bytes,2,opt,name=swap_hash,json=swapHash,proto3" json:"swap_hash,omitempty"` + Amt uint64 `protobuf:"varint,3,opt,name=amt,proto3" json:"amt,omitempty"` + SwapInvoice string `protobuf:"bytes,4,opt,name=swap_invoice,json=swapInvoice,proto3" json:"swap_invoice,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ServerLoopInRequest) Reset() { *m = ServerLoopInRequest{} } +func (m *ServerLoopInRequest) String() string { return proto.CompactTextString(m) } +func (*ServerLoopInRequest) ProtoMessage() {} +func (*ServerLoopInRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_server_1af9bb28f4c6b777, []int{4} +} +func (m *ServerLoopInRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ServerLoopInRequest.Unmarshal(m, b) +} +func (m *ServerLoopInRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ServerLoopInRequest.Marshal(b, m, deterministic) +} +func (dst *ServerLoopInRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ServerLoopInRequest.Merge(dst, src) +} +func (m *ServerLoopInRequest) XXX_Size() int { + return xxx_messageInfo_ServerLoopInRequest.Size(m) +} +func (m *ServerLoopInRequest) XXX_DiscardUnknown() { + xxx_messageInfo_ServerLoopInRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_ServerLoopInRequest proto.InternalMessageInfo + +func (m *ServerLoopInRequest) GetSenderKey() []byte { + if m != nil { + return m.SenderKey + } + return nil +} + +func (m *ServerLoopInRequest) GetSwapHash() []byte { + if m != nil { + return m.SwapHash + } + return nil +} + +func (m *ServerLoopInRequest) GetAmt() uint64 { + if m != nil { + return m.Amt + } + return 0 +} + +func (m *ServerLoopInRequest) GetSwapInvoice() string { + if m != nil { + return m.SwapInvoice + } + return "" +} + +type ServerLoopInResponse struct { + PrepayInvoice string `protobuf:"bytes,1,opt,name=prepay_invoice,json=prepayInvoice,proto3" json:"prepay_invoice,omitempty"` + ReceiverKey []byte `protobuf:"bytes,2,opt,name=receiver_key,json=receiverKey,proto3" json:"receiver_key,omitempty"` + Expiry int32 `protobuf:"varint,3,opt,name=expiry,proto3" json:"expiry,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ServerLoopInResponse) Reset() { *m = ServerLoopInResponse{} } +func (m *ServerLoopInResponse) String() string { return proto.CompactTextString(m) } +func (*ServerLoopInResponse) ProtoMessage() {} +func (*ServerLoopInResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_server_1af9bb28f4c6b777, []int{5} +} +func (m *ServerLoopInResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ServerLoopInResponse.Unmarshal(m, b) +} +func (m *ServerLoopInResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ServerLoopInResponse.Marshal(b, m, deterministic) +} +func (dst *ServerLoopInResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ServerLoopInResponse.Merge(dst, src) +} +func (m *ServerLoopInResponse) XXX_Size() int { + return xxx_messageInfo_ServerLoopInResponse.Size(m) +} +func (m *ServerLoopInResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ServerLoopInResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ServerLoopInResponse proto.InternalMessageInfo + +func (m *ServerLoopInResponse) GetPrepayInvoice() string { + if m != nil { + return m.PrepayInvoice + } + return "" +} + +func (m *ServerLoopInResponse) GetReceiverKey() []byte { + if m != nil { + return m.ReceiverKey + } + return nil +} + +func (m *ServerLoopInResponse) GetExpiry() int32 { + if m != nil { + return m.Expiry + } + return 0 +} + +type ServerLoopInQuoteRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ServerLoopInQuoteRequest) Reset() { *m = ServerLoopInQuoteRequest{} } +func (m *ServerLoopInQuoteRequest) String() string { return proto.CompactTextString(m) } +func (*ServerLoopInQuoteRequest) ProtoMessage() {} +func (*ServerLoopInQuoteRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_server_1af9bb28f4c6b777, []int{6} +} +func (m *ServerLoopInQuoteRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ServerLoopInQuoteRequest.Unmarshal(m, b) +} +func (m *ServerLoopInQuoteRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ServerLoopInQuoteRequest.Marshal(b, m, deterministic) +} +func (dst *ServerLoopInQuoteRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ServerLoopInQuoteRequest.Merge(dst, src) +} +func (m *ServerLoopInQuoteRequest) XXX_Size() int { + return xxx_messageInfo_ServerLoopInQuoteRequest.Size(m) +} +func (m *ServerLoopInQuoteRequest) XXX_DiscardUnknown() { + xxx_messageInfo_ServerLoopInQuoteRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_ServerLoopInQuoteRequest proto.InternalMessageInfo + +type ServerLoopInQuoteResponse struct { + SwapFeeBase int64 `protobuf:"varint,1,opt,name=swap_fee_base,json=swapFeeBase,proto3" json:"swap_fee_base,omitempty"` + SwapFeeRate int64 `protobuf:"varint,2,opt,name=swap_fee_rate,json=swapFeeRate,proto3" json:"swap_fee_rate,omitempty"` + PrepayAmt uint64 `protobuf:"varint,3,opt,name=prepay_amt,json=prepayAmt,proto3" json:"prepay_amt,omitempty"` + MinSwapAmount uint64 `protobuf:"varint,4,opt,name=min_swap_amount,json=minSwapAmount,proto3" json:"min_swap_amount,omitempty"` + MaxSwapAmount uint64 `protobuf:"varint,5,opt,name=max_swap_amount,json=maxSwapAmount,proto3" json:"max_swap_amount,omitempty"` + CltvDelta int32 `protobuf:"varint,6,opt,name=cltv_delta,json=cltvDelta,proto3" json:"cltv_delta,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ServerLoopInQuoteResponse) Reset() { *m = ServerLoopInQuoteResponse{} } +func (m *ServerLoopInQuoteResponse) String() string { return proto.CompactTextString(m) } +func (*ServerLoopInQuoteResponse) ProtoMessage() {} +func (*ServerLoopInQuoteResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_server_1af9bb28f4c6b777, []int{7} +} +func (m *ServerLoopInQuoteResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ServerLoopInQuoteResponse.Unmarshal(m, b) +} +func (m *ServerLoopInQuoteResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ServerLoopInQuoteResponse.Marshal(b, m, deterministic) +} +func (dst *ServerLoopInQuoteResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ServerLoopInQuoteResponse.Merge(dst, src) +} +func (m *ServerLoopInQuoteResponse) XXX_Size() int { + return xxx_messageInfo_ServerLoopInQuoteResponse.Size(m) +} +func (m *ServerLoopInQuoteResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ServerLoopInQuoteResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ServerLoopInQuoteResponse proto.InternalMessageInfo + +func (m *ServerLoopInQuoteResponse) GetSwapFeeBase() int64 { + if m != nil { + return m.SwapFeeBase + } + return 0 +} + +func (m *ServerLoopInQuoteResponse) GetSwapFeeRate() int64 { + if m != nil { + return m.SwapFeeRate + } + return 0 +} + +func (m *ServerLoopInQuoteResponse) GetPrepayAmt() uint64 { + if m != nil { + return m.PrepayAmt + } + return 0 +} + +func (m *ServerLoopInQuoteResponse) GetMinSwapAmount() uint64 { + if m != nil { + return m.MinSwapAmount + } + return 0 +} + +func (m *ServerLoopInQuoteResponse) GetMaxSwapAmount() uint64 { + if m != nil { + return m.MaxSwapAmount + } + return 0 +} + +func (m *ServerLoopInQuoteResponse) GetCltvDelta() int32 { + if m != nil { + return m.CltvDelta + } + return 0 +} + func init() { proto.RegisterType((*ServerLoopOutRequest)(nil), "looprpc.ServerLoopOutRequest") proto.RegisterType((*ServerLoopOutResponse)(nil), "looprpc.ServerLoopOutResponse") proto.RegisterType((*ServerLoopOutQuoteRequest)(nil), "looprpc.ServerLoopOutQuoteRequest") proto.RegisterType((*ServerLoopOutQuote)(nil), "looprpc.ServerLoopOutQuote") + proto.RegisterType((*ServerLoopInRequest)(nil), "looprpc.ServerLoopInRequest") + proto.RegisterType((*ServerLoopInResponse)(nil), "looprpc.ServerLoopInResponse") + proto.RegisterType((*ServerLoopInQuoteRequest)(nil), "looprpc.ServerLoopInQuoteRequest") + proto.RegisterType((*ServerLoopInQuoteResponse)(nil), "looprpc.ServerLoopInQuoteResponse") } // Reference imports to suppress errors if they are not otherwise used. @@ -277,6 +505,8 @@ const _ = grpc.SupportPackageIsVersion4 type SwapServerClient interface { NewLoopOutSwap(ctx context.Context, in *ServerLoopOutRequest, opts ...grpc.CallOption) (*ServerLoopOutResponse, error) LoopOutQuote(ctx context.Context, in *ServerLoopOutQuoteRequest, opts ...grpc.CallOption) (*ServerLoopOutQuote, error) + NewLoopInSwap(ctx context.Context, in *ServerLoopInRequest, opts ...grpc.CallOption) (*ServerLoopInResponse, error) + LoopInQuote(ctx context.Context, in *ServerLoopInQuoteRequest, opts ...grpc.CallOption) (*ServerLoopInQuoteResponse, error) } type swapServerClient struct { @@ -305,10 +535,30 @@ func (c *swapServerClient) LoopOutQuote(ctx context.Context, in *ServerLoopOutQu return out, nil } +func (c *swapServerClient) NewLoopInSwap(ctx context.Context, in *ServerLoopInRequest, opts ...grpc.CallOption) (*ServerLoopInResponse, error) { + out := new(ServerLoopInResponse) + err := c.cc.Invoke(ctx, "/looprpc.SwapServer/NewLoopInSwap", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *swapServerClient) LoopInQuote(ctx context.Context, in *ServerLoopInQuoteRequest, opts ...grpc.CallOption) (*ServerLoopInQuoteResponse, error) { + out := new(ServerLoopInQuoteResponse) + err := c.cc.Invoke(ctx, "/looprpc.SwapServer/LoopInQuote", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // SwapServerServer is the server API for SwapServer service. type SwapServerServer interface { NewLoopOutSwap(context.Context, *ServerLoopOutRequest) (*ServerLoopOutResponse, error) LoopOutQuote(context.Context, *ServerLoopOutQuoteRequest) (*ServerLoopOutQuote, error) + NewLoopInSwap(context.Context, *ServerLoopInRequest) (*ServerLoopInResponse, error) + LoopInQuote(context.Context, *ServerLoopInQuoteRequest) (*ServerLoopInQuoteResponse, error) } func RegisterSwapServerServer(s *grpc.Server, srv SwapServerServer) { @@ -351,6 +601,42 @@ func _SwapServer_LoopOutQuote_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _SwapServer_NewLoopInSwap_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ServerLoopInRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwapServerServer).NewLoopInSwap(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.SwapServer/NewLoopInSwap", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwapServerServer).NewLoopInSwap(ctx, req.(*ServerLoopInRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SwapServer_LoopInQuote_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ServerLoopInQuoteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwapServerServer).LoopInQuote(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.SwapServer/LoopInQuote", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwapServerServer).LoopInQuote(ctx, req.(*ServerLoopInQuoteRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _SwapServer_serviceDesc = grpc.ServiceDesc{ ServiceName: "looprpc.SwapServer", HandlerType: (*SwapServerServer)(nil), @@ -363,43 +649,59 @@ var _SwapServer_serviceDesc = grpc.ServiceDesc{ MethodName: "LoopOutQuote", Handler: _SwapServer_LoopOutQuote_Handler, }, + { + MethodName: "NewLoopInSwap", + Handler: _SwapServer_NewLoopInSwap_Handler, + }, + { + MethodName: "LoopInQuote", + Handler: _SwapServer_LoopInQuote_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "server.proto", } -func init() { proto.RegisterFile("server.proto", fileDescriptor_server_76a33eec530f39a2) } - -var fileDescriptor_server_76a33eec530f39a2 = []byte{ - // 467 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x93, 0xd1, 0x6e, 0xd3, 0x30, - 0x14, 0x86, 0x95, 0xb6, 0xeb, 0xe8, 0x59, 0xbb, 0x81, 0x05, 0x28, 0xb4, 0x14, 0x95, 0x48, 0x83, - 0x8a, 0x8b, 0x56, 0x82, 0x27, 0xd8, 0x34, 0x21, 0x10, 0x88, 0x42, 0x76, 0xc7, 0x4d, 0x74, 0xda, - 0x1e, 0x12, 0x8b, 0xc4, 0x36, 0xb1, 0xd3, 0x36, 0x2f, 0x82, 0x78, 0x0a, 0x9e, 0x11, 0xd9, 0xf1, - 0x60, 0x85, 0xb1, 0xbb, 0xe4, 0x3f, 0xdf, 0xf1, 0xf9, 0xfd, 0xe7, 0x04, 0xfa, 0x9a, 0xca, 0x0d, - 0x95, 0x33, 0x55, 0x4a, 0x23, 0xd9, 0x61, 0x2e, 0xa5, 0x2a, 0xd5, 0x6a, 0xf8, 0x38, 0x95, 0x32, - 0xcd, 0x69, 0x8e, 0x8a, 0xcf, 0x51, 0x08, 0x69, 0xd0, 0x70, 0x29, 0x74, 0x83, 0x45, 0x19, 0xdc, - 0xbf, 0x74, 0x6d, 0xef, 0xa5, 0x54, 0x8b, 0xca, 0xc4, 0xf4, 0xad, 0x22, 0x6d, 0xd8, 0x53, 0xe8, - 0x97, 0xb4, 0x22, 0xbe, 0xa1, 0x32, 0xf9, 0x4a, 0x75, 0x18, 0x4c, 0x82, 0x69, 0x3f, 0x3e, 0xba, - 0xd2, 0xde, 0x51, 0xcd, 0x46, 0xd0, 0xd3, 0x5b, 0x54, 0x49, 0x86, 0x3a, 0x0b, 0x5b, 0xae, 0x7e, - 0xc7, 0x0a, 0x6f, 0x50, 0x67, 0xec, 0x2e, 0xb4, 0xb1, 0x30, 0x61, 0x7b, 0x12, 0x4c, 0x3b, 0xb1, - 0x7d, 0x8c, 0x7e, 0x04, 0xf0, 0xe0, 0xaf, 0x51, 0x5a, 0x49, 0xa1, 0xc9, 0xce, 0x72, 0x07, 0x71, - 0xb1, 0x91, 0x7c, 0x45, 0x6e, 0x56, 0x2f, 0x3e, 0xb2, 0xda, 0xdb, 0x46, 0x62, 0xa7, 0x70, 0xac, - 0x4a, 0x52, 0x58, 0xff, 0x86, 0x5a, 0x0e, 0x1a, 0x34, 0xea, 0x15, 0x36, 0x06, 0xd0, 0x24, 0xd6, - 0xde, 0x73, 0xdb, 0x79, 0xea, 0x35, 0x8a, 0x75, 0xfc, 0x10, 0xba, 0xb4, 0x53, 0xbc, 0xac, 0xc3, - 0xce, 0x24, 0x98, 0x1e, 0xc4, 0xfe, 0x2d, 0x1a, 0xc1, 0xa3, 0x3d, 0x67, 0x9f, 0x2a, 0x69, 0xc8, - 0x27, 0x11, 0x7d, 0x6f, 0x01, 0xfb, 0xb7, 0xca, 0x5e, 0xc0, 0x3d, 0x67, 0x5a, 0x61, 0x5d, 0x90, - 0x30, 0xc9, 0x9a, 0xb4, 0xf1, 0xce, 0x4f, 0x6c, 0xe1, 0x63, 0xa3, 0x5f, 0xd8, 0x30, 0x23, 0x18, - 0x38, 0xf6, 0x0b, 0x51, 0xb2, 0x44, 0xdd, 0x98, 0x6f, 0x37, 0x37, 0x7c, 0x4d, 0x74, 0x8e, 0x9a, - 0xf6, 0x98, 0x12, 0x0d, 0x39, 0xf7, 0x7f, 0x98, 0x18, 0x8d, 0xbb, 0x9e, 0x4f, 0xc1, 0x66, 0xdb, - 0x71, 0xd9, 0xf6, 0x1a, 0xe5, 0xac, 0x30, 0xec, 0x19, 0x9c, 0x14, 0x5c, 0x24, 0xee, 0x18, 0x2c, - 0x64, 0x25, 0x4c, 0x78, 0xe0, 0x98, 0x41, 0xc1, 0xc5, 0xe5, 0x16, 0xd5, 0x99, 0x13, 0x1d, 0x87, - 0xbb, 0x3d, 0xae, 0xeb, 0x39, 0xdc, 0x5d, 0xe3, 0xc6, 0x00, 0xab, 0xdc, 0x6c, 0x92, 0x35, 0xe5, - 0x06, 0xc3, 0x43, 0x17, 0x59, 0xcf, 0x2a, 0x17, 0x56, 0x78, 0xf9, 0x33, 0x00, 0xb0, 0x74, 0x13, - 0x0e, 0x5b, 0xc0, 0xf1, 0x07, 0xda, 0xfa, 0x8c, 0xac, 0xce, 0xc6, 0x33, 0xbf, 0x83, 0xb3, 0x9b, - 0x56, 0x6c, 0xf8, 0xe4, 0x7f, 0x65, 0xbf, 0x16, 0x0b, 0xe8, 0xef, 0x25, 0x1e, 0xdd, 0xcc, 0x5f, - 0xff, 0x58, 0xc3, 0xd1, 0x2d, 0xcc, 0xf9, 0xf3, 0xcf, 0xa7, 0x29, 0x37, 0x59, 0xb5, 0x9c, 0xad, - 0x64, 0x31, 0xcf, 0x79, 0x9a, 0x19, 0xc1, 0x45, 0x9a, 0xe3, 0x52, 0xcf, 0x6d, 0xdb, 0xdc, 0xf7, - 0x2e, 0xbb, 0xee, 0xdf, 0x78, 0xf5, 0x2b, 0x00, 0x00, 0xff, 0xff, 0x94, 0x23, 0x1c, 0x6e, 0x52, - 0x03, 0x00, 0x00, +func init() { proto.RegisterFile("server.proto", fileDescriptor_server_1af9bb28f4c6b777) } + +var fileDescriptor_server_1af9bb28f4c6b777 = []byte{ + // 600 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x94, 0xdf, 0x6e, 0xd3, 0x30, + 0x18, 0xc5, 0x95, 0xa4, 0xeb, 0xe8, 0xb7, 0x76, 0x03, 0xf3, 0x47, 0xa1, 0x5b, 0xd1, 0x16, 0x69, + 0x30, 0x71, 0xd1, 0x4a, 0xf0, 0x04, 0x9b, 0x26, 0xc4, 0xc4, 0xc4, 0x20, 0xe3, 0x8a, 0x9b, 0xc8, + 0xed, 0x3e, 0x9a, 0x88, 0xc4, 0x36, 0xb1, 0xdb, 0xb5, 0x2f, 0xc0, 0x23, 0x20, 0x9e, 0x8b, 0x37, + 0xe1, 0x0d, 0x90, 0x1d, 0x6f, 0x4b, 0x9a, 0x6e, 0x2b, 0x77, 0xed, 0xf9, 0x4e, 0xec, 0x93, 0xe3, + 0x5f, 0x0c, 0x6d, 0x89, 0xf9, 0x14, 0xf3, 0xbe, 0xc8, 0xb9, 0xe2, 0x64, 0x3d, 0xe5, 0x5c, 0xe4, + 0x62, 0xd4, 0xdd, 0x19, 0x73, 0x3e, 0x4e, 0x71, 0x40, 0x45, 0x32, 0xa0, 0x8c, 0x71, 0x45, 0x55, + 0xc2, 0x99, 0x2c, 0x6c, 0x41, 0x0c, 0x4f, 0xce, 0xcd, 0x63, 0xa7, 0x9c, 0x8b, 0xb3, 0x89, 0x0a, + 0xf1, 0xc7, 0x04, 0xa5, 0x22, 0x7b, 0xd0, 0xce, 0x71, 0x84, 0xc9, 0x14, 0xf3, 0xe8, 0x3b, 0xce, + 0x7d, 0x67, 0xd7, 0x39, 0x68, 0x87, 0x1b, 0x57, 0xda, 0x07, 0x9c, 0x93, 0x6d, 0x68, 0xc9, 0x4b, + 0x2a, 0xa2, 0x98, 0xca, 0xd8, 0x77, 0xcd, 0xfc, 0x81, 0x16, 0xde, 0x53, 0x19, 0x93, 0x87, 0xe0, + 0xd1, 0x4c, 0xf9, 0xde, 0xae, 0x73, 0xd0, 0x08, 0xf5, 0xcf, 0xe0, 0xb7, 0x03, 0x4f, 0x17, 0xb6, + 0x92, 0x82, 0x33, 0x89, 0x7a, 0x2f, 0xb3, 0x50, 0xc2, 0xa6, 0x3c, 0x19, 0xa1, 0xd9, 0xab, 0x15, + 0x6e, 0x68, 0xed, 0xa4, 0x90, 0xc8, 0x3e, 0x6c, 0x8a, 0x1c, 0x05, 0x9d, 0x5f, 0x9b, 0x5c, 0x63, + 0xea, 0x14, 0xea, 0x95, 0xad, 0x07, 0x20, 0x91, 0x5d, 0xd8, 0xcc, 0x9e, 0xc9, 0xd4, 0x2a, 0x14, + 0x9d, 0xf8, 0x19, 0x34, 0x71, 0x26, 0x92, 0x7c, 0xee, 0x37, 0x76, 0x9d, 0x83, 0xb5, 0xd0, 0xfe, + 0x0b, 0xb6, 0xe1, 0x79, 0x25, 0xd9, 0xe7, 0x09, 0x57, 0x68, 0x9b, 0x08, 0x7e, 0xb9, 0x40, 0xea, + 0x53, 0xf2, 0x1a, 0x1e, 0x99, 0xd0, 0x82, 0xce, 0x33, 0x64, 0x2a, 0xba, 0x40, 0xa9, 0x6c, 0xf2, + 0x2d, 0x3d, 0xf8, 0x54, 0xe8, 0xc7, 0xba, 0xcc, 0x00, 0x3a, 0xc6, 0xfb, 0x0d, 0x31, 0x1a, 0x52, + 0x59, 0x84, 0xf7, 0x8a, 0x37, 0x7c, 0x87, 0x78, 0x44, 0x25, 0x56, 0x3c, 0x39, 0x55, 0x68, 0xd2, + 0xdf, 0x78, 0x42, 0xaa, 0xcc, 0xeb, 0xd9, 0x16, 0x74, 0xb7, 0x0d, 0xd3, 0x6d, 0xab, 0x50, 0x0e, + 0x33, 0x45, 0x5e, 0xc2, 0x56, 0x96, 0xb0, 0xc8, 0x2c, 0x43, 0x33, 0x3e, 0x61, 0xca, 0x5f, 0x33, + 0x9e, 0x4e, 0x96, 0xb0, 0xf3, 0x4b, 0x2a, 0x0e, 0x8d, 0x68, 0x7c, 0x74, 0x56, 0xf1, 0x35, 0xad, + 0x8f, 0xce, 0x4a, 0xbe, 0x1e, 0xc0, 0x28, 0x55, 0xd3, 0xe8, 0x02, 0x53, 0x45, 0xfd, 0x75, 0x53, + 0x59, 0x4b, 0x2b, 0xc7, 0x5a, 0x08, 0x7e, 0x3a, 0xf0, 0xf8, 0xa6, 0x98, 0x13, 0x76, 0x85, 0x4e, + 0xf5, 0x10, 0x9c, 0xc5, 0x43, 0xf8, 0x3f, 0x6c, 0x6a, 0x70, 0x34, 0x6a, 0x70, 0x04, 0xb3, 0x32, + 0xc3, 0x3a, 0x87, 0xe5, 0xaa, 0x0e, 0x8d, 0xb3, 0x0c, 0x9a, 0x45, 0xd4, 0xdd, 0x3a, 0xea, 0x37, + 0xe0, 0x78, 0x15, 0x70, 0xba, 0xe0, 0x97, 0x77, 0xae, 0x70, 0xf3, 0xd7, 0x29, 0x53, 0x75, 0x3d, + 0xb4, 0xd9, 0x6a, 0x48, 0x38, 0x2b, 0x20, 0xe1, 0xde, 0x87, 0x84, 0xb7, 0x02, 0x12, 0x8d, 0x15, + 0x91, 0x58, 0xbb, 0x1f, 0x89, 0xe6, 0x02, 0x12, 0x6f, 0xfe, 0xb8, 0x00, 0xda, 0x5d, 0xbc, 0x37, + 0x39, 0x83, 0xcd, 0x8f, 0x78, 0x69, 0x3f, 0x1b, 0xad, 0x93, 0x5e, 0xdf, 0x5e, 0x4b, 0xfd, 0x65, + 0xb7, 0x4e, 0xf7, 0xc5, 0x6d, 0x63, 0xdb, 0xda, 0x19, 0xb4, 0x2b, 0x1f, 0x61, 0xb0, 0xdc, 0x5f, + 0x3e, 0x87, 0xee, 0xf6, 0x1d, 0x1e, 0x72, 0x0a, 0x1d, 0x9b, 0xf0, 0xc4, 0xd4, 0x41, 0x76, 0x96, + 0xb8, 0xaf, 0xd1, 0xee, 0xf6, 0x6e, 0x99, 0xda, 0x78, 0x5f, 0x60, 0xa3, 0x74, 0xd6, 0x64, 0x6f, + 0xa9, 0xbb, 0x12, 0x2e, 0xb8, 0xcb, 0x52, 0xac, 0x7a, 0xf4, 0xea, 0xeb, 0xfe, 0x38, 0x51, 0xf1, + 0x64, 0xd8, 0x1f, 0xf1, 0x6c, 0x90, 0x26, 0xe3, 0x58, 0xb1, 0x84, 0x8d, 0x53, 0x3a, 0x94, 0x03, + 0xfd, 0xf4, 0xc0, 0x2e, 0x31, 0x6c, 0x9a, 0x2b, 0xfd, 0xed, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, + 0x0a, 0x0e, 0xe1, 0xc1, 0x09, 0x06, 0x00, 0x00, } diff --git a/looprpc/server.proto b/looprpc/server.proto index d486516..3ed9c94 100644 --- a/looprpc/server.proto +++ b/looprpc/server.proto @@ -10,6 +10,10 @@ service SwapServer { rpc NewLoopOutSwap(ServerLoopOutRequest) returns (ServerLoopOutResponse); rpc LoopOutQuote(ServerLoopOutQuoteRequest) returns (ServerLoopOutQuote); + + rpc NewLoopInSwap(ServerLoopInRequest) returns (ServerLoopInResponse); + + rpc LoopInQuote(ServerLoopInQuoteRequest) returns (ServerLoopInQuoteResponse); } message ServerLoopOutRequest { @@ -48,3 +52,28 @@ message ServerLoopOutQuote { int32 cltv_delta = 7; } + +message ServerLoopInRequest { + bytes sender_key = 1; + bytes swap_hash = 2; + uint64 amt = 3; + string swap_invoice = 4; +} + +message ServerLoopInResponse { + string prepay_invoice = 1; + bytes receiver_key = 2; + int32 expiry = 3; +} + +message ServerLoopInQuoteRequest { +} + +message ServerLoopInQuoteResponse { + int64 swap_fee_base = 1; + int64 swap_fee_rate = 2; + uint64 prepay_amt = 3; + uint64 min_swap_amount = 4; + uint64 max_swap_amount = 5; + int32 cltv_delta = 6; +} \ No newline at end of file diff --git a/server_mock_test.go b/server_mock_test.go index 9b82ffd..8af4c29 100644 --- a/server_mock_test.go +++ b/server_mock_test.go @@ -19,6 +19,7 @@ var ( testTime = time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC) testLoopOutOnChainCltvDelta = int32(30) + testChargeOnChainCltvDelta = int32(100) testCltvDelta = 50 testSwapFeeBase = btcutil.Amount(21) testSwapFeeRate = int64(100) @@ -123,3 +124,50 @@ func getInvoice(hash lntypes.Hash, amt btcutil.Amount, memo string) (string, err return reqString, nil } + +func (s *serverMock) NewLoopInSwap(ctx context.Context, + swapHash lntypes.Hash, amount btcutil.Amount, + senderKey [33]byte, swapInvoice string) ( + *newLoopInResponse, error) { + + _, receiverKey := test.CreateKey(101) + + if amount != s.expectedSwapAmt { + return nil, errors.New("unexpected test swap amount") + } + + prepayHash := lntypes.Hash{1, 2, 3} + + prePayReqString, err := getInvoice(prepayHash, s.prepayInvoiceAmt, + prepayInvoiceDesc) + if err != nil { + return nil, err + } + + var receiverKeyArray [33]byte + copy(receiverKeyArray[:], receiverKey.SerializeCompressed()) + + s.swapInvoice = swapInvoice + s.swapHash = swapHash + + resp := &newLoopInResponse{ + expiry: s.height + testChargeOnChainCltvDelta, + receiverKey: receiverKeyArray, + prepayInvoice: prePayReqString, + } + + return resp, nil +} + +func (s *serverMock) GetLoopInTerms(ctx context.Context) ( + *LoopInTerms, error) { + + return &LoopInTerms{ + SwapFeeBase: testSwapFeeBase, + SwapFeeRate: testSwapFeeRate, + CltvDelta: testChargeOnChainCltvDelta, + MinSwapAmount: testMinSwapAmount, + MaxSwapAmount: testMaxSwapAmount, + PrepayAmt: testFixedPrepayAmount, + }, nil +} diff --git a/store_mock_test.go b/store_mock_test.go index fe1832d..dbbfe22 100644 --- a/store_mock_test.go +++ b/store_mock_test.go @@ -17,6 +17,11 @@ type storeMock struct { loopOutStoreChan chan loopdb.LoopOutContract loopOutUpdateChan chan loopdb.SwapState + loopInSwaps map[lntypes.Hash]*loopdb.LoopInContract + loopInUpdates map[lntypes.Hash][]loopdb.SwapState + loopInStoreChan chan loopdb.LoopInContract + loopInUpdateChan chan loopdb.SwapState + t *testing.T } @@ -33,7 +38,11 @@ func newStoreMock(t *testing.T) *storeMock { loopOutSwaps: make(map[lntypes.Hash]*loopdb.LoopOutContract), loopOutUpdates: make(map[lntypes.Hash][]loopdb.SwapState), - t: t, + loopInStoreChan: make(chan loopdb.LoopInContract, 1), + loopInUpdateChan: make(chan loopdb.SwapState, 1), + loopInSwaps: make(map[lntypes.Hash]*loopdb.LoopInContract), + loopInUpdates: make(map[lntypes.Hash][]loopdb.SwapState), + t: t, } } @@ -45,17 +54,19 @@ func (s *storeMock) FetchLoopOutSwaps() ([]*loopdb.LoopOut, error) { for hash, contract := range s.loopOutSwaps { updates := s.loopOutUpdates[hash] - events := make([]*loopdb.LoopOutEvent, len(updates)) + events := make([]*loopdb.LoopEvent, len(updates)) for i, u := range updates { - events[i] = &loopdb.LoopOutEvent{ + events[i] = &loopdb.LoopEvent{ State: u, } } swap := &loopdb.LoopOut{ - Hash: hash, + Loop: loopdb.Loop{ + Hash: hash, + Events: events, + }, Contract: contract, - Events: events, } result = append(result, swap) } @@ -81,6 +92,48 @@ func (s *storeMock) CreateLoopOut(hash lntypes.Hash, return nil } +// getChargeSwaps returns all swaps currently in the store. +func (s *storeMock) FetchLoopInSwaps() ([]*loopdb.LoopIn, error) { + result := []*loopdb.LoopIn{} + + for hash, contract := range s.loopInSwaps { + updates := s.loopInUpdates[hash] + events := make([]*loopdb.LoopEvent, len(updates)) + for i, u := range updates { + events[i] = &loopdb.LoopEvent{ + State: u, + } + } + + swap := &loopdb.LoopIn{ + Loop: loopdb.Loop{ + Hash: hash, + Events: events, + }, + Contract: contract, + } + result = append(result, swap) + } + + return result, nil +} + +// createCharge adds an initiated swap to the store. +func (s *storeMock) CreateLoopIn(hash lntypes.Hash, + swap *loopdb.LoopInContract) error { + + _, ok := s.loopInSwaps[hash] + if ok { + return errors.New("swap already exists") + } + + s.loopInSwaps[hash] = swap + s.loopInUpdates[hash] = []loopdb.SwapState{} + s.loopInStoreChan <- *swap + + return nil +} + // UpdateLoopOut stores a new event for a target loop out swap. This appends to // the event log for a particular swap as it goes through the various stages in // its lifetime. @@ -101,6 +154,26 @@ func (s *storeMock) UpdateLoopOut(hash lntypes.Hash, time time.Time, return nil } +// UpdateLoopIn stores a new event for a target loop in swap. This appends to +// the event log for a particular swap as it goes through the various stages in +// its lifetime. +// +// NOTE: Part of the loopdb.SwapStore interface. +func (s *storeMock) UpdateLoopIn(hash lntypes.Hash, time time.Time, + state loopdb.SwapState) error { + + updates, ok := s.loopInUpdates[hash] + if !ok { + return errors.New("swap does not exists") + } + + updates = append(updates, state) + s.loopOutUpdates[hash] = updates + s.loopOutUpdateChan <- state + + return nil +} + func (s *storeMock) Close() error { return nil } @@ -130,6 +203,21 @@ func (s *storeMock) assertLoopOutStored() { } } +func (s *storeMock) assertLoopInStored() { + s.t.Helper() + + <-s.loopInStoreChan +} + +func (s *storeMock) assertLoopInState(expectedState loopdb.SwapState) { + s.t.Helper() + + state := <-s.loopOutUpdateChan + if state != expectedState { + s.t.Fatalf("unexpected state") + } +} + func (s *storeMock) assertStorePreimageReveal() { s.t.Helper() diff --git a/swap_server_client.go b/swap_server_client.go index 0e1ac55..dee950d 100644 --- a/swap_server_client.go +++ b/swap_server_client.go @@ -20,10 +20,18 @@ type swapServerClient interface { GetLoopOutTerms(ctx context.Context) ( *LoopOutTerms, error) + GetLoopInTerms(ctx context.Context) ( + *LoopInTerms, error) + NewLoopOutSwap(ctx context.Context, swapHash lntypes.Hash, amount btcutil.Amount, receiverKey [33]byte) ( *newLoopOutResponse, error) + + NewLoopInSwap(ctx context.Context, + swapHash lntypes.Hash, amount btcutil.Amount, + senderKey [33]byte, swapInvoice string) ( + *newLoopInResponse, error) } type grpcSwapServerClient struct { @@ -80,6 +88,28 @@ func (s *grpcSwapServerClient) GetLoopOutTerms(ctx context.Context) ( }, nil } +func (s *grpcSwapServerClient) GetLoopInTerms(ctx context.Context) ( + *LoopInTerms, error) { + + rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout) + defer rpcCancel() + quoteResp, err := s.server.LoopInQuote(rpcCtx, + &looprpc.ServerLoopInQuoteRequest{}, + ) + if err != nil { + return nil, err + } + + return &LoopInTerms{ + MinSwapAmount: btcutil.Amount(quoteResp.MinSwapAmount), + MaxSwapAmount: btcutil.Amount(quoteResp.MaxSwapAmount), + PrepayAmt: btcutil.Amount(quoteResp.PrepayAmt), + SwapFeeBase: btcutil.Amount(quoteResp.SwapFeeBase), + SwapFeeRate: quoteResp.SwapFeeRate, + CltvDelta: quoteResp.CltvDelta, + }, nil +} + func (s *grpcSwapServerClient) NewLoopOutSwap(ctx context.Context, swapHash lntypes.Hash, amount btcutil.Amount, receiverKey [33]byte) (*newLoopOutResponse, error) { @@ -114,6 +144,40 @@ func (s *grpcSwapServerClient) NewLoopOutSwap(ctx context.Context, }, nil } +func (s *grpcSwapServerClient) NewLoopInSwap(ctx context.Context, + swapHash lntypes.Hash, amount btcutil.Amount, senderKey [33]byte, + swapInvoice string) (*newLoopInResponse, error) { + + rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout) + defer rpcCancel() + swapResp, err := s.server.NewLoopInSwap(rpcCtx, + &looprpc.ServerLoopInRequest{ + SwapHash: swapHash[:], + Amt: uint64(amount), + SenderKey: senderKey[:], + SwapInvoice: swapInvoice, + }, + ) + if err != nil { + return nil, err + } + + var receiverKey [33]byte + copy(receiverKey[:], swapResp.ReceiverKey) + + // Validate receiver key. + _, err = btcec.ParsePubKey(receiverKey[:], btcec.S256()) + if err != nil { + return nil, fmt.Errorf("invalid sender key: %v", err) + } + + return &newLoopInResponse{ + prepayInvoice: swapResp.PrepayInvoice, + receiverKey: receiverKey, + expiry: swapResp.Expiry, + }, nil +} + func (s *grpcSwapServerClient) Close() { s.conn.Close() } @@ -143,3 +207,9 @@ type newLoopOutResponse struct { senderKey [33]byte expiry int32 } + +type newLoopInResponse struct { + prepayInvoice string + receiverKey [33]byte + expiry int32 +} diff --git a/test/context.go b/test/context.go index f11ff1d..53258ea 100644 --- a/test/context.go +++ b/test/context.go @@ -226,15 +226,4 @@ func (ctx *Context) NotifyServerHeight(height int32) { if err := ctx.Lnd.NotifyHeight(height); err != nil { ctx.T.Fatal(err) } - - // TODO: Fix race condition with height not processed yet. - - // select { - // case h := <-ctx.swapServer.testEpochChan: - // if h != height { - // ctx.T.Fatal("height not set") - // } - // case <-time.After(test.Timeout): - // ctx.T.Fatal("no height response") - // } } diff --git a/test/lightning_client_mock.go b/test/lightning_client_mock.go index e90a0f5..ef5ec1a 100644 --- a/test/lightning_client_mock.go +++ b/test/lightning_client_mock.go @@ -29,7 +29,7 @@ func (h *mockLightningClient) PayInvoice(ctx context.Context, invoice string, done := make(chan lndclient.PaymentResult, 1) - mockChan := make(chan error) + mockChan := make(chan error, 1) h.wg.Add(1) go func() { defer h.wg.Done() diff --git a/testcontext_test.go b/testcontext_test.go index e986db6..9dca8ae 100644 --- a/testcontext_test.go +++ b/testcontext_test.go @@ -117,10 +117,12 @@ func createClientTestContext(t *testing.T, ctx.stop = stop go func() { - ctx.runErr <- swapClient.Run( + err := swapClient.Run( runCtx, statusChan, ) + logger.Errorf("client run: %v", err) + ctx.runErr <- err }() return ctx