rpcclient: Implement importmulti JSON-RPC client command

This commit is contained in:
Anirudha Bose 2020-08-24 20:53:41 +02:00 committed by Jake Sylvestre
parent d2c0123bef
commit fffe4a909b
5 changed files with 550 additions and 11 deletions

View file

@ -7,6 +7,11 @@
package btcjson
import (
"encoding/json"
"fmt"
)
// AddMultisigAddressCmd defines the addmutisigaddress JSON-RPC command.
type AddMultisigAddressCmd struct {
NRequired int
@ -686,6 +691,208 @@ func NewWalletPassphraseChangeCmd(oldPassphrase, newPassphrase string) *WalletPa
}
}
// TimestampOrNow defines a type to represent a timestamp value in seconds,
// since epoch.
//
// The value can either be a integer, or the string "now".
//
// NOTE: Interpretation of the timestamp value depends upon the specific
// JSON-RPC command, where it is used.
type TimestampOrNow struct {
Value interface{}
}
// MarshalJSON implements the json.Marshaler interface for TimestampOrNow
func (t TimestampOrNow) MarshalJSON() ([]byte, error) {
return json.Marshal(t.Value)
}
// UnmarshalJSON implements the json.Unmarshaler interface for TimestampOrNow
func (t *TimestampOrNow) UnmarshalJSON(data []byte) error {
var unmarshalled interface{}
if err := json.Unmarshal(data, &unmarshalled); err != nil {
return err
}
switch v := unmarshalled.(type) {
case float64:
t.Value = int(v)
case string:
if v != "now" {
return fmt.Errorf("invalid timestamp value: %v", unmarshalled)
}
t.Value = v
default:
return fmt.Errorf("invalid timestamp value: %v", unmarshalled)
}
return nil
}
// ScriptPubKeyAddress represents an address, to be used in conjunction with
// ScriptPubKey.
type ScriptPubKeyAddress struct {
Address string `json:"address"`
}
// ScriptPubKey represents a script (as a string) or an address
// (as a ScriptPubKeyAddress).
type ScriptPubKey struct {
Value interface{}
}
// MarshalJSON implements the json.Marshaler interface for ScriptPubKey
func (s ScriptPubKey) MarshalJSON() ([]byte, error) {
return json.Marshal(s.Value)
}
// UnmarshalJSON implements the json.Unmarshaler interface for ScriptPubKey
func (s *ScriptPubKey) UnmarshalJSON(data []byte) error {
var unmarshalled interface{}
if err := json.Unmarshal(data, &unmarshalled); err != nil {
return err
}
switch v := unmarshalled.(type) {
case string:
s.Value = v
case map[string]interface{}:
s.Value = ScriptPubKeyAddress{Address: v["address"].(string)}
default:
return fmt.Errorf("invalid scriptPubKey value: %v", unmarshalled)
}
return nil
}
// DescriptorRange specifies the limits of a ranged Descriptor.
//
// Descriptors are typically ranged when specified in the form of generic HD
// chain paths.
// Example of a ranged descriptor: pkh(tpub.../*)
//
// The value can be an int to specify the end of the range, or the range
// itself, as []int{begin, end}.
type DescriptorRange struct {
Value interface{}
}
// MarshalJSON implements the json.Marshaler interface for DescriptorRange
func (r DescriptorRange) MarshalJSON() ([]byte, error) {
return json.Marshal(r.Value)
}
// UnmarshalJSON implements the json.Unmarshaler interface for DescriptorRange
func (r *DescriptorRange) UnmarshalJSON(data []byte) error {
var unmarshalled interface{}
if err := json.Unmarshal(data, &unmarshalled); err != nil {
return err
}
switch v := unmarshalled.(type) {
case float64:
r.Value = int(v)
case []interface{}:
if len(v) != 2 {
return fmt.Errorf("expected [begin,end] integer range, got: %v", unmarshalled)
}
r.Value = []int{
int(v[0].(float64)),
int(v[1].(float64)),
}
default:
return fmt.Errorf("invalid descriptor range value: %v", unmarshalled)
}
return nil
}
// ImportMultiRequest defines the request struct to be passed to the
// ImportMultiCmd, as an array.
type ImportMultiRequest struct {
// Descriptor to import, in canonical form. If using Descriptor, do not
// also provide ScriptPubKey, RedeemScript, WitnessScript, PubKeys, or Keys.
Descriptor *string `json:"desc,omitempty"`
// Script/address to import. Should not be provided if using Descriptor.
ScriptPubKey *ScriptPubKey `json:"scriptPubKey,omitempty"`
// Creation time of the key in seconds since epoch (Jan 1 1970 GMT), or
// the string "now" to substitute the current synced blockchain time.
//
// The timestamp of the oldest key will determine how far back blockchain
// rescans need to begin for missing wallet transactions.
//
// Specifying "now" bypasses scanning. Useful for keys that are known to
// never have been used.
//
// Specifying 0 scans the entire blockchain.
Timestamp TimestampOrNow `json:"timestamp"`
// Allowed only if the ScriptPubKey is a P2SH or P2SH-P2WSH
// address/scriptPubKey.
RedeemScript *string `json:"redeemscript,omitempty"`
// Allowed only if the ScriptPubKey is a P2SH-P2WSH or P2WSH
// address/scriptPubKey.
WitnessScript *string `json:"witnessscript,omitempty"`
// Array of strings giving pubkeys to import. They must occur in P2PKH or
// P2WPKH scripts. They are not required when the private key is also
// provided (see Keys).
PubKeys *[]string `json:"pubkeys,omitempty"`
// Array of strings giving private keys to import. The corresponding
// public keys must occur in the output or RedeemScript.
Keys *[]string `json:"keys,omitempty"`
// If the provided Descriptor is ranged, this specifies the end
// (as an int) or the range (as []int{begin, end}) to import.
Range *DescriptorRange `json:"range,omitempty"`
// States whether matching outputs should be treated as not incoming
// payments (also known as change).
Internal *bool `json:"internal,omitempty"`
// States whether matching outputs should be considered watchonly.
//
// If an address/script is imported without all of the private keys
// required to spend from that address, set this field to true.
//
// If all the private keys are provided and the address/script is
// spendable, set this field to false.
WatchOnly *bool `json:"watchonly,omitempty"`
// Label to assign to the address. Only allowed when Internal is false.
Label *string `json:"label,omitempty"`
// States whether imported public keys should be added to the keypool for
// when users request new addresses. Only allowed when wallet private keys
// are disabled.
KeyPool *bool `json:"keypool,omitempty"`
}
// ImportMultiRequest defines the options struct, provided to the
// ImportMultiCmd as a pointer argument.
type ImportMultiOptions struct {
Rescan bool `json:"rescan"` // Rescan the blockchain after all imports
}
// ImportMultiCmd defines the importmulti JSON-RPC command.
type ImportMultiCmd struct {
Requests []ImportMultiRequest
Options *ImportMultiOptions
}
// NewImportMultiCmd returns a new instance which can be used to issue
// an importmulti JSON-RPC command.
//
// The parameters which are pointers indicate they are optional. Passing nil
// for optional parameters will use the default value.
func NewImportMultiCmd(requests []ImportMultiRequest, options *ImportMultiOptions) *ImportMultiCmd {
return &ImportMultiCmd{
Requests: requests,
Options: options,
}
}
func init() {
// The commands in this file are only usable with a wallet server.
flags := UFWalletOnly
@ -709,6 +916,7 @@ func init() {
MustRegisterCmd("getreceivedbyaddress", (*GetReceivedByAddressCmd)(nil), flags)
MustRegisterCmd("gettransaction", (*GetTransactionCmd)(nil), flags)
MustRegisterCmd("getwalletinfo", (*GetWalletInfoCmd)(nil), flags)
MustRegisterCmd("importmulti", (*ImportMultiCmd)(nil), flags)
MustRegisterCmd("importprivkey", (*ImportPrivKeyCmd)(nil), flags)
MustRegisterCmd("keypoolrefill", (*KeyPoolRefillCmd)(nil), flags)
MustRegisterCmd("listaccounts", (*ListAccountsCmd)(nil), flags)

View file

@ -1243,6 +1243,258 @@ func TestWalletSvrCmds(t *testing.T) {
NewPassphrase: "new",
},
},
{
name: "importmulti with descriptor + options",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd(
"importmulti",
// Cannot use a native string, due to special types like timestamp.
[]btcjson.ImportMultiRequest{
{Descriptor: btcjson.String("123"), Timestamp: btcjson.TimestampOrNow{Value: 0}},
},
`{"rescan": true}`,
)
},
staticCmd: func() interface{} {
requests := []btcjson.ImportMultiRequest{
{Descriptor: btcjson.String("123"), Timestamp: btcjson.TimestampOrNow{Value: 0}},
}
options := btcjson.ImportMultiOptions{Rescan: true}
return btcjson.NewImportMultiCmd(requests, &options)
},
marshalled: `{"jsonrpc":"1.0","method":"importmulti","params":[[{"desc":"123","timestamp":0}],{"rescan":true}],"id":1}`,
unmarshalled: &btcjson.ImportMultiCmd{
Requests: []btcjson.ImportMultiRequest{
{
Descriptor: btcjson.String("123"),
Timestamp: btcjson.TimestampOrNow{Value: 0},
},
},
Options: &btcjson.ImportMultiOptions{Rescan: true},
},
},
{
name: "importmulti with descriptor + no options",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd(
"importmulti",
// Cannot use a native string, due to special types like timestamp.
[]btcjson.ImportMultiRequest{
{
Descriptor: btcjson.String("123"),
Timestamp: btcjson.TimestampOrNow{Value: 0},
WatchOnly: btcjson.Bool(false),
Internal: btcjson.Bool(true),
Label: btcjson.String("aaa"),
KeyPool: btcjson.Bool(false),
},
},
)
},
staticCmd: func() interface{} {
requests := []btcjson.ImportMultiRequest{
{
Descriptor: btcjson.String("123"),
Timestamp: btcjson.TimestampOrNow{Value: 0},
WatchOnly: btcjson.Bool(false),
Internal: btcjson.Bool(true),
Label: btcjson.String("aaa"),
KeyPool: btcjson.Bool(false),
},
}
return btcjson.NewImportMultiCmd(requests, nil)
},
marshalled: `{"jsonrpc":"1.0","method":"importmulti","params":[[{"desc":"123","timestamp":0,"internal":true,"watchonly":false,"label":"aaa","keypool":false}]],"id":1}`,
unmarshalled: &btcjson.ImportMultiCmd{
Requests: []btcjson.ImportMultiRequest{
{
Descriptor: btcjson.String("123"),
Timestamp: btcjson.TimestampOrNow{Value: 0},
WatchOnly: btcjson.Bool(false),
Internal: btcjson.Bool(true),
Label: btcjson.String("aaa"),
KeyPool: btcjson.Bool(false),
},
},
},
},
{
name: "importmulti with descriptor + string timestamp",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd(
"importmulti",
// Cannot use a native string, due to special types like timestamp.
[]btcjson.ImportMultiRequest{
{
Descriptor: btcjson.String("123"),
Timestamp: btcjson.TimestampOrNow{Value: "now"},
},
},
)
},
staticCmd: func() interface{} {
requests := []btcjson.ImportMultiRequest{
{Descriptor: btcjson.String("123"), Timestamp: btcjson.TimestampOrNow{Value: "now"}},
}
return btcjson.NewImportMultiCmd(requests, nil)
},
marshalled: `{"jsonrpc":"1.0","method":"importmulti","params":[[{"desc":"123","timestamp":"now"}]],"id":1}`,
unmarshalled: &btcjson.ImportMultiCmd{
Requests: []btcjson.ImportMultiRequest{
{Descriptor: btcjson.String("123"), Timestamp: btcjson.TimestampOrNow{Value: "now"}},
},
},
},
{
name: "importmulti with scriptPubKey script",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd(
"importmulti",
// Cannot use a native string, due to special types like timestamp and scriptPubKey
[]btcjson.ImportMultiRequest{
{
ScriptPubKey: &btcjson.ScriptPubKey{Value: "script"},
RedeemScript: btcjson.String("123"),
Timestamp: btcjson.TimestampOrNow{Value: 0},
PubKeys: &[]string{"aaa"},
},
},
)
},
staticCmd: func() interface{} {
requests := []btcjson.ImportMultiRequest{
{
ScriptPubKey: &btcjson.ScriptPubKey{Value: "script"},
RedeemScript: btcjson.String("123"),
Timestamp: btcjson.TimestampOrNow{Value: 0},
PubKeys: &[]string{"aaa"},
},
}
return btcjson.NewImportMultiCmd(requests, nil)
},
marshalled: `{"jsonrpc":"1.0","method":"importmulti","params":[[{"scriptPubKey":"script","timestamp":0,"redeemscript":"123","pubkeys":["aaa"]}]],"id":1}`,
unmarshalled: &btcjson.ImportMultiCmd{
Requests: []btcjson.ImportMultiRequest{
{
ScriptPubKey: &btcjson.ScriptPubKey{Value: "script"},
RedeemScript: btcjson.String("123"),
Timestamp: btcjson.TimestampOrNow{Value: 0},
PubKeys: &[]string{"aaa"},
},
},
},
},
{
name: "importmulti with scriptPubKey address",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd(
"importmulti",
// Cannot use a native string, due to special types like timestamp and scriptPubKey
[]btcjson.ImportMultiRequest{
{
ScriptPubKey: &btcjson.ScriptPubKey{Value: btcjson.ScriptPubKeyAddress{Address: "addr"}},
WitnessScript: btcjson.String("123"),
Timestamp: btcjson.TimestampOrNow{Value: 0},
Keys: &[]string{"aaa"},
},
},
)
},
staticCmd: func() interface{} {
requests := []btcjson.ImportMultiRequest{
{
ScriptPubKey: &btcjson.ScriptPubKey{Value: btcjson.ScriptPubKeyAddress{Address: "addr"}},
WitnessScript: btcjson.String("123"),
Timestamp: btcjson.TimestampOrNow{Value: 0},
Keys: &[]string{"aaa"},
},
}
return btcjson.NewImportMultiCmd(requests, nil)
},
marshalled: `{"jsonrpc":"1.0","method":"importmulti","params":[[{"scriptPubKey":{"address":"addr"},"timestamp":0,"witnessscript":"123","keys":["aaa"]}]],"id":1}`,
unmarshalled: &btcjson.ImportMultiCmd{
Requests: []btcjson.ImportMultiRequest{
{
ScriptPubKey: &btcjson.ScriptPubKey{Value: btcjson.ScriptPubKeyAddress{Address: "addr"}},
WitnessScript: btcjson.String("123"),
Timestamp: btcjson.TimestampOrNow{Value: 0},
Keys: &[]string{"aaa"},
},
},
},
},
{
name: "importmulti with ranged (int) descriptor",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd(
"importmulti",
// Cannot use a native string, due to special types like timestamp.
[]btcjson.ImportMultiRequest{
{
Descriptor: btcjson.String("123"),
Timestamp: btcjson.TimestampOrNow{Value: 0},
Range: &btcjson.DescriptorRange{Value: 7},
},
},
)
},
staticCmd: func() interface{} {
requests := []btcjson.ImportMultiRequest{
{
Descriptor: btcjson.String("123"),
Timestamp: btcjson.TimestampOrNow{Value: 0},
Range: &btcjson.DescriptorRange{Value: 7},
},
}
return btcjson.NewImportMultiCmd(requests, nil)
},
marshalled: `{"jsonrpc":"1.0","method":"importmulti","params":[[{"desc":"123","timestamp":0,"range":7}]],"id":1}`,
unmarshalled: &btcjson.ImportMultiCmd{
Requests: []btcjson.ImportMultiRequest{
{
Descriptor: btcjson.String("123"),
Timestamp: btcjson.TimestampOrNow{Value: 0},
Range: &btcjson.DescriptorRange{Value: 7},
},
},
},
},
{
name: "importmulti with ranged (slice) descriptor",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd(
"importmulti",
// Cannot use a native string, due to special types like timestamp.
[]btcjson.ImportMultiRequest{
{
Descriptor: btcjson.String("123"),
Timestamp: btcjson.TimestampOrNow{Value: 0},
Range: &btcjson.DescriptorRange{Value: []int{1, 7}},
},
},
)
},
staticCmd: func() interface{} {
requests := []btcjson.ImportMultiRequest{
{
Descriptor: btcjson.String("123"),
Timestamp: btcjson.TimestampOrNow{Value: 0},
Range: &btcjson.DescriptorRange{Value: []int{1, 7}},
},
}
return btcjson.NewImportMultiCmd(requests, nil)
},
marshalled: `{"jsonrpc":"1.0","method":"importmulti","params":[[{"desc":"123","timestamp":0,"range":[1,7]}]],"id":1}`,
unmarshalled: &btcjson.ImportMultiCmd{
Requests: []btcjson.ImportMultiRequest{
{
Descriptor: btcjson.String("123"),
Timestamp: btcjson.TimestampOrNow{Value: 0},
Range: &btcjson.DescriptorRange{Value: []int{1, 7}},
},
},
},
},
}
t.Logf("Running %d tests", len(tests))

