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/asserter.go b/asserter/asserter.go index 3cd9029e..ba638dc1 100644 --- a/asserter/asserter.go +++ b/asserter/asserter.go @@ -15,8 +15,11 @@ package asserter import ( - "context" + "encoding/json" "errors" + "fmt" + "io/ioutil" + "path" "github.com/coinbase/rosetta-sdk-go/types" ) @@ -30,20 +33,43 @@ 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 - genesisIndex int64 + 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 +// NewClientWithResponses constructs a new Asserter // from a NetworkStatusResponse and // NetworkOptionsResponse. -func NewWithResponses( - ctx context.Context, +func NewClientWithResponses( + 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 } @@ -52,28 +78,81 @@ func NewWithResponses( return nil, err } - return NewWithOptions( - ctx, + return NewClientWithOptions( + network, networkStatus.GenesisBlockIdentifier, networkOptions.Allow.OperationTypes, networkOptions.Allow.OperationStatuses, networkOptions.Allow.Errors, - ), nil + ) +} + +// 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"` } -// NewWithOptions constructs a new Asserter using the provided +// 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 NewClientWithFile( + filePath string, +) (*Asserter, error) { + content, err := ioutil.ReadFile(path.Clean(filePath)) + if err != nil { + return nil, err + } + + config := &FileConfiguration{} + if err := json.Unmarshal(content, config); err != nil { + return nil, err + } + + return NewClientWithOptions( + config.NetworkIdentifier, + config.GenesisBlockIdentifier, + config.AllowedOperationTypes, + config.AllowedOperationStatuses, + config.AllowedErrors, + ) +} + +// NewClientWithOptions constructs a new Asserter using the provided // arguments instead of using a NetworkStatusResponse and a // NetworkOptionsResponse. -func NewWithOptions( - ctx context.Context, +func NewClientWithOptions( + network *types.NetworkIdentifier, genesisBlockIdentifier *types.BlockIdentifier, 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 + } + + if err := OperationStatuses(operationStatuses); err != nil { + return nil, err + } + + if err := OperationTypes(operationTypes); err != nil { + return nil, err + } + asserter := &Asserter{ + network: network, operationTypes: operationTypes, - genesisIndex: genesisBlockIdentifier.Index, + genesisBlock: genesisBlockIdentifier, } asserter.operationStatusMap = map[string]bool{} @@ -86,5 +165,51 @@ func NewWithOptions( asserter.errorTypeMap[err.Code] = err } - return asserter + return asserter, nil +} + +// ClientConfiguration returns all variables currently set in an Asserter. +// This function will error if it is called on an uninitialized asserter. +func (a *Asserter) ClientConfiguration() ( + *types.NetworkIdentifier, + *types.BlockIdentifier, + []string, + []*types.OperationStatus, + []*types.Error, + error, +) { + if a == nil { + return nil, nil, nil, nil, nil, ErrAsserterNotInitialized + } + + 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, 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/asserter_test.go b/asserter/asserter_test.go index 3f9d861b..2edb164b 100644 --- a/asserter/asserter_test.go +++ b/asserter/asserter_test.go @@ -15,8 +15,11 @@ package asserter import ( - "context" + "encoding/json" "errors" + "fmt" + "io/ioutil" + "os" "testing" "github.com/coinbase/rosetta-sdk-go/types" @@ -24,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, @@ -35,7 +43,7 @@ func TestNewWithResponses(t *testing.T) { Index: 100, Hash: "block 100", }, - CurrentBlockTimestamp: 100, + CurrentBlockTimestamp: MinUnixEpoch + 1, Peers: []*types.Peer{ { PeerID: "peer 1", @@ -44,11 +52,11 @@ 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, + CurrentBlockTimestamp: MinUnixEpoch + 1, Peers: []*types.Peer{ { PeerID: "peer 1", @@ -82,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{ { @@ -91,6 +151,7 @@ func TestNewWithResponses(t *testing.T) { }, OperationTypes: []string{ "Transfer", + "Transfer", }, Errors: []*types.Error{ { @@ -104,44 +165,135 @@ 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) { - asserter, err := NewWithResponses( - context.Background(), + t.Run(fmt.Sprintf("%s with responses", name), func(t *testing.T) { + asserter, err := NewClientWithResponses( + 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.ClientConfiguration() + assert.NoError(t, err) + assert.Equal(t, test.network, network) + assert.Equal(t, test.networkStatus.GenesisBlockIdentifier, genesis) + assert.ElementsMatch(t, test.networkOptions.Allow.OperationTypes, opTypes) + assert.ElementsMatch(t, test.networkOptions.Allow.OperationStatuses, opStatuses) + assert.ElementsMatch(t, test.networkOptions.Allow.Errors, errors) + }) + + 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 := NewClientWithFile( + tmpfile.Name(), + ) assert.Equal(t, test.err, err) + + if test.err != nil { + return + } + + assert.NotNil(t, asserter) + 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) + 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("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/block.go b/asserter/block.go index 384c6537..f4ade1a1 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 = 946713600000 + + // MaxUnixEpoch is the unix epoch time in milliseconds of + // 01/01/2040 at 12:00:00 AM. + MaxUnixEpoch = 2209017600000 +) + // Amount ensures a types.Amount has an // integer value, specified precision, and symbol. func Amount(amount *types.Amount) error { @@ -49,18 +59,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 +99,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 +161,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 { @@ -256,11 +262,14 @@ 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) + switch { + case timestamp < MinUnixEpoch: + return fmt.Errorf("Timestamp %d is before 01/01/2000", timestamp) + case timestamp > MaxUnixEpoch: + return fmt.Errorf("Timestamp %d is after 01/01/2040", timestamp) + default: + return nil } - - return nil } // Block runs a basic set of assertions for each returned block. @@ -285,7 +294,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") } @@ -293,10 +302,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 c7f12e50..0cf14864 100644 --- a/asserter/block_test.go +++ b/asserter/block_test.go @@ -15,7 +15,6 @@ package asserter import ( - "context" "errors" "fmt" "testing" @@ -384,8 +383,11 @@ func TestOperation(t *testing.T) { } for name, test := range tests { - asserter, err := NewWithResponses( - context.Background(), + asserter, err := NewClientWithResponses( + &types.NetworkIdentifier{ + Blockchain: "hello", + Network: "world", + }, &types.NetworkStatusResponse{ GenesisBlockIdentifier: &types.BlockIdentifier{ Index: 0, @@ -395,7 +397,7 @@ func TestOperation(t *testing.T) { Index: 100, Hash: "block 100", }, - CurrentBlockTimestamp: 100, + CurrentBlockTimestamp: MinUnixEpoch + 1, Peers: []*types.Peer{ { PeerID: "peer 1", @@ -462,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, @@ -471,7 +473,6 @@ func TestBlock(t *testing.T) { block: &types.Block{ BlockIdentifier: validBlockIdentifier, ParentBlockIdentifier: validBlockIdentifier, - Timestamp: 1, Transactions: []*types.Transaction{validTransaction}, }, genesisIndex: validBlockIdentifier.Index, @@ -485,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"), @@ -494,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"), @@ -503,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"), @@ -515,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"), @@ -527,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 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 is invalid 0"), + 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{ {}, }, @@ -555,8 +565,11 @@ func TestBlock(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - asserter, err := NewWithResponses( - context.Background(), + asserter, err := NewClientWithResponses( + &types.NetworkIdentifier{ + Blockchain: "hello", + Network: "world", + }, &types.NetworkStatusResponse{ GenesisBlockIdentifier: &types.BlockIdentifier{ Index: test.genesisIndex, @@ -566,7 +579,7 @@ func TestBlock(t *testing.T) { Index: 100, Hash: "block 100", }, - CurrentBlockTimestamp: 100, + CurrentBlockTimestamp: MinUnixEpoch + 1, Peers: []*types.Peer{ { PeerID: "peer 1", 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/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 } diff --git a/asserter/request.go b/asserter/request.go index 653d4cbe..ff384294 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,29 @@ 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 a == nil { + return ErrAsserterNotInitialized + } + if request == nil { return errors.New("MempoolTransactionRequest is nil") } @@ -127,12 +222,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 +245,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) } diff --git a/asserter/request_test.go b/asserter/request_test.go index 85a33c99..3cc0b962 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", } @@ -49,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 @@ -61,6 +108,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 +151,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 +182,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 +216,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 +239,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 +276,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) }) } @@ -213,17 +294,24 @@ func TestConstructionMetadataRequest(t *testing.T) { "valid request": { request: &types.ConstructionMetadataRequest{ NetworkIdentifier: validNetworkIdentifier, - Options: &map[string]interface{}{}, + Options: map[string]interface{}{}, }, 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"), }, "missing network": { request: &types.ConstructionMetadataRequest{ - Options: &map[string]interface{}{}, + Options: map[string]interface{}{}, }, err: errors.New("NetworkIdentifier is nil"), }, @@ -237,7 +325,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 +342,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 +387,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 +405,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 +427,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 +455,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 +482,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 +503,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 +521,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/codegen.sh b/codegen.sh index 3d63e13e..7408345a 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 \ @@ -115,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/examples/client/main.go b/examples/client/main.go index 2c26d287..d65a593e 100644 --- a/examples/client/main.go +++ b/examples/client/main.go @@ -137,8 +137,8 @@ func main() { // // This will be used later to assert that a fetched block is // valid. - asserter, err := asserter.NewWithResponses( - ctx, + asserter, err := asserter.NewClientWithResponses( + primaryNetwork, 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/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)) } 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/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() 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/fetcher.go b/fetcher/fetcher.go index a97e5a9f..c08354ac 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 { @@ -152,8 +151,8 @@ func (f *Fetcher) InitializeAsserter( return nil, nil, err } - f.Asserter, err = asserter.NewWithResponses( - ctx, + f.Asserter, err = asserter.NewClientWithResponses( + primaryNetwork, networkStatus, networkOptions, ) 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/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) 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"` }