From b88ff9db26b6e351ae521cebeec9451717172bfa Mon Sep 17 00:00:00 2001 From: Anirudha Bose Date: Tue, 15 Sep 2020 23:30:59 +0200 Subject: [PATCH] rpcclient: implement getaddressinfo command Fields such as label, and labelspurpose are not included, since they are deprecated, and will be removed in Bitcoin Core 0.21. --- btcjson/walletsvrcmds.go | 16 ++++- btcjson/walletsvrcmds_test.go | 15 +++- btcjson/walletsvrresults.go | 117 ++++++++++++++++++++++++++++++- btcjson/walletsvrresults_test.go | 80 +++++++++++++++++++++ rpcclient/example_test.go | 31 +++++++- rpcclient/wallet.go | 37 +++++++++- txscript/standard.go | 20 +++++- txscript/standard_test.go | 40 ++++++++++- 8 files changed, 349 insertions(+), 7 deletions(-) create mode 100644 btcjson/walletsvrresults_test.go diff --git a/btcjson/walletsvrcmds.go b/btcjson/walletsvrcmds.go index 97cc7ed679..b21fb18712 100644 --- a/btcjson/walletsvrcmds.go +++ b/btcjson/walletsvrcmds.go @@ -1,4 +1,4 @@ -// Copyright (c) 2014 The btcsuite developers +// Copyright (c) 2014-2020 The btcsuite developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -178,6 +178,19 @@ func NewGetAddressesByAccountCmd(account string) *GetAddressesByAccountCmd { } } +// GetAddressInfoCmd defines the getaddressinfo JSON-RPC command. +type GetAddressInfoCmd struct { + Address string +} + +// NewGetAddressInfoCmd returns a new instance which can be used to issue a +// getaddressinfo JSON-RPC command. +func NewGetAddressInfoCmd(address string) *GetAddressInfoCmd { + return &GetAddressInfoCmd{ + Address: address, + } +} + // GetBalanceCmd defines the getbalance JSON-RPC command. type GetBalanceCmd struct { Account *string @@ -993,6 +1006,7 @@ func init() { MustRegisterCmd("getaccount", (*GetAccountCmd)(nil), flags) MustRegisterCmd("getaccountaddress", (*GetAccountAddressCmd)(nil), flags) MustRegisterCmd("getaddressesbyaccount", (*GetAddressesByAccountCmd)(nil), flags) + MustRegisterCmd("getaddressinfo", (*GetAddressInfoCmd)(nil), flags) MustRegisterCmd("getbalance", (*GetBalanceCmd)(nil), flags) MustRegisterCmd("getbalances", (*GetBalancesCmd)(nil), flags) MustRegisterCmd("getnewaddress", (*GetNewAddressCmd)(nil), flags) diff --git a/btcjson/walletsvrcmds_test.go b/btcjson/walletsvrcmds_test.go index 2e1780c10d..d19aa32aac 100644 --- a/btcjson/walletsvrcmds_test.go +++ b/btcjson/walletsvrcmds_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2014 The btcsuite developers +// Copyright (c) 2014-2020 The btcsuite developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -209,6 +209,19 @@ func TestWalletSvrCmds(t *testing.T) { Account: "acct", }, }, + { + name: "getaddressinfo", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getaddressinfo", "1234") + }, + staticCmd: func() interface{} { + return btcjson.NewGetAddressInfoCmd("1234") + }, + marshalled: `{"jsonrpc":"1.0","method":"getaddressinfo","params":["1234"],"id":1}`, + unmarshalled: &btcjson.GetAddressInfoCmd{ + Address: "1234", + }, + }, { name: "getbalance", newCmd: func() (interface{}, error) { diff --git a/btcjson/walletsvrresults.go b/btcjson/walletsvrresults.go index 0cb78d482f..1b9393ab5c 100644 --- a/btcjson/walletsvrresults.go +++ b/btcjson/walletsvrresults.go @@ -1,9 +1,124 @@ -// Copyright (c) 2014 The btcsuite developers +// Copyright (c) 2014-2020 The btcsuite developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package btcjson +import ( + "encoding/json" + "github.com/btcsuite/btcd/txscript" +) + +// embeddedAddressInfo includes all getaddressinfo output fields, excluding +// metadata and relation to the wallet. +// +// It represents the non-metadata/non-wallet fields for GetAddressInfo, as well +// as the precise fields for an embedded P2SH or P2WSH address. +type embeddedAddressInfo struct { + Address string `json:"address"` + ScriptPubKey string `json:"scriptPubKey"` + Solvable bool `json:"solvable"` + Descriptor *string `json:"desc,omitempty"` + IsScript bool `json:"isscript"` + IsChange bool `json:"ischange"` + IsWitness bool `json:"iswitness"` + WitnessVersion int `json:"witness_version,omitempty"` + WitnessProgram *string `json:"witness_program,omitempty"` + ScriptType *txscript.ScriptClass `json:"script,omitempty"` + Hex *string `json:"hex,omitempty"` + PubKeys *[]string `json:"pubkeys,omitempty"` + SignaturesRequired *int `json:"sigsrequired,omitempty"` + PubKey *string `json:"pubkey,omitempty"` + IsCompressed *bool `json:"iscompressed,omitempty"` + HDMasterFingerprint *string `json:"hdmasterfingerprint,omitempty"` + Labels []string `json:"labels"` +} + +// GetAddressInfoResult models the result of the getaddressinfo command. It +// contains information about a bitcoin address. +// +// Reference: https://bitcoincore.org/en/doc/0.20.0/rpc/wallet/getaddressinfo +// +// The GetAddressInfoResult has three segments: +// 1. General information about the address. +// 2. Metadata (Timestamp, HDKeyPath, HDSeedID) and wallet fields +// (IsMine, IsWatchOnly). +// 3. Information about the embedded address in case of P2SH or P2WSH. +// Same structure as (1). +type GetAddressInfoResult struct { + embeddedAddressInfo + IsMine bool `json:"ismine"` + IsWatchOnly bool `json:"iswatchonly"` + Timestamp *int `json:"timestamp,omitempty"` + HDKeyPath *string `json:"hdkeypath,omitempty"` + HDSeedID *string `json:"hdseedid,omitempty"` + Embedded *embeddedAddressInfo `json:"embedded,omitempty"` +} + +// UnmarshalJSON provides a custom unmarshaller for GetAddressInfoResult. +// It is adapted to avoid creating a duplicate raw struct for unmarshalling +// the JSON bytes into. +// +// Reference: http://choly.ca/post/go-json-marshalling +func (e *GetAddressInfoResult) UnmarshalJSON(data []byte) error { + // Step 1: Create type aliases of the original struct, including the + // embedded one. + type Alias GetAddressInfoResult + type EmbeddedAlias embeddedAddressInfo + + // Step 2: Create an anonymous struct with raw replacements for the special + // fields. + aux := &struct { + ScriptType *string `json:"script,omitempty"` + Embedded *struct { + ScriptType *string `json:"script,omitempty"` + *EmbeddedAlias + } `json:"embedded,omitempty"` + *Alias + }{ + Alias: (*Alias)(e), + } + + // Step 3: Unmarshal the data into the anonymous struct. + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + // Step 4: Convert the raw fields to the desired types + var ( + sc *txscript.ScriptClass + err error + ) + + if aux.ScriptType != nil { + sc, err = txscript.NewScriptClass(*aux.ScriptType) + if err != nil { + return err + } + } + + e.ScriptType = sc + + if aux.Embedded != nil { + var ( + embeddedSc *txscript.ScriptClass + err error + ) + + if aux.Embedded.ScriptType != nil { + embeddedSc, err = txscript.NewScriptClass(*aux.Embedded.ScriptType) + if err != nil { + return err + } + } + + e.Embedded = (*embeddedAddressInfo)(aux.Embedded.EmbeddedAlias) + e.Embedded.ScriptType = embeddedSc + } + + return nil +} + // GetTransactionDetailsResult models the details data from the gettransaction command. // // This models the "short" version of the ListTransactionsResult type, which diff --git a/btcjson/walletsvrresults_test.go b/btcjson/walletsvrresults_test.go new file mode 100644 index 0000000000..173226b8b5 --- /dev/null +++ b/btcjson/walletsvrresults_test.go @@ -0,0 +1,80 @@ +// Copyright (c) 2020 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package btcjson + +import ( + "encoding/json" + "errors" + "reflect" + "testing" + + "github.com/btcsuite/btcd/txscript" + "github.com/davecgh/go-spew/spew" +) + +// TestGetAddressInfoResult ensures that custom unmarshalling of +// GetAddressInfoResult works as intended. +func TestGetAddressInfoResult(t *testing.T) { + t.Parallel() + + // arbitrary script class to use in tests + nonStandard, _ := txscript.NewScriptClass("nonstandard") + + tests := []struct { + name string + result string + want GetAddressInfoResult + wantErr error + }{ + { + name: "GetAddressInfoResult - no ScriptType", + result: `{}`, + want: GetAddressInfoResult{}, + }, + { + name: "GetAddressInfoResult - ScriptType", + result: `{"script":"nonstandard","address":"1abc"}`, + want: GetAddressInfoResult{ + embeddedAddressInfo: embeddedAddressInfo{ + Address: "1abc", + ScriptType: nonStandard, + }, + }, + }, + { + name: "GetAddressInfoResult - embedded ScriptType", + result: `{"embedded": {"script":"nonstandard","address":"121313"}}`, + want: GetAddressInfoResult{ + Embedded: &embeddedAddressInfo{ + Address: "121313", + ScriptType: nonStandard, + }, + }, + }, + { + name: "GetAddressInfoResult - invalid ScriptType", + result: `{"embedded": {"script":"foo","address":"121313"}}`, + wantErr: txscript.ErrUnsupportedScriptType, + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + var out GetAddressInfoResult + err := json.Unmarshal([]byte(test.result), &out) + if err != nil && !errors.Is(err, test.wantErr) { + t.Errorf("Test #%d (%s) unexpected error: %v, want: %v", i, + test.name, err, test.wantErr) + continue + } + + if !reflect.DeepEqual(out, test.want) { + t.Errorf("Test #%d (%s) unexpected unmarshalled data - "+ + "got %v, want %v", i, test.name, spew.Sdump(out), + spew.Sdump(test.want)) + continue + } + } +} diff --git a/rpcclient/example_test.go b/rpcclient/example_test.go index c9474512fa..e930778a01 100644 --- a/rpcclient/example_test.go +++ b/rpcclient/example_test.go @@ -1,8 +1,11 @@ +// Copyright (c) 2020 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + package rpcclient import ( "fmt" - "github.com/btcsuite/btcd/btcjson" ) @@ -77,3 +80,29 @@ func ExampleClient_DeriveAddresses() { fmt.Printf("%+v\n", addrs) // &[14NjenDKkGGq1McUgoSkeUHJpW3rrKLbPW 1Pn6i3cvdGhqbdgNjXHfbaYfiuviPiymXj 181x1NbgGYKLeMXkDdXEAqepG75EgU8XtG] } + +func ExampleClient_GetAddressInfo() { + connCfg = &ConnConfig{ + Host: "localhost:18332", + User: "user", + Pass: "pass", + HTTPPostMode: true, + DisableTLS: true, + } + + client, err := New(connCfg, nil) + if err != nil { + panic(err) + } + defer client.Shutdown() + + info, err := client.GetAddressInfo("2NF1FbxtUAsvdU4uW1UC2xkBVatp6cYQuJ3") + if err != nil { + panic(err) + } + + fmt.Println(info.Address) // 2NF1FbxtUAsvdU4uW1UC2xkBVatp6cYQuJ3 + fmt.Println(info.ScriptType.String()) // witness_v0_keyhash + fmt.Println(*info.HDKeyPath) // m/49'/1'/0'/0/4 + fmt.Println(info.Embedded.Address) // tb1q3x2h2kh57wzg7jz00jhwn0ycvqtdk2ane37j27 +} diff --git a/rpcclient/wallet.go b/rpcclient/wallet.go index c80f6ba7d2..10bdfca6d8 100644 --- a/rpcclient/wallet.go +++ b/rpcclient/wallet.go @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2017 The btcsuite developers +// Copyright (c) 2014-2020 The btcsuite developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -939,6 +939,41 @@ func (c *Client) CreateNewAccount(account string) error { return c.CreateNewAccountAsync(account).Receive() } +// FutureGetAddressInfoResult is a future promise to deliver the result of an +// GetAddressInfoAsync RPC invocation (or an applicable error). +type FutureGetAddressInfoResult chan *response + +// Receive waits for the response promised by the future and returns the information +// about the given bitcoin address. +func (r FutureGetAddressInfoResult) Receive() (*btcjson.GetAddressInfoResult, error) { + res, err := receiveFuture(r) + if err != nil { + return nil, err + } + + var getAddressInfoResult btcjson.GetAddressInfoResult + err = json.Unmarshal(res, &getAddressInfoResult) + if err != nil { + return nil, err + } + return &getAddressInfoResult, nil +} + +// GetAddressInfoAsync 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 GetAddressInfo for the blocking version and more details. +func (c *Client) GetAddressInfoAsync(address string) FutureGetAddressInfoResult { + cmd := btcjson.NewGetAddressInfoCmd(address) + return c.sendCmd(cmd) +} + +// GetAddressInfo returns information about the given bitcoin address. +func (c *Client) GetAddressInfo(address string) (*btcjson.GetAddressInfoResult, error) { + return c.GetAddressInfoAsync(address).Receive() +} + // FutureGetNewAddressResult is a future promise to deliver the result of a // GetNewAddressAsync RPC invocation (or an applicable error). type FutureGetNewAddressResult struct { diff --git a/txscript/standard.go b/txscript/standard.go index a7e929d101..2cad218e95 100644 --- a/txscript/standard.go +++ b/txscript/standard.go @@ -1,4 +1,4 @@ -// Copyright (c) 2013-2017 The btcsuite developers +// Copyright (c) 2013-2020 The btcsuite developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -58,6 +58,7 @@ const ( WitnessV0ScriptHashTy // Pay to witness script hash. MultiSigTy // Multi signature. NullDataTy // Empty data-only (provably prunable). + WitnessUnknownTy // Witness unknown ) // scriptClassToName houses the human-readable strings which describe each @@ -71,6 +72,7 @@ var scriptClassToName = []string{ WitnessV0ScriptHashTy: "witness_v0_scripthash", MultiSigTy: "multisig", NullDataTy: "nulldata", + WitnessUnknownTy: "witness_unknown", } // String implements the Stringer interface by returning the name of @@ -188,6 +190,22 @@ func GetScriptClass(script []byte) ScriptClass { return typeOfScript(pops) } +// NewScriptClass returns the ScriptClass corresponding to the string name +// provided as argument. ErrUnsupportedScriptType error is returned if the +// name doesn't correspond to any known ScriptClass. +// +// Not to be confused with GetScriptClass. +func NewScriptClass(name string) (*ScriptClass, error) { + for i, n := range scriptClassToName { + if n == name { + value := ScriptClass(i) + return &value, nil + } + } + + return nil, fmt.Errorf("%w: %s", ErrUnsupportedScriptType, name) +} + // expectedInputs returns the number of arguments required by a script. // If the script is of unknown type such that the number can not be determined // then -1 is returned. We are an internal function and thus assume that class diff --git a/txscript/standard_test.go b/txscript/standard_test.go index e24d5f615b..37dd8f8a37 100644 --- a/txscript/standard_test.go +++ b/txscript/standard_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2013-2017 The btcsuite developers +// Copyright (c) 2013-2020 The btcsuite developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -7,6 +7,7 @@ package txscript import ( "bytes" "encoding/hex" + "errors" "reflect" "testing" @@ -1213,3 +1214,40 @@ func TestNullDataScript(t *testing.T) { } } } + +// TestNewScriptClass tests whether NewScriptClass returns a valid ScriptClass. +func TestNewScriptClass(t *testing.T) { + tests := []struct { + name string + scriptName string + want *ScriptClass + wantErr error + }{ + { + name: "NewScriptClass - ok", + scriptName: NullDataTy.String(), + want: func() *ScriptClass { + s := NullDataTy + return &s + }(), + }, + { + name: "NewScriptClass - invalid", + scriptName: "foo", + wantErr: ErrUnsupportedScriptType, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewScriptClass(tt.scriptName) + if err != nil && !errors.Is(err, tt.wantErr) { + t.Errorf("NewScriptClass() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewScriptClass() got = %v, want %v", got, tt.want) + } + }) + } +}