From 3e960b8b54f021d754ff12ce6eaaf9486472b7f8 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 | 124 ++++++- cmd/loop/loopin.go | 102 ++++++ cmd/loop/loopout.go | 6 +- cmd/loop/main.go | 53 ++- cmd/loop/terms.go | 21 +- cmd/loopd/daemon.go | 18 +- cmd/loopd/swapclient_server.go | 85 ++++- interface.go | 94 +++++ lndclient/chainnotifier_client.go | 15 +- lndclient/invoices_client.go | 24 +- lndclient/lightning_client.go | 44 ++- loopin.go | 585 ++++++++++++++++++++++++++++++ loopin_test.go | 370 +++++++++++++++++++ loopin_testcontext_test.go | 62 ++++ loopout.go | 2 +- looprpc/client.pb.go | 356 ++++++++++++++---- looprpc/client.proto | 62 +++- looprpc/client.swagger.json | 13 +- looprpc/server.pb.go | 361 ++++++++++++++++-- looprpc/server.proto | 27 ++ server_mock_test.go | 38 ++ swap_server_client.go | 67 ++++ test/context.go | 17 +- test/invoices_mock.go | 6 +- test/lightning_client_mock.go | 56 +-- test/lnd_services_mock.go | 5 +- test/testutils.go | 34 +- testcontext_test.go | 4 +- 28 files changed, 2377 insertions(+), 274 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 daccb82..28af7a0 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,85 @@ 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) { + + // Retrieve current server terms to calculate swap fee. + terms, err := s.Server.GetLoopInTerms(ctx) + if err != nil { + return nil, err + } + + // Check amount limits. + if request.Amount < terms.MinSwapAmount { + return nil, ErrSwapAmountTooLow + } + + if request.Amount > terms.MaxSwapAmount { + return nil, ErrSwapAmountTooHigh + } + + // Calculate swap fee. + swapFee := terms.SwapFeeBase + + request.Amount*btcutil.Amount(terms.SwapFeeRate)/ + btcutil.Amount(swap.FeeRateTotalParts) + + // Get estimate for miner fee. + minerFee, err := s.lndServices.Client.EstimateFeeToP2WSH( + ctx, request.Amount, request.HtlcConfTarget, + ) + if err != nil { + return nil, err + } + + return &LoopInQuote{ + SwapFee: swapFee, + MinerFee: minerFee, + }, 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/cmd/loop/loopin.go b/cmd/loop/loopin.go new file mode 100644 index 0000000..68de775 --- /dev/null +++ b/cmd/loop/loopin.go @@ -0,0 +1,102 @@ +package main + +import ( + "context" + "fmt" + + "github.com/btcsuite/btcutil" + "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", + }, + cli.Uint64Flag{ + Name: "amt", + Usage: "the amount in satoshis to loop out", + }, + }, + Action: loopIn, +} + +func loopIn(ctx *cli.Context) error { + args := ctx.Args() + + var amtStr string + switch { + case ctx.IsSet("amt"): + amtStr = ctx.String("amt") + case ctx.NArg() > 0: + amtStr = args[0] + args = args.Tail() + default: + // Show command help if no arguments and flags were provided. + cli.ShowCommandHelp(ctx, "in") + return nil + } + + amt, err := parseAmt(amtStr) + 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 := getInLimits(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), + MaxSwapFee: int64(limits.maxSwapFee), + 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 +} + +func getInLimits(amt btcutil.Amount, quote *looprpc.QuoteResponse) *limits { + return &limits{ + // Apply a multiplier to the estimated miner fee, to not get + // the swap canceled because fees increased in the mean time. + maxMinerFee: btcutil.Amount(quote.MinerFee) * 3, + maxSwapFee: btcutil.Amount(quote.SwapFee), + } +} diff --git a/cmd/loop/loopout.go b/cmd/loop/loopout.go index f2390dc..328f90f 100644 --- a/cmd/loop/loopout.go +++ b/cmd/loop/loopout.go @@ -97,10 +97,10 @@ func loopOut(ctx *cli.Context) error { Amt: int64(amt), Dest: destAddr, MaxMinerFee: int64(limits.maxMinerFee), - MaxPrepayAmt: int64(limits.maxPrepayAmt), + MaxPrepayAmt: int64(*limits.maxPrepayAmt), MaxSwapFee: int64(limits.maxSwapFee), - MaxPrepayRoutingFee: int64(limits.maxPrepayRoutingFee), - MaxSwapRoutingFee: int64(limits.maxSwapRoutingFee), + MaxPrepayRoutingFee: int64(*limits.maxPrepayRoutingFee), + MaxSwapRoutingFee: int64(*limits.maxSwapRoutingFee), LoopOutChannel: unchargeChannel, }) if err != nil { diff --git a/cmd/loop/main.go b/cmd/loop/main.go index efeabbe..cd7bcf3 100644 --- a/cmd/loop/main.go +++ b/cmd/loop/main.go @@ -62,7 +62,8 @@ func main() { }, } app.Commands = []cli.Command{ - loopOutCommand, termsCommand, monitorCommand, quoteCommand, + loopOutCommand, loopInCommand, termsCommand, + monitorCommand, quoteCommand, } err := app.Run(os.Args) @@ -88,32 +89,41 @@ func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount { } type limits struct { - maxSwapRoutingFee btcutil.Amount - maxPrepayRoutingFee btcutil.Amount + maxSwapRoutingFee *btcutil.Amount + maxPrepayRoutingFee *btcutil.Amount maxMinerFee btcutil.Amount maxSwapFee btcutil.Amount - maxPrepayAmt btcutil.Amount + maxPrepayAmt *btcutil.Amount } func getLimits(amt btcutil.Amount, quote *looprpc.QuoteResponse) *limits { + maxSwapRoutingFee := getMaxRoutingFee(btcutil.Amount(amt)) + maxPrepayRoutingFee := getMaxRoutingFee(btcutil.Amount( + quote.PrepayAmt, + )) + maxPrepayAmt := btcutil.Amount(quote.PrepayAmt) + return &limits{ - maxSwapRoutingFee: getMaxRoutingFee(btcutil.Amount(amt)), - maxPrepayRoutingFee: getMaxRoutingFee(btcutil.Amount( - quote.PrepayAmt, - )), + maxSwapRoutingFee: &maxSwapRoutingFee, + maxPrepayRoutingFee: &maxPrepayRoutingFee, // Apply a multiplier to the estimated miner fee, to not get // the swap canceled because fees increased in the mean time. maxMinerFee: btcutil.Amount(quote.MinerFee) * 3, maxSwapFee: btcutil.Amount(quote.SwapFee), - maxPrepayAmt: btcutil.Amount(quote.PrepayAmt), + maxPrepayAmt: &maxPrepayAmt, } } func displayLimits(amt btcutil.Amount, l *limits) error { - totalSuccessMax := l.maxSwapRoutingFee + l.maxPrepayRoutingFee + - l.maxMinerFee + l.maxSwapFee + totalSuccessMax := l.maxMinerFee + l.maxSwapFee + if l.maxSwapRoutingFee != nil { + totalSuccessMax += *l.maxSwapRoutingFee + } + if l.maxPrepayRoutingFee != nil { + totalSuccessMax += *l.maxPrepayRoutingFee + } fmt.Printf("Max swap fees for %d loop out: %d\n", btcutil.Amount(amt), totalSuccessMax, @@ -130,13 +140,22 @@ 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) - fmt.Printf("Max off-chain prepay routing fee: %d\n", - l.maxPrepayRoutingFee) + + if l.maxSwapRoutingFee != nil { + fmt.Printf("Max off-chain swap routing fee: %d\n", + *l.maxSwapRoutingFee) + } + + if l.maxPrepayRoutingFee != nil { + fmt.Printf("Max off-chain prepay routing fee: %d\n", + *l.maxPrepayRoutingFee) + } fmt.Printf("Max swap fee: %d\n", l.maxSwapFee) - fmt.Printf("Max no show penalty: %d\n", - l.maxPrepayAmt) + + if l.maxPrepayAmt != nil { + fmt.Printf("Max no show penalty: %d\n", + *l.maxPrepayAmt) + } fmt.Printf("CONTINUE SWAP? (y/n): ") fmt.Scanln(&answer) 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 0df40ea..6d66882 100644 --- a/cmd/loopd/daemon.go +++ b/cmd/loopd/daemon.go @@ -43,11 +43,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, @@ -57,6 +57,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 a0085db..0557f97 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 == "" { @@ -85,6 +86,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: @@ -105,6 +108,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(), @@ -112,7 +125,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 } @@ -214,7 +227,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 { @@ -250,3 +263,69 @@ 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), + 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), + 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), + 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..d4010f2 100644 --- a/interface.go +++ b/interface.go @@ -158,6 +158,90 @@ 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 + + // 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 + + // 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 + + // 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 + + // 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 +275,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..319788e 100644 --- a/lndclient/chainnotifier_client.go +++ b/lndclient/chainnotifier_client.go @@ -11,8 +11,6 @@ import ( "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/lnrpc/chainrpc" "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) // ChainNotifierClient exposes base lightning functionality. @@ -101,9 +99,7 @@ func (s *chainNotifierClient) RegisterSpendNtfn(ctx context.Context, for { spendEvent, err := resp.Recv() if err != nil { - if status.Code(err) != codes.Canceled { - errChan <- err - } + errChan <- err return } @@ -125,7 +121,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[:] @@ -155,9 +150,7 @@ func (s *chainNotifierClient) RegisterConfirmationsNtfn(ctx context.Context, var confEvent *chainrpc.ConfEvent confEvent, err := confStream.Recv() if err != nil { - if status.Code(err) != codes.Canceled { - errChan <- err - } + errChan <- err return } @@ -226,9 +219,7 @@ func (s *chainNotifierClient) RegisterBlockEpochNtfn(ctx context.Context) ( for { epoch, err := blockEpochClient.Recv() if err != nil { - if status.Code(err) != codes.Canceled { - blockErrorChan <- err - } + blockErrorChan <- err return } diff --git a/lndclient/invoices_client.go b/lndclient/invoices_client.go index 9738b1d..5e92138 100644 --- a/lndclient/invoices_client.go +++ b/lndclient/invoices_client.go @@ -5,19 +5,18 @@ import ( "errors" "sync" + "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lntypes" "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) // InvoicesClient exposes invoice functionality. type InvoicesClient interface { SubscribeSingleInvoice(ctx context.Context, hash lntypes.Hash) ( - <-chan channeldb.ContractState, <-chan error, error) + <-chan InvoiceUpdate, <-chan error, error) SettleInvoice(ctx context.Context, preimage lntypes.Preimage) error @@ -27,6 +26,12 @@ type InvoicesClient interface { string, error) } +// InvoiceUpdate contains a state update for an invoice. +type InvoiceUpdate struct { + State channeldb.ContractState + AmtPaid btcutil.Amount +} + type invoicesClient struct { client invoicesrpc.InvoicesClient wg sync.WaitGroup @@ -69,7 +74,7 @@ func (s *invoicesClient) CancelInvoice(ctx context.Context, } func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context, - hash lntypes.Hash) (<-chan channeldb.ContractState, + hash lntypes.Hash) (<-chan InvoiceUpdate, <-chan error, error) { invoiceStream, err := s.client. @@ -81,7 +86,7 @@ func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context, return nil, nil, err } - updateChan := make(chan channeldb.ContractState) + updateChan := make(chan InvoiceUpdate) errChan := make(chan error, 1) // Invoice updates goroutine. @@ -91,9 +96,7 @@ func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context, for { invoice, err := invoiceStream.Recv() if err != nil { - if status.Code(err) != codes.Canceled { - errChan <- err - } + errChan <- err return } @@ -104,7 +107,10 @@ func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context, } select { - case updateChan <- state: + case updateChan <- InvoiceUpdate{ + State: state, + AmtPaid: btcutil.Amount(invoice.AmtPaidSat), + }: case <-ctx.Done(): return } diff --git a/lndclient/lightning_client.go b/lndclient/lightning_client.go index 9067a9d..d74c538 100644 --- a/lndclient/lightning_client.go +++ b/lndclient/lightning_client.go @@ -14,7 +14,6 @@ import ( "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lntypes" - "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/zpay32" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -29,8 +28,8 @@ type LightningClient interface { GetInfo(ctx context.Context) (*Info, error) - GetFeeEstimate(ctx context.Context, amt btcutil.Amount, dest [33]byte) ( - lnwire.MilliSatoshi, error) + EstimateFeeToP2WSH(ctx context.Context, amt btcutil.Amount, + confTarget int32) (btcutil.Amount, error) ConfirmedWalletBalance(ctx context.Context) (btcutil.Amount, error) @@ -144,28 +143,35 @@ func (s *lightningClient) GetInfo(ctx context.Context) (*Info, error) { }, nil } -func (s *lightningClient) GetFeeEstimate(ctx context.Context, amt btcutil.Amount, - dest [33]byte) (lnwire.MilliSatoshi, error) { +func (s *lightningClient) EstimateFeeToP2WSH(ctx context.Context, + amt btcutil.Amount, confTarget int32) (btcutil.Amount, + error) { rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout) defer cancel() - routeResp, err := s.client.QueryRoutes( + // Generate dummy p2wsh address for fee estimation. + wsh := [32]byte{} + p2wshAddress, err := btcutil.NewAddressWitnessScriptHash( + wsh[:], s.params, + ) + if err != nil { + return 0, err + } + + resp, err := s.client.EstimateFee( rpcCtx, - &lnrpc.QueryRoutesRequest{ - Amt: int64(amt), - NumRoutes: 1, - PubKey: hex.EncodeToString(dest[:]), + &lnrpc.EstimateFeeRequest{ + TargetConf: confTarget, + AddrToAmount: map[string]int64{ + p2wshAddress.String(): int64(amt), + }, }, ) if err != nil { return 0, err } - if len(routeResp.Routes) == 0 { - return 0, ErrNoRouteToServer - } - - return lnwire.MilliSatoshi(routeResp.Routes[0].TotalFeesMsat), nil + return btcutil.Amount(resp.FeeSat), nil } // PayInvoice pays an invoice. @@ -310,13 +316,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..25af146 --- /dev/null +++ b/loopin.go @@ -0,0 +1,585 @@ +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) + + // TimeoutTxConfTarget defines the confirmation target for the loop in + // timeout tx. + TimeoutTxConfTarget = int32(2) +) + +// loopInSwap contains all the in-memory state related to a pending loop in +// swap. +type loopInSwap struct { + swapKit + + loopdb.LoopInContract + + 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 + + // 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 success key 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, + ReceiverKey: swapResp.receiverKey, + SenderKey: senderKey, + Preimage: swapPreimage, + AmountRequested: request.Amount, + CltvExpiry: swapResp.expiry, + MaxMinerFee: request.MaxMinerFee, + MaxSwapFee: request.MaxSwapFee, + }, + } + + 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 { + + // 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 +} + +// 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, htlcValue, err := swap.GetScriptOutput( + conf.Tx, s.htlc.ScriptHash, + ) + if err != nil { + return err + } + + // TODO: Add miner fee of htlc tx to swap cost balance. + + // 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) { + s.state = loopdb.StateSuccess + + // Server swept the htlc. The htlc value can be added to the + // server cost balance. + s.cost.Server += htlcValue + } else { + s.state = loopdb.StateFailTimeout + + // Now that the timeout tx confirmed, we can safely cancel the + // swap invoice. We still need to query the final invoice state. + // This is not a hodl invoice, so it may be that the invoice was + // already settled. This means that the server didn't succeed in + // sweeping the htlc after paying the invoice. + err := s.lnd.Invoices.CancelInvoice(globalCtx, s.hash) + if err != nil && err != channeldb.ErrInvoiceAlreadySettled { + return err + } + + // TODO: Add miner fee of timeout tx to swap cost balance. + } + + // Wait for a final state of the swap invoice. It should either be + // settled because the server successfully paid it or canceled because + // we canceled after our timeout tx confirmed. + err = s.waitForSwapInvoiceResult(globalCtx) + if 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 + +} + +// 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) waitForSwapInvoiceResult(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 update := <-swapInvoiceChan: + s.log.Infof("Received swap invoice update: %v", + update.State) + + switch update.State { + + // Swap invoice was paid, so update server cost balance. + case channeldb.ContractSettled: + s.cost.Server -= update.AmtPaid + return nil + + // Canceled invoice has no effect on server cost + // balance. + case channeldb.ContractCanceled: + 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 + fee, err := s.sweeper.GetSweepFee( + ctx, s.htlc.MaxTimeoutWitnessSize, TimeoutTxConfTarget, + ) + 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..68a5a27 --- /dev/null +++ b/loopin_test.go @@ -0,0 +1,370 @@ +package loop + +import ( + "context" + "testing" + + "github.com/lightninglabs/loop/lndclient" + "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{ + Amount: btcutil.Amount(50000), + MaxSwapFee: btcutil.Amount(1000), + HtlcConfTarget: 2, + } +) + +// TestLoopInSuccess tests the success scenario where the swap completes the +// happy flow. +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, + } + + // Client starts listening for spend of htlc. + <-ctx.lnd.RegisterSpendChannel + + // Server spends htlc. + successTx := wire.MsgTx{} + successTx.AddTxIn(&wire.TxIn{ + 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 <- lndclient.InvoiceUpdate{ + State: channeldb.ContractSettled, + AmtPaid: 49000, + } + + ctx.assertState(loopdb.StateSuccess) + ctx.store.assertLoopInState(loopdb.StateSuccess) + + err = <-errChan + if err != nil { + t.Fatal(err) + } +} + +// TestLoopInTimeout tests the scenario where the server doesn't sweep the htlc +// and the client is forced to reclaim the funds using the timeout tx. +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, + } + + // 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, + } + + // Now that timeout tx has confirmed, the client should be able to + // safely cancel the swap invoice. + <-ctx.lnd.FailInvoiceChannel + + // Client starts listening for swap invoice updates. + subscription := <-ctx.lnd.SingleInvoiceSubcribeChannel + if subscription.Hash != ctx.server.swapHash { + t.Fatal("client subscribing to wrong invoice") + } + + // Signal the the invoice was canceled. + subscription.Update <- lndclient.InvoiceUpdate{ + State: channeldb.ContractCanceled, + } + + ctx.assertState(loopdb.StateFailTimeout) + ctx.store.assertLoopInState(loopdb.StateFailTimeout) + + err = <-errChan + if err != nil { + t.Fatal(err) + } +} + +// TestLoopInResume tests resuming swaps in various states. +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} + + contract := &loopdb.LoopInContract{ + HtlcConfTarget: 2, + SwapContract: loopdb.SwapContract{ + Preimage: testPreimage, + AmountRequested: 100000, + CltvExpiry: 744, + ReceiverKey: receiverKey, + SenderKey: senderKey, + MaxSwapFee: 60000, + 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, + } + + // 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 <- lndclient.InvoiceUpdate{ + State: channeldb.ContractSettled, + AmtPaid: 49000, + } + + 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 7e52b5d..8a46734 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..c2bcd06 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_e75b35e98d3b3b20, []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_e75b35e98d3b3b20, []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_e75b35e98d3b3b20, []int{0} } func (m *LoopOutRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_LoopOutRequest.Unmarshal(m, b) @@ -221,6 +231,85 @@ 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 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,2,opt,name=max_swap_fee,json=maxSwapFee,proto3" json:"max_swap_fee,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,3,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,4,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_e75b35e98d3b3b20, []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) GetMaxSwapFee() int64 { + if m != nil { + return m.MaxSwapFee + } + 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 +324,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_e75b35e98d3b3b20, []int{2} } func (m *SwapResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_SwapResponse.Unmarshal(m, b) @@ -272,7 +361,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_e75b35e98d3b3b20, []int{3} } func (m *MonitorRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_MonitorRequest.Unmarshal(m, b) @@ -325,7 +414,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_e75b35e98d3b3b20, []int{4} } func (m *SwapStatus) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_SwapStatus.Unmarshal(m, b) @@ -404,7 +493,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_e75b35e98d3b3b20, []int{5} } func (m *TermsRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_TermsRequest.Unmarshal(m, b) @@ -446,10 +535,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 +545,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_e75b35e98d3b3b20, []int{6} } func (m *TermsResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_TermsResponse.Unmarshal(m, b) @@ -528,13 +614,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 +627,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_e75b35e98d3b3b20, []int{7} } func (m *QuoteRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_QuoteRequest.Unmarshal(m, b) @@ -594,7 +673,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_e75b35e98d3b3b20, []int{8} } func (m *QuoteResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_QuoteResponse.Unmarshal(m, b) @@ -637,6 +716,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 +746,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 +763,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 +788,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 +847,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 +873,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 +890,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 +920,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 +995,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 +1039,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 +1051,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 +1070,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_e75b35e98d3b3b20) } + +var fileDescriptor_client_e75b35e98d3b3b20 = []byte{ + // 901 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x55, 0xdd, 0x72, 0xdb, 0x44, + 0x18, 0xad, 0x64, 0xc7, 0x3f, 0x5f, 0x64, 0x45, 0xde, 0xb6, 0xa9, 0x31, 0x94, 0x31, 0x1a, 0x5a, + 0x3c, 0xb9, 0x88, 0x21, 0xbd, 0x60, 0xe0, 0x86, 0x71, 0x6d, 0xb7, 0xf5, 0x8c, 0xd3, 0x18, 0xd9, + 0x61, 0x86, 0x1b, 0x34, 0x5b, 0x7b, 0x9b, 0x6a, 0x46, 0xda, 0x55, 0xa5, 0x55, 0x9b, 0x0c, 0xc3, + 0x0d, 0x2f, 0xc0, 0x00, 0x6f, 0xc2, 0xab, 0xf0, 0x0a, 0xbc, 0x02, 0xf7, 0xcc, 0xfe, 0x48, 0x95, + 0x1c, 0x72, 0x93, 0x3b, 0xf9, 0xdb, 0xb3, 0x67, 0xcf, 0x9e, 0xef, 0x7c, 0x6b, 0xb0, 0x36, 0x61, + 0x40, 0x28, 0x3f, 0x8e, 0x13, 0xc6, 0x19, 0x6a, 0x86, 0x8c, 0xc5, 0x49, 0xbc, 0xe9, 0x7f, 0x72, + 0xc1, 0xd8, 0x45, 0x48, 0x46, 0x38, 0x0e, 0x46, 0x98, 0x52, 0xc6, 0x31, 0x0f, 0x18, 0x4d, 0x15, + 0xcc, 0xfd, 0xcb, 0x04, 0x7b, 0xc1, 0x58, 0x7c, 0x96, 0x71, 0x8f, 0xbc, 0xcd, 0x48, 0xca, 0x91, + 0x03, 0x35, 0x1c, 0xf1, 0x9e, 0x31, 0x30, 0x86, 0x35, 0x4f, 0x7c, 0x22, 0x04, 0xf5, 0x2d, 0x49, + 0x79, 0xcf, 0x1c, 0x18, 0xc3, 0xb6, 0x27, 0xbf, 0xd1, 0x08, 0xee, 0x45, 0xf8, 0xd2, 0x4f, 0xdf, + 0xe3, 0xd8, 0x4f, 0x58, 0xc6, 0x03, 0x7a, 0xe1, 0xbf, 0x26, 0xa4, 0x57, 0x93, 0xdb, 0xba, 0x11, + 0xbe, 0x5c, 0xbd, 0xc7, 0xb1, 0xa7, 0x56, 0x9e, 0x11, 0x82, 0x9e, 0xc0, 0xa1, 0xd8, 0x10, 0x27, + 0x24, 0xc6, 0x57, 0x95, 0x2d, 0x75, 0xb9, 0xe5, 0x6e, 0x84, 0x2f, 0x97, 0x72, 0xb1, 0xb4, 0x69, + 0x00, 0x56, 0x71, 0x8a, 0x80, 0xee, 0x49, 0x28, 0x68, 0x76, 0x81, 0xf8, 0x1c, 0xec, 0x12, 0xad, + 0x10, 0xde, 0x90, 0x18, 0xab, 0xa0, 0x1b, 0x47, 0x1c, 0xb9, 0xd0, 0x11, 0xa8, 0x28, 0xa0, 0x24, + 0x91, 0x44, 0x4d, 0x09, 0xda, 0x8f, 0xf0, 0xe5, 0xa9, 0xa8, 0x09, 0xa6, 0x21, 0x38, 0xc2, 0x33, + 0x9f, 0x65, 0xdc, 0xdf, 0xbc, 0xc1, 0x94, 0x92, 0xb0, 0xd7, 0x1a, 0x18, 0xc3, 0xba, 0x67, 0x87, + 0xca, 0xa1, 0x89, 0xaa, 0xba, 0xbf, 0x19, 0xd0, 0x11, 0xa6, 0xcd, 0xe9, 0xcd, 0x9e, 0xed, 0x2a, + 0x37, 0xaf, 0x29, 0xbf, 0xa6, 0xa9, 0x76, 0x5d, 0xd3, 0x63, 0x38, 0x90, 0x9a, 0x02, 0x5a, 0x48, + 0xaa, 0x4b, 0x49, 0x9d, 0x50, 0x9e, 0x9f, 0x2b, 0xfa, 0x14, 0x2c, 0x69, 0x37, 0x49, 0x63, 0x46, + 0x53, 0x82, 0x6c, 0x30, 0x83, 0xad, 0x94, 0xd3, 0xf6, 0xcc, 0x60, 0xeb, 0x3a, 0x60, 0x9f, 0x32, + 0x1a, 0x70, 0x96, 0x68, 0xc5, 0xee, 0xbf, 0x06, 0x80, 0xd8, 0xb2, 0xe2, 0x98, 0x67, 0xe9, 0xff, + 0x5c, 0x40, 0x51, 0x98, 0x39, 0x05, 0x7a, 0x04, 0x75, 0x7e, 0x15, 0x2b, 0x95, 0xf6, 0x49, 0xf7, + 0x58, 0xe7, 0xeb, 0x58, 0x90, 0xac, 0xaf, 0x62, 0xe2, 0xc9, 0x65, 0x34, 0x84, 0xbd, 0x94, 0x63, + 0xae, 0xba, 0x6a, 0x9f, 0xa0, 0x0a, 0x4e, 0x1c, 0x46, 0x3c, 0x05, 0x40, 0x5f, 0xc0, 0x41, 0x40, + 0x03, 0x1e, 0xc8, 0x3c, 0xfa, 0x3c, 0x88, 0xf2, 0xf6, 0xda, 0x1f, 0xca, 0xeb, 0x20, 0x52, 0x8d, + 0xc1, 0x29, 0xf7, 0xb3, 0x78, 0x8b, 0x39, 0x51, 0x48, 0xd5, 0x64, 0x5b, 0xd4, 0xcf, 0x65, 0x59, + 0x22, 0x3f, 0x03, 0xeb, 0x0d, 0x0f, 0x37, 0x3e, 0xde, 0x6e, 0x13, 0x92, 0xa6, 0xb2, 0xcb, 0x6d, + 0x6f, 0x5f, 0xd4, 0xc6, 0xaa, 0xe4, 0xda, 0x60, 0xad, 0x49, 0x12, 0xa5, 0xb9, 0x0f, 0xbf, 0x9b, + 0xd0, 0xd1, 0x05, 0xed, 0xdd, 0x11, 0x74, 0x65, 0xd7, 0x62, 0x7c, 0x15, 0x11, 0xca, 0x7d, 0x19, + 0x7d, 0x65, 0xe5, 0x81, 0x58, 0x58, 0xaa, 0xfa, 0x54, 0xf4, 0xdd, 0x85, 0x4e, 0xde, 0x61, 0xff, + 0x15, 0x4e, 0xf3, 0x36, 0xef, 0xa7, 0xaa, 0xc7, 0x4f, 0x71, 0x4a, 0x2a, 0x98, 0x44, 0x38, 0x53, + 0xab, 0x60, 0x3c, 0xe1, 0xc5, 0x43, 0x80, 0x52, 0x82, 0xd5, 0x40, 0xb4, 0xe3, 0x22, 0xbe, 0x8f, + 0xe1, 0x20, 0x0a, 0xa8, 0x0a, 0x13, 0x8e, 0x58, 0x46, 0xb9, 0xb6, 0xaa, 0x13, 0x05, 0x54, 0x18, + 0x3b, 0x96, 0x45, 0x89, 0xcb, 0x43, 0xa7, 0x71, 0x0d, 0x8d, 0x53, 0xb9, 0xd3, 0xb8, 0x87, 0x00, + 0x9b, 0x90, 0xbf, 0xf3, 0xb7, 0x24, 0xe4, 0x58, 0xba, 0xb4, 0xe7, 0xb5, 0x45, 0x65, 0x2a, 0x0a, + 0xee, 0x00, 0xac, 0xef, 0x33, 0xc6, 0xc9, 0x8d, 0xe9, 0x76, 0x5f, 0x43, 0x47, 0x23, 0xb4, 0x69, + 0x1f, 0x41, 0xab, 0x88, 0xba, 0xc2, 0x35, 0xf5, 0xfd, 0x76, 0xee, 0x66, 0xee, 0xde, 0xed, 0x63, + 0x68, 0xef, 0x8e, 0x40, 0x2b, 0xd2, 0xf9, 0x3f, 0x7a, 0x04, 0xad, 0x3c, 0x5f, 0xc8, 0x82, 0xd6, + 0xe2, 0xec, 0x6c, 0xe9, 0x9f, 0x9d, 0xaf, 0x9d, 0x3b, 0x68, 0x1f, 0x9a, 0xf2, 0xd7, 0xfc, 0xa5, + 0x63, 0x1c, 0xfd, 0x04, 0xed, 0x22, 0x5e, 0xa8, 0x03, 0xed, 0xf9, 0xcb, 0xf9, 0x7a, 0x3e, 0x5e, + 0xcf, 0xa6, 0xce, 0x1d, 0x74, 0x1f, 0xba, 0x4b, 0x6f, 0x36, 0x3f, 0x1d, 0x3f, 0x9f, 0xf9, 0xde, + 0xec, 0x87, 0xd9, 0x78, 0x31, 0x9b, 0x3a, 0x06, 0x42, 0x60, 0xbf, 0x58, 0x2f, 0x26, 0xfe, 0xf2, + 0xfc, 0xe9, 0x62, 0xbe, 0x7a, 0x31, 0x9b, 0x3a, 0xa6, 0xe0, 0x5c, 0x9d, 0x4f, 0x26, 0xb3, 0xd5, + 0xca, 0xa9, 0x21, 0x80, 0xc6, 0xb3, 0xf1, 0x5c, 0x80, 0xeb, 0x27, 0x7f, 0xd4, 0xd5, 0xb0, 0x4c, + 0xe4, 0x0b, 0x8b, 0x3c, 0x68, 0xea, 0x37, 0x13, 0x3d, 0x28, 0xf2, 0x5d, 0x7d, 0x45, 0xfb, 0xf7, + 0x2b, 0xc1, 0xcf, 0x7d, 0x72, 0x1f, 0xfc, 0xfa, 0xf7, 0x3f, 0x7f, 0x9a, 0x5d, 0xd7, 0x1a, 0xbd, + 0xfb, 0x6a, 0x24, 0x10, 0x23, 0x96, 0xf1, 0x6f, 0x8d, 0x23, 0xf4, 0x35, 0x34, 0xd4, 0x93, 0x82, + 0x0e, 0x2b, 0x94, 0xc5, 0x1b, 0x73, 0x03, 0x23, 0xfa, 0x06, 0x9a, 0x7a, 0xb4, 0x4b, 0x62, 0xaa, + 0xc3, 0xde, 0xbf, 0x7b, 0x6d, 0x0a, 0xb3, 0xf4, 0x4b, 0x03, 0xfd, 0x08, 0x96, 0x56, 0x2d, 0x27, + 0x00, 0x7d, 0x38, 0xa1, 0x3c, 0x22, 0xfd, 0xc3, 0xdd, 0xb2, 0xbe, 0x4b, 0x5f, 0xde, 0xe5, 0x1e, + 0x42, 0xe5, 0xbb, 0x8c, 0xb8, 0xa4, 0xf2, 0x0b, 0x6a, 0x99, 0x93, 0x12, 0x75, 0x39, 0x59, 0x25, + 0xea, 0x4a, 0x9c, 0xdc, 0x81, 0xa4, 0xee, 0xa3, 0x5e, 0x85, 0xfa, 0xad, 0xc0, 0x8c, 0x7e, 0xc6, + 0x11, 0xff, 0x05, 0x7d, 0x07, 0xf6, 0x73, 0xc2, 0x95, 0x43, 0xb7, 0x51, 0x5f, 0x21, 0xb8, 0x8d, + 0xc6, 0x57, 0x0d, 0xf9, 0x0f, 0xfa, 0xe4, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x17, 0x23, 0x70, + 0x9a, 0x78, 0x07, 0x00, 0x00, } diff --git a/looprpc/client.proto b/looprpc/client.proto index 104994b..d7d5e7e 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,36 @@ 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 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. + */ + int64 max_swap_fee = 2; + + /** + 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 = 3; + + /** + 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 = 4; +} message SwapResponse { /** @@ -165,6 +213,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 +235,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 +293,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..395f48d 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_1f6c9db5f7136644, []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_1f6c9db5f7136644, []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_1f6c9db5f7136644, []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_1f6c9db5f7136644, []int{3} } func (m *ServerLoopOutQuote) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ServerLoopOutQuote.Unmarshal(m, b) @@ -256,11 +256,223 @@ 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_1f6c9db5f7136644, []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 { + ReceiverKey []byte `protobuf:"bytes,1,opt,name=receiver_key,json=receiverKey,proto3" json:"receiver_key,omitempty"` + Expiry int32 `protobuf:"varint,2,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_1f6c9db5f7136644, []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) 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_1f6c9db5f7136644, []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"` + 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_1f6c9db5f7136644, []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) 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 +489,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 +519,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 +585,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 +633,58 @@ 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_1f6c9db5f7136644) } + +var fileDescriptor_server_1f6c9db5f7136644 = []byte{ + // 589 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x54, 0xdd, 0x6e, 0xd3, 0x4c, + 0x14, 0x94, 0x9d, 0x34, 0xfd, 0x72, 0x9a, 0xb4, 0x1f, 0xcb, 0x8f, 0x4c, 0xda, 0xa0, 0xd6, 0x52, + 0xa1, 0xe2, 0x22, 0x91, 0xe0, 0x09, 0x5a, 0x55, 0x88, 0x8a, 0x8a, 0x52, 0x97, 0x2b, 0x6e, 0xac, + 0x4d, 0x72, 0x88, 0x2d, 0xec, 0xdd, 0xc5, 0xbb, 0xf9, 0x7b, 0x01, 0x1e, 0x01, 0xf1, 0x5c, 0x7d, + 0x22, 0xb4, 0xeb, 0x4d, 0x62, 0x27, 0xe9, 0x0f, 0x77, 0xed, 0x9c, 0xf1, 0xd9, 0xd9, 0x99, 0xc9, + 0x42, 0x43, 0x62, 0x36, 0xc6, 0xac, 0x23, 0x32, 0xae, 0x38, 0xd9, 0x4e, 0x38, 0x17, 0x99, 0xe8, + 0xb7, 0x0e, 0x86, 0x9c, 0x0f, 0x13, 0xec, 0x52, 0x11, 0x77, 0x29, 0x63, 0x5c, 0x51, 0x15, 0x73, + 0x26, 0x73, 0x9a, 0x1f, 0xc1, 0xb3, 0x1b, 0xf3, 0xd9, 0x25, 0xe7, 0xe2, 0x6a, 0xa4, 0x02, 0xfc, + 0x39, 0x42, 0xa9, 0xc8, 0x11, 0x34, 0x32, 0xec, 0x63, 0x3c, 0xc6, 0x2c, 0xfc, 0x81, 0x33, 0xcf, + 0x39, 0x74, 0x4e, 0x1a, 0xc1, 0xce, 0x1c, 0xfb, 0x84, 0x33, 0xb2, 0x0f, 0x75, 0x39, 0xa1, 0x22, + 0x8c, 0xa8, 0x8c, 0x3c, 0xd7, 0xcc, 0xff, 0xd3, 0xc0, 0x47, 0x2a, 0x23, 0xf2, 0x3f, 0x54, 0x68, + 0xaa, 0xbc, 0xca, 0xa1, 0x73, 0x52, 0x0d, 0xf4, 0x9f, 0xfe, 0x1f, 0x07, 0x9e, 0xaf, 0x1c, 0x25, + 0x05, 0x67, 0x12, 0xf5, 0x59, 0x66, 0x51, 0xcc, 0xc6, 0x3c, 0xee, 0xa3, 0x39, 0xab, 0x1e, 0xec, + 0x68, 0xec, 0x22, 0x87, 0xc8, 0x31, 0xec, 0x8a, 0x0c, 0x05, 0x9d, 0x2d, 0x48, 0xae, 0x21, 0x35, + 0x73, 0x74, 0x4e, 0x6b, 0x03, 0x48, 0x64, 0x03, 0xab, 0xb9, 0x62, 0x34, 0xd5, 0x73, 0x44, 0x2b, + 0x7e, 0x01, 0x35, 0x9c, 0x8a, 0x38, 0x9b, 0x79, 0xd5, 0x43, 0xe7, 0x64, 0x2b, 0xb0, 0xff, 0xf9, + 0xfb, 0xf0, 0xb2, 0xa4, 0xec, 0x7a, 0xc4, 0x15, 0x5a, 0x27, 0xfc, 0xdf, 0x2e, 0x90, 0xf5, 0x29, + 0x79, 0x0b, 0x4f, 0x8c, 0x68, 0x41, 0x67, 0x29, 0x32, 0x15, 0x0e, 0x50, 0x2a, 0xab, 0x7c, 0x4f, + 0x0f, 0xbe, 0xe4, 0xf8, 0xb9, 0x36, 0xd3, 0x87, 0xa6, 0xe1, 0x7e, 0x47, 0x0c, 0x7b, 0x54, 0xe6, + 0xe2, 0x2b, 0xf9, 0x0d, 0x3f, 0x20, 0x9e, 0x51, 0x89, 0x25, 0x4e, 0x46, 0x15, 0x1a, 0xf5, 0x4b, + 0x4e, 0x40, 0x95, 0xb9, 0x9e, 0x75, 0x41, 0x7b, 0x5b, 0x35, 0xde, 0xd6, 0x73, 0xe4, 0x34, 0x55, + 0xe4, 0x35, 0xec, 0xa5, 0x31, 0x0b, 0xcd, 0x1a, 0x9a, 0xf2, 0x11, 0x53, 0xde, 0x96, 0xe1, 0x34, + 0xd3, 0x98, 0xdd, 0x4c, 0xa8, 0x38, 0x35, 0xa0, 0xe1, 0xd1, 0x69, 0x89, 0x57, 0xb3, 0x3c, 0x3a, + 0x2d, 0xf0, 0xda, 0x00, 0xfd, 0x44, 0x8d, 0xc3, 0x01, 0x26, 0x8a, 0x7a, 0xdb, 0xc6, 0xb2, 0xba, + 0x46, 0xce, 0x35, 0xe0, 0xff, 0x72, 0xe0, 0xe9, 0xd2, 0x98, 0x0b, 0x36, 0xaf, 0x4e, 0x39, 0x04, + 0x67, 0x35, 0x84, 0x7f, 0xab, 0xcd, 0x5a, 0x39, 0xaa, 0x6b, 0xe5, 0xf0, 0xaf, 0x8b, 0x1d, 0xd6, + 0x3a, 0x96, 0xbd, 0x7a, 0xa8, 0xc3, 0xcb, 0x46, 0xb8, 0xa5, 0x46, 0xb4, 0xc0, 0x2b, 0xae, 0x2c, + 0x15, 0xe2, 0xd6, 0x29, 0xd6, 0x65, 0x31, 0xb4, 0x87, 0xae, 0x65, 0xed, 0x3c, 0x22, 0x6b, 0x77, + 0x3d, 0xeb, 0x0d, 0x61, 0x56, 0x1f, 0x19, 0xe6, 0xd6, 0xc3, 0x61, 0xd6, 0x56, 0xc2, 0x7c, 0x77, + 0xeb, 0x02, 0x68, 0x76, 0x7e, 0x31, 0x72, 0x05, 0xbb, 0x9f, 0x71, 0x62, 0x0b, 0xaf, 0x71, 0xd2, + 0xee, 0xd8, 0x07, 0xa5, 0xb3, 0xe9, 0xbd, 0x68, 0xbd, 0xba, 0x6b, 0x6c, 0x6d, 0xb9, 0x82, 0x46, + 0xe9, 0xe7, 0xe3, 0x6f, 0xe6, 0x17, 0x8d, 0x6e, 0xed, 0xdf, 0xc3, 0x21, 0x97, 0xd0, 0xb4, 0x0a, + 0x2f, 0x8c, 0x1d, 0xe4, 0x60, 0x03, 0x7b, 0x51, 0xca, 0x56, 0xfb, 0x8e, 0xa9, 0x95, 0xf7, 0x15, + 0x76, 0x0a, 0x61, 0x92, 0xa3, 0x8d, 0xec, 0x92, 0x38, 0xff, 0x3e, 0x4a, 0xbe, 0xf5, 0xec, 0xcd, + 0xb7, 0xe3, 0x61, 0xac, 0xa2, 0x51, 0xaf, 0xd3, 0xe7, 0x69, 0x37, 0x89, 0x87, 0x91, 0x62, 0x31, + 0x1b, 0x26, 0xb4, 0x27, 0xbb, 0xfa, 0xeb, 0xae, 0x5d, 0xd1, 0xab, 0x99, 0xc7, 0xf8, 0xfd, 0xdf, + 0x00, 0x00, 0x00, 0xff, 0xff, 0x94, 0xad, 0x2a, 0x29, 0xc3, 0x05, 0x00, 0x00, } diff --git a/looprpc/server.proto b/looprpc/server.proto index d486516..7b1a73b 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,26 @@ 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 { + bytes receiver_key = 1; + int32 expiry = 2; +} + +message ServerLoopInQuoteRequest { +} + +message ServerLoopInQuoteResponse { + int64 swap_fee_base = 1; + int64 swap_fee_rate = 2; + 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..9c59fe6 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,40 @@ 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") + } + + var receiverKeyArray [33]byte + copy(receiverKeyArray[:], receiverKey.SerializeCompressed()) + + s.swapInvoice = swapInvoice + s.swapHash = swapHash + + resp := &newLoopInResponse{ + expiry: s.height + testChargeOnChainCltvDelta, + receiverKey: receiverKeyArray, + } + + return resp, nil +} + +func (s *serverMock) GetLoopInTerms(ctx context.Context) ( + *LoopInTerms, error) { + + return &LoopInTerms{ + SwapFeeBase: testSwapFeeBase, + SwapFeeRate: testSwapFeeRate, + CltvDelta: testChargeOnChainCltvDelta, + MinSwapAmount: testMinSwapAmount, + MaxSwapAmount: testMaxSwapAmount, + }, nil +} diff --git a/swap_server_client.go b/swap_server_client.go index 0e1ac55..466ea64 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,27 @@ 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), + 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 +143,39 @@ 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{ + receiverKey: receiverKey, + expiry: swapResp.Expiry, + }, nil +} + func (s *grpcSwapServerClient) Close() { s.conn.Close() } @@ -143,3 +205,8 @@ type newLoopOutResponse struct { senderKey [33]byte expiry int32 } + +type newLoopInResponse struct { + receiverKey [33]byte + expiry int32 +} diff --git a/test/context.go b/test/context.go index f11ff1d..96f7d2b 100644 --- a/test/context.go +++ b/test/context.go @@ -7,6 +7,7 @@ import ( "time" "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/lndclient" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/zpay32" @@ -140,7 +141,9 @@ func (ctx *Context) AssertPaid( done := func(result error) { select { - case swapPayment.Done <- result: + case swapPayment.Done <- lndclient.PaymentResult{ + Err: result, + }: case <-time.After(Timeout): ctx.T.Fatalf("payment result not consumed") } @@ -206,6 +209,7 @@ func (ctx *Context) DecodeInvoice(request string) *zpay32.Invoice { return payReq } +// GetOutputIndex returns the index in the tx outs of the given script hash. func (ctx *Context) GetOutputIndex(tx *wire.MsgTx, script []byte) int { @@ -226,15 +230,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/invoices_mock.go b/test/invoices_mock.go index 1b6133f..6b216bd 100644 --- a/test/invoices_mock.go +++ b/test/invoices_mock.go @@ -7,7 +7,7 @@ import ( "time" "github.com/btcsuite/btcd/btcec" - "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightninglabs/loop/lndclient" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" @@ -43,10 +43,10 @@ func (s *mockInvoices) CancelInvoice(ctx context.Context, } func (s *mockInvoices) SubscribeSingleInvoice(ctx context.Context, - hash lntypes.Hash) (<-chan channeldb.ContractState, + hash lntypes.Hash) (<-chan lndclient.InvoiceUpdate, <-chan error, error) { - updateChan := make(chan channeldb.ContractState, 2) + updateChan := make(chan lndclient.InvoiceUpdate, 2) errChan := make(chan error) select { diff --git a/test/lightning_client_mock.go b/test/lightning_client_mock.go index e90a0f5..6ab004e 100644 --- a/test/lightning_client_mock.go +++ b/test/lightning_client_mock.go @@ -1,15 +1,14 @@ package test import ( + "crypto/rand" "fmt" "sync" "time" "github.com/btcsuite/btcd/btcec" - "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcutil" "github.com/lightninglabs/loop/lndclient" - "github.com/lightninglabs/loop/swap" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" @@ -29,44 +28,9 @@ func (h *mockLightningClient) PayInvoice(ctx context.Context, invoice string, done := make(chan lndclient.PaymentResult, 1) - mockChan := make(chan error) - h.wg.Add(1) - go func() { - defer h.wg.Done() - - amt, err := swap.GetInvoiceAmt(&chaincfg.TestNet3Params, invoice) - if err != nil { - select { - case done <- lndclient.PaymentResult{ - Err: err, - }: - case <-ctx.Done(): - } - return - } - - var paidFee btcutil.Amount - - err = <-mockChan - if err != nil { - amt = 0 - } else { - paidFee = 1 - } - - select { - case done <- lndclient.PaymentResult{ - Err: err, - PaidFee: paidFee, - PaidAmt: amt, - }: - case <-ctx.Done(): - } - }() - h.lnd.SendPaymentChannel <- PaymentChannelMessage{ PaymentRequest: invoice, - Done: mockChan, + Done: done, } return done @@ -92,10 +56,11 @@ func (h *mockLightningClient) GetInfo(ctx context.Context) (*lndclient.Info, }, nil } -func (h *mockLightningClient) GetFeeEstimate(ctx context.Context, amt btcutil.Amount, dest [33]byte) ( - lnwire.MilliSatoshi, error) { +func (h *mockLightningClient) EstimateFeeToP2WSH(ctx context.Context, + amt btcutil.Amount, confTarget int32) (btcutil.Amount, + error) { - return 0, nil + return 3000, nil } func (h *mockLightningClient) AddInvoice(ctx context.Context, @@ -105,10 +70,15 @@ func (h *mockLightningClient) AddInvoice(ctx context.Context, defer h.lnd.lock.Unlock() var hash lntypes.Hash - if in.Hash != nil { + switch { + case in.Hash != nil: hash = *in.Hash - } else { + case in.Preimage != nil: hash = (*in.Preimage).Hash() + default: + if _, err := rand.Read(hash[:]); err != nil { + return lntypes.Hash{}, "", err + } } // Create and encode the payment request as a bech32 (zpay32) string. diff --git a/test/lnd_services_mock.go b/test/lnd_services_mock.go index ff32ade..6946853 100644 --- a/test/lnd_services_mock.go +++ b/test/lnd_services_mock.go @@ -12,7 +12,6 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/loop/lndclient" "github.com/lightningnetwork/lnd/chainntnfs" - "github.com/lightningnetwork/lnd/channeldb" ) var testStartingHeight = int32(600) @@ -67,13 +66,13 @@ func NewMockLnd() *LndMockServices { // PaymentChannelMessage is the data that passed through SendPaymentChannel. type PaymentChannelMessage struct { PaymentRequest string - Done chan error + Done chan lndclient.PaymentResult } // SingleInvoiceSubscription contains the single invoice subscribers type SingleInvoiceSubscription struct { Hash lntypes.Hash - Update chan channeldb.ContractState + Update chan lndclient.InvoiceUpdate Err chan error } diff --git a/test/testutils.go b/test/testutils.go index 3ed8941..de7d7c7 100644 --- a/test/testutils.go +++ b/test/testutils.go @@ -3,14 +3,17 @@ package test import ( "errors" "fmt" - "github.com/btcsuite/btcd/btcec" - "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcutil" - "github.com/lightningnetwork/lnd/zpay32" "os" "runtime/pprof" "testing" "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/zpay32" ) var ( @@ -20,6 +23,8 @@ var ( // ErrTimeout is returned on timeout. ErrTimeout = errors.New("test timeout") + + testTime = time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC) ) // GetDestAddr deterministically generates a sweep address for testing. @@ -63,6 +68,27 @@ func EncodePayReq(payReq *zpay32.Invoice) (string, error) { return reqString, nil } +// GetInvoice creates a testnet payment request with the given parameters. +func GetInvoice(hash lntypes.Hash, amt btcutil.Amount, memo string) ( + string, error) { + + req, err := zpay32.NewInvoice( + &chaincfg.TestNet3Params, hash, testTime, + zpay32.Description(memo), + zpay32.Amount(lnwire.NewMSatFromSatoshis(amt)), + ) + if err != nil { + return "", err + } + + reqString, err := EncodePayReq(req) + if err != nil { + return "", err + } + + return reqString, nil +} + // DumpGoroutines dumps all currently running goroutines. func DumpGoroutines() { pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) 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