View file

@ -173,3 +173,14 @@ type GetBalancesResult struct {
Mine BalanceDetailsResult `json:"mine"`
WatchOnly *BalanceDetailsResult `json:"watchonly"`
}
// ImportMultiResults is a slice that models the result of the importmulti command.
//
// Each item in the slice contains the execution result corresponding to the input
// requests of type btcjson.ImportMultiRequest, passed to the ImportMulti[Async]
// function.
type ImportMultiResults []struct {
Success bool `json:"success"`
Error *RPCError `json:"error,omitempty"`
Warnings *[]string `json:"warnings,omitempty"`
}

View file

@ -2,30 +2,60 @@ package rpcclient
import (
"fmt"
"github.com/btcsuite/btcd/btcjson"
)
var connCfg = &ConnConfig{
Host: "localhost:8332",
User: "yourrpcuser",
Pass: "yourrpcpass",
HTTPPostMode: true,
DisableTLS: true,
}
func ExampleClient_GetDescriptorInfo() {
connCfg := &ConnConfig{
Host: "localhost:8332",
User: "yourrpcuser",
Pass: "yourrpcpass",
HTTPPostMode: true,
DisableTLS: true,
}
client, err := New(connCfg, nil)
if err != nil {
log.Error(err)
return
panic(err)
}
defer client.Shutdown()
descriptorInfo, err := client.GetDescriptorInfo(
"wpkh([d34db33f/84h/0h/0h]0279be667ef9dcbbac55a06295Ce870b07029Bfcdb2dce28d959f2815b16f81798)")
if err != nil {
log.Error(err)
return
panic(err)
}
fmt.Printf("%+v\n", descriptorInfo)
// &{Descriptor:wpkh([d34db33f/84'/0'/0']0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)#n9g43y4k Checksum:qwlqgth7 IsRange:false IsSolvable:true HasPrivateKeys:false}
}
func ExampleClient_ImportMulti() {
client, err := New(connCfg, nil)
if err != nil {
panic(err)
}
defer client.Shutdown()
requests := []btcjson.ImportMultiRequest{
{
Descriptor: btcjson.String(
"pkh([f34db33f/44'/0'/0']xpub6Cc939fyHvfB9pPLWd3bSyyQFvgKbwhidca49jGCM5Hz5ypEPGf9JVXB4NBuUfPgoHnMjN6oNgdC9KRqM11RZtL8QLW6rFKziNwHDYhZ6Kx/0/*)#ed7px9nu"),
Range: &btcjson.DescriptorRange{Value: []int{0, 100}},
Timestamp: btcjson.TimestampOrNow{Value: 0}, // scan from genesis
WatchOnly: btcjson.Bool(true),
KeyPool: btcjson.Bool(false),
Internal: btcjson.Bool(false),
},
}
opts := &btcjson.ImportMultiOptions{Rescan: true}
resp, err := client.ImportMulti(requests, opts)
if err != nil {
panic(err)
}
fmt.Println(resp[0].Success)
// true
}

