Skip to content

Commit

Permalink
btcjson,rpcclient: add support for PSBT commands to rpcclient
Browse files Browse the repository at this point in the history
  • Loading branch information
federicobond committed Aug 28, 2020
1 parent 1db1b6f commit 6f1c59e
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 3 deletions.
15 changes: 14 additions & 1 deletion btcjson/chainsvrcmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions btcjson/chainsvrcmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
90 changes: 90 additions & 0 deletions btcjson/walletsvrcmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
76 changes: 76 additions & 0 deletions btcjson/walletsvrcmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
15 changes: 15 additions & 0 deletions btcjson/walletsvrresults.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
89 changes: 89 additions & 0 deletions rpcclient/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 6f1c59e

Please sign in to comment.