lncli: add new PSBT wallet commands
This commit is contained in:
parent
9a063355e2
commit
c206d062d5
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -305,7 +324,6 @@ 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.",
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user