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 }