diff --git a/cmd/lncli/types.go b/cmd/lncli/types.go index 78d640f9..30d559a7 100644 --- a/cmd/lncli/types.go +++ b/cmd/lncli/types.go @@ -1,7 +1,11 @@ package main import ( + "encoding/hex" + "errors" "fmt" + "strconv" + "strings" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightningnetwork/lnd/lnrpc" @@ -17,6 +21,27 @@ func NewOutPointFromProto(op *lnrpc.OutPoint) OutPoint { return OutPoint(fmt.Sprintf("%v:%d", hash, op.OutputIndex)) } +// NewProtoOutPoint parses an OutPoint into its corresponding lnrpc.OutPoint +// type. +func NewProtoOutPoint(op string) (*lnrpc.OutPoint, error) { + parts := strings.Split(op, ":") + if len(parts) != 2 { + return nil, errors.New("outpoint should be of the form txid:index") + } + txid := parts[0] + if hex.DecodedLen(len(txid)) != chainhash.HashSize { + return nil, fmt.Errorf("invalid hex-encoded txid %v", txid) + } + outputIndex, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, fmt.Errorf("invalid output index: %v", err) + } + return &lnrpc.OutPoint{ + TxidStr: txid, + OutputIndex: uint32(outputIndex), + }, nil +} + // Utxo displays information about an unspent output, including its address, // amount, pkscript, and confirmations. type Utxo struct { diff --git a/cmd/lncli/walletrpc_active.go b/cmd/lncli/walletrpc_active.go index e86819bb..35dc562d 100644 --- a/cmd/lncli/walletrpc_active.go +++ b/cmd/lncli/walletrpc_active.go @@ -4,6 +4,7 @@ package main import ( "context" + "fmt" "sort" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" @@ -21,6 +22,7 @@ func walletCommands() []cli.Command { Description: "", Subcommands: []cli.Command{ pendingSweepsCommand, + bumpFeeCommand, }, }, } @@ -81,3 +83,89 @@ func pendingSweeps(ctx *cli.Context) error { return nil } + +var bumpFeeCommand = cli.Command{ + Name: "bumpfee", + Usage: "Bumps the fee of an arbitrary input/transaction.", + ArgsUsage: "outpoint", + Description: ` + This command takes a different approach than bitcoind's bumpfee command. + lnd has a central batching engine in which inputs with similar fee rates + are batched together to save on transaction fees. Due to this, we cannot + rely on bumping the fee on a specific transaction, since transactions + can change at any point with the addition of new inputs. The list of + inputs that currently exist within lnd's central batching engine can be + retrieved through lncli pendingsweeps. + + When bumping the fee of an input that currently exists within lnd's + central batching engine, a higher fee transaction will be created that + replaces the lower fee transaction through the Replace-By-Fee (RBF) + policy. + + This command also serves useful when wanting to perform a + Child-Pays-For-Parent (CPFP), where the child transaction pays for its + parent's fee. This can be done by specifying an outpoint within the low + fee transaction that is under the control of the wallet. + + A fee preference must be provided, either through the conf_target or + sat_per_byte parameters. + + Note that this command currently doesn't perform any validation checks + on the fee preference being provided. For now, the responsibility of + ensuring that the new fee preference is sufficient is delegated to the + user.`, + Flags: []cli.Flag{ + cli.Uint64Flag{ + Name: "conf_target", + Usage: "the number of blocks that the output should " + + "be swept on-chain within", + }, + cli.Uint64Flag{ + Name: "sat_per_byte", + Usage: "a manual fee expressed in sat/byte that " + + "should be used when sweeping the output", + }, + }, + Action: actionDecorator(bumpFee), +} + +func bumpFee(ctx *cli.Context) error { + // Display the command's help message if we do not have the expected + // number of arguments/flags. + if ctx.NArg() != 1 || ctx.NumFlags() != 1 { + return cli.ShowCommandHelp(ctx, "bumpfee") + } + + // Validate and parse the relevant arguments/flags. + protoOutPoint, err := NewProtoOutPoint(ctx.Args().Get(0)) + if err != nil { + return err + } + + var confTarget, satPerByte uint32 + switch { + case ctx.IsSet("conf_target") && ctx.IsSet("sat_per_byte"): + return fmt.Errorf("either conf_target or sat_per_byte should " + + "be set, but not both") + case ctx.IsSet("conf_target"): + confTarget = uint32(ctx.Uint64("conf_target")) + case ctx.IsSet("sat_per_byte"): + satPerByte = uint32(ctx.Uint64("sat_per_byte")) + } + + client, cleanUp := getWalletClient(ctx) + defer cleanUp() + + resp, err := client.BumpFee(context.Background(), &walletrpc.BumpFeeRequest{ + Outpoint: protoOutPoint, + TargetConf: confTarget, + SatPerByte: satPerByte, + }) + if err != nil { + return err + } + + printRespJSON(resp) + + return nil +}