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.
This commit is contained in:
Anirudha Bose 2020-09-15 23:30:59 +02:00 committed by John C. Vernaleo
parent 6daaf73544
commit ac3f235eb9
8 changed files with 349 additions and 7 deletions

View file

@ -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)

View file

@ -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) {

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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

View file

@ -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)
}
})
}
}