lncli: add new PSBT wallet commands
This commit is contained in:
parent
9a063355e2
commit
c206d062d5
@ -4,6 +4,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
@ -14,6 +17,20 @@ import (
|
|||||||
"github.com/urfave/cli"
|
"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
|
// walletCommands will return the set of commands to enable for walletrpc
|
||||||
// builds.
|
// builds.
|
||||||
func walletCommands() []cli.Command {
|
func walletCommands() []cli.Command {
|
||||||
@ -29,6 +46,8 @@ func walletCommands() []cli.Command {
|
|||||||
bumpCloseFeeCommand,
|
bumpCloseFeeCommand,
|
||||||
listSweepsCommand,
|
listSweepsCommand,
|
||||||
labelTxCommand,
|
labelTxCommand,
|
||||||
|
releaseOutputCommand,
|
||||||
|
psbtCommand,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -304,9 +323,8 @@ func getWaitingCloseCommitments(client lnrpc.LightningClient,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var listSweepsCommand = cli.Command{
|
var listSweepsCommand = cli.Command{
|
||||||
Name: "listsweeps",
|
Name: "listsweeps",
|
||||||
Category: "On-chain",
|
Usage: "Lists all sweeps that have been published by our node.",
|
||||||
Usage: "Lists all sweeps that have been published by our node.",
|
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "verbose",
|
Name: "verbose",
|
||||||
@ -396,3 +414,330 @@ func labelTransaction(ctx *cli.Context) error {
|
|||||||
|
|
||||||
return nil
|
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