github.com/decred/dcrlnd@v0.7.6/cmd/dcrlncli/walletrpc_active.go (about)

     1  //go:build !no_walletrpc
     2  // +build !no_walletrpc
     3  
     4  package main
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/base64"
     9  	"encoding/hex"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"sort"
    14  
    15  	"github.com/decred/dcrd/chaincfg/chainhash"
    16  	"github.com/decred/dcrd/wire"
    17  	"github.com/decred/dcrlnd/lnrpc"
    18  	"github.com/decred/dcrlnd/lnrpc/walletrpc"
    19  	"github.com/urfave/cli"
    20  )
    21  
    22  var (
    23  	// psbtCommand is a wallet subcommand that is responsible for PSBT
    24  	// operations.
    25  	psbtCommand = cli.Command{
    26  		Name: "psbt",
    27  		Usage: "Interact with partially signed bitcoin transactions " +
    28  			"(PSBTs).",
    29  		Subcommands: []cli.Command{
    30  			fundPsbtCommand,
    31  			finalizePsbtCommand,
    32  		},
    33  	}
    34  
    35  	// accountsCommand is a wallet subcommand that is responsible for
    36  	// account management operations.
    37  	accountsCommand = cli.Command{
    38  		Name:  "accounts",
    39  		Usage: "Interact with wallet accounts.",
    40  		Subcommands: []cli.Command{
    41  			listAccountsCommand,
    42  			importAccountCommand,
    43  			importPubKeyCommand,
    44  		},
    45  	}
    46  )
    47  
    48  // walletCommands will return the set of commands to enable for walletrpc
    49  // builds.
    50  func walletCommands() []cli.Command {
    51  	return []cli.Command{
    52  		{
    53  			Name:        "wallet",
    54  			Category:    "Wallet",
    55  			Usage:       "Interact with the wallet.",
    56  			Description: "",
    57  			Subcommands: []cli.Command{
    58  				pendingSweepsCommand,
    59  				bumpFeeCommand,
    60  				bumpCloseFeeCommand,
    61  				listSweepsCommand,
    62  				labelTxCommand,
    63  				publishTxCommand,
    64  				releaseOutputCommand,
    65  				listLeasesCommand,
    66  				psbtCommand,
    67  				accountsCommand,
    68  				rescanWalletCommand,
    69  			},
    70  		},
    71  	}
    72  }
    73  
    74  func getWalletClient(ctx *cli.Context) (walletrpc.WalletKitClient, func()) {
    75  	conn := getClientConn(ctx, false)
    76  	cleanUp := func() {
    77  		conn.Close()
    78  	}
    79  	return walletrpc.NewWalletKitClient(conn), cleanUp
    80  }
    81  
    82  var pendingSweepsCommand = cli.Command{
    83  	Name:      "pendingsweeps",
    84  	Usage:     "List all outputs that are pending to be swept within lnd.",
    85  	ArgsUsage: "",
    86  	Description: `
    87  	List all on-chain outputs that lnd is currently attempting to sweep
    88  	within its central batching engine. Outputs with similar fee rates are
    89  	batched together in order to sweep them within a single transaction.
    90  	`,
    91  	Flags:  []cli.Flag{},
    92  	Action: actionDecorator(pendingSweeps),
    93  }
    94  
    95  func pendingSweeps(ctx *cli.Context) error {
    96  	ctxc := getContext()
    97  	client, cleanUp := getWalletClient(ctx)
    98  	defer cleanUp()
    99  
   100  	req := &walletrpc.PendingSweepsRequest{}
   101  	resp, err := client.PendingSweeps(ctxc, req)
   102  	if err != nil {
   103  		return err
   104  	}
   105  
   106  	// Sort them in ascending fee rate order for display purposes.
   107  	sort.Slice(resp.PendingSweeps, func(i, j int) bool {
   108  		return resp.PendingSweeps[i].AtomsPerByte <
   109  			resp.PendingSweeps[j].AtomsPerByte
   110  	})
   111  
   112  	var pendingSweepsResp = struct {
   113  		PendingSweeps []*PendingSweep `json:"pending_sweeps"`
   114  	}{
   115  		PendingSweeps: make([]*PendingSweep, 0, len(resp.PendingSweeps)),
   116  	}
   117  
   118  	for _, protoPendingSweep := range resp.PendingSweeps {
   119  		pendingSweep := NewPendingSweepFromProto(protoPendingSweep)
   120  		pendingSweepsResp.PendingSweeps = append(
   121  			pendingSweepsResp.PendingSweeps, pendingSweep,
   122  		)
   123  	}
   124  
   125  	printJSON(pendingSweepsResp)
   126  
   127  	return nil
   128  }
   129  
   130  var bumpFeeCommand = cli.Command{
   131  	Name:      "bumpfee",
   132  	Usage:     "Bumps the fee of an arbitrary input/transaction.",
   133  	ArgsUsage: "outpoint",
   134  	Description: `
   135  	This command takes a different approach than bitcoind's bumpfee command.
   136  	lnd has a central batching engine in which inputs with similar fee rates
   137  	are batched together to save on transaction fees. Due to this, we cannot
   138  	rely on bumping the fee on a specific transaction, since transactions
   139  	can change at any point with the addition of new inputs. The list of
   140  	inputs that currently exist within lnd's central batching engine can be
   141  	retrieved through lncli pendingsweeps.
   142  
   143  	When bumping the fee of an input that currently exists within lnd's
   144  	central batching engine, a higher fee transaction will be created that
   145  	replaces the lower fee transaction through the Replace-By-Fee (RBF)
   146  	policy.
   147  
   148  	This command also serves useful when wanting to perform a
   149  	Child-Pays-For-Parent (CPFP), where the child transaction pays for its
   150  	parent's fee. This can be done by specifying an outpoint within the low
   151  	fee transaction that is under the control of the wallet.
   152  
   153  	A fee preference must be provided, either through the conf_target or
   154  	atoms_per_byte parameters.
   155  
   156  	Note that this command currently doesn't perform any validation checks
   157  	on the fee preference being provided. For now, the responsibility of
   158  	ensuring that the new fee preference is sufficient is delegated to the
   159  	user.
   160  
   161  	The force flag enables sweeping of inputs that are negatively yielding.
   162  	Normally it does not make sense to lose money on sweeping, unless a
   163  	parent transaction needs to get confirmed and there is only a small
   164  	output available to attach the child transaction to.
   165  	`,
   166  	Flags: []cli.Flag{
   167  		cli.Uint64Flag{
   168  			Name: "conf_target",
   169  			Usage: "the number of blocks that the output should " +
   170  				"be swept on-chain within",
   171  		},
   172  		cli.Uint64Flag{
   173  			Name: "atoms_per_byte",
   174  			Usage: "a manual fee expressed in sat/byte that " +
   175  				"should be used when sweeping the output",
   176  		},
   177  		cli.BoolFlag{
   178  			Name:  "force",
   179  			Usage: "sweep even if the yield is negative",
   180  		},
   181  	},
   182  	Action: actionDecorator(bumpFee),
   183  }
   184  
   185  func bumpFee(ctx *cli.Context) error {
   186  	ctxc := getContext()
   187  
   188  	// Display the command's help message if we do not have the expected
   189  	// number of arguments/flags.
   190  	if ctx.NArg() != 1 {
   191  		return cli.ShowCommandHelp(ctx, "bumpfee")
   192  	}
   193  
   194  	// Validate and parse the relevant arguments/flags.
   195  	protoOutPoint, err := NewProtoOutPoint(ctx.Args().Get(0))
   196  	if err != nil {
   197  		return err
   198  	}
   199  
   200  	client, cleanUp := getWalletClient(ctx)
   201  	defer cleanUp()
   202  
   203  	resp, err := client.BumpFee(ctxc, &walletrpc.BumpFeeRequest{
   204  		Outpoint:     protoOutPoint,
   205  		TargetConf:   uint32(ctx.Uint64("conf_target")),
   206  		AtomsPerByte: uint32(ctx.Uint64("atoms_per_byte")),
   207  		Force:        ctx.Bool("force"),
   208  	})
   209  	if err != nil {
   210  		return err
   211  	}
   212  
   213  	printRespJSON(resp)
   214  
   215  	return nil
   216  }
   217  
   218  var bumpCloseFeeCommand = cli.Command{
   219  	Name:      "bumpclosefee",
   220  	Usage:     "Bumps the fee of a channel closing transaction.",
   221  	ArgsUsage: "channel_point",
   222  	Description: `
   223  	This command allows the fee of a channel closing transaction to be
   224  	increased by using the child-pays-for-parent mechanism. It will instruct
   225  	the sweeper to sweep the anchor outputs of transactions in the set
   226  	of valid commitments for the specified channel at the requested fee
   227  	rate or confirmation target.
   228  	`,
   229  	Flags: []cli.Flag{
   230  		cli.Uint64Flag{
   231  			Name: "conf_target",
   232  			Usage: "the number of blocks that the output should " +
   233  				"be swept on-chain within",
   234  		},
   235  		cli.Uint64Flag{
   236  			Name: "atoms_per_byte",
   237  			Usage: "a manual fee expressed in atoms/byte that " +
   238  				"should be used when sweeping the output",
   239  		},
   240  	},
   241  	Action: actionDecorator(bumpCloseFee),
   242  }
   243  
   244  func bumpCloseFee(ctx *cli.Context) error {
   245  	ctxc := getContext()
   246  
   247  	// Display the command's help message if we do not have the expected
   248  	// number of arguments/flags.
   249  	if ctx.NArg() != 1 {
   250  		return cli.ShowCommandHelp(ctx, "bumpclosefee")
   251  	}
   252  
   253  	// Validate the channel point.
   254  	channelPoint := ctx.Args().Get(0)
   255  	_, err := NewProtoOutPoint(channelPoint)
   256  	if err != nil {
   257  		return err
   258  	}
   259  
   260  	// Fetch all waiting close channels.
   261  	client, cleanUp := getClient(ctx)
   262  	defer cleanUp()
   263  
   264  	// Fetch waiting close channel commitments.
   265  	commitments, err := getWaitingCloseCommitments(client, channelPoint)
   266  	if err != nil {
   267  		return err
   268  	}
   269  
   270  	// Retrieve pending sweeps.
   271  	walletClient, cleanUp := getWalletClient(ctx)
   272  	defer cleanUp()
   273  
   274  	sweeps, err := walletClient.PendingSweeps(
   275  		ctxc, &walletrpc.PendingSweepsRequest{},
   276  	)
   277  	if err != nil {
   278  		return err
   279  	}
   280  
   281  	// Match pending sweeps with commitments of the channel for which a bump
   282  	// is requested and bump their fees.
   283  	commitSet := map[string]struct{}{
   284  		commitments.LocalTxid:  {},
   285  		commitments.RemoteTxid: {},
   286  	}
   287  	if commitments.RemotePendingTxid != "" {
   288  		commitSet[commitments.RemotePendingTxid] = struct{}{}
   289  	}
   290  
   291  	for _, sweep := range sweeps.PendingSweeps {
   292  		// Only bump anchor sweeps.
   293  		if sweep.WitnessType != walletrpc.WitnessType_COMMITMENT_ANCHOR {
   294  			continue
   295  		}
   296  
   297  		// Skip unrelated sweeps.
   298  		sweepTxID, err := chainhash.NewHash(sweep.Outpoint.TxidBytes)
   299  		if err != nil {
   300  			return err
   301  		}
   302  		if _, match := commitSet[sweepTxID.String()]; !match {
   303  			continue
   304  		}
   305  
   306  		// Bump fee of the anchor sweep.
   307  		fmt.Printf("Bumping fee of %v:%v\n",
   308  			sweepTxID, sweep.Outpoint.OutputIndex)
   309  
   310  		_, err = walletClient.BumpFee(ctxc, &walletrpc.BumpFeeRequest{
   311  			Outpoint:     sweep.Outpoint,
   312  			TargetConf:   uint32(ctx.Uint64("conf_target")),
   313  			AtomsPerByte: uint32(ctx.Uint64("atoms_per_byte")),
   314  			Force:        true,
   315  		})
   316  		if err != nil {
   317  			return err
   318  		}
   319  	}
   320  
   321  	return nil
   322  }
   323  
   324  func getWaitingCloseCommitments(client lnrpc.LightningClient,
   325  	channelPoint string) (*lnrpc.PendingChannelsResponse_Commitments,
   326  	error) {
   327  
   328  	ctxc := getContext()
   329  
   330  	req := &lnrpc.PendingChannelsRequest{}
   331  	resp, err := client.PendingChannels(ctxc, req)
   332  	if err != nil {
   333  		return nil, err
   334  	}
   335  
   336  	// Lookup the channel commit tx hashes.
   337  	for _, channel := range resp.WaitingCloseChannels {
   338  		if channel.Channel.ChannelPoint == channelPoint {
   339  			return channel.Commitments, nil
   340  		}
   341  	}
   342  
   343  	return nil, errors.New("channel not found")
   344  }
   345  
   346  var listSweepsCommand = cli.Command{
   347  	Name:  "listsweeps",
   348  	Usage: "Lists all sweeps that have been published by our node.",
   349  	Flags: []cli.Flag{
   350  		cli.BoolFlag{
   351  			Name:  "verbose",
   352  			Usage: "lookup full transaction",
   353  		},
   354  	},
   355  	Description: `
   356  	Get a list of the hex-encoded transaction ids of every sweep that our
   357  	node has published. Note that these sweeps may not be confirmed on chain
   358  	yet, as we store them on transaction broadcast, not confirmation.
   359  
   360  	If the verbose flag is set, the full set of transactions will be 
   361  	returned, otherwise only the sweep transaction ids will be returned. 
   362  	`,
   363  	Action: actionDecorator(listSweeps),
   364  }
   365  
   366  func listSweeps(ctx *cli.Context) error {
   367  	ctxc := getContext()
   368  	client, cleanUp := getWalletClient(ctx)
   369  	defer cleanUp()
   370  
   371  	resp, err := client.ListSweeps(
   372  		ctxc, &walletrpc.ListSweepsRequest{
   373  			Verbose: ctx.IsSet("verbose"),
   374  		},
   375  	)
   376  	if err != nil {
   377  		return err
   378  	}
   379  
   380  	printJSON(resp)
   381  
   382  	return nil
   383  }
   384  
   385  var labelTxCommand = cli.Command{
   386  	Name:      "labeltx",
   387  	Usage:     "Adds a label to a transaction.",
   388  	ArgsUsage: "txid label",
   389  	Description: `
   390  	Add a label to a transaction. If the transaction already has a label, 
   391  	this call will fail unless the overwrite option is set. The label is 
   392  	limited to 500 characters. Note that multi word labels must be contained
   393  	in quotation marks ("").
   394  	`,
   395  	Flags: []cli.Flag{
   396  		cli.BoolFlag{
   397  			Name:  "overwrite",
   398  			Usage: "set to overwrite existing labels",
   399  		},
   400  	},
   401  	Action: actionDecorator(labelTransaction),
   402  }
   403  
   404  func labelTransaction(ctx *cli.Context) error {
   405  	ctxc := getContext()
   406  
   407  	// Display the command's help message if we do not have the expected
   408  	// number of arguments/flags.
   409  	if ctx.NArg() != 2 {
   410  		return cli.ShowCommandHelp(ctx, "labeltx")
   411  	}
   412  
   413  	// Get the transaction id and check that it is a valid hash.
   414  	txid := ctx.Args().Get(0)
   415  	hash, err := chainhash.NewHashFromStr(txid)
   416  	if err != nil {
   417  		return err
   418  	}
   419  
   420  	label := ctx.Args().Get(1)
   421  
   422  	walletClient, cleanUp := getWalletClient(ctx)
   423  	defer cleanUp()
   424  
   425  	_, err = walletClient.LabelTransaction(
   426  		ctxc, &walletrpc.LabelTransactionRequest{
   427  			Txid:      hash[:],
   428  			Label:     label,
   429  			Overwrite: ctx.Bool("overwrite"),
   430  		},
   431  	)
   432  	if err != nil {
   433  		return err
   434  	}
   435  
   436  	fmt.Printf("Transaction: %v labelled with: %v\n", txid, label)
   437  
   438  	return nil
   439  }
   440  
   441  var publishTxCommand = cli.Command{
   442  	Name:      "publishtx",
   443  	Usage:     "Attempts to publish the passed transaction to the network.",
   444  	ArgsUsage: "tx_hex",
   445  	Description: `
   446  	Publish a hex-encoded raw transaction to the on-chain network. The 
   447  	wallet will continually attempt to re-broadcast the transaction on start up, until it 
   448  	enters the chain. The label parameter is optional and limited to 500 characters. Note 
   449  	that multi word labels must be contained in quotation marks ("").
   450  	`,
   451  	Flags: []cli.Flag{
   452  		cli.StringFlag{
   453  			Name:  "label",
   454  			Usage: "(optional) transaction label",
   455  		},
   456  	},
   457  	Action: actionDecorator(publishTransaction),
   458  }
   459  
   460  func publishTransaction(ctx *cli.Context) error {
   461  	ctxc := getContext()
   462  
   463  	// Display the command's help message if we do not have the expected
   464  	// number of arguments/flags.
   465  	if ctx.NArg() != 1 || ctx.NumFlags() > 1 {
   466  		return cli.ShowCommandHelp(ctx, "publishtx")
   467  	}
   468  
   469  	walletClient, cleanUp := getWalletClient(ctx)
   470  	defer cleanUp()
   471  
   472  	tx, err := hex.DecodeString(ctx.Args().First())
   473  	if err != nil {
   474  		return err
   475  	}
   476  
   477  	// Deserialize the transaction to get the transaction hash.
   478  	msgTx := &wire.MsgTx{}
   479  	txReader := bytes.NewReader(tx)
   480  	if err := msgTx.Deserialize(txReader); err != nil {
   481  		return err
   482  	}
   483  
   484  	req := &walletrpc.Transaction{
   485  		TxHex: tx,
   486  		Label: ctx.String("label"),
   487  	}
   488  
   489  	_, err = walletClient.PublishTransaction(ctxc, req)
   490  	if err != nil {
   491  		return err
   492  	}
   493  
   494  	printJSON(&struct {
   495  		TXID string `json:"txid"`
   496  	}{
   497  		TXID: msgTx.TxHash().String(),
   498  	})
   499  
   500  	return nil
   501  }
   502  
   503  // utxoLease contains JSON annotations for a lease on an unspent output.
   504  type utxoLease struct {
   505  	ID         string   `json:"id"`
   506  	OutPoint   OutPoint `json:"outpoint"`
   507  	Expiration uint64   `json:"expiration"`
   508  }
   509  
   510  // fundPsbtResponse is a struct that contains JSON annotations for nice result
   511  // serialization.
   512  type fundPsbtResponse struct {
   513  	Psbt              string       `json:"psbt"`
   514  	ChangeOutputIndex int32        `json:"change_output_index"`
   515  	Locks             []*utxoLease `json:"locks"`
   516  }
   517  
   518  var fundPsbtCommand = cli.Command{
   519  	Name:  "fund",
   520  	Usage: "Fund a Partially Signed Bitcoin Transaction (PSBT).",
   521  	ArgsUsage: "[--template_psbt=T | [--outputs=O [--inputs=I]]] " +
   522  		"[--conf_target=C | --sat_per_vbyte=S]",
   523  	Description: `
   524  	The fund command creates a fully populated PSBT that contains enough
   525  	inputs to fund the outputs specified in either the PSBT or the
   526  	--outputs flag.
   527  
   528  	If there are no inputs specified in the template (or --inputs flag),
   529  	coin selection is performed automatically. If inputs are specified, the
   530  	wallet assumes that full coin selection happened externally and it will
   531  	not add any additional inputs to the PSBT. If the specified inputs
   532  	aren't enough to fund the outputs with the given fee rate, an error is
   533  	returned.
   534  
   535  	After either selecting or verifying the inputs, all input UTXOs are
   536  	locked with an internal app ID.
   537  
   538  	The 'outputs' flag decodes addresses and the amount to send respectively
   539  	in the following JSON format:
   540  
   541  	    --outputs='{"ExampleAddr": NumCoinsInSatoshis, "SecondAddr": Sats}'
   542  
   543  	The optional 'inputs' flag decodes a JSON list of UTXO outpoints as
   544  	returned by the listunspent command for example:
   545  
   546  	    --inputs='["<txid1>:<output-index1>","<txid2>:<output-index2>",...]'
   547  	`,
   548  	Flags: []cli.Flag{
   549  		cli.StringFlag{
   550  			Name: "template_psbt",
   551  			Usage: "the outputs to fund and optional inputs to " +
   552  				"spend provided in the base64 PSBT format",
   553  		},
   554  		cli.StringFlag{
   555  			Name: "outputs",
   556  			Usage: "a JSON compatible map of destination " +
   557  				"addresses to amounts to send, must not " +
   558  				"include a change address as that will be " +
   559  				"added automatically by the wallet",
   560  		},
   561  		cli.StringFlag{
   562  			Name: "inputs",
   563  			Usage: "an optional JSON compatible list of UTXO " +
   564  				"outpoints to use as the PSBT's inputs",
   565  		},
   566  		cli.Uint64Flag{
   567  			Name: "conf_target",
   568  			Usage: "the number of blocks that the transaction " +
   569  				"should be confirmed on-chain within",
   570  			Value: 6,
   571  		},
   572  		cli.Uint64Flag{
   573  			Name: "sat_per_vbyte",
   574  			Usage: "a manual fee expressed in sat/vbyte that " +
   575  				"should be used when creating the transaction",
   576  		},
   577  	},
   578  	Action: actionDecorator(fundPsbt),
   579  }
   580  
   581  func fundPsbt(ctx *cli.Context) error {
   582  	ctxc := getContext()
   583  
   584  	// Display the command's help message if there aren't any flags
   585  	// specified.
   586  	if ctx.NumFlags() == 0 {
   587  		return cli.ShowCommandHelp(ctx, "fund")
   588  	}
   589  
   590  	req := &walletrpc.FundPsbtRequest{}
   591  
   592  	// Parse template flags.
   593  	switch {
   594  	// The PSBT flag is mutally exclusive with the outputs/inputs flags.
   595  	case ctx.IsSet("template_psbt") &&
   596  		(ctx.IsSet("inputs") || ctx.IsSet("outputs")):
   597  
   598  		return fmt.Errorf("cannot set template_psbt and inputs/" +
   599  			"outputs flags at the same time")
   600  
   601  	// Use a pre-existing PSBT as the transaction template.
   602  	case len(ctx.String("template_psbt")) > 0:
   603  		psbtBase64 := ctx.String("template_psbt")
   604  		psbtBytes, err := base64.StdEncoding.DecodeString(psbtBase64)
   605  		if err != nil {
   606  			return err
   607  		}
   608  
   609  		req.Template = &walletrpc.FundPsbtRequest_Psbt{
   610  			Psbt: psbtBytes,
   611  		}
   612  
   613  	// The user manually specified outputs and/or inputs in JSON
   614  	// format.
   615  	case len(ctx.String("outputs")) > 0 || len(ctx.String("inputs")) > 0:
   616  		var (
   617  			tpl          = &walletrpc.TxTemplate{}
   618  			amountToAddr map[string]uint64
   619  		)
   620  
   621  		if len(ctx.String("outputs")) > 0 {
   622  			// Parse the address to amount map as JSON now. At least one
   623  			// entry must be present.
   624  			jsonMap := []byte(ctx.String("outputs"))
   625  			if err := json.Unmarshal(jsonMap, &amountToAddr); err != nil {
   626  				return fmt.Errorf("error parsing outputs JSON: %v",
   627  					err)
   628  			}
   629  			tpl.Outputs = amountToAddr
   630  		}
   631  
   632  		// Inputs are optional.
   633  		if len(ctx.String("inputs")) > 0 {
   634  			var inputs []string
   635  
   636  			jsonList := []byte(ctx.String("inputs"))
   637  			if err := json.Unmarshal(jsonList, &inputs); err != nil {
   638  				return fmt.Errorf("error parsing inputs JSON: "+
   639  					"%v", err)
   640  			}
   641  
   642  			for idx, input := range inputs {
   643  				op, err := NewProtoOutPoint(input)
   644  				if err != nil {
   645  					return fmt.Errorf("error parsing "+
   646  						"UTXO outpoint %d: %v", idx,
   647  						err)
   648  				}
   649  				tpl.Inputs = append(tpl.Inputs, op)
   650  			}
   651  		}
   652  
   653  		req.Template = &walletrpc.FundPsbtRequest_Raw{
   654  			Raw: tpl,
   655  		}
   656  
   657  	default:
   658  		return fmt.Errorf("must specify either template_psbt or " +
   659  			"inputs/outputs flag")
   660  	}
   661  
   662  	// Parse fee flags.
   663  	switch {
   664  	case ctx.IsSet("conf_target") && ctx.IsSet("atoms_per_byte"):
   665  		return fmt.Errorf("cannot set conf_target and sat_per_vbyte " +
   666  			"at the same time")
   667  
   668  	case ctx.Uint64("atoms_per_byte") > 0:
   669  		req.Fees = &walletrpc.FundPsbtRequest_AtomsPerByte{
   670  			AtomsPerByte: ctx.Uint64("atoms_per_byte"),
   671  		}
   672  
   673  	// Check conf_target last because it has a default value.
   674  	case ctx.Uint64("conf_target") > 0:
   675  		req.Fees = &walletrpc.FundPsbtRequest_TargetConf{
   676  			TargetConf: uint32(ctx.Uint64("conf_target")),
   677  		}
   678  	}
   679  
   680  	walletClient, cleanUp := getWalletClient(ctx)
   681  	defer cleanUp()
   682  
   683  	response, err := walletClient.FundPsbt(ctxc, req)
   684  	if err != nil {
   685  		return err
   686  	}
   687  
   688  	jsonLocks := marshallLocks(response.LockedUtxos)
   689  
   690  	printJSON(&fundPsbtResponse{
   691  		Psbt: base64.StdEncoding.EncodeToString(
   692  			response.FundedPsbt,
   693  		),
   694  		ChangeOutputIndex: response.ChangeOutputIndex,
   695  		Locks:             jsonLocks,
   696  	})
   697  
   698  	return nil
   699  }
   700  
   701  // marshallLocks converts the rpc lease information to a more json-friendly
   702  // format.
   703  func marshallLocks(lockedUtxos []*walletrpc.UtxoLease) []*utxoLease {
   704  	jsonLocks := make([]*utxoLease, len(lockedUtxos))
   705  	for idx, lock := range lockedUtxos {
   706  		jsonLocks[idx] = &utxoLease{
   707  			ID:         hex.EncodeToString(lock.Id),
   708  			OutPoint:   NewOutPointFromProto(lock.Outpoint),
   709  			Expiration: lock.Expiration,
   710  		}
   711  	}
   712  
   713  	return jsonLocks
   714  }
   715  
   716  // finalizePsbtResponse is a struct that contains JSON annotations for nice
   717  // result serialization.
   718  type finalizePsbtResponse struct {
   719  	Psbt    string `json:"psbt"`
   720  	FinalTx string `json:"final_tx"`
   721  }
   722  
   723  var finalizePsbtCommand = cli.Command{
   724  	Name:      "finalize",
   725  	Usage:     "Finalize a Partially Signed Bitcoin Transaction (PSBT).",
   726  	ArgsUsage: "funded_psbt",
   727  	Description: `
   728  	The finalize command expects a partial transaction with all inputs
   729  	and outputs fully declared and tries to sign all inputs that belong to
   730  	the wallet. Lnd must be the last signer of the transaction. That means,
   731  	if there are any unsigned non-witness inputs or inputs without UTXO
   732  	information attached or inputs without witness data that do not belong
   733  	to lnd's wallet, this method will fail. If no error is returned, the
   734  	PSBT is ready to be extracted and the final TX within to be broadcast.
   735  
   736  	This method does NOT publish the transaction after it's been finalized
   737  	successfully.
   738  	`,
   739  	Flags: []cli.Flag{
   740  		cli.StringFlag{
   741  			Name:  "funded_psbt",
   742  			Usage: "the base64 encoded PSBT to finalize",
   743  		},
   744  	},
   745  	Action: actionDecorator(finalizePsbt),
   746  }
   747  
   748  func finalizePsbt(ctx *cli.Context) error {
   749  	ctxc := getContext()
   750  
   751  	// Display the command's help message if we do not have the expected
   752  	// number of arguments/flags.
   753  	if ctx.NArg() != 1 && ctx.NumFlags() != 1 {
   754  		return cli.ShowCommandHelp(ctx, "finalize")
   755  	}
   756  
   757  	var (
   758  		args       = ctx.Args()
   759  		psbtBase64 string
   760  	)
   761  	switch {
   762  	case ctx.IsSet("funded_psbt"):
   763  		psbtBase64 = ctx.String("funded_psbt")
   764  	case args.Present():
   765  		psbtBase64 = args.First()
   766  	default:
   767  		return fmt.Errorf("funded_psbt argument missing")
   768  	}
   769  
   770  	psbtBytes, err := base64.StdEncoding.DecodeString(psbtBase64)
   771  	if err != nil {
   772  		return err
   773  	}
   774  	req := &walletrpc.FinalizePsbtRequest{
   775  		FundedPsbt: psbtBytes,
   776  	}
   777  
   778  	walletClient, cleanup := getWalletClient(ctx)
   779  	defer cleanup()
   780  
   781  	response, err := walletClient.FinalizePsbt(ctxc, req)
   782  	if err != nil {
   783  		return err
   784  	}
   785  
   786  	printJSON(&finalizePsbtResponse{
   787  		Psbt:    base64.StdEncoding.EncodeToString(response.SignedPsbt),
   788  		FinalTx: hex.EncodeToString(response.RawFinalTx),
   789  	})
   790  
   791  	return nil
   792  }
   793  
   794  var releaseOutputCommand = cli.Command{
   795  	Name:      "releaseoutput",
   796  	Usage:     "Release an output previously locked by lnd.",
   797  	ArgsUsage: "outpoint",
   798  	Description: `
   799  	The releaseoutput command unlocks an output, allowing it to be available
   800  	for coin selection if it remains unspent.
   801  
   802  	The internal lnd app lock ID is used when releasing the output.
   803  	Therefore only UTXOs locked by the fundpsbt command can currently be
   804  	released with this command.
   805  	`,
   806  	Flags: []cli.Flag{
   807  		cli.StringFlag{
   808  			Name:  "outpoint",
   809  			Usage: "the output to unlock",
   810  		},
   811  	},
   812  	Action: actionDecorator(releaseOutput),
   813  }
   814  
   815  func releaseOutput(ctx *cli.Context) error {
   816  	ctxc := getContext()
   817  
   818  	// Display the command's help message if we do not have the expected
   819  	// number of arguments/flags.
   820  	if ctx.NArg() != 1 && ctx.NumFlags() != 1 {
   821  		return cli.ShowCommandHelp(ctx, "releaseoutput")
   822  	}
   823  
   824  	var (
   825  		args        = ctx.Args()
   826  		outpointStr string
   827  	)
   828  	switch {
   829  	case ctx.IsSet("outpoint"):
   830  		outpointStr = ctx.String("outpoint")
   831  	case args.Present():
   832  		outpointStr = args.First()
   833  	default:
   834  		return fmt.Errorf("outpoint argument missing")
   835  	}
   836  
   837  	outpoint, err := NewProtoOutPoint(outpointStr)
   838  	if err != nil {
   839  		return fmt.Errorf("error parsing outpoint: %v", err)
   840  	}
   841  	req := &walletrpc.ReleaseOutputRequest{
   842  		Outpoint: outpoint,
   843  		Id:       walletrpc.LndInternalLockID[:],
   844  	}
   845  
   846  	walletClient, cleanUp := getWalletClient(ctx)
   847  	defer cleanUp()
   848  
   849  	response, err := walletClient.ReleaseOutput(ctxc, req)
   850  	if err != nil {
   851  		return err
   852  	}
   853  
   854  	printRespJSON(response)
   855  
   856  	return nil
   857  }
   858  
   859  var listLeasesCommand = cli.Command{
   860  	Name:   "listleases",
   861  	Usage:  "Return a list of currently held leases.",
   862  	Action: actionDecorator(listLeases),
   863  }
   864  
   865  func listLeases(ctx *cli.Context) error {
   866  	ctxc := getContext()
   867  
   868  	walletClient, cleanUp := getWalletClient(ctx)
   869  	defer cleanUp()
   870  
   871  	req := &walletrpc.ListLeasesRequest{}
   872  	response, err := walletClient.ListLeases(ctxc, req)
   873  	if err != nil {
   874  		return err
   875  	}
   876  	printJSON(marshallLocks(response.LockedUtxos))
   877  
   878  	return nil
   879  }
   880  
   881  var listAccountsCommand = cli.Command{
   882  	Name:  "list",
   883  	Usage: "Retrieve information of existing on-chain wallet accounts.",
   884  	Description: `
   885  	Retrieves all accounts belonging to the wallet by default. A name
   886  	filter can be provided to filter through all of the wallet
   887  	accounts and return only those matching.
   888  	`,
   889  	Flags: []cli.Flag{
   890  		cli.StringFlag{
   891  			Name: "name",
   892  			Usage: "(optional) only accounts matching this name " +
   893  				"are returned",
   894  		},
   895  	},
   896  	Action: actionDecorator(listAccounts),
   897  }
   898  
   899  func listAccounts(ctx *cli.Context) error {
   900  	ctxc := getContext()
   901  
   902  	// Display the command's help message if we do not have the expected
   903  	// number of arguments/flags.
   904  	if ctx.NArg() > 0 || ctx.NumFlags() > 2 {
   905  		return cli.ShowCommandHelp(ctx, "list")
   906  
   907  	}
   908  
   909  	walletClient, cleanUp := getWalletClient(ctx)
   910  	defer cleanUp()
   911  
   912  	req := &walletrpc.ListAccountsRequest{
   913  		Name: ctx.String("name"),
   914  	}
   915  	resp, err := walletClient.ListAccounts(ctxc, req)
   916  	if err != nil {
   917  		return err
   918  	}
   919  
   920  	printRespJSON(resp)
   921  
   922  	return nil
   923  }
   924  
   925  var importAccountCommand = cli.Command{
   926  	Name: "import",
   927  	Usage: "Import an on-chain account into the wallet through its " +
   928  		"extended public key.",
   929  	ArgsUsage: "extended_public_key name",
   930  	Description: `
   931  	Imports an account backed by an account extended public key.
   932  
   933  	NOTE: Events (deposits/spends) for keys derived from an account will
   934  	only be detected by lnd if they happen after the import. Rescans to
   935  	detect past events will be supported later on.
   936  	`,
   937  	Flags: []cli.Flag{
   938  		cli.StringFlag{
   939  			Name: "master_key_fingerprint",
   940  			Usage: "(optional) the fingerprint of the root key " +
   941  				"(derivation path m/) corresponding to the " +
   942  				"account public key",
   943  		},
   944  		cli.BoolFlag{
   945  			Name:  "dry_run",
   946  			Usage: "(optional) perform a dry run",
   947  		},
   948  	},
   949  	Action: actionDecorator(importAccount),
   950  }
   951  
   952  func importAccount(ctx *cli.Context) error {
   953  	ctxc := getContext()
   954  
   955  	// Display the command's help message if we do not have the expected
   956  	// number of arguments/flags.
   957  	if ctx.NArg() != 2 || ctx.NumFlags() > 3 {
   958  		return cli.ShowCommandHelp(ctx, "import")
   959  	}
   960  
   961  	walletClient, cleanup := getWalletClient(ctx)
   962  	defer cleanup()
   963  
   964  	dryRun := ctx.Bool("dry_run")
   965  	req := &walletrpc.ImportAccountRequest{
   966  		Name:              ctx.Args().Get(1),
   967  		ExtendedPublicKey: ctx.Args().Get(0),
   968  		DryRun:            dryRun,
   969  	}
   970  	resp, err := walletClient.ImportAccount(ctxc, req)
   971  	if err != nil {
   972  		return err
   973  	}
   974  
   975  	printRespJSON(resp)
   976  	return nil
   977  }
   978  
   979  var importPubKeyCommand = cli.Command{
   980  	Name:      "import-pubkey",
   981  	Usage:     "Import a public key as watch-only into the wallet.",
   982  	ArgsUsage: "public_key",
   983  	Description: `
   984  	Imports a public key represented in hex as watch-only into the wallet.
   985  
   986  	NOTE: Events (deposits/spends) for a key will only be detected by lnd if
   987  	they happen after the import. Rescans to detect past events will be
   988  	supported later on.
   989  	`,
   990  	Action: actionDecorator(importPubKey),
   991  }
   992  
   993  func importPubKey(ctx *cli.Context) error {
   994  	ctxc := getContext()
   995  
   996  	// Display the command's help message if we do not have the expected
   997  	// number of arguments/flags.
   998  	if ctx.NArg() != 2 || ctx.NumFlags() > 0 {
   999  		return cli.ShowCommandHelp(ctx, "import-pubkey")
  1000  	}
  1001  
  1002  	pubKeyBytes, err := hex.DecodeString(ctx.Args().Get(0))
  1003  	if err != nil {
  1004  		return err
  1005  	}
  1006  
  1007  	walletClient, cleanUp := getWalletClient(ctx)
  1008  	defer cleanUp()
  1009  
  1010  	req := &walletrpc.ImportPublicKeyRequest{
  1011  		PublicKey: pubKeyBytes,
  1012  	}
  1013  	resp, err := walletClient.ImportPublicKey(ctxc, req)
  1014  	if err != nil {
  1015  		return err
  1016  	}
  1017  
  1018  	printRespJSON(resp)
  1019  
  1020  	return nil
  1021  }