diff --git a/btcjson/chainsvrcmds.go b/btcjson/chainsvrcmds.go index d66478c3050..328125fc30c 100644 --- a/btcjson/chainsvrcmds.go +++ b/btcjson/chainsvrcmds.go @@ -80,11 +80,24 @@ func NewCreateRawTransactionCmd(inputs []TransactionInput, amounts map[string]fl } } +// ChangeType defines the different output types to use for the change address +// of a transaction built by the node. +type ChangeType string + +var ( + // ChangeTypeLegacy indicates a P2PKH change address type. + ChangeTypeLegacy ChangeType = "legacy" + // ChangeTypeP2SHSegWit indicates a P2WPKH-in-P2SH change address type. + ChangeTypeP2SHSegWit ChangeType = "p2sh-segwit" + // ChangeTypeBech32 indicates a P2WPKH change address type. + ChangeTypeBech32 ChangeType = "bech32" +) + // FundRawTransactionOpts are the different options that can be passed to rawtransaction type FundRawTransactionOpts struct { ChangeAddress *string `json:"changeAddress,omitempty"` ChangePosition *int `json:"changePosition,omitempty"` - ChangeType *string `json:"change_type,omitempty"` + ChangeType *ChangeType `json:"change_type,omitempty"` IncludeWatching *bool `json:"includeWatching,omitempty"` LockUnspents *bool `json:"lockUnspents,omitempty"` FeeRate *float64 `json:"feeRate,omitempty"` // BTC/kB diff --git a/btcjson/chainsvrcmds_test.go b/btcjson/chainsvrcmds_test.go index e2b5025727c..0e77c77e3bf 100644 --- a/btcjson/chainsvrcmds_test.go +++ b/btcjson/chainsvrcmds_test.go @@ -127,7 +127,7 @@ func TestChainSvrCmds(t *testing.T) { } changeAddress := "bcrt1qeeuctq9wutlcl5zatge7rjgx0k45228cxez655" change := 1 - changeType := "legacy" + changeType := btcjson.ChangeTypeLegacy watching := true lockUnspents := true feeRate := 0.7 @@ -151,7 +151,7 @@ func TestChainSvrCmds(t *testing.T) { unmarshalled: func() interface{} { changeAddress := "bcrt1qeeuctq9wutlcl5zatge7rjgx0k45228cxez655" change := 1 - changeType := "legacy" + changeType := btcjson.ChangeTypeLegacy watching := true lockUnspents := true feeRate := 0.7 diff --git a/btcjson/walletsvrcmds.go b/btcjson/walletsvrcmds.go index be1a67fbf01..96574f23dbc 100644 --- a/btcjson/walletsvrcmds.go +++ b/btcjson/walletsvrcmds.go @@ -7,6 +7,12 @@ package btcjson +import ( + "encoding/hex" + + "github.com/btcsuite/btcutil" +) + // AddMultisigAddressCmd defines the addmutisigaddress JSON-RPC command. type AddMultisigAddressCmd struct { NRequired int @@ -686,6 +692,88 @@ func NewWalletPassphraseChangeCmd(oldPassphrase, newPassphrase string) *WalletPa } } +// PsbtInput represents an input to include in the PSBT created by the +// WalletCreateFundedPsbtCmd command. +type PsbtInput struct { + Txid string `json:"txid"` + Vout uint32 `json:"vout"` + Sequence uint32 `json:"sequence"` +} + +// PsbtOutput represents an output to include in the PSBT created by the +// WalletCreateFundedPsbtCmd command. +type PsbtOutput map[string]interface{} + +// NewPsbtOutput returns a new instance of a PSBT output to use with the +// WalletCreateFundedPsbtCmd command. +func NewPsbtOutput(address string, amount btcutil.Amount) PsbtOutput { + return PsbtOutput{address: amount.ToBTC()} +} + +// NewPsbtDataOutput returns a new instance of a PSBT data output to use with +// the WalletCreateFundedPsbtCmd command. +func NewPsbtDataOutput(data []byte) PsbtOutput { + return PsbtOutput{"data": hex.EncodeToString(data)} +} + +// WalletCreateFundedPsbtOpts represents the optional options struct provided +// with a WalletCreateFundedPsbtCmd command. +type WalletCreateFundedPsbtOpts struct { + ChangeAddress *string `json:"changeAddress,omitempty"` + ChangePosition *int64 `json:"changePosition,omitempty"` + ChangeType *ChangeType `json:"change_type,omitempty"` + IncludeWatching *bool `json:"includeWatching,omitempty"` + LockUnspents *bool `json:"lockUnspents,omitempty"` + FeeRate *int64 `json:"feeRate,omitempty"` + SubtractFeeFromOutputs *[]int64 `json:"subtractFeeFromOutputs,omitempty"` + Replaceable *bool `json:"replaceable,omitempty"` + ConfTarget *int64 `json:"conf_target,omitempty"` + EstimateMode *string `json:"estimate_mode,omitempty"` +} + +// WalletCreateFundedPsbtCmd defines the walletcreatefundedpsbt JSON-RPC command. +type WalletCreateFundedPsbtCmd struct { + Inputs []PsbtInput + Outputs []PsbtOutput + Locktime *uint32 + Options *WalletCreateFundedPsbtOpts + Bip32Derivs *bool +} + +// NewWalletCreateFundedPsbtCmd returns a new instance which can be used to issue a +// walletcreatefundedpsbt JSON-RPC command. +func NewWalletCreateFundedPsbtCmd( + inputs []PsbtInput, outputs []PsbtOutput, locktime *uint32, + options *WalletCreateFundedPsbtOpts, bip32Derivs *bool, +) *WalletCreateFundedPsbtCmd { + return &WalletCreateFundedPsbtCmd{ + Inputs: inputs, + Outputs: outputs, + Locktime: locktime, + Options: options, + Bip32Derivs: bip32Derivs, + } +} + +// WalletProcessPsbtCmd defines the walletprocesspsbt JSON-RPC command. +type WalletProcessPsbtCmd struct { + Psbt string + Sign *bool `jsonrpcdefault:"true"` + SighashType *string `jsonrpcdefault:"\"ALL\""` + Bip32Derivs *bool +} + +// NewWalletProcessPsbtCmd returns a new instance which can be used to issue a +// walletprocesspsbt JSON-RPC command. +func NewWalletProcessPsbtCmd(psbt string, sign *bool, sighashType *string, bip32Derivs *bool) *WalletProcessPsbtCmd { + return &WalletProcessPsbtCmd{ + Psbt: psbt, + Sign: sign, + SighashType: sighashType, + Bip32Derivs: bip32Derivs, + } +} + func init() { // The commands in this file are only usable with a wallet server. flags := UFWalletOnly @@ -731,4 +819,6 @@ func init() { MustRegisterCmd("walletlock", (*WalletLockCmd)(nil), flags) MustRegisterCmd("walletpassphrase", (*WalletPassphraseCmd)(nil), flags) MustRegisterCmd("walletpassphrasechange", (*WalletPassphraseChangeCmd)(nil), flags) + MustRegisterCmd("walletcreatefundedpsbt", (*WalletCreateFundedPsbtCmd)(nil), flags) + MustRegisterCmd("walletprocesspsbt", (*WalletProcessPsbtCmd)(nil), flags) } diff --git a/btcjson/walletsvrcmds_test.go b/btcjson/walletsvrcmds_test.go index 554a87413ab..46f46872062 100644 --- a/btcjson/walletsvrcmds_test.go +++ b/btcjson/walletsvrcmds_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcutil" ) // TestWalletSvrCmds tests all of the wallet server commands marshal and @@ -1243,6 +1244,81 @@ func TestWalletSvrCmds(t *testing.T) { NewPassphrase: "new", }, }, + { + name: "walletcreatefundedpsbt", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd( + "walletcreatefundedpsbt", + []btcjson.PsbtInput{ + { + Txid: "1234", + Vout: 0, + Sequence: 0, + }, + }, + []btcjson.PsbtOutput{ + btcjson.NewPsbtOutput("1234", btcutil.Amount(1234)), + btcjson.NewPsbtDataOutput([]byte{1, 2, 3, 4}), + }, + btcjson.Uint32(1), + btcjson.WalletCreateFundedPsbtOpts{}, + btcjson.Bool(true), + ) + }, + staticCmd: func() interface{} { + return btcjson.NewWalletCreateFundedPsbtCmd( + []btcjson.PsbtInput{ + { + Txid: "1234", + Vout: 0, + Sequence: 0, + }, + }, + []btcjson.PsbtOutput{ + btcjson.NewPsbtOutput("1234", btcutil.Amount(1234)), + btcjson.NewPsbtDataOutput([]byte{1, 2, 3, 4}), + }, + btcjson.Uint32(1), + &btcjson.WalletCreateFundedPsbtOpts{}, + btcjson.Bool(true), + ) + }, + marshalled: `{"jsonrpc":"1.0","method":"walletcreatefundedpsbt","params":[[{"txid":"1234","vout":0,"sequence":0}],[{"1234":0.00001234},{"data":"01020304"}],1,{},true],"id":1}`, + unmarshalled: &btcjson.WalletCreateFundedPsbtCmd{ + Inputs: []btcjson.PsbtInput{ + { + Txid: "1234", + Vout: 0, + Sequence: 0, + }, + }, + Outputs: []btcjson.PsbtOutput{ + btcjson.NewPsbtOutput("1234", btcutil.Amount(1234)), + btcjson.NewPsbtDataOutput([]byte{1, 2, 3, 4}), + }, + Locktime: btcjson.Uint32(1), + Options: &btcjson.WalletCreateFundedPsbtOpts{}, + Bip32Derivs: btcjson.Bool(true), + }, + }, + { + name: "walletprocesspsbt", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd( + "walletprocesspsbt", "1234", btcjson.Bool(true), btcjson.String("ALL"), btcjson.Bool(true)) + }, + staticCmd: func() interface{} { + return btcjson.NewWalletProcessPsbtCmd( + "1234", btcjson.Bool(true), btcjson.String("ALL"), btcjson.Bool(true)) + }, + marshalled: `{"jsonrpc":"1.0","method":"walletprocesspsbt","params":["1234",true,"ALL",true],"id":1}`, + unmarshalled: &btcjson.WalletProcessPsbtCmd{ + Psbt: "1234", + Sign: btcjson.Bool(true), + SighashType: btcjson.String("ALL"), + Bip32Derivs: btcjson.Bool(true), + }, + }, } t.Logf("Running %d tests", len(tests)) diff --git a/btcjson/walletsvrresults.go b/btcjson/walletsvrresults.go index 6e69ed90666..7746bae545d 100644 --- a/btcjson/walletsvrresults.go +++ b/btcjson/walletsvrresults.go @@ -173,3 +173,18 @@ type GetBalancesResult struct { Mine BalanceDetailsResult `json:"mine"` WatchOnly *BalanceDetailsResult `json:"watchonly"` } + +// WalletCreateFundedPsbtResult models the data returned from the +// walletcreatefundedpsbtresult command. +type WalletCreateFundedPsbtResult struct { + Psbt string `json:"psbt"` + Fee float64 `json:"fee"` + ChangePos int64 `json:"changepos"` +} + +// WalletProcessPsbtResult models the data returned from the +// walletprocesspsbtresult command. +type WalletProcessPsbtResult struct { + Psbt string `json:"psbt"` + Complete bool `json:"complete"` +} diff --git a/rpcclient/wallet.go b/rpcclient/wallet.go index 37bf9471e0e..5b60594e43f 100644 --- a/rpcclient/wallet.go +++ b/rpcclient/wallet.go @@ -2367,6 +2367,95 @@ func (c *Client) GetInfo() (*btcjson.InfoWalletResult, error) { return c.GetInfoAsync().Receive() } +// FutureImportPubKeyResult is a future promise to deliver the result of an +// WalletCreateFundedPsbt RPC invocation (or an applicable error). +type FutureWalletCreateFundedPsbtResult chan *response + +// Receive waits for the response promised by the future and returns the +// partially signed transaction in PSBT format along with the resulting fee +// and change output index. +func (r FutureWalletCreateFundedPsbtResult) Receive() (*btcjson.WalletCreateFundedPsbtResult, error) { + res, err := receiveFuture(r) + if err != nil { + return nil, err + } + + // Unmarshal result as a getinfo result object. + var psbtRes btcjson.WalletCreateFundedPsbtResult + err = json.Unmarshal(res, &psbtRes) + if err != nil { + return nil, err + } + + return &psbtRes, nil +} + +// WalletCreateFundedPsbtAsync returns an instance of a type that can be used +// to get the result of the RPC at some future time by invoking the Receive +// function on the returned instance. +// +// See WalletCreateFundedPsbt for the blocking version and more details. +func (c *Client) WalletCreateFundedPsbtAsync( + inputs []btcjson.PsbtInput, outputs []btcjson.PsbtOutput, locktime *uint32, + options *btcjson.WalletCreateFundedPsbtOpts, bip32Derivs *bool, +) FutureWalletCreateFundedPsbtResult { + cmd := btcjson.NewWalletCreateFundedPsbtCmd(inputs, outputs, locktime, options, bip32Derivs) + return c.sendCmd(cmd) +} + +// WalletCreateFundedPsbt creates and funds a transaction in the Partially +// Signed Transaction format. Inputs will be added if supplied inputs are not +// enough. +func (c *Client) WalletCreateFundedPsbt( + inputs []btcjson.PsbtInput, outputs []btcjson.PsbtOutput, locktime *uint32, + options *btcjson.WalletCreateFundedPsbtOpts, bip32Derivs *bool, +) (*btcjson.WalletCreateFundedPsbtResult, error) { + return c.WalletCreateFundedPsbtAsync(inputs, outputs, locktime, options, bip32Derivs).Receive() +} + +// FutureWalletProcessPsbtResult is a future promise to deliver the result of a +// WalletCreateFundedPsb RPC invocation (or an applicable error). +type FutureWalletProcessPsbtResult chan *response + +// Receive waits for the response promised by the future and returns an updated +// PSBT with signed inputs from the wallet and a boolen indicating if the the +// transaction has a complete set of signatures. +func (r FutureWalletProcessPsbtResult) Receive() (*btcjson.WalletProcessPsbtResult, error) { + res, err := receiveFuture(r) + if err != nil { + return nil, err + } + + // Unmarshal result as a getinfo result object. + var psbtRes btcjson.WalletProcessPsbtResult + err = json.Unmarshal(res, &psbtRes) + if err != nil { + return nil, err + } + + return &psbtRes, nil +} + +// WalletProcessPsbtAsync returns an instance of a type that can be used +// to get the result of the RPC at some future time by invoking the Receive +// function on the returned instance. +// +// See WalletProcessPsbt for the blocking version and more details. +func (c *Client) WalletProcessPsbtAsync( + psbt string, sign *bool, sighashType SigHashType, bip32Derivs *bool, +) FutureWalletProcessPsbtResult { + cmd := btcjson.NewWalletProcessPsbtCmd(psbt, sign, btcjson.String(string(sighashType)), bip32Derivs) + return c.sendCmd(cmd) +} + +// WalletProcessPsbt updates a PSBT with input information from our wallet and +// then signs inputs. +func (c *Client) WalletProcessPsbt( + psbt string, sign *bool, sighashType SigHashType, bip32Derivs *bool, +) (*btcjson.WalletProcessPsbtResult, error) { + return c.WalletProcessPsbtAsync(psbt, sign, sighashType, bip32Derivs).Receive() +} + // TODO(davec): Implement // backupwallet (NYI in btcwallet) // encryptwallet (Won't be supported by btcwallet since it's always encrypted)