View file

@ -2203,6 +2203,44 @@ func (c *Client) ImportAddressRescan(address string, account string, rescan bool
return c.ImportAddressRescanAsync(address, account, rescan).Receive()
}
// FutureImportMultiResult is a future promise to deliver the result of an
// ImportMultiAsync RPC invocation (or an applicable error).
type FutureImportMultiResult chan *response
// Receive waits for the response promised by the future and returns the result
// of importing multiple addresses/scripts.
func (r FutureImportMultiResult) Receive() (btcjson.ImportMultiResults, error) {
res, err := receiveFuture(r)
if err != nil {
return nil, err
}
var importMultiResults btcjson.ImportMultiResults
err = json.Unmarshal(res, &importMultiResults)
if err != nil {
return nil, err
}
return importMultiResults, nil
}
// ImportMultiAsync 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 ImportMulti for the blocking version and more details.
func (c *Client) ImportMultiAsync(requests []btcjson.ImportMultiRequest, options *btcjson.ImportMultiOptions) FutureImportMultiResult {
cmd := btcjson.NewImportMultiCmd(requests, options)
return c.sendCmd(cmd)
}
// ImportMulti imports addresses/scripts, optionally rescanning the blockchain
// from the earliest creation time of the imported scripts.
//
// See btcjson.ImportMultiRequest for details on the requests parameter.
func (c *Client) ImportMulti(requests []btcjson.ImportMultiRequest, options *btcjson.ImportMultiOptions) (btcjson.ImportMultiResults, error) {
return c.ImportMultiAsync(requests, options).Receive()
}
// FutureImportPrivKeyResult is a future promise to deliver the result of an
// ImportPrivKeyAsync RPC invocation (or an applicable error).
type FutureImportPrivKeyResult chan *response