From 4bfd26fccbee11ebdea391cf13d8c55ca5bf2c4d Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 30 Jul 2024 15:47:29 +0200 Subject: [PATCH] cmd: static address loop-in --- cmd/loop/loopin.go | 3 - cmd/loop/main.go | 6 + cmd/loop/quote.go | 6 +- cmd/loop/staticaddr.go | 241 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 242 insertions(+), 14 deletions(-) diff --git a/cmd/loop/loopin.go b/cmd/loop/loopin.go index 2d84a5e8d..52e889009 100644 --- a/cmd/loop/loopin.go +++ b/cmd/loop/loopin.go @@ -52,9 +52,6 @@ var ( Name: "in", Usage: "perform an on-chain to off-chain swap (loop in)", ArgsUsage: "amt", - Subcommands: []cli.Command{ - staticAddressCommands, - }, Description: ` Send the amount in satoshis specified by the amt argument off-chain. diff --git a/cmd/loop/main.go b/cmd/loop/main.go index 6fbb40a23..17f6f6480 100644 --- a/cmd/loop/main.go +++ b/cmd/loop/main.go @@ -280,6 +280,12 @@ func displayInDetails(req *looprpc.QuoteRequest, "wallet.\n\n") } + if req.DepositOutpoints != nil { + fmt.Printf("On-chain fees for static address loop-ins are not " + + "included.\nThey were already paid when the deposits " + + "were created.\n\n") + } + printQuoteInResp(req, resp, verbose) fmt.Printf("\nCONTINUE SWAP? (y/n): ") diff --git a/cmd/loop/quote.go b/cmd/loop/quote.go index f92b7243c..ec2cdb657 100644 --- a/cmd/loop/quote.go +++ b/cmd/loop/quote.go @@ -228,7 +228,11 @@ func printQuoteInResp(req *looprpc.QuoteRequest, totalFee := resp.HtlcPublishFeeSat + resp.SwapFeeSat - fmt.Printf(satAmtFmt, "Send on-chain:", req.Amt) + if req.DepositOutpoints != nil { + fmt.Printf(satAmtFmt, "Previously deposited on-chain:", req.Amt) + } else { + fmt.Printf(satAmtFmt, "Send on-chain:", req.Amt) + } fmt.Printf(satAmtFmt, "Receive off-chain:", req.Amt-totalFee) switch { diff --git a/cmd/loop/staticaddr.go b/cmd/loop/staticaddr.go index dcd02834c..de8d3a57a 100644 --- a/cmd/loop/staticaddr.go +++ b/cmd/loop/staticaddr.go @@ -9,21 +9,55 @@ import ( "strings" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightninglabs/loop/labels" "github.com/lightninglabs/loop/looprpc" + "github.com/lightninglabs/loop/staticaddr/loopin" + "github.com/lightninglabs/loop/swapserverrpc" + "github.com/lightningnetwork/lnd/routing/route" "github.com/urfave/cli" ) var staticAddressCommands = cli.Command{ Name: "static", ShortName: "s", - Usage: "manage static loop-in addresses", - Category: "StaticAddress", + Usage: "perform on-chain to off-chain swaps using static addresses.", Subcommands: []cli.Command{ newStaticAddressCommand, listUnspentCommand, withdrawalCommand, summaryCommand, }, + Description: ` + Requests a loop-in swap based on static address deposits. After the + creation of a static address funds can be send to it. Once the funds are + confirmed on-chain they can be swapped instantaneously. If deposited + funds are not needed they can we withdrawn back to the local lnd wallet. + `, + Flags: []cli.Flag{ + cli.StringSliceFlag{ + Name: "utxo", + Usage: "specify the utxos of deposits as " + + "outpoints(tx:idx) that should be looped in.", + }, + cli.BoolFlag{ + Name: "all", + Usage: "loop in all static address deposits.", + }, + cli.DurationFlag{ + Name: "payment_timeout", + Usage: "the maximum time in seconds that the server " + + "is allowed to take for the swap payment. " + + "The client can retry the swap with adjusted " + + "parameters after the payment timed out.", + }, + lastHopFlag, + labelFlag, + routeHintsFlag, + privateFlag, + forceFlag, + verboseFlag, + }, + Action: staticAddressLoopIn, } var newStaticAddressCommand = cli.Command{ @@ -169,10 +203,11 @@ func withdraw(ctx *cli.Context) error { return fmt.Errorf("unknown withdrawal request") } - resp, err := client.WithdrawDeposits(ctxb, &looprpc.WithdrawDepositsRequest{ - Outpoints: outpoints, - All: isAllSelected, - }) + resp, err := client.WithdrawDeposits(ctxb, + &looprpc.WithdrawDepositsRequest{ + Outpoints: outpoints, + All: isAllSelected, + }) if err != nil { return err } @@ -194,10 +229,14 @@ var summaryCommand = cli.Command{ cli.StringFlag{ Name: "filter", Usage: "specify a filter to only display deposits in " + - "the specified state. The state can be one " + - "of [deposited|withdrawing|withdrawn|" + - "publish_expired_deposit|" + - "wait_for_expiry_sweep|expired|failed].", + "the specified state. Leaving out the filter " + + "returns all deposits.\nThe state can be one " + + "of the following: \n" + + "deposited\nwithdrawing\nwithdrawn\n" + + "looping_in\nlooped_in\n" + + "publish_expired_deposit\n" + + "sweep_htlc_timeout\nhtlc_timeout_swept\n" + + "wait_for_expiry_sweep\nexpired\nfailed\n.", }, }, Action: summary, @@ -229,9 +268,21 @@ func summary(ctx *cli.Context) error { case "withdrawn": filterState = looprpc.DepositState_WITHDRAWN + case "looping_in": + filterState = looprpc.DepositState_LOOPING_IN + + case "looped_in": + filterState = looprpc.DepositState_LOOPED_IN + case "publish_expired_deposit": filterState = looprpc.DepositState_PUBLISH_EXPIRED + case "sweep_htlc_timeout": + filterState = looprpc.DepositState_SWEEP_HTLC_TIMEOUT + + case "htlc_timeout_swept": + filterState = looprpc.DepositState_HTLC_TIMEOUT_SWEPT + case "wait_for_expiry_sweep": filterState = looprpc.DepositState_WAIT_FOR_EXPIRY_SWEEP @@ -294,3 +345,173 @@ func NewProtoOutPoint(op string) (*looprpc.OutPoint, error) { OutputIndex: uint32(outputIndex), }, nil } + +func staticAddressLoopIn(ctx *cli.Context) error { + if ctx.NumFlags() == 0 && ctx.NArg() == 0 { + return cli.ShowAppHelp(ctx) + } + + client, cleanup, err := getClient(ctx) + if err != nil { + return err + } + defer cleanup() + + var ( + ctxb = context.Background() + isAllSelected = ctx.IsSet("all") + isUtxoSelected = ctx.IsSet("utxo") + label = ctx.String("static-loop-in") + hints []*swapserverrpc.RouteHint + lastHop []byte + paymentTimeoutSeconds = uint32(loopin.DefaultPaymentTimeoutSeconds) + ) + + // Validate our label early so that we can fail before getting a quote. + if err := labels.Validate(label); err != nil { + return err + } + + // Private and route hints are mutually exclusive as setting private + // means we retrieve our own route hints from the connected node. + hints, err = validateRouteHints(ctx) + if err != nil { + return err + } + + if ctx.IsSet(lastHopFlag.Name) { + lastHopVertex, err := route.NewVertexFromStr( + ctx.String(lastHopFlag.Name), + ) + if err != nil { + return err + } + + lastHop = lastHopVertex[:] + } + + // Get the amount we need to quote for. + summaryResp, err := client.GetStaticAddressSummary( + ctxb, &looprpc.StaticAddressSummaryRequest{ + StateFilter: looprpc.DepositState_DEPOSITED, + }, + ) + if err != nil { + return err + } + + var depositOutpoints []string + switch { + case isAllSelected == isUtxoSelected: + return errors.New("must select either all or some utxos") + + case isAllSelected: + depositOutpoints = depositsToOutpoints( + summaryResp.FilteredDeposits, + ) + + case isUtxoSelected: + depositOutpoints = ctx.StringSlice("utxo") + + default: + return fmt.Errorf("unknown quote request") + } + + if containsDuplicates(depositOutpoints) { + return errors.New("duplicate outpoints detected") + } + + quoteReq := &looprpc.QuoteRequest{ + LoopInRouteHints: hints, + LoopInLastHop: lastHop, + Private: ctx.Bool(privateFlag.Name), + DepositOutpoints: depositOutpoints, + } + quote, err := client.GetLoopInQuote(ctxb, quoteReq) + if err != nil { + return err + } + + limits := getInLimits(quote) + + // populate the quote request with the sum of selected deposits and + // prompt the user for acceptance. + quoteReq.Amt, err = sumDeposits( + depositOutpoints, summaryResp.FilteredDeposits, + ) + if err != nil { + return err + } + + if !(ctx.Bool("force") || ctx.Bool("f")) { + err = displayInDetails(quoteReq, quote, ctx.Bool("verbose")) + if err != nil { + return err + } + } + + if ctx.IsSet("payment_timeout") { + paymentTimeoutSeconds = uint32(ctx.Duration("payment_timeout").Seconds()) + } + + req := &looprpc.StaticAddressLoopInRequest{ + Outpoints: depositOutpoints, + MaxSwapFeeSatoshis: int64(limits.maxSwapFee), + LastHop: lastHop, + Label: ctx.String(labelFlag.Name), + Initiator: defaultInitiator, + RouteHints: hints, + Private: ctx.Bool("private"), + PaymentTimeoutSeconds: paymentTimeoutSeconds, + } + + resp, err := client.StaticAddressLoopIn(ctxb, req) + if err != nil { + return err + } + + printRespJSON(resp) + + return nil +} + +func containsDuplicates(outpoints []string) bool { + found := make(map[string]struct{}) + for _, outpoint := range outpoints { + if _, ok := found[outpoint]; ok { + return true + } + found[outpoint] = struct{}{} + } + + return false +} + +func sumDeposits(outpoints []string, deposits []*looprpc.Deposit) (int64, + error) { + + var sum int64 + depositMap := make(map[string]*looprpc.Deposit) + for _, deposit := range deposits { + depositMap[deposit.Outpoint] = deposit + } + + for _, outpoint := range outpoints { + if _, ok := depositMap[outpoint]; !ok { + return 0, fmt.Errorf("deposit %v not found", outpoint) + } + + sum += depositMap[outpoint].Value + } + + return sum, nil +} + +func depositsToOutpoints(deposits []*looprpc.Deposit) []string { + outpoints := make([]string, 0, len(deposits)) + for _, deposit := range deposits { + outpoints = append(outpoints, deposit.Outpoint) + } + + return outpoints +}