You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
loop/cmd/loop/loopout.go

271 lines
7.4 KiB
Go

package main
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/looprpc"
"github.com/urfave/cli"
)
var (
channelFlag = cli.StringFlag{
Name: "channel",
Usage: "the comma-separated list of short " +
"channel IDs of the channels to loop out",
}
)
var loopOutCommand = cli.Command{
Name: "out",
Usage: "perform an off-chain to on-chain swap (looping out)",
ArgsUsage: "amt [addr]",
Description: `
Attempts to loop out the target amount into either the backing lnd's
wallet, or a targeted address.
The amount is to be specified in satoshis.
Optionally a BASE58/bech32 encoded bitcoin destination address may be
specified. If not specified, a new wallet address will be generated.`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "addr",
Usage: "the optional address that the looped out funds " +
"should be sent to, if let blank the funds " +
"will go to lnd's wallet",
},
cli.StringFlag{
Name: "account",
Usage: "the name of the account to generate a new " +
"address from. You can list the names of " +
"valid accounts in your backing lnd " +
"instance with \"lncli wallet accounts list\"",
Value: "",
},
cli.StringFlag{
Name: "account_addr_type",
Usage: "the address type of the extended public key " +
"specified in account. Currently only " +
"pay-to-taproot-pubkey(p2tr) is supported",
Value: "p2tr",
},
cli.Uint64Flag{
Name: "amt",
Usage: "the amount in satoshis to loop out. To check " +
"for the minimum and maximum amounts to loop " +
"out please consult \"loop terms\"",
},
cli.Uint64Flag{
Name: "htlc_confs",
Usage: "the number of confirmations (in blocks) " +
"that we require for the htlc extended by " +
"the server before we reveal the preimage",
Value: uint64(loopdb.DefaultLoopOutHtlcConfirmations),
},
cli.Uint64Flag{
Name: "conf_target",
Usage: "the number of blocks from the swap " +
"initiation height that the on-chain HTLC " +
"should be swept within",
Value: uint64(loop.DefaultSweepConfTarget),
},
cli.Int64Flag{
Name: "max_swap_routing_fee",
Usage: "the max off-chain swap routing fee in " +
"satoshis, if not specified, a default max " +
"fee will be used",
},
cli.BoolFlag{
Name: "fast",
Usage: "indicate you want to swap immediately, " +
"paying potentially a higher fee. If not " +
"set the swap server might choose to wait up " +
"to 30 minutes before publishing the swap " +
"HTLC on-chain, to save on its chain fees. " +
"Not setting this flag therefore might " +
"result in a lower swap fee",
},
forceFlag,
labelFlag,
verboseFlag,
channelFlag,
},
Action: loopOut,
}
func loopOut(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.
return cli.ShowCommandHelp(ctx, "out")
}
amt, err := parseAmt(amtStr)
if err != nil {
return err
}
// Parse outgoing channel set. Don't string split if the flag is empty.
// Otherwise, strings.Split returns a slice of length one with an empty
// element.
var outgoingChanSet []uint64
if ctx.IsSet("channel") {
chanStrings := strings.Split(ctx.String("channel"), ",")
for _, chanString := range chanStrings {
chanID, err := strconv.ParseUint(chanString, 10, 64)
if err != nil {
return fmt.Errorf("error parsing channel id "+
"\"%v\"", chanString)
}
outgoingChanSet = append(outgoingChanSet, chanID)
}
}
// Validate our label early so that we can fail before getting a quote.
label := ctx.String(labelFlag.Name)
if err := labels.Validate(label); err != nil {
return err
}
if ctx.IsSet("addr") && ctx.IsSet("account") {
return fmt.Errorf("cannot set --addr and --account at the " +
"same time. Please specify only one source for a new " +
"address to sweep the loop amount to")
}
var destAddr string
var account string
switch {
case ctx.IsSet("addr"):
destAddr = ctx.String("addr")
case ctx.IsSet("account"):
account = ctx.String("account")
case args.Present():
destAddr = args.First()
}
if ctx.IsSet("account") != ctx.IsSet("account_addr_type") {
return fmt.Errorf("cannot set account without specifying " +
"account address type and vice versa")
}
var accountAddrType looprpc.AddressType
if ctx.IsSet("account_addr_type") {
switch ctx.String("account_addr_type") {
case "p2tr":
accountAddrType = looprpc.AddressType_TAPROOT_PUBKEY
default:
return fmt.Errorf("unknown account address type")
}
}
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
// Set our maximum swap wait time. If a fast swap is requested we set
// it to now, otherwise to 30 minutes in the future.
fast := ctx.Bool("fast")
swapDeadline := time.Now()
if !fast {
swapDeadline = time.Now().Add(defaultSwapWaitTime)
}
sweepConfTarget := int32(ctx.Uint64("conf_target"))
htlcConfs := int32(ctx.Uint64("htlc_confs"))
if htlcConfs == 0 {
return fmt.Errorf("at least 1 confirmation required for htlcs")
}
quoteReq := &looprpc.QuoteRequest{
Amt: int64(amt),
ConfTarget: sweepConfTarget,
SwapPublicationDeadline: uint64(swapDeadline.Unix()),
}
quote, err := client.LoopOutQuote(context.Background(), quoteReq)
if err != nil {
return err
}
// Show a warning if a slow swap was requested.
var warning string
if fast {
warning = "Fast swap requested."
} else {
warning = fmt.Sprintf("Regular swap speed requested, it "+
"might take up to %v for the swap to be executed.",
defaultSwapWaitTime)
}
limits := getOutLimits(amt, quote)
// If configured, use the specified maximum swap routing fee.
if ctx.IsSet("max_swap_routing_fee") {
limits.maxSwapRoutingFee = btcutil.Amount(
ctx.Int64("max_swap_routing_fee"),
)
}
// Skip showing details if configured
if !(ctx.Bool("force") || ctx.Bool("f")) {
err = displayOutDetails(
limits, warning, quoteReq, quote, ctx.Bool("verbose"),
)
if err != nil {
return err
}
}
resp, err := client.LoopOut(context.Background(), &looprpc.LoopOutRequest{
Amt: int64(amt),
Dest: destAddr,
IsExternalAddr: destAddr != "",
Account: account,
AccountAddrType: accountAddrType,
MaxMinerFee: int64(limits.maxMinerFee),
MaxPrepayAmt: int64(limits.maxPrepayAmt),
MaxSwapFee: int64(limits.maxSwapFee),
MaxPrepayRoutingFee: int64(limits.maxPrepayRoutingFee),
MaxSwapRoutingFee: int64(limits.maxSwapRoutingFee),
OutgoingChanSet: outgoingChanSet,
SweepConfTarget: sweepConfTarget,
HtlcConfirmations: htlcConfs,
SwapPublicationDeadline: uint64(swapDeadline.Unix()),
Label: label,
Initiator: defaultInitiator,
})
if err != nil {
return err
}
fmt.Printf("Swap initiated\n")
fmt.Printf("ID: %x\n", resp.IdBytes)
fmt.Printf("HTLC address: %v\n", resp.HtlcAddress) // nolint:staticcheck
if resp.ServerMessage != "" {
fmt.Printf("Server message: %v\n", resp.ServerMessage)
}
fmt.Println()
fmt.Printf("Run `loop monitor` to monitor progress.\n")
return nil
}