From 96c752638c581c5810c77e90965fa73e0d8593ba 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/main.go | 6 ++ cmd/loop/quote.go | 6 +- cmd/loop/staticaddr.go | 212 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 219 insertions(+), 5 deletions(-) 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 4de4e7c28..4505414ef 100644 --- a/cmd/loop/staticaddr.go +++ b/cmd/loop/staticaddr.go @@ -9,7 +9,10 @@ import ( "strings" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightninglabs/loop/labels" "github.com/lightninglabs/loop/looprpc" + "github.com/lightninglabs/loop/swapserverrpc" + "github.com/lightningnetwork/lnd/routing/route" "github.com/urfave/cli" ) @@ -24,6 +27,27 @@ var staticAddressCommands = cli.Command{ withdrawalCommand, summaryCommand, }, + Description: ` + Requests a loop-in swap based on static address deposits. + `, + 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.", + }, + lastHopFlag, + labelFlag, + routeHintsFlag, + privateFlag, + forceFlag, + verboseFlag, + }, + Action: staticAddressLoopIn, } var newStaticAddressCommand = cli.Command{ @@ -194,10 +218,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 +257,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 @@ -297,3 +337,167 @@ 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 + ) + + // 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 + } + } + + req := &looprpc.StaticAddressLoopInRequest{ + Outpoints: depositOutpoints, + MaxSwapFeeSatoshis: int64(limits.maxSwapFee), + LastHop: lastHop, + Label: ctx.String(labelFlag.Name), + Initiator: defaultInitiator, + RouteHints: hints, + Private: ctx.Bool("private"), + } + + resp, err := client.StaticAddressLoopIn(ctxb, req) + if err != nil { + return err + } + + fmt.Printf("Static loop-in response from the server: %v\n", 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 +}