From fffe4a909b895f97f75e343d50387f053d8e1f1c Mon Sep 17 00:00:00 2001 From: Anirudha Bose Date: Mon, 24 Aug 2020 20:53:41 +0200 Subject: [PATCH] rpcclient: Implement importmulti JSON-RPC client command --- btcjson/walletsvrcmds.go | 208 ++++++++++++++++++++++++++++ btcjson/walletsvrcmds_test.go | 252 ++++++++++++++++++++++++++++++++++ btcjson/walletsvrresults.go | 11 ++ rpcclient/example_test.go | 52 +++++-- rpcclient/wallet.go | 38 +++++ 5 files changed, 550 insertions(+), 11 deletions(-) diff --git a/btcjson/walletsvrcmds.go b/btcjson/walletsvrcmds.go index be1a67fb..a2c73988 100644 --- a/btcjson/walletsvrcmds.go +++ b/btcjson/walletsvrcmds.go @@ -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) diff --git a/btcjson/walletsvrcmds_test.go b/btcjson/walletsvrcmds_test.go index 554a8741..bc095882 100644 --- a/btcjson/walletsvrcmds_test.go +++ b/btcjson/walletsvrcmds_test.go @@ -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)) diff --git a/btcjson/walletsvrresults.go b/btcjson/walletsvrresults.go index 6e69ed90..d860eceb 100644 --- a/btcjson/walletsvrresults.go +++ b/btcjson/walletsvrresults.go @@ -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"` +} diff --git a/rpcclient/example_test.go b/rpcclient/example_test.go index c2c52905..6083e115 100644 --- a/rpcclient/example_test.go +++ b/rpcclient/example_test.go @@ -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 +} diff --git a/rpcclient/wallet.go b/rpcclient/wallet.go index 37bf9471..d4069aad 100644 --- a/rpcclient/wallet.go +++ b/rpcclient/wallet.go @@ -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