From 4dde2d16c368659692aff6e592269af8f915b767 Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Tue, 21 Apr 2020 13:52:04 -0700 Subject: [PATCH 01/13] Remove context from initialization --- asserter/asserter.go | 20 ++++++++++++++++---- asserter/asserter_test.go | 2 -- asserter/block_test.go | 3 --- examples/client/main.go | 1 - examples/fetcher/main.go | 1 - fetcher/fetcher.go | 2 -- 6 files changed, 16 insertions(+), 13 deletions(-) diff --git a/asserter/asserter.go b/asserter/asserter.go index 3cd9029e..de60aa33 100644 --- a/asserter/asserter.go +++ b/asserter/asserter.go @@ -15,7 +15,6 @@ package asserter import ( - "context" "errors" "github.com/coinbase/rosetta-sdk-go/types" @@ -40,7 +39,6 @@ type Asserter struct { // from a NetworkStatusResponse and // NetworkOptionsResponse. func NewWithResponses( - ctx context.Context, networkStatus *types.NetworkStatusResponse, networkOptions *types.NetworkOptionsResponse, ) (*Asserter, error) { @@ -53,7 +51,6 @@ func NewWithResponses( } return NewWithOptions( - ctx, networkStatus.GenesisBlockIdentifier, networkOptions.Allow.OperationTypes, networkOptions.Allow.OperationStatuses, @@ -61,11 +58,26 @@ func NewWithResponses( ), nil } +// NewWithFile constructs a new Asserter using a specification +// file instead of responses. This can be useful for running reliable +// systems that error when updates to the server (more error types, +// more operations, etc.) significantly change how to parse the chain. +func NewWithFile( + filePath string, +) (*Asserter, error) { + // load file + + // parse items + + // run NewWithOptions + + return nil, errors.New("not implemented") +} + // NewWithOptions constructs a new Asserter using the provided // arguments instead of using a NetworkStatusResponse and a // NetworkOptionsResponse. func NewWithOptions( - ctx context.Context, genesisBlockIdentifier *types.BlockIdentifier, operationTypes []string, operationStatuses []*types.OperationStatus, diff --git a/asserter/asserter_test.go b/asserter/asserter_test.go index 3f9d861b..620a0e98 100644 --- a/asserter/asserter_test.go +++ b/asserter/asserter_test.go @@ -15,7 +15,6 @@ package asserter import ( - "context" "errors" "testing" @@ -132,7 +131,6 @@ func TestNewWithResponses(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { asserter, err := NewWithResponses( - context.Background(), test.networkStatus, test.networkOptions, ) diff --git a/asserter/block_test.go b/asserter/block_test.go index c7f12e50..26c3ccf6 100644 --- a/asserter/block_test.go +++ b/asserter/block_test.go @@ -15,7 +15,6 @@ package asserter import ( - "context" "errors" "fmt" "testing" @@ -385,7 +384,6 @@ func TestOperation(t *testing.T) { for name, test := range tests { asserter, err := NewWithResponses( - context.Background(), &types.NetworkStatusResponse{ GenesisBlockIdentifier: &types.BlockIdentifier{ Index: 0, @@ -556,7 +554,6 @@ func TestBlock(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { asserter, err := NewWithResponses( - context.Background(), &types.NetworkStatusResponse{ GenesisBlockIdentifier: &types.BlockIdentifier{ Index: test.genesisIndex, diff --git a/examples/client/main.go b/examples/client/main.go index 2c26d287..eef40dcb 100644 --- a/examples/client/main.go +++ b/examples/client/main.go @@ -138,7 +138,6 @@ func main() { // This will be used later to assert that a fetched block is // valid. asserter, err := asserter.NewWithResponses( - ctx, networkStatus, networkOptions, ) diff --git a/examples/fetcher/main.go b/examples/fetcher/main.go index 61d42e3d..8070fca9 100644 --- a/examples/fetcher/main.go +++ b/examples/fetcher/main.go @@ -33,7 +33,6 @@ func main() { // Step 1: Create a new fetcher newFetcher := fetcher.New( - ctx, serverURL, ) diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go index a97e5a9f..2234bd12 100644 --- a/fetcher/fetcher.go +++ b/fetcher/fetcher.go @@ -70,7 +70,6 @@ type Fetcher struct { // New constructs a new Fetcher with provided options. func New( - ctx context.Context, serverAddress string, options ...Option, ) *Fetcher { @@ -153,7 +152,6 @@ func (f *Fetcher) InitializeAsserter( } f.Asserter, err = asserter.NewWithResponses( - ctx, networkStatus, networkOptions, ) From 3543397caf122a2b0a3b9cfcc32b65574a38a5ec Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Tue, 21 Apr 2020 13:58:25 -0700 Subject: [PATCH 02/13] Lock codegen at version --- codegen.sh | 7 +++++-- fetcher/block.go | 24 ++++++------------------ 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/codegen.sh b/codegen.sh index 3d63e13e..8aa6dffa 100755 --- a/codegen.sh +++ b/codegen.sh @@ -53,7 +53,9 @@ done rm -rf tmp; # Generate client + types code -docker run --user "$(id -u):$(id -g)" --rm -v "${PWD}":/local openapitools/openapi-generator-cli generate \ +GENERATOR_VERSION='v4.3.0' +docker run --user "$(id -u):$(id -g)" --rm -v "${PWD}":/local \ + openapitools/openapi-generator-cli:${GENERATOR_VERSION} generate \ -i /local/templates/spec.json \ -g go \ -t /local/templates/client \ @@ -76,7 +78,8 @@ mv client_tmp/* client; rm -rf client_tmp; # Add server code -docker run --user "$(id -u):$(id -g)" --rm -v "${PWD}":/local openapitools/openapi-generator-cli generate \ +docker run --user "$(id -u):$(id -g)" --rm -v "${PWD}":/local \ + openapitools/openapi-generator-cli:${GENERATOR_VERSION} generate \ -i /local/templates/spec.json \ -g go-server \ -t /local/templates/server \ diff --git a/fetcher/block.go b/fetcher/block.go index f9079306..ca2281ce 100644 --- a/fetcher/block.go +++ b/fetcher/block.go @@ -18,7 +18,6 @@ import ( "context" "errors" "fmt" - "time" "github.com/coinbase/rosetta-sdk-go/asserter" "github.com/coinbase/rosetta-sdk-go/types" @@ -227,13 +226,6 @@ func (f *Fetcher) BlockRetry( return nil, errors.New("exhausted retries for block") } -// BlockAndLatency is utilized to track the latency -// of concurrent block fetches. -type BlockAndLatency struct { - Block *types.Block - Latency float64 -} - // addIndicies appends a range of indicies (from // startIndex to endIndex, inclusive) to the // blockIndicies channel. When all indicies are added, @@ -263,10 +255,9 @@ func (f *Fetcher) fetchChannelBlocks( ctx context.Context, network *types.NetworkIdentifier, blockIndicies chan int64, - results chan *BlockAndLatency, + results chan *types.Block, ) error { for b := range blockIndicies { - start := time.Now() block, err := f.BlockRetry( ctx, network, @@ -279,10 +270,7 @@ func (f *Fetcher) fetchChannelBlocks( } select { - case results <- &BlockAndLatency{ - Block: block, - Latency: time.Since(start).Seconds(), - }: + case results <- block: case <-ctx.Done(): return ctx.Err() } @@ -301,9 +289,9 @@ func (f *Fetcher) BlockRange( network *types.NetworkIdentifier, startIndex int64, endIndex int64, -) (map[int64]*BlockAndLatency, error) { +) (map[int64]*types.Block, error) { blockIndicies := make(chan int64) - results := make(chan *BlockAndLatency) + results := make(chan *types.Block) g, ctx := errgroup.WithContext(ctx) g.Go(func() error { return addBlockIndicies(ctx, blockIndicies, startIndex, endIndex) @@ -322,9 +310,9 @@ func (f *Fetcher) BlockRange( close(results) }() - m := make(map[int64]*BlockAndLatency) + m := make(map[int64]*types.Block) for b := range results { - m[b.Block.BlockIdentifier.Index] = b + m[b.BlockIdentifier.Index] = b } err := g.Wait() From ff81499939b8be39e0d19c78529974ee5febf154 Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Tue, 21 Apr 2020 14:02:34 -0700 Subject: [PATCH 03/13] Remove map pointers --- asserter/account_test.go | 8 ++++---- asserter/construction_test.go | 2 +- asserter/request_test.go | 4 ++-- codegen.sh | 3 +++ fetcher/account.go | 4 ++-- fetcher/construction.go | 6 +++--- fetcher/mempool.go | 2 +- fetcher/network.go | 12 ++++++------ types/account_balance_response.go | 2 +- types/account_identifier.go | 2 +- types/amount.go | 6 +++--- types/block.go | 6 +++--- types/construction_metadata_request.go | 2 +- types/construction_metadata_response.go | 2 +- types/construction_submit_response.go | 4 ++-- types/currency.go | 2 +- types/mempool_transaction_response.go | 4 ++-- types/metadata_request.go | 2 +- types/network_request.go | 4 ++-- types/operation.go | 8 ++++---- types/peer.go | 4 ++-- types/sub_account_identifier.go | 2 +- types/sub_network_identifier.go | 4 ++-- types/transaction.go | 2 +- types/version.go | 2 +- 25 files changed, 51 insertions(+), 48 deletions(-) diff --git a/asserter/account_test.go b/asserter/account_test.go index 611ad772..12994fe8 100644 --- a/asserter/account_test.go +++ b/asserter/account_test.go @@ -48,7 +48,7 @@ func TestContainsCurrency(t *testing.T) { { Symbol: "BTC", Decimals: 8, - Metadata: &map[string]interface{}{ + Metadata: map[string]interface{}{ "blah": "hello", }, }, @@ -56,7 +56,7 @@ func TestContainsCurrency(t *testing.T) { currency: &types.Currency{ Symbol: "BTC", Decimals: 8, - Metadata: &map[string]interface{}{ + Metadata: map[string]interface{}{ "blah": "hello", }, }, @@ -101,7 +101,7 @@ func TestContainsCurrency(t *testing.T) { { Symbol: "BTC", Decimals: 8, - Metadata: &map[string]interface{}{ + Metadata: map[string]interface{}{ "blah": "hello", }, }, @@ -109,7 +109,7 @@ func TestContainsCurrency(t *testing.T) { currency: &types.Currency{ Symbol: "BTC", Decimals: 8, - Metadata: &map[string]interface{}{ + Metadata: map[string]interface{}{ "blah": "bye", }, }, diff --git a/asserter/construction_test.go b/asserter/construction_test.go index a585e265..120ec86a 100644 --- a/asserter/construction_test.go +++ b/asserter/construction_test.go @@ -30,7 +30,7 @@ func TestConstructionMetadata(t *testing.T) { }{ "valid response": { response: &types.ConstructionMetadataResponse{ - Metadata: &map[string]interface{}{}, + Metadata: map[string]interface{}{}, }, err: nil, }, diff --git a/asserter/request_test.go b/asserter/request_test.go index 85a33c99..04d70057 100644 --- a/asserter/request_test.go +++ b/asserter/request_test.go @@ -213,7 +213,7 @@ func TestConstructionMetadataRequest(t *testing.T) { "valid request": { request: &types.ConstructionMetadataRequest{ NetworkIdentifier: validNetworkIdentifier, - Options: &map[string]interface{}{}, + Options: map[string]interface{}{}, }, err: nil, }, @@ -223,7 +223,7 @@ func TestConstructionMetadataRequest(t *testing.T) { }, "missing network": { request: &types.ConstructionMetadataRequest{ - Options: &map[string]interface{}{}, + Options: map[string]interface{}{}, }, err: errors.New("NetworkIdentifier is nil"), }, diff --git a/codegen.sh b/codegen.sh index 8aa6dffa..7408345a 100755 --- a/codegen.sh +++ b/codegen.sh @@ -118,6 +118,9 @@ sed "${SED_IFLAG[@]}" 's/<\/code>//g' client/* server/*; # Fix slice containing pointers sed "${SED_IFLAG[@]}" 's/\*\[\]/\[\]\*/g' client/* server/*; +# Fix map pointers +sed "${SED_IFLAG[@]}" 's/\*map/map/g' client/* server/*; + # Move model files to types/ mv client/model_*.go types/; for file in types/model_*.go; do diff --git a/fetcher/account.go b/fetcher/account.go index e51cc29e..3d60e1b6 100644 --- a/fetcher/account.go +++ b/fetcher/account.go @@ -32,7 +32,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, map[string]interface{}, error) { response, _, err := f.rosettaClient.AccountAPI.AccountBalance(ctx, &types.AccountBalanceRequest{ NetworkIdentifier: network, @@ -65,7 +65,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, map[string]interface{}, error) { backoffRetries := backoffRetries( f.retryElapsedTime, f.maxRetries, diff --git a/fetcher/construction.go b/fetcher/construction.go index d7207a05..16b24980 100644 --- a/fetcher/construction.go +++ b/fetcher/construction.go @@ -27,8 +27,8 @@ import ( func (f *Fetcher) ConstructionMetadata( ctx context.Context, network *types.NetworkIdentifier, - options *map[string]interface{}, -) (*map[string]interface{}, error) { + options map[string]interface{}, +) (map[string]interface{}, error) { metadata, _, err := f.rosettaClient.ConstructionAPI.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{ NetworkIdentifier: network, @@ -52,7 +52,7 @@ func (f *Fetcher) ConstructionSubmit( ctx context.Context, network *types.NetworkIdentifier, signedTransaction string, -) (*types.TransactionIdentifier, *map[string]interface{}, error) { +) (*types.TransactionIdentifier, map[string]interface{}, error) { submitResponse, _, err := f.rosettaClient.ConstructionAPI.ConstructionSubmit( ctx, &types.ConstructionSubmitRequest{ diff --git a/fetcher/mempool.go b/fetcher/mempool.go index ec83bbdf..17244b34 100644 --- a/fetcher/mempool.go +++ b/fetcher/mempool.go @@ -52,7 +52,7 @@ func (f *Fetcher) MempoolTransaction( ctx context.Context, network *types.NetworkIdentifier, transaction *types.TransactionIdentifier, -) (*types.Transaction, *map[string]interface{}, error) { +) (*types.Transaction, map[string]interface{}, error) { response, _, err := f.rosettaClient.MempoolAPI.MempoolTransaction( ctx, &types.MempoolTransactionRequest{ diff --git a/fetcher/network.go b/fetcher/network.go index 86d7310b..9b94a332 100644 --- a/fetcher/network.go +++ b/fetcher/network.go @@ -28,7 +28,7 @@ import ( func (f *Fetcher) NetworkStatus( ctx context.Context, network *types.NetworkIdentifier, - metadata *map[string]interface{}, + metadata map[string]interface{}, ) (*types.NetworkStatusResponse, error) { networkStatus, _, err := f.rosettaClient.NetworkAPI.NetworkStatus( ctx, @@ -53,7 +53,7 @@ func (f *Fetcher) NetworkStatus( func (f *Fetcher) NetworkStatusRetry( ctx context.Context, network *types.NetworkIdentifier, - metadata *map[string]interface{}, + metadata map[string]interface{}, ) (*types.NetworkStatusResponse, error) { backoffRetries := backoffRetries( f.retryElapsedTime, @@ -82,7 +82,7 @@ func (f *Fetcher) NetworkStatusRetry( // from the NetworList method. func (f *Fetcher) NetworkList( ctx context.Context, - metadata *map[string]interface{}, + metadata map[string]interface{}, ) (*types.NetworkListResponse, error) { networkList, _, err := f.rosettaClient.NetworkAPI.NetworkList( ctx, @@ -105,7 +105,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 map[string]interface{}, ) (*types.NetworkListResponse, error) { backoffRetries := backoffRetries( f.retryElapsedTime, @@ -134,7 +134,7 @@ func (f *Fetcher) NetworkListRetry( func (f *Fetcher) NetworkOptions( ctx context.Context, network *types.NetworkIdentifier, - metadata *map[string]interface{}, + metadata map[string]interface{}, ) (*types.NetworkOptionsResponse, error) { NetworkOptions, _, err := f.rosettaClient.NetworkAPI.NetworkOptions( ctx, @@ -159,7 +159,7 @@ func (f *Fetcher) NetworkOptions( func (f *Fetcher) NetworkOptionsRetry( ctx context.Context, network *types.NetworkIdentifier, - metadata *map[string]interface{}, + metadata map[string]interface{}, ) (*types.NetworkOptionsResponse, error) { backoffRetries := backoffRetries( f.retryElapsedTime, diff --git a/types/account_balance_response.go b/types/account_balance_response.go index ccd875d8..d5dd3387 100644 --- a/types/account_balance_response.go +++ b/types/account_balance_response.go @@ -26,5 +26,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 map[string]interface{} `json:"metadata,omitempty"` } diff --git a/types/account_identifier.go b/types/account_identifier.go index f2d9909c..f977ef3f 100644 --- a/types/account_identifier.go +++ b/types/account_identifier.go @@ -26,5 +26,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 map[string]interface{} `json:"metadata,omitempty"` } diff --git a/types/amount.go b/types/amount.go index bdba0666..89542922 100644 --- a/types/amount.go +++ b/types/amount.go @@ -21,7 +21,7 @@ package types 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 map[string]interface{} `json:"metadata,omitempty"` } diff --git a/types/block.go b/types/block.go index ca3506fc..95dfb0c6 100644 --- a/types/block.go +++ b/types/block.go @@ -22,7 +22,7 @@ type Block struct { 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 map[string]interface{} `json:"metadata,omitempty"` } diff --git a/types/construction_metadata_request.go b/types/construction_metadata_request.go index b1f12b6d..aa9126f7 100644 --- a/types/construction_metadata_request.go +++ b/types/construction_metadata_request.go @@ -26,5 +26,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 map[string]interface{} `json:"options"` } diff --git a/types/construction_metadata_response.go b/types/construction_metadata_response.go index bac07296..219f95d6 100644 --- a/types/construction_metadata_response.go +++ b/types/construction_metadata_response.go @@ -20,5 +20,5 @@ package types // 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 map[string]interface{} `json:"metadata"` } diff --git a/types/construction_submit_response.go b/types/construction_submit_response.go index a0608065..ff3243bf 100644 --- a/types/construction_submit_response.go +++ b/types/construction_submit_response.go @@ -19,6 +19,6 @@ package types // 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"` + TransactionIdentifier *TransactionIdentifier `json:"transaction_identifier"` + Metadata map[string]interface{} `json:"metadata,omitempty"` } diff --git a/types/currency.go b/types/currency.go index 4da5875c..26aafef1 100644 --- a/types/currency.go +++ b/types/currency.go @@ -27,5 +27,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 map[string]interface{} `json:"metadata,omitempty"` } diff --git a/types/mempool_transaction_response.go b/types/mempool_transaction_response.go index 565f4aaa..7d8f036a 100644 --- a/types/mempool_transaction_response.go +++ b/types/mempool_transaction_response.go @@ -20,6 +20,6 @@ package types // 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 map[string]interface{} `json:"metadata,omitempty"` } diff --git a/types/metadata_request.go b/types/metadata_request.go index e900249e..127bfab6 100644 --- a/types/metadata_request.go +++ b/types/metadata_request.go @@ -19,5 +19,5 @@ package types // 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 map[string]interface{} `json:"metadata,omitempty"` } diff --git a/types/network_request.go b/types/network_request.go index 6ad034d2..2fe28e86 100644 --- a/types/network_request.go +++ b/types/network_request.go @@ -19,6 +19,6 @@ package types // 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 map[string]interface{} `json:"metadata,omitempty"` } diff --git a/types/operation.go b/types/operation.go index 6a26638c..8c7dff31 100644 --- a/types/operation.go +++ b/types/operation.go @@ -34,8 +34,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 map[string]interface{} `json:"metadata,omitempty"` } diff --git a/types/peer.go b/types/peer.go index c5935369..dde3878c 100644 --- a/types/peer.go +++ b/types/peer.go @@ -18,6 +18,6 @@ package types // 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 map[string]interface{} `json:"metadata,omitempty"` } diff --git a/types/sub_account_identifier.go b/types/sub_account_identifier.go index 977add8d..b110a46f 100644 --- a/types/sub_account_identifier.go +++ b/types/sub_account_identifier.go @@ -26,5 +26,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 map[string]interface{} `json:"metadata,omitempty"` } diff --git a/types/sub_network_identifier.go b/types/sub_network_identifier.go index 86033533..c9839149 100644 --- a/types/sub_network_identifier.go +++ b/types/sub_network_identifier.go @@ -20,6 +20,6 @@ package types // 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 map[string]interface{} `json:"metadata,omitempty"` } diff --git a/types/transaction.go b/types/transaction.go index f4970e74..ab918322 100644 --- a/types/transaction.go +++ b/types/transaction.go @@ -23,5 +23,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 map[string]interface{} `json:"metadata,omitempty"` } diff --git a/types/version.go b/types/version.go index 7c143998..e7184306 100644 --- a/types/version.go +++ b/types/version.go @@ -30,5 +30,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 map[string]interface{} `json:"metadata,omitempty"` } From 22dbfe40dce669556ffe718482d9c5153e5e921e Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Tue, 21 Apr 2020 14:19:44 -0700 Subject: [PATCH 04/13] Add method on asserter to get configuration --- asserter/asserter.go | 34 +++++++++++++++++++++++++++++++--- asserter/asserter_test.go | 4 ++++ asserter/block.go | 2 +- asserter/block_test.go | 8 ++++++++ examples/client/main.go | 1 + fetcher/fetcher.go | 1 + 6 files changed, 46 insertions(+), 4 deletions(-) diff --git a/asserter/asserter.go b/asserter/asserter.go index de60aa33..2faf8cce 100644 --- a/asserter/asserter.go +++ b/asserter/asserter.go @@ -15,6 +15,7 @@ package asserter import ( + "context" "errors" "github.com/coinbase/rosetta-sdk-go/types" @@ -29,19 +30,25 @@ var ( // Asserter contains all logic to perform static // validation on Rosetta Server responses. type Asserter struct { + network *types.NetworkIdentifier operationTypes []string operationStatusMap map[string]bool errorTypeMap map[int32]*types.Error - genesisIndex int64 + genesisBlock *types.BlockIdentifier } // NewWithResponses constructs a new Asserter // from a NetworkStatusResponse and // NetworkOptionsResponse. func NewWithResponses( + network *types.NetworkIdentifier, networkStatus *types.NetworkStatusResponse, networkOptions *types.NetworkOptionsResponse, ) (*Asserter, error) { + if err := NetworkIdentifier(network); err != nil { + return nil, err + } + if err := NetworkStatusResponse(networkStatus); err != nil { return nil, err } @@ -51,6 +58,7 @@ func NewWithResponses( } return NewWithOptions( + network, networkStatus.GenesisBlockIdentifier, networkOptions.Allow.OperationTypes, networkOptions.Allow.OperationStatuses, @@ -76,16 +84,19 @@ func NewWithFile( // NewWithOptions constructs a new Asserter using the provided // arguments instead of using a NetworkStatusResponse and a -// NetworkOptionsResponse. +// NetworkOptionsResponse. NewWithOptions does not check the +// correctness of inputs. func NewWithOptions( + network *types.NetworkIdentifier, genesisBlockIdentifier *types.BlockIdentifier, operationTypes []string, operationStatuses []*types.OperationStatus, errors []*types.Error, ) *Asserter { asserter := &Asserter{ + network: network, operationTypes: operationTypes, - genesisIndex: genesisBlockIdentifier.Index, + genesisBlock: genesisBlockIdentifier, } asserter.operationStatusMap = map[string]bool{} @@ -100,3 +111,20 @@ func NewWithOptions( return asserter } + +func (a *Asserter) Configuration(ctx context.Context) (*types.NetworkIdentifier, *types.BlockIdentifier, []string, []*types.OperationStatus, []*types.Error) { + operationStatuses := []*types.OperationStatus{} + for k, v := range a.operationStatusMap { + operationStatuses = append(operationStatuses, &types.OperationStatus{ + Status: k, + Successful: v, + }) + } + + errors := []*types.Error{} + for _, v := range a.errorTypeMap { + errors = append(errors, v) + } + + return a.network, a.genesisBlock, a.operationTypes, operationStatuses, errors +} diff --git a/asserter/asserter_test.go b/asserter/asserter_test.go index 620a0e98..c64f5e20 100644 --- a/asserter/asserter_test.go +++ b/asserter/asserter_test.go @@ -131,6 +131,10 @@ func TestNewWithResponses(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { asserter, err := NewWithResponses( + &types.NetworkIdentifier{ + Blockchain: "hello", + Network: "world", + }, test.networkStatus, test.networkOptions, ) diff --git a/asserter/block.go b/asserter/block.go index 384c6537..499b6219 100644 --- a/asserter/block.go +++ b/asserter/block.go @@ -285,7 +285,7 @@ func (a *Asserter) Block( // Only apply some assertions if the block index is not the // genesis index. - if a.genesisIndex != block.BlockIdentifier.Index { + if a.genesisBlock.Index != block.BlockIdentifier.Index { if block.BlockIdentifier.Hash == block.ParentBlockIdentifier.Hash { return errors.New("BlockIdentifier.Hash == ParentBlockIdentifier.Hash") } diff --git a/asserter/block_test.go b/asserter/block_test.go index 26c3ccf6..2a4d4a59 100644 --- a/asserter/block_test.go +++ b/asserter/block_test.go @@ -384,6 +384,10 @@ func TestOperation(t *testing.T) { for name, test := range tests { asserter, err := NewWithResponses( + &types.NetworkIdentifier{ + Blockchain: "hello", + Network: "world", + }, &types.NetworkStatusResponse{ GenesisBlockIdentifier: &types.BlockIdentifier{ Index: 0, @@ -554,6 +558,10 @@ func TestBlock(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { asserter, err := NewWithResponses( + &types.NetworkIdentifier{ + Blockchain: "hello", + Network: "world", + }, &types.NetworkStatusResponse{ GenesisBlockIdentifier: &types.BlockIdentifier{ Index: test.genesisIndex, diff --git a/examples/client/main.go b/examples/client/main.go index eef40dcb..1dafa272 100644 --- a/examples/client/main.go +++ b/examples/client/main.go @@ -138,6 +138,7 @@ func main() { // This will be used later to assert that a fetched block is // valid. asserter, err := asserter.NewWithResponses( + primaryNetwork, networkStatus, networkOptions, ) diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go index 2234bd12..3ddb5ac2 100644 --- a/fetcher/fetcher.go +++ b/fetcher/fetcher.go @@ -152,6 +152,7 @@ func (f *Fetcher) InitializeAsserter( } f.Asserter, err = asserter.NewWithResponses( + primaryNetwork, networkStatus, networkOptions, ) From ef50ee9b0c39cb701939fd8af22a6f372f68ff83 Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Tue, 21 Apr 2020 15:45:50 -0700 Subject: [PATCH 05/13] Assert arguments to NewWithOptions --- asserter/asserter.go | 42 ++++++++++++++++++++++---- asserter/block.go | 70 +++++++++++++++++++++----------------------- 2 files changed, 69 insertions(+), 43 deletions(-) diff --git a/asserter/asserter.go b/asserter/asserter.go index 2faf8cce..7fff99e1 100644 --- a/asserter/asserter.go +++ b/asserter/asserter.go @@ -15,8 +15,8 @@ package asserter import ( - "context" "errors" + "fmt" "github.com/coinbase/rosetta-sdk-go/types" ) @@ -63,7 +63,7 @@ func NewWithResponses( networkOptions.Allow.OperationTypes, networkOptions.Allow.OperationStatuses, networkOptions.Allow.Errors, - ), nil + ) } // NewWithFile constructs a new Asserter using a specification @@ -92,7 +92,15 @@ func NewWithOptions( operationTypes []string, operationStatuses []*types.OperationStatus, errors []*types.Error, -) *Asserter { +) (*Asserter, error) { + if err := NetworkIdentifier(network); err != nil { + return nil, err + } + + if err := BlockIdentifier(genesisBlockIdentifier); err != nil { + return nil, err + } + asserter := &Asserter{ network: network, operationTypes: operationTypes, @@ -109,10 +117,16 @@ func NewWithOptions( asserter.errorTypeMap[err.Code] = err } - return asserter + return asserter, nil } -func (a *Asserter) Configuration(ctx context.Context) (*types.NetworkIdentifier, *types.BlockIdentifier, []string, []*types.OperationStatus, []*types.Error) { +// Configuration returns all variables currently set in an Asserter. +// This function will error if it is called on an uninitialized asserter. +func (a *Asserter) Configuration() (*types.NetworkIdentifier, *types.BlockIdentifier, []string, []*types.OperationStatus, []*types.Error, error) { + if a == nil { + return nil, nil, nil, nil, nil, ErrAsserterNotInitialized + } + operationStatuses := []*types.OperationStatus{} for k, v := range a.operationStatusMap { operationStatuses = append(operationStatuses, &types.OperationStatus{ @@ -126,5 +140,21 @@ func (a *Asserter) Configuration(ctx context.Context) (*types.NetworkIdentifier, errors = append(errors, v) } - return a.network, a.genesisBlock, a.operationTypes, operationStatuses, errors + return a.network, a.genesisBlock, a.operationTypes, operationStatuses, errors, nil +} + +// OperationSuccessful returns a boolean indicating if a types.Operation is +// successful and should be applied in a transaction. This should only be called +// AFTER an operation has been validated. +func (a *Asserter) OperationSuccessful(operation *types.Operation) (bool, error) { + if a == nil { + return false, ErrAsserterNotInitialized + } + + val, ok := a.operationStatusMap[operation.Status] + if !ok { + return false, fmt.Errorf("%s not found", operation.Status) + } + + return val, nil } diff --git a/asserter/block.go b/asserter/block.go index 499b6219..5253fd87 100644 --- a/asserter/block.go +++ b/asserter/block.go @@ -49,18 +49,6 @@ func Amount(amount *types.Amount) error { return nil } -// contains checks if a string is contained in a slice -// of strings. -func contains(valid []string, value string) bool { - for _, v := range valid { - if v == value { - return true - } - } - - return false -} - // OperationIdentifier returns an error if index of the // types.Operation is out-of-order or if the NetworkIndex is // invalid. @@ -101,35 +89,48 @@ func AccountIdentifier(account *types.AccountIdentifier) error { return nil } -// OperationSuccessful returns a boolean indicating if a types.Operation is -// successful and should be applied in a transaction. This should only be called -// AFTER an operation has been validated. -func (a *Asserter) OperationSuccessful(operation *types.Operation) (bool, error) { +// contains checks if a string is contained in a slice +// of strings. +func contains(valid []string, value string) bool { + for _, v := range valid { + if v == value { + return true + } + } + + return false +} + +// OperationStatus returns an error if an operation.Status +// is not valid. +func (a *Asserter) OperationStatus(status string) error { if a == nil { - return false, ErrAsserterNotInitialized + return ErrAsserterNotInitialized } - val, ok := a.operationStatusMap[operation.Status] - if !ok { - return false, fmt.Errorf("%s not found", operation.Status) + if status == "" { + return errors.New("operation.Status is empty") } - return val, nil + if _, ok := a.operationStatusMap[status]; !ok { + return fmt.Errorf("Operation.Status %s is invalid", status) + } + + return nil } -// operationStatuses returns all operation statuses the -// asserter consider valid. -func (a *Asserter) operationStatuses() ([]string, error) { +// OperationType returns an error if an operation.Type +// is not valid. +func (a *Asserter) OperationType(t string) error { if a == nil { - return nil, ErrAsserterNotInitialized + return ErrAsserterNotInitialized } - statuses := []string{} - for k := range a.operationStatusMap { - statuses = append(statuses, k) + if t == "" || !contains(a.operationTypes, t) { + return fmt.Errorf("Operation.Type %s is invalid", t) } - return statuses, nil + return nil } // Operation ensures a types.Operation has a valid @@ -150,17 +151,12 @@ func (a *Asserter) Operation( return err } - if operation.Type == "" || !contains(a.operationTypes, operation.Type) { - return fmt.Errorf("Operation.Type %s is invalid", operation.Type) - } - - validOperationStatuses, err := a.operationStatuses() - if err != nil { + if err := a.OperationType(operation.Type); err != nil { return err } - if operation.Status == "" || !contains(validOperationStatuses, operation.Status) { - return fmt.Errorf("Operation.Status %s is invalid", operation.Status) + if err := a.OperationStatus(operation.Status); err != nil { + return err } if operation.Amount == nil { From 5dd41362debff62790c90f31e3ee37a76a06fdf5 Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Tue, 21 Apr 2020 16:00:34 -0700 Subject: [PATCH 06/13] Parsing from configuration file --- asserter/asserter.go | 40 ++++++++-- asserter/asserter_test.go | 149 +++++++++++++++++++++++++++++++++++--- asserter/network.go | 28 +++++-- 3 files changed, 194 insertions(+), 23 deletions(-) diff --git a/asserter/asserter.go b/asserter/asserter.go index 7fff99e1..e769baa2 100644 --- a/asserter/asserter.go +++ b/asserter/asserter.go @@ -15,8 +15,10 @@ package asserter import ( + "encoding/json" "errors" "fmt" + "io/ioutil" "github.com/coinbase/rosetta-sdk-go/types" ) @@ -66,20 +68,40 @@ func NewWithResponses( ) } +// FileConfiguration is the structure of the JSON configuration file. +type FileConfiguration struct { + NetworkIdentifier *types.NetworkIdentifier `json:"network_identifier"` + GenesisBlockIdentifier *types.BlockIdentifier `json:"genesis_block_identifier"` + AllowedOperationTypes []string `json:"allowed_operation_types"` + AllowedOperationStatuses []*types.OperationStatus `json:"allowed_operation_statuses"` + AllowedErrors []*types.Error `json:"allowed_errors"` +} + // NewWithFile constructs a new Asserter using a specification // file instead of responses. This can be useful for running reliable // systems that error when updates to the server (more error types, // more operations, etc.) significantly change how to parse the chain. +// The filePath provided is parsed relative to the current directory. func NewWithFile( filePath string, ) (*Asserter, error) { - // load file - - // parse items + content, err := ioutil.ReadFile(filePath) + if err != nil { + return nil, err + } - // run NewWithOptions + config := &FileConfiguration{} + if err := json.Unmarshal(content, config); err != nil { + return nil, err + } - return nil, errors.New("not implemented") + return NewWithOptions( + config.NetworkIdentifier, + config.GenesisBlockIdentifier, + config.AllowedOperationTypes, + config.AllowedOperationStatuses, + config.AllowedErrors, + ) } // NewWithOptions constructs a new Asserter using the provided @@ -101,6 +123,14 @@ func NewWithOptions( return nil, err } + if err := OperationStatuses(operationStatuses); err != nil { + return nil, err + } + + if err := OperationTypes(operationTypes); err != nil { + return nil, err + } + asserter := &Asserter{ network: network, operationTypes: operationTypes, diff --git a/asserter/asserter_test.go b/asserter/asserter_test.go index c64f5e20..2aaa787c 100644 --- a/asserter/asserter_test.go +++ b/asserter/asserter_test.go @@ -15,7 +15,11 @@ package asserter import ( + "encoding/json" "errors" + "fmt" + "io/ioutil" + "os" "testing" "github.com/coinbase/rosetta-sdk-go/types" @@ -23,8 +27,13 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNewWithResponses(t *testing.T) { +func TestNew(t *testing.T) { var ( + validNetwork = &types.NetworkIdentifier{ + Blockchain: "hello", + Network: "world", + } + validNetworkStatus = &types.NetworkStatusResponse{ GenesisBlockIdentifier: &types.BlockIdentifier{ Index: 0, @@ -43,9 +52,9 @@ func TestNewWithResponses(t *testing.T) { } invalidNetworkStatus = &types.NetworkStatusResponse{ - GenesisBlockIdentifier: &types.BlockIdentifier{ - Index: 0, - Hash: "block 0", + CurrentBlockIdentifier: &types.BlockIdentifier{ + Index: 100, + Hash: "block 100", }, CurrentBlockTimestamp: 100, Peers: []*types.Peer{ @@ -81,6 +90,58 @@ func TestNewWithResponses(t *testing.T) { } invalidNetworkOptions = &types.NetworkOptionsResponse{ + Version: &types.Version{ + RosettaVersion: "1.2.3", + NodeVersion: "1.0", + }, + Allow: &types.Allow{ + OperationTypes: []string{ + "Transfer", + }, + Errors: []*types.Error{ + { + Code: 1, + Message: "error", + Retriable: true, + }, + }, + }, + } + + duplicateStatuses = &types.NetworkOptionsResponse{ + Version: &types.Version{ + RosettaVersion: "1.2.3", + NodeVersion: "1.0", + }, + Allow: &types.Allow{ + OperationStatuses: []*types.OperationStatus{ + { + Status: "Success", + Successful: true, + }, + { + Status: "Success", + Successful: false, + }, + }, + OperationTypes: []string{ + "Transfer", + }, + Errors: []*types.Error{ + { + Code: 1, + Message: "error", + Retriable: true, + }, + }, + }, + } + + duplicateTypes = &types.NetworkOptionsResponse{ + Version: &types.Version{ + RosettaVersion: "1.2.3", + NodeVersion: "1.0", + }, Allow: &types.Allow{ OperationStatuses: []*types.OperationStatus{ { @@ -90,6 +151,7 @@ func TestNewWithResponses(t *testing.T) { }, OperationTypes: []string{ "Transfer", + "Transfer", }, Errors: []*types.Error{ { @@ -103,47 +165,110 @@ func TestNewWithResponses(t *testing.T) { ) var tests = map[string]struct { + network *types.NetworkIdentifier networkStatus *types.NetworkStatusResponse networkOptions *types.NetworkOptionsResponse err error }{ "valid responses": { + network: validNetwork, networkStatus: validNetworkStatus, networkOptions: validNetworkOptions, err: nil, }, "invalid network status": { + network: validNetwork, networkStatus: invalidNetworkStatus, networkOptions: validNetworkOptions, err: errors.New("BlockIdentifier is nil"), }, "invalid network options": { + network: validNetwork, networkStatus: validNetworkStatus, networkOptions: invalidNetworkOptions, - err: errors.New("version is nil"), + err: errors.New("no Allow.OperationStatuses found"), + }, + "duplicate operation statuses": { + network: validNetwork, + networkStatus: validNetworkStatus, + networkOptions: duplicateStatuses, + + err: errors.New("Allow.OperationStatuses contains a duplicate Success"), + }, + "duplicate operation types": { + network: validNetwork, + networkStatus: validNetworkStatus, + networkOptions: duplicateTypes, + + err: errors.New("Allow.OperationTypes contains a duplicate Transfer"), }, } for name, test := range tests { - t.Run(name, func(t *testing.T) { + t.Run(fmt.Sprintf("%s with responses", name), func(t *testing.T) { asserter, err := NewWithResponses( - &types.NetworkIdentifier{ - Blockchain: "hello", - Network: "world", - }, + test.network, test.networkStatus, test.networkOptions, ) - if err == nil { - assert.NotNil(t, asserter) + assert.Equal(t, test.err, err) + + if test.err != nil { + return } + assert.NotNil(t, asserter) + network, genesis, opTypes, opStatuses, errors, err := asserter.Configuration() + 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) + }) + + t.Run(fmt.Sprintf("%s with file", name), func(t *testing.T) { + fileConfig := FileConfiguration{ + NetworkIdentifier: test.network, + GenesisBlockIdentifier: test.networkStatus.GenesisBlockIdentifier, + AllowedOperationTypes: test.networkOptions.Allow.OperationTypes, + AllowedOperationStatuses: test.networkOptions.Allow.OperationStatuses, + AllowedErrors: test.networkOptions.Allow.Errors, + } + tmpfile, err := ioutil.TempFile("", "test.json") + assert.NoError(t, err) + defer os.Remove(tmpfile.Name()) + + file, err := json.MarshalIndent(fileConfig, "", " ") + assert.NoError(t, err) + + _, err = tmpfile.Write(file) + assert.NoError(t, err) + assert.NoError(t, tmpfile.Close()) + + asserter, err := NewWithFile( + tmpfile.Name(), + ) + assert.Equal(t, test.err, err) + + if test.err != nil { + return + } + + assert.NotNil(t, asserter) + network, genesis, opTypes, opStatuses, errors, err := asserter.Configuration() + 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) }) } } diff --git a/asserter/network.go b/asserter/network.go index 1a277437..570e03bf 100644 --- a/asserter/network.go +++ b/asserter/network.go @@ -81,16 +81,23 @@ func Version(version *types.Version) error { } // StringArray ensures all strings in an array -// are non-empty strings. +// are non-empty strings and not duplicates. func StringArray(arrName string, arr []string) error { if len(arr) == 0 { return fmt.Errorf("no %s found", arrName) } - for _, s := range arr { + parsed := make([]string, len(arr)) + for i, s := range arr { if s == "" { return fmt.Errorf("%s has an empty string", arrName) } + + if contains(parsed, s) { + return fmt.Errorf("%s contains a duplicate %s", arrName, s) + } + + parsed[i] = s } return nil @@ -124,15 +131,16 @@ func NetworkStatusResponse(response *types.NetworkStatusResponse) error { return nil } -// OperationStatuses ensures all items in Options.OperationStatuses +// OperationStatuses ensures all items in Options.Allow.OperationStatuses // are valid and that there exists at least 1 successful status. func OperationStatuses(statuses []*types.OperationStatus) error { if len(statuses) == 0 { return errors.New("no Allow.OperationStatuses found") } + statusStatuses := make([]string, len(statuses)) foundSuccessful := false - for _, status := range statuses { + for i, status := range statuses { if status.Status == "" { return errors.New("Operation.Status is missing") } @@ -140,13 +148,21 @@ func OperationStatuses(statuses []*types.OperationStatus) error { if status.Successful { foundSuccessful = true } + + statusStatuses[i] = status.Status } if !foundSuccessful { return errors.New("no successful Allow.OperationStatuses found") } - return nil + return StringArray("Allow.OperationStatuses", statusStatuses) +} + +// OperationTypes ensures all items in Options.Allow.OperationStatuses +// are valid and that there are no repeats. +func OperationTypes(types []string) error { + return StringArray("Allow.OperationTypes", types) } // Error ensures a types.Error is valid. @@ -197,7 +213,7 @@ func Allow(allowed *types.Allow) error { return err } - if err := StringArray("Allow.OperationTypes", allowed.OperationTypes); err != nil { + if err := OperationTypes(allowed.OperationTypes); err != nil { return err } From e8ab281cebfd3c3446b5882d8e2a2df4ed8ffc65 Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Tue, 21 Apr 2020 18:06:08 -0700 Subject: [PATCH 07/13] Assert timestamp is in a range --- asserter/asserter_test.go | 4 ++-- asserter/block.go | 26 +++++++++++++++++++------- asserter/block_test.go | 32 ++++++++++++++++++++------------ 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/asserter/asserter_test.go b/asserter/asserter_test.go index 2aaa787c..f584a969 100644 --- a/asserter/asserter_test.go +++ b/asserter/asserter_test.go @@ -43,7 +43,7 @@ func TestNew(t *testing.T) { Index: 100, Hash: "block 100", }, - CurrentBlockTimestamp: 100, + CurrentBlockTimestamp: MinUnixEpoch + 1, Peers: []*types.Peer{ { PeerID: "peer 1", @@ -56,7 +56,7 @@ func TestNew(t *testing.T) { Index: 100, Hash: "block 100", }, - CurrentBlockTimestamp: 100, + CurrentBlockTimestamp: MinUnixEpoch + 1, Peers: []*types.Peer{ { PeerID: "peer 1", diff --git a/asserter/block.go b/asserter/block.go index 5253fd87..87855211 100644 --- a/asserter/block.go +++ b/asserter/block.go @@ -22,6 +22,16 @@ import ( "github.com/coinbase/rosetta-sdk-go/types" ) +const ( + // MinUnixEpoch is the unix epoch time in milliseconds of + // 01/01/2000 at 12:00:00 AM. + MinUnixEpoch = int64(946713600000) + + // MaxUnixEpoch is the unix epoch time in milliseconds of + // 01/01/2040 at 12:00:00 AM. + MaxUnixEpoch = int64(2209017600000) +) + // Amount ensures a types.Amount has an // integer value, specified precision, and symbol. func Amount(amount *types.Amount) error { @@ -252,11 +262,13 @@ func (a *Asserter) Transaction( // Timestamp returns an error if the timestamp // on a block is less than or equal to 0. func Timestamp(timestamp int64) error { - if timestamp <= 0 { - return fmt.Errorf("Timestamp is invalid %d", timestamp) + if timestamp < MinUnixEpoch { + return fmt.Errorf("Timestamp %d is before 01/01/2000", timestamp) + } else if timestamp > MaxUnixEpoch { + return fmt.Errorf("Timestamp %d is after 01/01/2040", timestamp) + } else { + return nil } - - return nil } // Block runs a basic set of assertions for each returned block. @@ -289,10 +301,10 @@ func (a *Asserter) Block( if block.BlockIdentifier.Index <= block.ParentBlockIdentifier.Index { return errors.New("BlockIdentifier.Index <= ParentBlockIdentifier.Index") } - } - if err := Timestamp(block.Timestamp); err != nil { - return err + if err := Timestamp(block.Timestamp); err != nil { + return err + } } for _, transaction := range block.Transactions { diff --git a/asserter/block_test.go b/asserter/block_test.go index 2a4d4a59..56b1256f 100644 --- a/asserter/block_test.go +++ b/asserter/block_test.go @@ -397,7 +397,7 @@ func TestOperation(t *testing.T) { Index: 100, Hash: "block 100", }, - CurrentBlockTimestamp: 100, + CurrentBlockTimestamp: MinUnixEpoch + 1, Peers: []*types.Peer{ { PeerID: "peer 1", @@ -464,7 +464,7 @@ func TestBlock(t *testing.T) { block: &types.Block{ BlockIdentifier: validBlockIdentifier, ParentBlockIdentifier: validParentBlockIdentifier, - Timestamp: 1, + Timestamp: MinUnixEpoch + 1, Transactions: []*types.Transaction{validTransaction}, }, err: nil, @@ -473,7 +473,6 @@ func TestBlock(t *testing.T) { block: &types.Block{ BlockIdentifier: validBlockIdentifier, ParentBlockIdentifier: validBlockIdentifier, - Timestamp: 1, Transactions: []*types.Transaction{validTransaction}, }, genesisIndex: validBlockIdentifier.Index, @@ -487,7 +486,7 @@ func TestBlock(t *testing.T) { block: &types.Block{ BlockIdentifier: nil, ParentBlockIdentifier: validParentBlockIdentifier, - Timestamp: 1, + Timestamp: MinUnixEpoch + 1, Transactions: []*types.Transaction{validTransaction}, }, err: errors.New("BlockIdentifier is nil"), @@ -496,7 +495,7 @@ func TestBlock(t *testing.T) { block: &types.Block{ BlockIdentifier: &types.BlockIdentifier{}, ParentBlockIdentifier: validParentBlockIdentifier, - Timestamp: 1, + Timestamp: MinUnixEpoch + 1, Transactions: []*types.Transaction{validTransaction}, }, err: errors.New("BlockIdentifier.Hash is missing"), @@ -505,7 +504,7 @@ func TestBlock(t *testing.T) { block: &types.Block{ BlockIdentifier: validBlockIdentifier, ParentBlockIdentifier: &types.BlockIdentifier{}, - Timestamp: 1, + Timestamp: MinUnixEpoch + 1, Transactions: []*types.Transaction{validTransaction}, }, err: errors.New("BlockIdentifier.Hash is missing"), @@ -517,7 +516,7 @@ func TestBlock(t *testing.T) { Hash: validParentBlockIdentifier.Hash, Index: validBlockIdentifier.Index, }, - Timestamp: 1, + Timestamp: MinUnixEpoch + 1, Transactions: []*types.Transaction{validTransaction}, }, err: errors.New("BlockIdentifier.Index <= ParentBlockIdentifier.Index"), @@ -529,24 +528,33 @@ func TestBlock(t *testing.T) { Hash: validBlockIdentifier.Hash, Index: validParentBlockIdentifier.Index, }, - Timestamp: 1, + Timestamp: MinUnixEpoch + 1, Transactions: []*types.Transaction{validTransaction}, }, err: errors.New("BlockIdentifier.Hash == ParentBlockIdentifier.Hash"), }, - "invalid block timestamp": { + "invalid block timestamp less than MinUnixEpoch": { block: &types.Block{ BlockIdentifier: validBlockIdentifier, ParentBlockIdentifier: validParentBlockIdentifier, Transactions: []*types.Transaction{validTransaction}, }, - err: errors.New("Timestamp is invalid 0"), + err: errors.New("Timestamp 0 is before 01/01/2000"), + }, + "invalid block timestamp greater than MaxUnixEpoch": { + block: &types.Block{ + BlockIdentifier: validBlockIdentifier, + ParentBlockIdentifier: validParentBlockIdentifier, + Transactions: []*types.Transaction{validTransaction}, + Timestamp: MaxUnixEpoch + 1, + }, + err: errors.New("Timestamp 2209017600001 is after 01/01/2040"), }, "invalid block transaction": { block: &types.Block{ BlockIdentifier: validBlockIdentifier, ParentBlockIdentifier: validParentBlockIdentifier, - Timestamp: 1, + Timestamp: MinUnixEpoch + 1, Transactions: []*types.Transaction{ {}, }, @@ -571,7 +579,7 @@ func TestBlock(t *testing.T) { Index: 100, Hash: "block 100", }, - CurrentBlockTimestamp: 100, + CurrentBlockTimestamp: MinUnixEpoch + 1, Peers: []*types.Peer{ { PeerID: "peer 1", From 1236d4ffeb04d4eb2f1be2a78ba17af0ea16a032 Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Tue, 21 Apr 2020 18:16:33 -0700 Subject: [PATCH 08/13] Lint fixes --- asserter/asserter.go | 12 ++++++++++-- asserter/block.go | 11 ++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/asserter/asserter.go b/asserter/asserter.go index e769baa2..54048ad9 100644 --- a/asserter/asserter.go +++ b/asserter/asserter.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "io/ioutil" + "path" "github.com/coinbase/rosetta-sdk-go/types" ) @@ -85,7 +86,7 @@ type FileConfiguration struct { func NewWithFile( filePath string, ) (*Asserter, error) { - content, err := ioutil.ReadFile(filePath) + content, err := ioutil.ReadFile(path.Clean(filePath)) if err != nil { return nil, err } @@ -152,7 +153,14 @@ func NewWithOptions( // Configuration returns all variables currently set in an Asserter. // This function will error if it is called on an uninitialized asserter. -func (a *Asserter) Configuration() (*types.NetworkIdentifier, *types.BlockIdentifier, []string, []*types.OperationStatus, []*types.Error, error) { +func (a *Asserter) Configuration() ( + *types.NetworkIdentifier, + *types.BlockIdentifier, + []string, + []*types.OperationStatus, + []*types.Error, + error, +) { if a == nil { return nil, nil, nil, nil, nil, ErrAsserterNotInitialized } diff --git a/asserter/block.go b/asserter/block.go index 87855211..f4ade1a1 100644 --- a/asserter/block.go +++ b/asserter/block.go @@ -25,11 +25,11 @@ import ( const ( // MinUnixEpoch is the unix epoch time in milliseconds of // 01/01/2000 at 12:00:00 AM. - MinUnixEpoch = int64(946713600000) + MinUnixEpoch = 946713600000 // MaxUnixEpoch is the unix epoch time in milliseconds of // 01/01/2040 at 12:00:00 AM. - MaxUnixEpoch = int64(2209017600000) + MaxUnixEpoch = 2209017600000 ) // Amount ensures a types.Amount has an @@ -262,11 +262,12 @@ func (a *Asserter) Transaction( // Timestamp returns an error if the timestamp // on a block is less than or equal to 0. func Timestamp(timestamp int64) error { - if timestamp < MinUnixEpoch { + switch { + case timestamp < MinUnixEpoch: return fmt.Errorf("Timestamp %d is before 01/01/2000", timestamp) - } else if timestamp > MaxUnixEpoch { + case timestamp > MaxUnixEpoch: return fmt.Errorf("Timestamp %d is after 01/01/2040", timestamp) - } else { + default: return nil } } From 64b75b29e6cb0fd041c115a5db924e312e341f9b Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Wed, 22 Apr 2020 09:21:13 -0700 Subject: [PATCH 09/13] Add network validation to asserter --- asserter/asserter.go | 18 ++++++ asserter/request.go | 129 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 136 insertions(+), 11 deletions(-) diff --git a/asserter/asserter.go b/asserter/asserter.go index 54048ad9..dedccabd 100644 --- a/asserter/asserter.go +++ b/asserter/asserter.go @@ -33,11 +33,29 @@ var ( // Asserter contains all logic to perform static // validation on Rosetta Server responses. type Asserter struct { + // These variables are used for response assertion. network *types.NetworkIdentifier operationTypes []string operationStatusMap map[string]bool errorTypeMap map[int32]*types.Error genesisBlock *types.BlockIdentifier + + // These variables are used for request assertion. + supportedNetworks []*types.NetworkIdentifier +} + +// NewServer constructs a new Asserter for use in the +// server package. +func NewServer( + supportedNetworks []*types.NetworkIdentifier, +) (*Asserter, error) { + if err := SupportedNetworks(supportedNetworks); err != nil { + return nil, err + } + + return &Asserter{ + supportedNetworks: supportedNetworks, + }, nil } // NewWithResponses constructs a new Asserter diff --git a/asserter/request.go b/asserter/request.go index 653d4cbe..cb9d4aa9 100644 --- a/asserter/request.go +++ b/asserter/request.go @@ -16,13 +16,56 @@ package asserter import ( "errors" + "fmt" "github.com/coinbase/rosetta-sdk-go/types" ) +// SupportedNetworks returns an error if there is an invalid +// types.NetworkIdentifier or there is a duplicate. +func SupportedNetworks(supportedNetworks []*types.NetworkIdentifier) error { + if len(supportedNetworks) == 0 { + return errors.New("no supported networks") + } + + parsed := make([]*types.NetworkIdentifier, len(supportedNetworks)) + for i, network := range supportedNetworks { + if err := NetworkIdentifier(network); err != nil { + return err + } + + if !containsNetworkIdentifier(parsed, network) { + return fmt.Errorf("supported network duplicate %+v", *network) + } + parsed[i] = network + } + + return nil +} + +// SupportedNetwork returns a boolean indicating if the requestNetwork +// is allowed. This should be called after the requestNetwork is asserted. +func (a *Asserter) SupportedNetwork( + requestNetwork *types.NetworkIdentifier, +) error { + if a == nil { + return ErrAsserterNotInitialized + } + + if !containsNetworkIdentifier(a.supportedNetworks, requestNetwork) { + return fmt.Errorf("%+v is not supported", *requestNetwork) + } + + return nil +} + // AccountBalanceRequest ensures that a types.AccountBalanceRequest // is well-formatted. -func AccountBalanceRequest(request *types.AccountBalanceRequest) error { +func (a *Asserter) AccountBalanceRequest(request *types.AccountBalanceRequest) error { + if a == nil { + return ErrAsserterNotInitialized + } + if request == nil { return errors.New("AccountBalanceRequest is nil") } @@ -31,6 +74,10 @@ func AccountBalanceRequest(request *types.AccountBalanceRequest) error { return err } + if err := a.SupportedNetwork(request.NetworkIdentifier); err != nil { + return err + } + if err := AccountIdentifier(request.AccountIdentifier); err != nil { return err } @@ -44,7 +91,11 @@ func AccountBalanceRequest(request *types.AccountBalanceRequest) error { // BlockRequest ensures that a types.BlockRequest // is well-formatted. -func BlockRequest(request *types.BlockRequest) error { +func (a *Asserter) BlockRequest(request *types.BlockRequest) error { + if a == nil { + return ErrAsserterNotInitialized + } + if request == nil { return errors.New("BlockRequest is nil") } @@ -53,12 +104,20 @@ func BlockRequest(request *types.BlockRequest) error { return err } + if err := a.SupportedNetwork(request.NetworkIdentifier); err != nil { + return err + } + return PartialBlockIdentifier(request.BlockIdentifier) } // BlockTransactionRequest ensures that a types.BlockTransactionRequest // is well-formatted. -func BlockTransactionRequest(request *types.BlockTransactionRequest) error { +func (a *Asserter) BlockTransactionRequest(request *types.BlockTransactionRequest) error { + if a == nil { + return ErrAsserterNotInitialized + } + if request == nil { return errors.New("BlockTransactionRequest is nil") } @@ -67,6 +126,10 @@ func BlockTransactionRequest(request *types.BlockTransactionRequest) error { return err } + if err := a.SupportedNetwork(request.NetworkIdentifier); err != nil { + return err + } + if err := BlockIdentifier(request.BlockIdentifier); err != nil { return err } @@ -76,7 +139,11 @@ func BlockTransactionRequest(request *types.BlockTransactionRequest) error { // ConstructionMetadataRequest ensures that a types.ConstructionMetadataRequest // is well-formatted. -func ConstructionMetadataRequest(request *types.ConstructionMetadataRequest) error { +func (a *Asserter) ConstructionMetadataRequest(request *types.ConstructionMetadataRequest) error { + if a == nil { + return ErrAsserterNotInitialized + } + if request == nil { return errors.New("ConstructionMetadataRequest is nil") } @@ -85,6 +152,10 @@ func ConstructionMetadataRequest(request *types.ConstructionMetadataRequest) err return err } + if err := a.SupportedNetwork(request.NetworkIdentifier); err != nil { + return err + } + if request.Options == nil { return errors.New("ConstructionMetadataRequest.Options is nil") } @@ -94,11 +165,23 @@ func ConstructionMetadataRequest(request *types.ConstructionMetadataRequest) err // ConstructionSubmitRequest ensures that a types.ConstructionSubmitRequest // is well-formatted. -func ConstructionSubmitRequest(request *types.ConstructionSubmitRequest) error { +func (a *Asserter) ConstructionSubmitRequest(request *types.ConstructionSubmitRequest) error { + if a == nil { + return ErrAsserterNotInitialized + } + if request == nil { return errors.New("ConstructionSubmitRequest is nil") } + if err := NetworkIdentifier(request.NetworkIdentifier); err != nil { + return err + } + + if err := a.SupportedNetwork(request.NetworkIdentifier); err != nil { + return err + } + if request.SignedTransaction == "" { return errors.New("ConstructionSubmitRequest.SignedTransaction is empty") } @@ -108,17 +191,25 @@ func ConstructionSubmitRequest(request *types.ConstructionSubmitRequest) error { // MempoolRequest ensures that a types.MempoolRequest // is well-formatted. -func MempoolRequest(request *types.MempoolRequest) error { +func (a *Asserter) MempoolRequest(request *types.MempoolRequest) error { + if a == nil { + return ErrAsserterNotInitialized + } + if request == nil { return errors.New("MempoolRequest is nil") } - return NetworkIdentifier(request.NetworkIdentifier) + if err := NetworkIdentifier(request.NetworkIdentifier); err != nil { + return err + } + + return a.SupportedNetwork(request.NetworkIdentifier) } // MempoolTransactionRequest ensures that a types.MempoolTransactionRequest // is well-formatted. -func MempoolTransactionRequest(request *types.MempoolTransactionRequest) error { +func (a *Asserter) MempoolTransactionRequest(request *types.MempoolTransactionRequest) error { if request == nil { return errors.New("MempoolTransactionRequest is nil") } @@ -127,12 +218,20 @@ func MempoolTransactionRequest(request *types.MempoolTransactionRequest) error { return err } + if err := a.SupportedNetwork(request.NetworkIdentifier); err != nil { + return err + } + return TransactionIdentifier(request.TransactionIdentifier) } // MetadataRequest ensures that a types.MetadataRequest // is well-formatted. -func MetadataRequest(request *types.MetadataRequest) error { +func (a *Asserter) MetadataRequest(request *types.MetadataRequest) error { + if a == nil { + return ErrAsserterNotInitialized + } + if request == nil { return errors.New("MetadataRequest is nil") } @@ -142,10 +241,18 @@ func MetadataRequest(request *types.MetadataRequest) error { // NetworkRequest ensures that a types.NetworkRequest // is well-formatted. -func NetworkRequest(request *types.NetworkRequest) error { +func (a *Asserter) NetworkRequest(request *types.NetworkRequest) error { + if a == nil { + return ErrAsserterNotInitialized + } + if request == nil { return errors.New("NetworkRequest is nil") } - return NetworkIdentifier(request.NetworkIdentifier) + if err := NetworkIdentifier(request.NetworkIdentifier); err != nil { + return err + } + + return a.SupportedNetwork(request.NetworkIdentifier) } From ee2125b1db6e1a4844f791da5f20dcfdd4b12071 Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Wed, 22 Apr 2020 10:50:53 -0700 Subject: [PATCH 10/13] Assert request network identifiers --- asserter/request.go | 4 +- asserter/request_test.go | 118 +++++++++++++++++++++-- server/api_account.go | 15 ++- server/api_block.go | 17 +++- server/api_construction.go | 17 +++- server/api_mempool.go | 17 +++- server/api_network.go | 19 ++-- templates/server/controller-api.mustache | 13 ++- 8 files changed, 180 insertions(+), 40 deletions(-) diff --git a/asserter/request.go b/asserter/request.go index cb9d4aa9..1521c87c 100644 --- a/asserter/request.go +++ b/asserter/request.go @@ -34,7 +34,7 @@ func SupportedNetworks(supportedNetworks []*types.NetworkIdentifier) error { return err } - if !containsNetworkIdentifier(parsed, network) { + if containsNetworkIdentifier(parsed, network) { return fmt.Errorf("supported network duplicate %+v", *network) } parsed[i] = network @@ -53,7 +53,7 @@ func (a *Asserter) SupportedNetwork( } if !containsNetworkIdentifier(a.supportedNetworks, requestNetwork) { - return fmt.Errorf("%+v is not supported", *requestNetwork) + return fmt.Errorf("%+v is not supported", requestNetwork) } return nil diff --git a/asserter/request_test.go b/asserter/request_test.go index 04d70057..1a793cf0 100644 --- a/asserter/request_test.go +++ b/asserter/request_test.go @@ -16,6 +16,7 @@ package asserter import ( "errors" + "fmt" "testing" "github.com/coinbase/rosetta-sdk-go/types" @@ -29,6 +30,11 @@ var ( Network: "Mainnet", } + wrongNetworkIdentifier = &types.NetworkIdentifier{ + Blockchain: "Bitcoin", + Network: "Testnet", + } + validAccountIdentifier = &types.AccountIdentifier{ Address: "acct1", } @@ -61,6 +67,13 @@ func TestAccountBalanceRequest(t *testing.T) { }, err: nil, }, + "invalid request wrong network": { + request: &types.AccountBalanceRequest{ + NetworkIdentifier: wrongNetworkIdentifier, + AccountIdentifier: validAccountIdentifier, + }, + err: fmt.Errorf("%+v is not supported", wrongNetworkIdentifier), + }, "nil request": { request: nil, err: errors.New("AccountBalanceRequest is nil"), @@ -97,7 +110,11 @@ func TestAccountBalanceRequest(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - err := AccountBalanceRequest(test.request) + a, err := NewServer([]*types.NetworkIdentifier{validNetworkIdentifier}) + assert.NoError(t, err) + assert.NotNil(t, a) + + err = a.AccountBalanceRequest(test.request) assert.Equal(t, test.err, err) }) } @@ -124,6 +141,13 @@ func TestBlockRequest(t *testing.T) { }, err: nil, }, + "invalid request wrong network": { + request: &types.BlockRequest{ + NetworkIdentifier: wrongNetworkIdentifier, + BlockIdentifier: validPartialBlockIdentifier, + }, + err: fmt.Errorf("%+v is not supported", wrongNetworkIdentifier), + }, "nil request": { request: nil, err: errors.New("BlockRequest is nil"), @@ -151,7 +175,11 @@ func TestBlockRequest(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - err := BlockRequest(test.request) + a, err := NewServer([]*types.NetworkIdentifier{validNetworkIdentifier}) + assert.NoError(t, err) + assert.NotNil(t, a) + + err = a.BlockRequest(test.request) assert.Equal(t, test.err, err) }) } @@ -170,6 +198,14 @@ func TestBlockTransactionRequest(t *testing.T) { }, err: nil, }, + "invalid request wrong network": { + request: &types.BlockTransactionRequest{ + NetworkIdentifier: wrongNetworkIdentifier, + BlockIdentifier: validBlockIdentifier, + TransactionIdentifier: validTransactionIdentifier, + }, + err: fmt.Errorf("%+v is not supported", wrongNetworkIdentifier), + }, "nil request": { request: nil, err: errors.New("BlockTransactionRequest is nil"), @@ -199,7 +235,11 @@ func TestBlockTransactionRequest(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - err := BlockTransactionRequest(test.request) + a, err := NewServer([]*types.NetworkIdentifier{validNetworkIdentifier}) + assert.NoError(t, err) + assert.NotNil(t, a) + + err = a.BlockTransactionRequest(test.request) assert.Equal(t, test.err, err) }) } @@ -217,6 +257,13 @@ func TestConstructionMetadataRequest(t *testing.T) { }, err: nil, }, + "invalid request wrong network": { + request: &types.ConstructionMetadataRequest{ + NetworkIdentifier: wrongNetworkIdentifier, + Options: map[string]interface{}{}, + }, + err: fmt.Errorf("%+v is not supported", wrongNetworkIdentifier), + }, "nil request": { request: nil, err: errors.New("ConstructionMetadataRequest is nil"), @@ -237,7 +284,11 @@ func TestConstructionMetadataRequest(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - err := ConstructionMetadataRequest(test.request) + a, err := NewServer([]*types.NetworkIdentifier{validNetworkIdentifier}) + assert.NoError(t, err) + assert.NotNil(t, a) + + err = a.ConstructionMetadataRequest(test.request) assert.Equal(t, test.err, err) }) } @@ -250,23 +301,35 @@ func TestConstructionSubmitRequest(t *testing.T) { }{ "valid request": { request: &types.ConstructionSubmitRequest{ + NetworkIdentifier: validNetworkIdentifier, SignedTransaction: "tx", }, err: nil, }, + "invalid request wrong network": { + request: &types.ConstructionSubmitRequest{ + NetworkIdentifier: wrongNetworkIdentifier, + SignedTransaction: "tx", + }, + err: fmt.Errorf("%+v is not supported", wrongNetworkIdentifier), + }, "nil request": { request: nil, err: errors.New("ConstructionSubmitRequest is nil"), }, "empty tx": { request: &types.ConstructionSubmitRequest{}, - err: errors.New("ConstructionSubmitRequest.SignedTransaction is empty"), + err: errors.New("NetworkIdentifier is nil"), }, } for name, test := range tests { t.Run(name, func(t *testing.T) { - err := ConstructionSubmitRequest(test.request) + a, err := NewServer([]*types.NetworkIdentifier{validNetworkIdentifier}) + assert.NoError(t, err) + assert.NotNil(t, a) + + err = a.ConstructionSubmitRequest(test.request) assert.Equal(t, test.err, err) }) } @@ -283,6 +346,12 @@ func TestMempoolRequest(t *testing.T) { }, err: nil, }, + "invalid request wrong network": { + request: &types.MempoolRequest{ + NetworkIdentifier: wrongNetworkIdentifier, + }, + err: fmt.Errorf("%+v is not supported", wrongNetworkIdentifier), + }, "nil request": { request: nil, err: errors.New("MempoolRequest is nil"), @@ -295,7 +364,11 @@ func TestMempoolRequest(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - err := MempoolRequest(test.request) + a, err := NewServer([]*types.NetworkIdentifier{validNetworkIdentifier}) + assert.NoError(t, err) + assert.NotNil(t, a) + + err = a.MempoolRequest(test.request) assert.Equal(t, test.err, err) }) } @@ -313,6 +386,13 @@ func TestMempoolTransactionRequest(t *testing.T) { }, err: nil, }, + "invalid request wrong network": { + request: &types.MempoolTransactionRequest{ + NetworkIdentifier: wrongNetworkIdentifier, + TransactionIdentifier: validTransactionIdentifier, + }, + err: fmt.Errorf("%+v is not supported", wrongNetworkIdentifier), + }, "nil request": { request: nil, err: errors.New("MempoolTransactionRequest is nil"), @@ -334,7 +414,11 @@ func TestMempoolTransactionRequest(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - err := MempoolTransactionRequest(test.request) + a, err := NewServer([]*types.NetworkIdentifier{validNetworkIdentifier}) + assert.NoError(t, err) + assert.NotNil(t, a) + + err = a.MempoolTransactionRequest(test.request) assert.Equal(t, test.err, err) }) } @@ -357,7 +441,11 @@ func TestMetadataRequest(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - err := MetadataRequest(test.request) + a, err := NewServer([]*types.NetworkIdentifier{validNetworkIdentifier}) + assert.NoError(t, err) + assert.NotNil(t, a) + + err = a.MetadataRequest(test.request) assert.Equal(t, test.err, err) }) } @@ -374,6 +462,12 @@ func TestNetworkRequest(t *testing.T) { }, err: nil, }, + "invalid request wrong network": { + request: &types.NetworkRequest{ + NetworkIdentifier: wrongNetworkIdentifier, + }, + err: fmt.Errorf("%+v is not supported", wrongNetworkIdentifier), + }, "nil request": { request: nil, err: errors.New("NetworkRequest is nil"), @@ -386,7 +480,11 @@ func TestNetworkRequest(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - err := NetworkRequest(test.request) + a, err := NewServer([]*types.NetworkIdentifier{validNetworkIdentifier}) + assert.NoError(t, err) + assert.NotNil(t, a) + + err = a.NetworkRequest(test.request) assert.Equal(t, test.err, err) }) } diff --git a/server/api_account.go b/server/api_account.go index de09bbf7..0eedeceb 100644 --- a/server/api_account.go +++ b/server/api_account.go @@ -28,12 +28,19 @@ import ( // A AccountAPIController binds http requests to an api service and writes the service results to // the http response type AccountAPIController struct { - service AccountAPIServicer + service AccountAPIServicer + asserter *asserter.Asserter } // NewAccountAPIController creates a default api controller -func NewAccountAPIController(s AccountAPIServicer) Router { - return &AccountAPIController{service: s} +func NewAccountAPIController( + s AccountAPIServicer, + asserter *asserter.Asserter, +) Router { + return &AccountAPIController{ + service: s, + asserter: asserter, + } } // Routes returns all of the api route for the AccountAPIController @@ -60,7 +67,7 @@ func (c *AccountAPIController) AccountBalance(w http.ResponseWriter, r *http.Req } // Assert that AccountBalanceRequest is correct - if err := asserter.AccountBalanceRequest(accountBalanceRequest); err != nil { + if err := c.asserter.AccountBalanceRequest(accountBalanceRequest); err != nil { EncodeJSONResponse(&types.Error{ Message: err.Error(), }, http.StatusInternalServerError, w) diff --git a/server/api_block.go b/server/api_block.go index de7bdc49..0e2c3b5a 100644 --- a/server/api_block.go +++ b/server/api_block.go @@ -28,12 +28,19 @@ import ( // A BlockAPIController binds http requests to an api service and writes the service results to the // http response type BlockAPIController struct { - service BlockAPIServicer + service BlockAPIServicer + asserter *asserter.Asserter } // NewBlockAPIController creates a default api controller -func NewBlockAPIController(s BlockAPIServicer) Router { - return &BlockAPIController{service: s} +func NewBlockAPIController( + s BlockAPIServicer, + asserter *asserter.Asserter, +) Router { + return &BlockAPIController{ + service: s, + asserter: asserter, + } } // Routes returns all of the api route for the BlockAPIController @@ -66,7 +73,7 @@ func (c *BlockAPIController) Block(w http.ResponseWriter, r *http.Request) { } // Assert that BlockRequest is correct - if err := asserter.BlockRequest(blockRequest); err != nil { + if err := c.asserter.BlockRequest(blockRequest); err != nil { EncodeJSONResponse(&types.Error{ Message: err.Error(), }, http.StatusInternalServerError, w) @@ -96,7 +103,7 @@ func (c *BlockAPIController) BlockTransaction(w http.ResponseWriter, r *http.Req } // Assert that BlockTransactionRequest is correct - if err := asserter.BlockTransactionRequest(blockTransactionRequest); err != nil { + if err := c.asserter.BlockTransactionRequest(blockTransactionRequest); err != nil { EncodeJSONResponse(&types.Error{ Message: err.Error(), }, http.StatusInternalServerError, w) diff --git a/server/api_construction.go b/server/api_construction.go index 7689be01..0d7e96b1 100644 --- a/server/api_construction.go +++ b/server/api_construction.go @@ -28,12 +28,19 @@ import ( // A ConstructionAPIController binds http requests to an api service and writes the service results // to the http response type ConstructionAPIController struct { - service ConstructionAPIServicer + service ConstructionAPIServicer + asserter *asserter.Asserter } // NewConstructionAPIController creates a default api controller -func NewConstructionAPIController(s ConstructionAPIServicer) Router { - return &ConstructionAPIController{service: s} +func NewConstructionAPIController( + s ConstructionAPIServicer, + asserter *asserter.Asserter, +) Router { + return &ConstructionAPIController{ + service: s, + asserter: asserter, + } } // Routes returns all of the api route for the ConstructionAPIController @@ -66,7 +73,7 @@ func (c *ConstructionAPIController) ConstructionMetadata(w http.ResponseWriter, } // Assert that ConstructionMetadataRequest is correct - if err := asserter.ConstructionMetadataRequest(constructionMetadataRequest); err != nil { + if err := c.asserter.ConstructionMetadataRequest(constructionMetadataRequest); err != nil { EncodeJSONResponse(&types.Error{ Message: err.Error(), }, http.StatusInternalServerError, w) @@ -96,7 +103,7 @@ func (c *ConstructionAPIController) ConstructionSubmit(w http.ResponseWriter, r } // Assert that ConstructionSubmitRequest is correct - if err := asserter.ConstructionSubmitRequest(constructionSubmitRequest); err != nil { + if err := c.asserter.ConstructionSubmitRequest(constructionSubmitRequest); err != nil { EncodeJSONResponse(&types.Error{ Message: err.Error(), }, http.StatusInternalServerError, w) diff --git a/server/api_mempool.go b/server/api_mempool.go index f742da50..155fa036 100644 --- a/server/api_mempool.go +++ b/server/api_mempool.go @@ -28,12 +28,19 @@ import ( // A MempoolAPIController binds http requests to an api service and writes the service results to // the http response type MempoolAPIController struct { - service MempoolAPIServicer + service MempoolAPIServicer + asserter *asserter.Asserter } // NewMempoolAPIController creates a default api controller -func NewMempoolAPIController(s MempoolAPIServicer) Router { - return &MempoolAPIController{service: s} +func NewMempoolAPIController( + s MempoolAPIServicer, + asserter *asserter.Asserter, +) Router { + return &MempoolAPIController{ + service: s, + asserter: asserter, + } } // Routes returns all of the api route for the MempoolAPIController @@ -66,7 +73,7 @@ func (c *MempoolAPIController) Mempool(w http.ResponseWriter, r *http.Request) { } // Assert that MempoolRequest is correct - if err := asserter.MempoolRequest(mempoolRequest); err != nil { + if err := c.asserter.MempoolRequest(mempoolRequest); err != nil { EncodeJSONResponse(&types.Error{ Message: err.Error(), }, http.StatusInternalServerError, w) @@ -96,7 +103,7 @@ func (c *MempoolAPIController) MempoolTransaction(w http.ResponseWriter, r *http } // Assert that MempoolTransactionRequest is correct - if err := asserter.MempoolTransactionRequest(mempoolTransactionRequest); err != nil { + if err := c.asserter.MempoolTransactionRequest(mempoolTransactionRequest); err != nil { EncodeJSONResponse(&types.Error{ Message: err.Error(), }, http.StatusInternalServerError, w) diff --git a/server/api_network.go b/server/api_network.go index 9ee06633..9d4507d5 100644 --- a/server/api_network.go +++ b/server/api_network.go @@ -28,12 +28,19 @@ import ( // A NetworkAPIController binds http requests to an api service and writes the service results to // the http response type NetworkAPIController struct { - service NetworkAPIServicer + service NetworkAPIServicer + asserter *asserter.Asserter } // NewNetworkAPIController creates a default api controller -func NewNetworkAPIController(s NetworkAPIServicer) Router { - return &NetworkAPIController{service: s} +func NewNetworkAPIController( + s NetworkAPIServicer, + asserter *asserter.Asserter, +) Router { + return &NetworkAPIController{ + service: s, + asserter: asserter, + } } // Routes returns all of the api route for the NetworkAPIController @@ -72,7 +79,7 @@ func (c *NetworkAPIController) NetworkList(w http.ResponseWriter, r *http.Reques } // Assert that MetadataRequest is correct - if err := asserter.MetadataRequest(metadataRequest); err != nil { + if err := c.asserter.MetadataRequest(metadataRequest); err != nil { EncodeJSONResponse(&types.Error{ Message: err.Error(), }, http.StatusInternalServerError, w) @@ -102,7 +109,7 @@ func (c *NetworkAPIController) NetworkOptions(w http.ResponseWriter, r *http.Req } // Assert that NetworkRequest is correct - if err := asserter.NetworkRequest(networkRequest); err != nil { + if err := c.asserter.NetworkRequest(networkRequest); err != nil { EncodeJSONResponse(&types.Error{ Message: err.Error(), }, http.StatusInternalServerError, w) @@ -132,7 +139,7 @@ func (c *NetworkAPIController) NetworkStatus(w http.ResponseWriter, r *http.Requ } // Assert that NetworkRequest is correct - if err := asserter.NetworkRequest(networkRequest); err != nil { + if err := c.asserter.NetworkRequest(networkRequest); err != nil { EncodeJSONResponse(&types.Error{ Message: err.Error(), }, http.StatusInternalServerError, w) diff --git a/templates/server/controller-api.mustache b/templates/server/controller-api.mustache index 2e60ce81..c11d6a07 100644 --- a/templates/server/controller-api.mustache +++ b/templates/server/controller-api.mustache @@ -13,11 +13,18 @@ import ( // A {{classname}}Controller binds http requests to an api service and writes the service results to the http response type {{classname}}Controller struct { service {{classname}}Servicer + asserter *asserter.Asserter } // New{{classname}}Controller creates a default api controller -func New{{classname}}Controller(s {{classname}}Servicer) Router { - return &{{classname}}Controller{ service: s } +func New{{classname}}Controller( + s {{classname}}Servicer, + asserter *asserter.Asserter, +) Router { + return &{{classname}}Controller{ + service: s, + asserter: asserter, + } } // Routes returns all of the api route for the {{classname}}Controller @@ -45,7 +52,7 @@ func (c *{{classname}}Controller) {{nickname}}(w http.ResponseWriter, r *http.Re } // Assert that {{dataType}} is correct - if err := asserter.{{dataType}}({{paramName}}); err != nil { + if err := c.asserter.{{dataType}}({{paramName}}); err != nil { EncodeJSONResponse(&types.Error{ Message: err.Error(), }, http.StatusInternalServerError, w) From c24359da551f5101f366a033131e7aa2efdee53d Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Wed, 22 Apr 2020 10:55:17 -0700 Subject: [PATCH 11/13] Update examples --- examples/server/main.go | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/examples/server/main.go b/examples/server/main.go index 73d12b8e..3f468c0c 100644 --- a/examples/server/main.go +++ b/examples/server/main.go @@ -19,6 +19,7 @@ import ( "log" "net/http" + "github.com/coinbase/rosetta-sdk-go/asserter" "github.com/coinbase/rosetta-sdk-go/examples/server/services" "github.com/coinbase/rosetta-sdk-go/server" "github.com/coinbase/rosetta-sdk-go/types" @@ -30,12 +31,21 @@ const ( // NewBlockchainRouter creates a Mux http.Handler from a collection // of server controllers. -func NewBlockchainRouter(network *types.NetworkIdentifier) http.Handler { +func NewBlockchainRouter( + network *types.NetworkIdentifier, + asserter *asserter.Asserter, +) http.Handler { networkAPIService := services.NewNetworkAPIService(network) - networkAPIController := server.NewNetworkAPIController(networkAPIService) + networkAPIController := server.NewNetworkAPIController( + networkAPIService, + asserter, + ) blockAPIService := services.NewBlockAPIService(network) - blockAPIController := server.NewBlockAPIController(blockAPIService) + blockAPIController := server.NewBlockAPIController( + blockAPIService, + asserter, + ) return server.NewRouter(networkAPIController, blockAPIController) } @@ -46,7 +56,14 @@ func main() { Network: "Testnet", } - router := NewBlockchainRouter(network) + // The asserter automatically rejects incorrectly formatted + // requests. + asserter, err := asserter.NewServer([]*types.NetworkIdentifier{network}) + if err != nil { + log.Fatal(err) + } + + router := NewBlockchainRouter(network, asserter) log.Printf("Listening on port %d\n", serverPort) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", serverPort), router)) } From 90e17e15d59d70902449ba506bf5b09a2b58df8c Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Wed, 22 Apr 2020 10:58:28 -0700 Subject: [PATCH 12/13] Update naming of asserter modules --- asserter/asserter.go | 23 +++++++++++------------ asserter/asserter_test.go | 8 ++++---- asserter/block_test.go | 4 ++-- examples/client/main.go | 2 +- fetcher/fetcher.go | 2 +- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/asserter/asserter.go b/asserter/asserter.go index dedccabd..ba638dc1 100644 --- a/asserter/asserter.go +++ b/asserter/asserter.go @@ -58,10 +58,10 @@ func NewServer( }, nil } -// NewWithResponses constructs a new Asserter +// NewClientWithResponses constructs a new Asserter // from a NetworkStatusResponse and // NetworkOptionsResponse. -func NewWithResponses( +func NewClientWithResponses( network *types.NetworkIdentifier, networkStatus *types.NetworkStatusResponse, networkOptions *types.NetworkOptionsResponse, @@ -78,7 +78,7 @@ func NewWithResponses( return nil, err } - return NewWithOptions( + return NewClientWithOptions( network, networkStatus.GenesisBlockIdentifier, networkOptions.Allow.OperationTypes, @@ -96,12 +96,12 @@ type FileConfiguration struct { AllowedErrors []*types.Error `json:"allowed_errors"` } -// NewWithFile constructs a new Asserter using a specification +// NewClientWithFile constructs a new Asserter using a specification // file instead of responses. This can be useful for running reliable // systems that error when updates to the server (more error types, // more operations, etc.) significantly change how to parse the chain. // The filePath provided is parsed relative to the current directory. -func NewWithFile( +func NewClientWithFile( filePath string, ) (*Asserter, error) { content, err := ioutil.ReadFile(path.Clean(filePath)) @@ -114,7 +114,7 @@ func NewWithFile( return nil, err } - return NewWithOptions( + return NewClientWithOptions( config.NetworkIdentifier, config.GenesisBlockIdentifier, config.AllowedOperationTypes, @@ -123,11 +123,10 @@ func NewWithFile( ) } -// NewWithOptions constructs a new Asserter using the provided +// NewClientWithOptions constructs a new Asserter using the provided // arguments instead of using a NetworkStatusResponse and a -// NetworkOptionsResponse. NewWithOptions does not check the -// correctness of inputs. -func NewWithOptions( +// NetworkOptionsResponse. +func NewClientWithOptions( network *types.NetworkIdentifier, genesisBlockIdentifier *types.BlockIdentifier, operationTypes []string, @@ -169,9 +168,9 @@ func NewWithOptions( return asserter, nil } -// Configuration returns all variables currently set in an Asserter. +// ClientConfiguration returns all variables currently set in an Asserter. // This function will error if it is called on an uninitialized asserter. -func (a *Asserter) Configuration() ( +func (a *Asserter) ClientConfiguration() ( *types.NetworkIdentifier, *types.BlockIdentifier, []string, diff --git a/asserter/asserter_test.go b/asserter/asserter_test.go index f584a969..1ee0a3c7 100644 --- a/asserter/asserter_test.go +++ b/asserter/asserter_test.go @@ -210,7 +210,7 @@ func TestNew(t *testing.T) { for name, test := range tests { t.Run(fmt.Sprintf("%s with responses", name), func(t *testing.T) { - asserter, err := NewWithResponses( + asserter, err := NewClientWithResponses( test.network, test.networkStatus, test.networkOptions, @@ -223,7 +223,7 @@ func TestNew(t *testing.T) { } assert.NotNil(t, asserter) - network, genesis, opTypes, opStatuses, errors, err := asserter.Configuration() + network, genesis, opTypes, opStatuses, errors, err := asserter.ClientConfiguration() assert.NoError(t, err) assert.Equal(t, test.network, network) assert.Equal(t, test.networkStatus.GenesisBlockIdentifier, genesis) @@ -251,7 +251,7 @@ func TestNew(t *testing.T) { assert.NoError(t, err) assert.NoError(t, tmpfile.Close()) - asserter, err := NewWithFile( + asserter, err := NewClientWithFile( tmpfile.Name(), ) @@ -262,7 +262,7 @@ func TestNew(t *testing.T) { } assert.NotNil(t, asserter) - network, genesis, opTypes, opStatuses, errors, err := asserter.Configuration() + network, genesis, opTypes, opStatuses, errors, err := asserter.ClientConfiguration() assert.NoError(t, err) assert.Equal(t, test.network, network) assert.Equal(t, test.networkStatus.GenesisBlockIdentifier, genesis) diff --git a/asserter/block_test.go b/asserter/block_test.go index 56b1256f..0cf14864 100644 --- a/asserter/block_test.go +++ b/asserter/block_test.go @@ -383,7 +383,7 @@ func TestOperation(t *testing.T) { } for name, test := range tests { - asserter, err := NewWithResponses( + asserter, err := NewClientWithResponses( &types.NetworkIdentifier{ Blockchain: "hello", Network: "world", @@ -565,7 +565,7 @@ func TestBlock(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - asserter, err := NewWithResponses( + asserter, err := NewClientWithResponses( &types.NetworkIdentifier{ Blockchain: "hello", Network: "world", diff --git a/examples/client/main.go b/examples/client/main.go index 1dafa272..d65a593e 100644 --- a/examples/client/main.go +++ b/examples/client/main.go @@ -137,7 +137,7 @@ func main() { // // This will be used later to assert that a fetched block is // valid. - asserter, err := asserter.NewWithResponses( + asserter, err := asserter.NewClientWithResponses( primaryNetwork, networkStatus, networkOptions, diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go index 3ddb5ac2..c08354ac 100644 --- a/fetcher/fetcher.go +++ b/fetcher/fetcher.go @@ -151,7 +151,7 @@ func (f *Fetcher) InitializeAsserter( return nil, nil, err } - f.Asserter, err = asserter.NewWithResponses( + f.Asserter, err = asserter.NewClientWithResponses( primaryNetwork, networkStatus, networkOptions, From be0fae1934a376de5971cc88c88164ac1cfee93e Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Wed, 22 Apr 2020 11:38:29 -0700 Subject: [PATCH 13/13] Increase coverage --- asserter/asserter_test.go | 25 ++++++++++++++++++++++++ asserter/request.go | 6 +++++- asserter/request_test.go | 41 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/asserter/asserter_test.go b/asserter/asserter_test.go index 1ee0a3c7..2edb164b 100644 --- a/asserter/asserter_test.go +++ b/asserter/asserter_test.go @@ -271,4 +271,29 @@ func TestNew(t *testing.T) { assert.ElementsMatch(t, test.networkOptions.Allow.Errors, errors) }) } + + t.Run("non-existent file", func(t *testing.T) { + asserter, err := NewClientWithFile( + "blah", + ) + assert.Error(t, err) + assert.Nil(t, asserter) + }) + + t.Run("file not formatted correctly", func(t *testing.T) { + tmpfile, err := ioutil.TempFile("", "test.json") + assert.NoError(t, err) + defer os.Remove(tmpfile.Name()) + + _, err = tmpfile.Write([]byte("blah")) + assert.NoError(t, err) + assert.NoError(t, tmpfile.Close()) + + asserter, err := NewClientWithFile( + tmpfile.Name(), + ) + + assert.Nil(t, asserter) + assert.Error(t, err) + }) } diff --git a/asserter/request.go b/asserter/request.go index 1521c87c..ff384294 100644 --- a/asserter/request.go +++ b/asserter/request.go @@ -35,7 +35,7 @@ func SupportedNetworks(supportedNetworks []*types.NetworkIdentifier) error { } if containsNetworkIdentifier(parsed, network) { - return fmt.Errorf("supported network duplicate %+v", *network) + return fmt.Errorf("supported network duplicate %+v", network) } parsed[i] = network } @@ -210,6 +210,10 @@ func (a *Asserter) MempoolRequest(request *types.MempoolRequest) error { // MempoolTransactionRequest ensures that a types.MempoolTransactionRequest // is well-formatted. func (a *Asserter) MempoolTransactionRequest(request *types.MempoolTransactionRequest) error { + if a == nil { + return ErrAsserterNotInitialized + } + if request == nil { return errors.New("MempoolTransactionRequest is nil") } diff --git a/asserter/request_test.go b/asserter/request_test.go index 1a793cf0..3cc0b962 100644 --- a/asserter/request_test.go +++ b/asserter/request_test.go @@ -55,6 +55,47 @@ var ( } ) +func TestSupportedNetworks(t *testing.T) { + var tests = map[string]struct { + networks []*types.NetworkIdentifier + + err error + }{ + "valid networks": { + networks: []*types.NetworkIdentifier{ + validNetworkIdentifier, + wrongNetworkIdentifier, + }, + err: nil, + }, + "no valid networks": { + networks: []*types.NetworkIdentifier{}, + err: errors.New("no supported networks"), + }, + "invalid network": { + networks: []*types.NetworkIdentifier{ + { + Blockchain: "blah", + }, + }, + err: errors.New("NetworkIdentifier.Network is missing"), + }, + "duplicate networks": { + networks: []*types.NetworkIdentifier{ + validNetworkIdentifier, + validNetworkIdentifier, + }, + err: fmt.Errorf("supported network duplicate %+v", validNetworkIdentifier), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert.Equal(t, test.err, SupportedNetworks(test.networks)) + }) + } +} + func TestAccountBalanceRequest(t *testing.T) { var tests = map[string]struct { request *types.AccountBalanceRequest