diff --git a/Makefile b/Makefile index 3838a12f..81fd7181 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ deps: | gen-deps gen-deps: ${GO_INSTALL} github.com/google/addlicense ${GO_INSTALL} github.com/segmentio/golines + ${GO_INSTALL} golang.org/x/tools/cmd/goimports gen: ./codegen.sh @@ -30,9 +31,11 @@ lint: | lint-examples format: gofmt -s -w -l . + goimports -w . check-format: ! gofmt -s -l . | read + ! goimports -l . | read test: ${TEST_SCRIPT} diff --git a/asserter/account.go b/asserter/account.go index 23764d54..5ffddccd 100644 --- a/asserter/account.go +++ b/asserter/account.go @@ -15,8 +15,8 @@ package asserter import ( + "encoding/json" "fmt" - "reflect" "github.com/coinbase/rosetta-sdk-go/types" ) @@ -28,7 +28,7 @@ import ( // struct (including currency.Metadata). func containsCurrency(currencies []*types.Currency, currency *types.Currency) bool { for _, curr := range currencies { - if reflect.DeepEqual(curr, currency) { + if types.Hash(curr) == types.Hash(currency) { return true } } @@ -66,6 +66,7 @@ func AccountBalanceResponse( requestBlock *types.PartialBlockIdentifier, responseBlock *types.BlockIdentifier, balances []*types.Amount, + metadata json.RawMessage, ) error { if err := BlockIdentifier(responseBlock); err != nil { return err @@ -95,5 +96,5 @@ func AccountBalanceResponse( ) } - return nil + return JSONObject(metadata) } diff --git a/asserter/account_test.go b/asserter/account_test.go index 12994fe8..8a2c2d75 100644 --- a/asserter/account_test.go +++ b/asserter/account_test.go @@ -15,6 +15,7 @@ package asserter import ( + "encoding/json" "errors" "fmt" "testing" @@ -48,18 +49,29 @@ func TestContainsCurrency(t *testing.T) { { Symbol: "BTC", Decimals: 8, - Metadata: map[string]interface{}{ - "blah": "hello", - }, + Metadata: json.RawMessage(`{"blah": "hello"}`), }, }, currency: &types.Currency{ Symbol: "BTC", Decimals: 8, - Metadata: map[string]interface{}{ - "blah": "hello", + Metadata: json.RawMessage(`{"blah": "hello"}`), + }, + contains: true, + }, + "more complex contains": { + currencies: []*types.Currency{ + { + Symbol: "BTC", + Decimals: 8, + Metadata: json.RawMessage(`{"blah2":"bye", "blah": "hello"}`), }, }, + currency: &types.Currency{ + Symbol: "BTC", + Decimals: 8, + Metadata: json.RawMessage(`{"blah": "hello", "blah2": "bye"}`), + }, contains: true, }, "empty": { @@ -101,17 +113,13 @@ func TestContainsCurrency(t *testing.T) { { Symbol: "BTC", Decimals: 8, - Metadata: map[string]interface{}{ - "blah": "hello", - }, + Metadata: json.RawMessage(`{"blah": "hello"}`), }, }, currency: &types.Currency{ Symbol: "BTC", Decimals: 8, - Metadata: map[string]interface{}{ - "blah": "bye", - }, + Metadata: json.RawMessage(`{"blah": "bye"}`), }, contains: false, }, @@ -153,6 +161,7 @@ func TestAccoutBalance(t *testing.T) { requestBlock *types.PartialBlockIdentifier responseBlock *types.BlockIdentifier balances []*types.Amount + metadata json.RawMessage err error }{ "simple balance": { @@ -203,7 +212,8 @@ func TestAccoutBalance(t *testing.T) { balances: []*types.Amount{ validAmount, }, - err: nil, + metadata: json.RawMessage(`{"sequence":1}`), + err: nil, }, "invalid historical request index": { requestBlock: &types.PartialBlockIdentifier{ @@ -243,6 +253,7 @@ func TestAccoutBalance(t *testing.T) { test.requestBlock, test.responseBlock, test.balances, + test.metadata, ) assert.Equal(t, test.err, err) }) diff --git a/asserter/asserter.go b/asserter/asserter.go index ba638dc1..2eb06d0b 100644 --- a/asserter/asserter.go +++ b/asserter/asserter.go @@ -87,8 +87,10 @@ func NewClientWithResponses( ) } -// FileConfiguration is the structure of the JSON configuration file. -type FileConfiguration struct { +// Configuration is the static configuration of an Asserter. This +// configuration can be exported by the Asserter and used to instantiate an +// Asserter. +type Configuration struct { NetworkIdentifier *types.NetworkIdentifier `json:"network_identifier"` GenesisBlockIdentifier *types.BlockIdentifier `json:"genesis_block_identifier"` AllowedOperationTypes []string `json:"allowed_operation_types"` @@ -109,7 +111,7 @@ func NewClientWithFile( return nil, err } - config := &FileConfiguration{} + config := &Configuration{} if err := json.Unmarshal(content, config); err != nil { return nil, err } @@ -170,16 +172,9 @@ func NewClientWithOptions( // ClientConfiguration returns all variables currently set in an Asserter. // This function will error if it is called on an uninitialized asserter. -func (a *Asserter) ClientConfiguration() ( - *types.NetworkIdentifier, - *types.BlockIdentifier, - []string, - []*types.OperationStatus, - []*types.Error, - error, -) { +func (a *Asserter) ClientConfiguration() (*Configuration, error) { if a == nil { - return nil, nil, nil, nil, nil, ErrAsserterNotInitialized + return nil, ErrAsserterNotInitialized } operationStatuses := []*types.OperationStatus{} @@ -195,7 +190,13 @@ func (a *Asserter) ClientConfiguration() ( errors = append(errors, v) } - return a.network, a.genesisBlock, a.operationTypes, operationStatuses, errors, nil + return &Configuration{ + NetworkIdentifier: a.network, + GenesisBlockIdentifier: a.genesisBlock, + AllowedOperationTypes: a.operationTypes, + AllowedOperationStatuses: operationStatuses, + AllowedErrors: errors, + }, nil } // OperationSuccessful returns a boolean indicating if a types.Operation is diff --git a/asserter/asserter_test.go b/asserter/asserter_test.go index 2edb164b..63c2eecf 100644 --- a/asserter/asserter_test.go +++ b/asserter/asserter_test.go @@ -223,17 +223,29 @@ func TestNew(t *testing.T) { } assert.NotNil(t, asserter) - network, genesis, opTypes, opStatuses, errors, err := asserter.ClientConfiguration() + configuration, err := asserter.ClientConfiguration() assert.NoError(t, err) - assert.Equal(t, test.network, network) - assert.Equal(t, test.networkStatus.GenesisBlockIdentifier, genesis) - assert.ElementsMatch(t, test.networkOptions.Allow.OperationTypes, opTypes) - assert.ElementsMatch(t, test.networkOptions.Allow.OperationStatuses, opStatuses) - assert.ElementsMatch(t, test.networkOptions.Allow.Errors, errors) + assert.Equal(t, test.network, configuration.NetworkIdentifier) + assert.Equal( + t, + test.networkStatus.GenesisBlockIdentifier, + configuration.GenesisBlockIdentifier, + ) + assert.ElementsMatch( + t, + test.networkOptions.Allow.OperationTypes, + configuration.AllowedOperationTypes, + ) + assert.ElementsMatch( + t, + test.networkOptions.Allow.OperationStatuses, + configuration.AllowedOperationStatuses, + ) + assert.ElementsMatch(t, test.networkOptions.Allow.Errors, configuration.AllowedErrors) }) t.Run(fmt.Sprintf("%s with file", name), func(t *testing.T) { - fileConfig := FileConfiguration{ + fileConfig := &Configuration{ NetworkIdentifier: test.network, GenesisBlockIdentifier: test.networkStatus.GenesisBlockIdentifier, AllowedOperationTypes: test.networkOptions.Allow.OperationTypes, @@ -262,13 +274,25 @@ func TestNew(t *testing.T) { } assert.NotNil(t, asserter) - network, genesis, opTypes, opStatuses, errors, err := asserter.ClientConfiguration() + configuration, err := asserter.ClientConfiguration() assert.NoError(t, err) - assert.Equal(t, test.network, network) - assert.Equal(t, test.networkStatus.GenesisBlockIdentifier, genesis) - assert.ElementsMatch(t, test.networkOptions.Allow.OperationTypes, opTypes) - assert.ElementsMatch(t, test.networkOptions.Allow.OperationStatuses, opStatuses) - assert.ElementsMatch(t, test.networkOptions.Allow.Errors, errors) + assert.Equal(t, test.network, configuration.NetworkIdentifier) + assert.Equal( + t, + test.networkStatus.GenesisBlockIdentifier, + configuration.GenesisBlockIdentifier, + ) + assert.ElementsMatch( + t, + test.networkOptions.Allow.OperationTypes, + configuration.AllowedOperationTypes, + ) + assert.ElementsMatch( + t, + test.networkOptions.Allow.OperationStatuses, + configuration.AllowedOperationStatuses, + ) + assert.ElementsMatch(t, test.networkOptions.Allow.Errors, configuration.AllowedErrors) }) } diff --git a/asserter/block.go b/asserter/block.go index f4ade1a1..3ad92f0c 100644 --- a/asserter/block.go +++ b/asserter/block.go @@ -56,7 +56,7 @@ func Amount(amount *types.Amount) error { return errors.New("Amount.Currency.Decimals must be > 0") } - return nil + return JSONObject(amount.Currency.Metadata) } // OperationIdentifier returns an error if index of the @@ -96,7 +96,7 @@ func AccountIdentifier(account *types.AccountIdentifier) error { return errors.New("Account.SubAccount.Address is missing") } - return nil + return JSONObject(account.SubAccount.Metadata) } // contains checks if a string is contained in a slice @@ -177,7 +177,11 @@ func (a *Asserter) Operation( return err } - return Amount(operation.Amount) + if err := Amount(operation.Amount); err != nil { + return err + } + + return JSONObject(operation.Metadata) } // BlockIdentifier ensures a types.BlockIdentifier @@ -256,7 +260,7 @@ func (a *Asserter) Transaction( } } - return nil + return JSONObject(transaction.Metadata) } // Timestamp returns an error if the timestamp @@ -314,5 +318,5 @@ func (a *Asserter) Block( } } - return nil + return JSONObject(block.Metadata) } diff --git a/asserter/construction.go b/asserter/construction.go index aabcd0ba..ac358a29 100644 --- a/asserter/construction.go +++ b/asserter/construction.go @@ -21,7 +21,7 @@ import ( ) // ConstructionMetadata returns an error if -// the NetworkFee is not a valid types.Amount. +// the metadata is not a JSON object. func ConstructionMetadata( response *types.ConstructionMetadataResponse, ) error { @@ -29,7 +29,7 @@ func ConstructionMetadata( return errors.New("Metadata is nil") } - return nil + return JSONObject(response.Metadata) } // ConstructionSubmit returns an error if @@ -43,5 +43,5 @@ func ConstructionSubmit( return err } - return nil + return JSONObject(response.Metadata) } diff --git a/asserter/construction_test.go b/asserter/construction_test.go index 120ec86a..f3c3fbcf 100644 --- a/asserter/construction_test.go +++ b/asserter/construction_test.go @@ -15,6 +15,7 @@ package asserter import ( + "encoding/json" "errors" "testing" @@ -30,7 +31,7 @@ func TestConstructionMetadata(t *testing.T) { }{ "valid response": { response: &types.ConstructionMetadataResponse{ - Metadata: map[string]interface{}{}, + Metadata: json.RawMessage(`{}`), }, err: nil, }, diff --git a/asserter/network.go b/asserter/network.go index 570e03bf..40c67f7e 100644 --- a/asserter/network.go +++ b/asserter/network.go @@ -17,7 +17,6 @@ package asserter import ( "errors" "fmt" - "reflect" "github.com/coinbase/rosetta-sdk-go/types" ) @@ -32,7 +31,7 @@ func SubNetworkIdentifier(subNetworkIdentifier *types.SubNetworkIdentifier) erro return errors.New("NetworkIdentifier.SubNetworkIdentifier.Network is missing") } - return nil + return JSONObject(subNetworkIdentifier.Metadata) } // NetworkIdentifier ensures a types.NetworkIdentifier has @@ -59,7 +58,7 @@ func Peer(peer *types.Peer) error { return errors.New("Peer.PeerID is missing") } - return nil + return JSONObject(peer.Metadata) } // Version ensures the version of the node is @@ -77,7 +76,7 @@ func Version(version *types.Version) error { return errors.New("Version.MiddlewareVersion is missing") } - return nil + return JSONObject(version.Metadata) } // StringArray ensures all strings in an array @@ -247,7 +246,7 @@ func containsNetworkIdentifier( network *types.NetworkIdentifier, ) bool { for _, net := range networks { - if reflect.DeepEqual(net, network) { + if types.Hash(net) == types.Hash(network) { return true } } diff --git a/asserter/request.go b/asserter/request.go index ff384294..46bd6676 100644 --- a/asserter/request.go +++ b/asserter/request.go @@ -160,7 +160,7 @@ func (a *Asserter) ConstructionMetadataRequest(request *types.ConstructionMetada return errors.New("ConstructionMetadataRequest.Options is nil") } - return nil + return JSONObject(request.Options) } // ConstructionSubmitRequest ensures that a types.ConstructionSubmitRequest @@ -240,7 +240,7 @@ func (a *Asserter) MetadataRequest(request *types.MetadataRequest) error { return errors.New("MetadataRequest is nil") } - return nil + return JSONObject(request.Metadata) } // NetworkRequest ensures that a types.NetworkRequest @@ -258,5 +258,9 @@ func (a *Asserter) NetworkRequest(request *types.NetworkRequest) error { return err } - return a.SupportedNetwork(request.NetworkIdentifier) + if err := a.SupportedNetwork(request.NetworkIdentifier); err != nil { + return err + } + + return JSONObject(request.Metadata) } diff --git a/asserter/request_test.go b/asserter/request_test.go index 3cc0b962..fc925a16 100644 --- a/asserter/request_test.go +++ b/asserter/request_test.go @@ -15,6 +15,7 @@ package asserter import ( + "encoding/json" "errors" "fmt" "testing" @@ -294,14 +295,14 @@ func TestConstructionMetadataRequest(t *testing.T) { "valid request": { request: &types.ConstructionMetadataRequest{ NetworkIdentifier: validNetworkIdentifier, - Options: map[string]interface{}{}, + Options: json.RawMessage(`{}`), }, err: nil, }, "invalid request wrong network": { request: &types.ConstructionMetadataRequest{ NetworkIdentifier: wrongNetworkIdentifier, - Options: map[string]interface{}{}, + Options: json.RawMessage(`{}`), }, err: fmt.Errorf("%+v is not supported", wrongNetworkIdentifier), }, @@ -311,7 +312,7 @@ func TestConstructionMetadataRequest(t *testing.T) { }, "missing network": { request: &types.ConstructionMetadataRequest{ - Options: map[string]interface{}{}, + Options: json.RawMessage(`{}`), }, err: errors.New("NetworkIdentifier is nil"), }, diff --git a/asserter/utils.go b/asserter/utils.go new file mode 100644 index 00000000..788fab9d --- /dev/null +++ b/asserter/utils.go @@ -0,0 +1,35 @@ +// Copyright 2020 Coinbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package asserter + +import ( + "encoding/json" + "fmt" +) + +// JSONObject returns an error if the provided interface cannot be unmarshaled +// into a JSON object. This is used to ensure metadata is a JSON object. +func JSONObject(obj json.RawMessage) error { + if obj == nil { + return nil + } + + var m map[string]interface{} + if err := json.Unmarshal(obj, &m); err != nil { + return fmt.Errorf("%w: interface is not a valid JSON object %+v", err, obj) + } + + return nil +} diff --git a/asserter/utils_test.go b/asserter/utils_test.go new file mode 100644 index 00000000..d4aad70c --- /dev/null +++ b/asserter/utils_test.go @@ -0,0 +1,60 @@ +// Copyright 2020 Coinbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package asserter + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestJSONObject(t *testing.T) { + var tests = map[string]struct { + obj json.RawMessage + err bool + }{ + "nil": { + obj: nil, + err: false, + }, + "json array": { + obj: json.RawMessage(`["hello"]`), + err: true, + }, + "json number": { + obj: json.RawMessage(`5`), + err: true, + }, + "json string": { + obj: json.RawMessage(`"hello"`), + err: true, + }, + "json object": { + obj: json.RawMessage(`{"hello":"cool", "bye":[1,2,3]}`), + err: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + if test.err { + assert.Error(t, JSONObject(test.obj)) + } else { + assert.NoError(t, JSONObject(test.obj)) + } + }) + } +} diff --git a/codegen.sh b/codegen.sh index f8671972..af13bdaa 100755 --- a/codegen.sh +++ b/codegen.sh @@ -126,7 +126,7 @@ sed "${SED_IFLAG[@]}" 's/<\/code>//g' client/* server/*; sed "${SED_IFLAG[@]}" 's/\*\[\]/\[\]\*/g' client/* server/*; # Fix map pointers -sed "${SED_IFLAG[@]}" 's/\*map/map/g' client/* server/*; +sed "${SED_IFLAG[@]}" 's/\*map\[string\]interface{}/json\.RawMessage/g' client/* server/*; # Move model files to types/ mv client/model_*.go types/; diff --git a/examples/client/main.go b/examples/client/main.go index d65a593e..ea9d2219 100644 --- a/examples/client/main.go +++ b/examples/client/main.go @@ -16,7 +16,6 @@ package main import ( "context" - "encoding/json" "log" "net/http" "time" @@ -72,11 +71,7 @@ func main() { primaryNetwork := networkList.NetworkIdentifiers[0] // Step 3: Print the primary network - prettyPrimaryNetwork, err := json.MarshalIndent(primaryNetwork, "", " ") - if err != nil { - log.Fatal(err) - } - log.Printf("Primary Network: %s\n", string(prettyPrimaryNetwork)) + log.Printf("Primary Network: %s\n", types.PrettyPrintStruct(primaryNetwork)) // Step 4: Fetch the network status networkStatus, rosettaErr, err := client.NetworkAPI.NetworkStatus( @@ -93,11 +88,7 @@ func main() { } // Step 5: Print the response - prettyNetworkStatus, err := json.MarshalIndent(networkStatus, "", " ") - if err != nil { - log.Fatal(err) - } - log.Printf("Network Status: %s\n", string(prettyNetworkStatus)) + log.Printf("Network Status: %s\n", types.PrettyPrintStruct(networkStatus)) // Step 6: Assert the response is valid err = asserter.NetworkStatusResponse(networkStatus) @@ -120,11 +111,7 @@ func main() { } // Step 8: Print the response - prettyNetworkOptions, err := json.MarshalIndent(networkOptions, "", " ") - if err != nil { - log.Fatal(err) - } - log.Printf("Network Options: %s\n", string(prettyNetworkOptions)) + log.Printf("Network Options: %s\n", types.PrettyPrintStruct(networkOptions)) // Step 9: Assert the response is valid err = asserter.NetworkOptionsResponse(networkOptions) @@ -164,11 +151,7 @@ func main() { } // Step 12: Print the block - prettyBlock, err := json.MarshalIndent(block.Block, "", " ") - if err != nil { - log.Fatal(err) - } - log.Printf("Current Block: %s\n", string(prettyBlock)) + log.Printf("Current Block: %s\n", types.PrettyPrintStruct(block.Block)) // Step 13: Assert the block response is valid // diff --git a/examples/fetcher/main.go b/examples/fetcher/main.go index 8070fca9..74c82b82 100644 --- a/examples/fetcher/main.go +++ b/examples/fetcher/main.go @@ -16,7 +16,6 @@ package main import ( "context" - "encoding/json" "log" "github.com/coinbase/rosetta-sdk-go/fetcher" @@ -47,25 +46,8 @@ func main() { } // Step 3: Print the primary network and network status - prettyPrimaryNetwork, err := json.MarshalIndent( - primaryNetwork, - "", - " ", - ) - if err != nil { - log.Fatal(err) - } - log.Printf("Primary Network: %s\n", string(prettyPrimaryNetwork)) - - prettyNetworkStatus, err := json.MarshalIndent( - networkStatus, - "", - " ", - ) - if err != nil { - log.Fatal(err) - } - log.Printf("Network Status: %s\n", string(prettyNetworkStatus)) + log.Printf("Primary Network: %s\n", types.PrettyPrintStruct(primaryNetwork)) + log.Printf("Network Status: %s\n", types.PrettyPrintStruct(networkStatus)) // Step 4: Fetch the current block with retries (automatically // asserted for correctness) @@ -95,15 +77,7 @@ func main() { } // Step 5: Print the block - prettyBlock, err := json.MarshalIndent( - block, - "", - " ", - ) - if err != nil { - log.Fatal(err) - } - log.Printf("Current Block: %s\n", string(prettyBlock)) + log.Printf("Current Block: %s\n", types.PrettyPrintStruct(block)) // Step 6: Get a range of blocks blockMap, err := newFetcher.BlockRange( @@ -117,13 +91,5 @@ func main() { } // Step 7: Print the block range - prettyBlockRange, err := json.MarshalIndent( - blockMap, - "", - " ", - ) - if err != nil { - log.Fatal(err) - } - log.Printf("Block Range: %s\n", string(prettyBlockRange)) + log.Printf("Block Range: %s\n", types.PrettyPrintStruct(blockMap)) } diff --git a/fetcher/account.go b/fetcher/account.go index 3d60e1b6..f5818127 100644 --- a/fetcher/account.go +++ b/fetcher/account.go @@ -16,6 +16,7 @@ package fetcher import ( "context" + "encoding/json" "errors" "fmt" @@ -32,7 +33,7 @@ func (f *Fetcher) AccountBalance( network *types.NetworkIdentifier, account *types.AccountIdentifier, block *types.PartialBlockIdentifier, -) (*types.BlockIdentifier, []*types.Amount, map[string]interface{}, error) { +) (*types.BlockIdentifier, []*types.Amount, json.RawMessage, error) { response, _, err := f.rosettaClient.AccountAPI.AccountBalance(ctx, &types.AccountBalanceRequest{ NetworkIdentifier: network, @@ -50,6 +51,7 @@ func (f *Fetcher) AccountBalance( block, responseBlock, balances, + response.Metadata, ); err != nil { return nil, nil, nil, err } @@ -65,7 +67,7 @@ func (f *Fetcher) AccountBalanceRetry( network *types.NetworkIdentifier, account *types.AccountIdentifier, block *types.PartialBlockIdentifier, -) (*types.BlockIdentifier, []*types.Amount, map[string]interface{}, error) { +) (*types.BlockIdentifier, []*types.Amount, json.RawMessage, error) { backoffRetries := backoffRetries( f.retryElapsedTime, f.maxRetries, diff --git a/fetcher/construction.go b/fetcher/construction.go index 16b24980..3b3c460d 100644 --- a/fetcher/construction.go +++ b/fetcher/construction.go @@ -16,6 +16,7 @@ package fetcher import ( "context" + "encoding/json" "github.com/coinbase/rosetta-sdk-go/asserter" @@ -27,8 +28,8 @@ import ( func (f *Fetcher) ConstructionMetadata( ctx context.Context, network *types.NetworkIdentifier, - options map[string]interface{}, -) (map[string]interface{}, error) { + options json.RawMessage, +) (json.RawMessage, error) { metadata, _, err := f.rosettaClient.ConstructionAPI.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{ NetworkIdentifier: network, @@ -52,7 +53,7 @@ func (f *Fetcher) ConstructionSubmit( ctx context.Context, network *types.NetworkIdentifier, signedTransaction string, -) (*types.TransactionIdentifier, map[string]interface{}, error) { +) (*types.TransactionIdentifier, json.RawMessage, error) { submitResponse, _, err := f.rosettaClient.ConstructionAPI.ConstructionSubmit( ctx, &types.ConstructionSubmitRequest{ diff --git a/fetcher/mempool.go b/fetcher/mempool.go index 17244b34..e1f6b830 100644 --- a/fetcher/mempool.go +++ b/fetcher/mempool.go @@ -16,6 +16,7 @@ package fetcher import ( "context" + "encoding/json" "github.com/coinbase/rosetta-sdk-go/asserter" @@ -52,7 +53,7 @@ func (f *Fetcher) MempoolTransaction( ctx context.Context, network *types.NetworkIdentifier, transaction *types.TransactionIdentifier, -) (*types.Transaction, map[string]interface{}, error) { +) (*types.Transaction, json.RawMessage, error) { response, _, err := f.rosettaClient.MempoolAPI.MempoolTransaction( ctx, &types.MempoolTransactionRequest{ diff --git a/fetcher/network.go b/fetcher/network.go index 9b94a332..cf9e1501 100644 --- a/fetcher/network.go +++ b/fetcher/network.go @@ -16,6 +16,7 @@ package fetcher import ( "context" + "encoding/json" "errors" "github.com/coinbase/rosetta-sdk-go/asserter" @@ -28,7 +29,7 @@ import ( func (f *Fetcher) NetworkStatus( ctx context.Context, network *types.NetworkIdentifier, - metadata map[string]interface{}, + metadata json.RawMessage, ) (*types.NetworkStatusResponse, error) { networkStatus, _, err := f.rosettaClient.NetworkAPI.NetworkStatus( ctx, @@ -53,7 +54,7 @@ func (f *Fetcher) NetworkStatus( func (f *Fetcher) NetworkStatusRetry( ctx context.Context, network *types.NetworkIdentifier, - metadata map[string]interface{}, + metadata json.RawMessage, ) (*types.NetworkStatusResponse, error) { backoffRetries := backoffRetries( f.retryElapsedTime, @@ -82,7 +83,7 @@ func (f *Fetcher) NetworkStatusRetry( // from the NetworList method. func (f *Fetcher) NetworkList( ctx context.Context, - metadata map[string]interface{}, + metadata json.RawMessage, ) (*types.NetworkListResponse, error) { networkList, _, err := f.rosettaClient.NetworkAPI.NetworkList( ctx, @@ -105,7 +106,7 @@ func (f *Fetcher) NetworkList( // with a specified number of retries and max elapsed time. func (f *Fetcher) NetworkListRetry( ctx context.Context, - metadata map[string]interface{}, + metadata json.RawMessage, ) (*types.NetworkListResponse, error) { backoffRetries := backoffRetries( f.retryElapsedTime, @@ -134,7 +135,7 @@ func (f *Fetcher) NetworkListRetry( func (f *Fetcher) NetworkOptions( ctx context.Context, network *types.NetworkIdentifier, - metadata map[string]interface{}, + metadata json.RawMessage, ) (*types.NetworkOptionsResponse, error) { NetworkOptions, _, err := f.rosettaClient.NetworkAPI.NetworkOptions( ctx, @@ -159,7 +160,7 @@ func (f *Fetcher) NetworkOptions( func (f *Fetcher) NetworkOptionsRetry( ctx context.Context, network *types.NetworkIdentifier, - metadata map[string]interface{}, + metadata json.RawMessage, ) (*types.NetworkOptionsResponse, error) { backoffRetries := backoffRetries( f.retryElapsedTime, diff --git a/templates/client/model.mustache b/templates/client/model.mustache index c972f58f..986fdddb 100644 --- a/templates/client/model.mustache +++ b/templates/client/model.mustache @@ -1,15 +1,7 @@ {{>partial_header}} package {{packageName}} {{#models}} -{{#imports}} -{{#-first}} -import ( -{{/-first}} - "{{import}}" -{{#-last}} -) -{{/-last}} -{{/imports}} +import "encoding/json" {{#model}} {{#isEnum}} // {{{classname}}} {{#description}}{{{.}}}{{/description}}{{^description}}the model '{{{classname}}}'{{/description}} diff --git a/types/account_balance_response.go b/types/account_balance_response.go index d5dd3387..7c762e04 100644 --- a/types/account_balance_response.go +++ b/types/account_balance_response.go @@ -16,6 +16,8 @@ package types +import "encoding/json" + // AccountBalanceResponse An AccountBalanceResponse is returned on the /account/balance endpoint. If // an account has a balance for each AccountIdentifier describing it (ex: an ERC-20 token balance on // a few smart contracts), an account balance request must be made with each AccountIdentifier. @@ -26,5 +28,5 @@ type AccountBalanceResponse struct { // Account-based blockchains that utilize a nonce or sequence number should include that number // in the metadata. This number could be unique to the identifier or global across the account // address. - Metadata map[string]interface{} `json:"metadata,omitempty"` + Metadata json.RawMessage `json:"metadata,omitempty"` } diff --git a/types/account_identifier.go b/types/account_identifier.go index f977ef3f..f7729c57 100644 --- a/types/account_identifier.go +++ b/types/account_identifier.go @@ -16,6 +16,8 @@ package types +import "encoding/json" + // AccountIdentifier The account_identifier uniquely identifies an account within a network. All // fields in the account_identifier are utilized to determine this uniqueness (including the // metadata field, if populated). @@ -26,5 +28,5 @@ type AccountIdentifier struct { SubAccount *SubAccountIdentifier `json:"sub_account,omitempty"` // Blockchains that utilize a username model (where the address is not a derivative of a // cryptographic public key) should specify the public key(s) owned by the address in metadata. - Metadata map[string]interface{} `json:"metadata,omitempty"` + Metadata json.RawMessage `json:"metadata,omitempty"` } diff --git a/types/amount.go b/types/amount.go index 89542922..541560de 100644 --- a/types/amount.go +++ b/types/amount.go @@ -16,12 +16,14 @@ package types +import "encoding/json" + // Amount Amount is some Value of a Currency. It is considered invalid to specify a Value without a // Currency. type Amount struct { // Value of the transaction in atomic units represented as an arbitrary-sized signed integer. // For example, 1 BTC would be represented by a value of 100000000. - Value string `json:"value"` - Currency *Currency `json:"currency"` - Metadata map[string]interface{} `json:"metadata,omitempty"` + Value string `json:"value"` + Currency *Currency `json:"currency"` + Metadata json.RawMessage `json:"metadata,omitempty"` } diff --git a/types/block.go b/types/block.go index 95dfb0c6..77143fbd 100644 --- a/types/block.go +++ b/types/block.go @@ -16,13 +16,15 @@ package types +import "encoding/json" + // Block Blocks contain an array of Transactions that occurred at a particular BlockIdentifier. type Block struct { BlockIdentifier *BlockIdentifier `json:"block_identifier"` ParentBlockIdentifier *BlockIdentifier `json:"parent_block_identifier"` // The timestamp of the block in milliseconds since the Unix Epoch. The timestamp is stored in // milliseconds because some blockchains produce blocks more often than once a second. - Timestamp int64 `json:"timestamp"` - Transactions []*Transaction `json:"transactions"` - Metadata map[string]interface{} `json:"metadata,omitempty"` + Timestamp int64 `json:"timestamp"` + Transactions []*Transaction `json:"transactions"` + Metadata json.RawMessage `json:"metadata,omitempty"` } diff --git a/types/construction_metadata_request.go b/types/construction_metadata_request.go index aa9126f7..c9dbca78 100644 --- a/types/construction_metadata_request.go +++ b/types/construction_metadata_request.go @@ -16,6 +16,8 @@ package types +import "encoding/json" + // ConstructionMetadataRequest A ConstructionMetadataRequest is utilized to get information required // to construct a transaction. The Options object used to specify which metadata to return is left // purposely unstructured to allow flexibility for implementers. @@ -26,5 +28,5 @@ type ConstructionMetadataRequest struct { // possible types of metadata for construction (which may require multiple node fetches), the // client can populate an options object to limit the metadata returned to only the subset // required. - Options map[string]interface{} `json:"options"` + Options json.RawMessage `json:"options"` } diff --git a/types/construction_metadata_response.go b/types/construction_metadata_response.go index 219f95d6..7c8eb9fc 100644 --- a/types/construction_metadata_response.go +++ b/types/construction_metadata_response.go @@ -16,9 +16,11 @@ package types +import "encoding/json" + // ConstructionMetadataResponse The ConstructionMetadataResponse returns network-specific metadata // used for transaction construction. It is likely that the client will not inspect this metadata // before passing it to a client SDK that uses it for construction. type ConstructionMetadataResponse struct { - Metadata map[string]interface{} `json:"metadata"` + Metadata json.RawMessage `json:"metadata"` } diff --git a/types/construction_submit_response.go b/types/construction_submit_response.go index ff3243bf..b93387ba 100644 --- a/types/construction_submit_response.go +++ b/types/construction_submit_response.go @@ -16,9 +16,11 @@ package types +import "encoding/json" + // ConstructionSubmitResponse A TransactionSubmitResponse contains the transaction_identifier of a // submitted transaction that was accepted into the mempool. type ConstructionSubmitResponse struct { TransactionIdentifier *TransactionIdentifier `json:"transaction_identifier"` - Metadata map[string]interface{} `json:"metadata,omitempty"` + Metadata json.RawMessage `json:"metadata,omitempty"` } diff --git a/types/currency.go b/types/currency.go index 26aafef1..352138d6 100644 --- a/types/currency.go +++ b/types/currency.go @@ -16,6 +16,8 @@ package types +import "encoding/json" + // Currency Currency is composed of a canonical Symbol and Decimals. This Decimals value is used to // convert an Amount.Value from atomic units (Satoshis) to standard units (Bitcoins). type Currency struct { @@ -27,5 +29,5 @@ type Currency struct { Decimals int32 `json:"decimals"` // Any additional information related to the currency itself. For example, it would be useful // to populate this object with the contract address of an ERC-20 token. - Metadata map[string]interface{} `json:"metadata,omitempty"` + Metadata json.RawMessage `json:"metadata,omitempty"` } diff --git a/types/mempool_transaction_response.go b/types/mempool_transaction_response.go index 7d8f036a..835d704a 100644 --- a/types/mempool_transaction_response.go +++ b/types/mempool_transaction_response.go @@ -16,10 +16,12 @@ package types +import "encoding/json" + // MempoolTransactionResponse A MempoolTransactionResponse contains an estimate of a mempool // transaction. It may not be possible to know the full impact of a transaction in the mempool (ex: // fee paid). type MempoolTransactionResponse struct { - Transaction *Transaction `json:"transaction"` - Metadata map[string]interface{} `json:"metadata,omitempty"` + Transaction *Transaction `json:"transaction"` + Metadata json.RawMessage `json:"metadata,omitempty"` } diff --git a/types/metadata_request.go b/types/metadata_request.go index 127bfab6..ebbf60e7 100644 --- a/types/metadata_request.go +++ b/types/metadata_request.go @@ -16,8 +16,10 @@ package types +import "encoding/json" + // MetadataRequest A MetadataRequest is utilized in any request where the only argument is optional // metadata. type MetadataRequest struct { - Metadata map[string]interface{} `json:"metadata,omitempty"` + Metadata json.RawMessage `json:"metadata,omitempty"` } diff --git a/types/network_request.go b/types/network_request.go index 2fe28e86..8f962a3a 100644 --- a/types/network_request.go +++ b/types/network_request.go @@ -16,9 +16,11 @@ package types +import "encoding/json" + // NetworkRequest A NetworkRequest is utilized to retrieve some data specific exclusively to a // NetworkIdentifier. type NetworkRequest struct { - NetworkIdentifier *NetworkIdentifier `json:"network_identifier"` - Metadata map[string]interface{} `json:"metadata,omitempty"` + NetworkIdentifier *NetworkIdentifier `json:"network_identifier"` + Metadata json.RawMessage `json:"metadata,omitempty"` } diff --git a/types/operation.go b/types/operation.go index 8c7dff31..91759946 100644 --- a/types/operation.go +++ b/types/operation.go @@ -16,6 +16,8 @@ package types +import "encoding/json" + // Operation Operations contain all balance-changing information within a transaction. They are // always one-sided (only affect 1 AccountIdentifier) and can succeed or fail independently from a // Transaction. @@ -34,8 +36,8 @@ type Operation struct { // because blockchains with smart contracts may have transactions that partially apply. // Blockchains with atomic transactions (all operations succeed or all operations fail) will // have the same status for each operation. - Status string `json:"status"` - Account *AccountIdentifier `json:"account,omitempty"` - Amount *Amount `json:"amount,omitempty"` - Metadata map[string]interface{} `json:"metadata,omitempty"` + Status string `json:"status"` + Account *AccountIdentifier `json:"account,omitempty"` + Amount *Amount `json:"amount,omitempty"` + Metadata json.RawMessage `json:"metadata,omitempty"` } diff --git a/types/peer.go b/types/peer.go index dde3878c..b6dd0ef1 100644 --- a/types/peer.go +++ b/types/peer.go @@ -16,8 +16,10 @@ package types +import "encoding/json" + // Peer A Peer is a representation of a node's peer. type Peer struct { - PeerID string `json:"peer_id"` - Metadata map[string]interface{} `json:"metadata,omitempty"` + PeerID string `json:"peer_id"` + Metadata json.RawMessage `json:"metadata,omitempty"` } diff --git a/types/sub_account_identifier.go b/types/sub_account_identifier.go index b110a46f..4ad8f8fd 100644 --- a/types/sub_account_identifier.go +++ b/types/sub_account_identifier.go @@ -16,6 +16,8 @@ package types +import "encoding/json" + // SubAccountIdentifier An account may have state specific to a contract address (ERC-20 token) // and/or a stake (delegated balance). The sub_account_identifier should specify which state (if // applicable) an account instantiation refers to. @@ -26,5 +28,5 @@ type SubAccountIdentifier struct { // If the SubAccount address is not sufficient to uniquely specify a SubAccount, any other // identifying information can be stored here. It is important to note that two SubAccounts // with identical addresses but differing metadata will not be considered equal by clients. - Metadata map[string]interface{} `json:"metadata,omitempty"` + Metadata json.RawMessage `json:"metadata,omitempty"` } diff --git a/types/sub_network_identifier.go b/types/sub_network_identifier.go index c9839149..385290c5 100644 --- a/types/sub_network_identifier.go +++ b/types/sub_network_identifier.go @@ -16,10 +16,12 @@ package types +import "encoding/json" + // SubNetworkIdentifier In blockchains with sharded state, the SubNetworkIdentifier is required to // query some object on a specific shard. This identifier is optional for all non-sharded // blockchains. type SubNetworkIdentifier struct { - Network string `json:"network"` - Metadata map[string]interface{} `json:"metadata,omitempty"` + Network string `json:"network"` + Metadata json.RawMessage `json:"metadata,omitempty"` } diff --git a/types/transaction.go b/types/transaction.go index ab918322..21a5320d 100644 --- a/types/transaction.go +++ b/types/transaction.go @@ -16,6 +16,8 @@ package types +import "encoding/json" + // Transaction Transactions contain an array of Operations that are attributable to the same // TransactionIdentifier. type Transaction struct { @@ -23,5 +25,5 @@ type Transaction struct { Operations []*Operation `json:"operations"` // Transactions that are related to other transactions (like a cross-shard transactioin) should // include the tranaction_identifier of these transactions in the metadata. - Metadata map[string]interface{} `json:"metadata,omitempty"` + Metadata json.RawMessage `json:"metadata,omitempty"` } diff --git a/types/utils.go b/types/utils.go index cd74e7e3..23f569c1 100644 --- a/types/utils.go +++ b/types/utils.go @@ -14,6 +14,15 @@ package types +import ( + "crypto/sha256" + "encoding/json" + "errors" + "fmt" + "log" + "math/big" +) + // ConstructPartialBlockIdentifier constructs a *PartialBlockIdentifier // from a *BlockIdentifier. // @@ -27,3 +36,164 @@ func ConstructPartialBlockIdentifier( Index: &blockIdentifier.Index, } } + +// hashBytes returns a hex-encoded sha256 hash of the provided +// byte slice. +func hashBytes(data []byte) string { + h := sha256.New() + _, err := h.Write(data) + if err != nil { + log.Fatal(fmt.Errorf("%w: unable to hash data %s", err, string(data))) + } + + return fmt.Sprintf("%x", h.Sum(nil)) +} + +// Hash returns a deterministic hash for any interface. +// This works because Golang's JSON marshaler sorts all map keys, recursively. +// Source: https://golang.org/pkg/encoding/json/#Marshal +// Inspiration: +// https://github.com/onsi/gomega/blob/c0be49994280db30b6b68390f67126d773bc5558/matchers/match_json_matcher.go#L16 +// +// It is important to note that any interface that is a slice +// or contains slices will not be equal if the slice ordering is +// different. +func Hash(i interface{}) string { + // Convert interface to JSON object (not necessarily ordered if struct + // contains json.RawMessage) + a, err := json.Marshal(i) + if err != nil { + log.Fatal(fmt.Errorf("%w: unable to marshal %+v", err, i)) + } + + // Convert JSON object to interface (all json.RawMessage converted to go types) + var b interface{} + if err := json.Unmarshal(a, &b); err != nil { + log.Fatal(fmt.Errorf("%w: unable to unmarshal %+v", err, a)) + } + + // Convert interface to JSON object (all map keys ordered) + c, err := json.Marshal(b) + if err != nil { + log.Fatal(fmt.Errorf("%w: unable to marshal %+v", err, b)) + } + + return hashBytes(c) +} + +// AddValues adds string amounts using +// big.Int. +func AddValues( + a string, + b string, +) (string, error) { + aVal, ok := new(big.Int).SetString(a, 10) + if !ok { + return "", fmt.Errorf("%s is not an integer", a) + } + + bVal, ok := new(big.Int).SetString(b, 10) + if !ok { + return "", fmt.Errorf("%s is not an integer", b) + } + + newVal := new(big.Int).Add(aVal, bVal) + return newVal.String(), nil +} + +// SubtractValues subtracts a-b using +// big.Int. +func SubtractValues( + a string, + b string, +) (string, error) { + aVal, ok := new(big.Int).SetString(a, 10) + if !ok { + return "", fmt.Errorf("%s is not an integer", a) + } + + bVal, ok := new(big.Int).SetString(b, 10) + if !ok { + return "", fmt.Errorf("%s is not an integer", b) + } + + newVal := new(big.Int).Sub(aVal, bVal) + return newVal.String(), nil +} + +// AccountString returns a human-readable representation of a +// *types.AccountIdentifier. +func AccountString(account *AccountIdentifier) (string, error) { + if account.SubAccount == nil { + return account.Address, nil + } + + if account.SubAccount.Metadata == nil { + return fmt.Sprintf( + "%s:%s", + account.Address, + account.SubAccount.Address, + ), nil + } + + var m map[string]interface{} + if err := json.Unmarshal(account.SubAccount.Metadata, &m); err != nil { + return "", err + } + + return fmt.Sprintf( + "%s:%s:%+v", + account.Address, + account.SubAccount.Address, + m, + ), nil +} + +// CurrencyString returns a human-readable representation +// of a *types.Currency. +func CurrencyString(currency *Currency) (string, error) { + if currency.Metadata == nil { + return fmt.Sprintf("%s:%d", currency.Symbol, currency.Decimals), nil + } + + var m map[string]interface{} + if err := json.Unmarshal(currency.Metadata, &m); err != nil { + return "", err + } + + return fmt.Sprintf( + "%s:%d:%+v", + currency.Symbol, + currency.Decimals, + m, + ), nil +} + +// PrettyPrintStruct marshals a struct to JSON and returns +// it as a string. +func PrettyPrintStruct(val interface{}) string { + prettyStruct, err := json.MarshalIndent( + val, + "", + " ", + ) + if err != nil { + log.Fatal(err) + } + + return string(prettyStruct) +} + +// JSONRawMessage returns a json.RawMessage given an interface. +func JSONRawMessage(i interface{}) (json.RawMessage, error) { + if i == nil { + return nil, errors.New("interface is nil") + } + + v, err := json.Marshal(i) + if err != nil { + return nil, err + } + + return json.RawMessage(v), nil +} diff --git a/types/utils_test.go b/types/utils_test.go index 260c29a2..127ac413 100644 --- a/types/utils_test.go +++ b/types/utils_test.go @@ -15,6 +15,8 @@ package types import ( + "encoding/json" + "errors" "testing" "github.com/stretchr/testify/assert" @@ -37,3 +39,349 @@ func TestConstructPartialBlockIdentifier(t *testing.T) { ConstructPartialBlockIdentifier(blockIdentifier), ) } + +func TestHash(t *testing.T) { + var tests = map[string][]interface{}{ + "simple": []interface{}{ + 1, + 1, + }, + "complex": []interface{}{ + map[string]interface{}{ + "a": "b", + "b": "c", + "c": "d", + "blahz": json.RawMessage(`{"test":6, "wha":{"sweet":3, "nice":true}, "neat0":"hello"}`), + "d": map[string]interface{}{ + "t": "p", + "e": 2, + "k": "l", + "blah": json.RawMessage(`{"test":2, "neat":"hello", "cool":{"sweet":3, "nice":true}}`), + }, + }, + map[string]interface{}{ + "b": "c", + "blahz": json.RawMessage(`{"wha":{"sweet":3, "nice":true},"test":6, "neat0":"hello"}`), + "a": "b", + "d": map[string]interface{}{ + "e": 2, + "k": "l", + "t": "p", + "blah": json.RawMessage(`{"test":2, "neat":"hello", "cool":{"nice":true, "sweet":3}}`), + }, + "c": "d", + }, + map[string]interface{}{ + "a": "b", + "d": map[string]interface{}{ + "k": "l", + "t": "p", + "blah": json.RawMessage(`{"test":2, "cool":{"nice":true, "sweet":3}, "neat":"hello"}`), + "e": 2, + }, + "c": "d", + "blahz": json.RawMessage(`{"wha":{"nice":true, "sweet":3},"test":6, "neat0":"hello"}`), + "b": "c", + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + var val string + for _, v := range test { + if val == "" { + val = Hash(v) + } else { + assert.Equal(t, val, Hash(v)) + } + } + }) + } +} + +func TestAddValues(t *testing.T) { + var tests = map[string]struct { + a string + b string + result string + err error + }{ + "simple": { + a: "1", + b: "1", + result: "2", + err: nil, + }, + "large": { + a: "1000000000000000000000000", + b: "100000000000000000000000000000000", + result: "100000001000000000000000000000000", + err: nil, + }, + "decimal": { + a: "10000000000000000000000.01", + b: "100000000000000000000000000000000", + result: "", + err: errors.New("10000000000000000000000.01 is not an integer"), + }, + "negative": { + a: "-13213", + b: "12332", + result: "-881", + err: nil, + }, + "invalid number": { + a: "-13213", + b: "hello", + result: "", + err: errors.New("hello is not an integer"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + result, err := AddValues(test.a, test.b) + assert.Equal(t, test.err, err) + assert.Equal(t, test.result, result) + }) + } +} + +func TestSubtractValues(t *testing.T) { + var tests = map[string]struct { + a string + b string + result string + err error + }{ + "simple": { + a: "1", + b: "1", + result: "0", + err: nil, + }, + "large": { + a: "1000000000000000000000000", + b: "100000000000000000000000000000000", + result: "-99999999000000000000000000000000", + err: nil, + }, + "decimal": { + a: "10000000000000000000000.01", + b: "100000000000000000000000000000000", + result: "", + err: errors.New("10000000000000000000000.01 is not an integer"), + }, + "negative": { + a: "-13213", + b: "12332", + result: "-25545", + err: nil, + }, + "invalid number": { + a: "-13213", + b: "hello", + result: "", + err: errors.New("hello is not an integer"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + result, err := SubtractValues(test.a, test.b) + assert.Equal(t, test.err, err) + assert.Equal(t, test.result, result) + }) + } +} + +func TestGetAccountString(t *testing.T) { + var tests = map[string]struct { + account *AccountIdentifier + err bool + key string + }{ + "simple account": { + account: &AccountIdentifier{ + Address: "hello", + }, + key: "hello", + }, + "subaccount": { + account: &AccountIdentifier{ + Address: "hello", + SubAccount: &SubAccountIdentifier{ + Address: "stake", + }, + }, + key: "hello:stake", + }, + "subaccount with string metadata": { + account: &AccountIdentifier{ + Address: "hello", + SubAccount: &SubAccountIdentifier{ + Address: "stake", + Metadata: json.RawMessage(`{ + "cool": "neat" + }`), + }, + }, + key: "hello:stake:map[cool:neat]", + }, + "subaccount with number metadata": { + account: &AccountIdentifier{ + Address: "hello", + SubAccount: &SubAccountIdentifier{ + Address: "stake", + Metadata: json.RawMessage(`{ + "cool": 1 + }`), + }, + }, + key: "hello:stake:map[cool:1]", + }, + "subaccount with complex metadata": { + account: &AccountIdentifier{ + Address: "hello", + SubAccount: &SubAccountIdentifier{ + Address: "stake", + Metadata: json.RawMessage(`{ + "cool": 1, + "awesome": "neat" + }`), + }, + }, + key: "hello:stake:map[awesome:neat cool:1]", + }, + "subaccount with invalid metadata": { + account: &AccountIdentifier{ + Address: "hello", + SubAccount: &SubAccountIdentifier{ + Address: "stake", + Metadata: json.RawMessage(`stuff`), + }, + }, + err: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + accountString, err := AccountString(test.account) + if test.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, test.key, accountString) + }) + } +} + +func TestCurrencyString(t *testing.T) { + var tests = map[string]struct { + currency *Currency + key string + err bool + }{ + "simple currency": { + currency: &Currency{ + Symbol: "BTC", + Decimals: 8, + }, + key: "BTC:8", + }, + "currency with string metadata": { + currency: &Currency{ + Symbol: "BTC", + Decimals: 8, + Metadata: json.RawMessage(`{ + "issuer": "satoshi" + }`), + }, + key: "BTC:8:map[issuer:satoshi]", + }, + "currency with number metadata": { + currency: &Currency{ + Symbol: "BTC", + Decimals: 8, + Metadata: json.RawMessage(`{ + "issuer": 1 + }`), + }, + key: "BTC:8:map[issuer:1]", + }, + "currency with complex metadata": { + currency: &Currency{ + Symbol: "BTC", + Decimals: 8, + Metadata: json.RawMessage(`{ + "issuer": "satoshi", + "count": 10 + }`), + }, + key: "BTC:8:map[count:10 issuer:satoshi]", + }, + "currency with invalid metadata": { + currency: &Currency{ + Symbol: "BTC", + Decimals: 8, + Metadata: json.RawMessage(`stuff`), + }, + err: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + currencyString, err := CurrencyString(test.currency) + if test.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, test.key, currencyString) + }) + } +} + +func TestJSONRawMessage(t *testing.T) { + var tests = map[string]struct { + i interface{} + result json.RawMessage + err bool + }{ + "simple": { + i: &Currency{ + Symbol: "BTC", + Decimals: 8, + }, + result: json.RawMessage(`{"symbol":"BTC","decimals":8}`), + }, + "nested": { + i: &Currency{ + Symbol: "BTC", + Decimals: 8, + Metadata: json.RawMessage(`{"issuer":"satoshi"}`), + }, + result: json.RawMessage(`{"symbol":"BTC","decimals":8,"metadata":{"issuer":"satoshi"}}`), + }, + "nil": { + i: nil, + err: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + m, err := JSONRawMessage(test.i) + assert.Equal(t, test.result, m) + if test.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/types/version.go b/types/version.go index e7184306..6db88078 100644 --- a/types/version.go +++ b/types/version.go @@ -16,6 +16,8 @@ package types +import "encoding/json" + // Version The Version object is utilized to inform the client of the versions of different // components of the Rosetta implementation. type Version struct { @@ -30,5 +32,5 @@ type Version struct { MiddlewareVersion *string `json:"middleware_version,omitempty"` // Any other information that may be useful about versioning of dependent services should be // returned here. - Metadata map[string]interface{} `json:"metadata,omitempty"` + Metadata json.RawMessage `json:"metadata,omitempty"` }