lncli: add new PSBT wallet commands

This commit is contained in:
Oliver Gugger 2020-10-01 16:21:48 +02:00
parent 9a063355e2
commit c206d062d5
No known key found for this signature in database
GPG Key ID: 8E4256593F177720

View File

@ -4,6 +4,9 @@ package main
import (
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sort"
@ -14,6 +17,20 @@ import (
"github.com/urfave/cli"
)
var (
// psbtCommand is a wallet subcommand that is responsible for PSBT
// operations.
psbtCommand = cli.Command{
Name: "psbt",
Usage: "Interact with partially signed bitcoin transactions " +
"(PSBTs).",
Subcommands: []cli.Command{
fundPsbtCommand,
finalizePsbtCommand,
},
}
)
// walletCommands will return the set of commands to enable for walletrpc
// builds.
func walletCommands() []cli.Command {
@ -29,6 +46,8 @@ func walletCommands() []cli.Command {
bumpCloseFeeCommand,
listSweepsCommand,
labelTxCommand,
releaseOutputCommand,
psbtCommand,
},
},
}
@ -304,9 +323,8 @@ func getWaitingCloseCommitments(client lnrpc.LightningClient,
}
var listSweepsCommand = cli.Command{
Name: "listsweeps",
Category: "On-chain",
Usage: "Lists all sweeps that have been published by our node.",
Name: "listsweeps",
Usage: "Lists all sweeps that have been published by our node.",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "verbose",
@ -396,3 +414,330 @@ func labelTransaction(ctx *cli.Context) error {
return nil
}
// fundPsbtResponse is a struct that contains JSOn annotations for nice result
// serialization.
type fundPsbtResponse struct {
Psbt string `json:"psbt"`
ChangeOutputIndex int32 `json:"change_output_index"`
Locks []*walletrpc.UtxoLease `json:"locks"`
}
var fundPsbtCommand = cli.Command{
Name: "fund",
Usage: "Fund a Partially Signed Bitcoin Transaction (PSBT).",
ArgsUsage: "[--template_psbt=T | [--outputs=O [--inputs=I]]] " +
"[--conf_target=C | --sat_per_vbyte=S]",
Description: `
The fund command creates a fully populated PSBT that contains enough
inputs to fund the outputs specified in either the PSBT or the
--outputs flag.
If there are no inputs specified in the template (or --inputs flag),
coin selection is performed automatically. If inputs are specified, the
wallet assumes that full coin selection happened externally and it will
not add any additional inputs to the PSBT. If the specified inputs
aren't enough to fund the outputs with the given fee rate, an error is
returned.
After either selecting or verifying the inputs, all input UTXOs are
locked with an internal app ID.
The 'outputs' flag decodes addresses and the amount to send respectively
in the following JSON format:
--outputs='{"ExampleAddr": NumCoinsInSatoshis, "SecondAddr": Sats}'
The optional 'inputs' flag decodes a JSON list of UTXO outpoints as
returned by the listunspent command for example:
--inputs='["<txid1>:<output-index1>","<txid2>:<output-index2>",...]'
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "template_psbt",
Usage: "the outputs to fund and optional inputs to " +
"spend provided in the base64 PSBT format",
},
cli.StringFlag{
Name: "outputs",
Usage: "a JSON compatible map of destination " +
"addresses to amounts to send, must not " +
"include a change address as that will be " +
"added automatically by the wallet",
},
cli.StringFlag{
Name: "inputs",
Usage: "an optional JSON compatible list of UTXO " +
"outpoints to use as the PSBT's inputs",
},
cli.Uint64Flag{
Name: "conf_target",
Usage: "the number of blocks that the transaction " +
"should be confirmed on-chain within",
Value: 6,
},
cli.Uint64Flag{
Name: "sat_per_vbyte",
Usage: "a manual fee expressed in sat/vbyte that " +
"should be used when creating the transaction",
},
},
Action: actionDecorator(fundPsbt),
}
func fundPsbt(ctx *cli.Context) error {
// Display the command's help message if there aren't any flags
// specified.
if ctx.NumFlags() == 0 {
return cli.ShowCommandHelp(ctx, "fund")
}
req := &walletrpc.FundPsbtRequest{}
// Parse template flags.
switch {
// The PSBT flag is mutally exclusive with the outputs/inputs flags.
case ctx.IsSet("template_psbt") &&
(ctx.IsSet("inputs") || ctx.IsSet("outputs")):
return fmt.Errorf("cannot set template_psbt and inputs/" +
"outputs flags at the same time")
// Use a pre-existing PSBT as the transaction template.
case len(ctx.String("template_psbt")) > 0:
psbtBase64 := ctx.String("template_psbt")
psbtBytes, err := base64.StdEncoding.DecodeString(psbtBase64)
if err != nil {
return err
}
req.Template = &walletrpc.FundPsbtRequest_Psbt{
Psbt: psbtBytes,
}
// The user manually specified outputs and optional inputs in JSON
// format.
case len(ctx.String("outputs")) > 0:
var (
tpl = &walletrpc.TxTemplate{}
amountToAddr map[string]uint64
)
// Parse the address to amount map as JSON now. At least one
// entry must be present.
jsonMap := []byte(ctx.String("outputs"))
if err := json.Unmarshal(jsonMap, &amountToAddr); err != nil {
return fmt.Errorf("error parsing outputs JSON: %v",
err)
}
if len(amountToAddr) == 0 {
return fmt.Errorf("at least one output must be " +
"specified")
}
tpl.Outputs = amountToAddr
// Inputs are optional.
if len(ctx.String("inputs")) > 0 {
var inputs []string
jsonList := []byte(ctx.String("inputs"))
if err := json.Unmarshal(jsonList, &inputs); err != nil {
return fmt.Errorf("error parsing inputs JSON: "+
"%v", err)
}
for idx, input := range inputs {
op, err := NewProtoOutPoint(input)
if err != nil {
return fmt.Errorf("error parsing "+
"UTXO outpoint %d: %v", idx,
err)
}
tpl.Inputs = append(tpl.Inputs, op)
}
}
req.Template = &walletrpc.FundPsbtRequest_Raw{
Raw: tpl,
}
default:
return fmt.Errorf("must specify either template_psbt or " +
"outputs flag")
}
// Parse fee flags.
switch {
case ctx.IsSet("conf_target") && ctx.IsSet("sat_per_vbyte"):
return fmt.Errorf("cannot set conf_target and sat_per_vbyte " +
"at the same time")
case ctx.Uint64("conf_target") > 0:
req.Fees = &walletrpc.FundPsbtRequest_TargetConf{
TargetConf: uint32(ctx.Uint64("conf_target")),
}
case ctx.Uint64("sat_per_vbyte") > 0:
req.Fees = &walletrpc.FundPsbtRequest_SatPerVbyte{
SatPerVbyte: uint32(ctx.Uint64("sat_per_vbyte")),
}
}
walletClient, cleanUp := getWalletClient(ctx)
defer cleanUp()
response, err := walletClient.FundPsbt(context.Background(), req)
if err != nil {
return err
}
printJSON(&fundPsbtResponse{
Psbt: base64.StdEncoding.EncodeToString(
response.FundedPsbt,
),
ChangeOutputIndex: response.ChangeOutputIndex,
Locks: response.LockedUtxos,
})
return nil
}
// finalizePsbtResponse is a struct that contains JSON annotations for nice
// result serialization.
type finalizePsbtResponse struct {
Psbt string `json:"psbt"`
FinalTx string `json:"final_tx"`
}
var finalizePsbtCommand = cli.Command{
Name: "finalize",
Usage: "Finalize a Partially Signed Bitcoin Transaction (PSBT).",
ArgsUsage: "funded_psbt",
Description: `
The finalize command expects a partial transaction with all inputs
and outputs fully declared and tries to sign all inputs that belong to
the wallet. Lnd must be the last signer of the transaction. That means,
if there are any unsigned non-witness inputs or inputs without UTXO
information attached or inputs without witness data that do not belong
to lnd's wallet, this method will fail. If no error is returned, the
PSBT is ready to be extracted and the final TX within to be broadcast.
This method does NOT publish the transaction after it's been finalized
successfully.
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "funded_psbt",
Usage: "the base64 encoded PSBT to finalize",
},
},
Action: actionDecorator(finalizePsbt),
}
func finalizePsbt(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, "finalize")
}
var (
args = ctx.Args()
psbtBase64 string
)
switch {
case ctx.IsSet("funded_psbt"):
psbtBase64 = ctx.String("funded_psbt")
case args.Present():
psbtBase64 = args.First()
default:
return fmt.Errorf("funded_psbt argument missing")
}
psbtBytes, err := base64.StdEncoding.DecodeString(psbtBase64)
if err != nil {
return err
}
req := &walletrpc.FinalizePsbtRequest{
FundedPsbt: psbtBytes,
}
walletClient, cleanUp := getWalletClient(ctx)
defer cleanUp()
response, err := walletClient.FinalizePsbt(context.Background(), req)
if err != nil {
return err
}
printJSON(&finalizePsbtResponse{
Psbt: base64.StdEncoding.EncodeToString(response.SignedPsbt),
FinalTx: hex.EncodeToString(response.RawFinalTx),
})
return nil
}
var releaseOutputCommand = cli.Command{
Name: "releaseoutput",
Usage: "Release an output previously locked by lnd.",
ArgsUsage: "outpoint",
Description: `
The releaseoutput command unlocks an output, allowing it to be available
for coin selection if it remains unspent.
The internal lnd app lock ID is used when releasing the output.
Therefore only UTXOs locked by the fundpsbt command can currently be
released with this command.
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "outpoint",
Usage: "the output to unlock",
},
},
Action: actionDecorator(releaseOutput),
}
func releaseOutput(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, "releaseoutput")
}
var (
args = ctx.Args()
outpointStr string
)
switch {
case ctx.IsSet("outpoint"):
outpointStr = ctx.String("outpoint")
case args.Present():
outpointStr = args.First()
default:
return fmt.Errorf("outpoint argument missing")
}
outpoint, err := NewProtoOutPoint(outpointStr)
if err != nil {
return fmt.Errorf("error parsing outpoint: %v", err)
}
req := &walletrpc.ReleaseOutputRequest{
Outpoint: outpoint,
Id: walletrpc.LndInternalLockID[:],
}
walletClient, cleanUp := getWalletClient(ctx)
defer cleanUp()
response, err := walletClient.ReleaseOutput(context.Background(), req)
if err != nil {
return err
}
printRespJSON(response)
return nil
}