diff --git a/types/allow.go b/types/allow.go index decef8175..dee27bb8f 100644 --- a/types/allow.go +++ b/types/allow.go @@ -16,6 +16,8 @@ package types +import "encoding/json" + // Allow Allow specifies supported Operation status, Operation types, and all possible error // statuses. This Allow object is used by clients to validate the correctness of a Rosetta Server // implementation. It is expected that these clients will error if they receive some response that @@ -34,24 +36,78 @@ type Allow struct { // the past should set this to true. HistoricalBalanceLookup bool `json:"historical_balance_lookup"` // If populated, `timestamp_start_index` indicates the first block index where block timestamps - // are considered valid (i.e. all blocks less than `timestamp_start_index` could have invalid - // timestamps). This is useful when the genesis block (or blocks) of a network have timestamp 0. - // If not populated, block timestamps are assumed to be valid for all available blocks. + // are consistent. This is useful for networks that have had timestamp issues in the past. TimestampStartIndex *int64 `json:"timestamp_start_index,omitempty"` - // All methods that are supported by the /call endpoint. Communicating which parameters should - // be provided to /call is the responsibility of the implementer (this is en lieu of defining an - // entire type system and requiring the implementer to define that in Allow). - CallMethods []string `json:"call_methods"` - // BalanceExemptions is an array of BalanceExemption indicating which account balances could - // change without a corresponding Operation. BalanceExemptions should be used sparingly as they - // may introduce significant complexity for integrators that attempt to reconcile all account - // balance changes. If your implementation relies on any BalanceExemptions, you MUST implement - // historical balance lookup (the ability to query an account balance at any BlockIdentifier). - BalanceExemptions []*BalanceExemption `json:"balance_exemptions"` - // Any Rosetta implementation that can update an AccountIdentifier's unspent coins based on the - // contents of the mempool should populate this field as true. If false, requests to - // `/account/coins` that set `include_mempool` as true will be automatically rejected. - MempoolCoins bool `json:"mempool_coins"` - BlockHashCase Case `json:"block_hash_case,omitempty"` - TransactionHashCase Case `json:"transaction_hash_case,omitempty"` + // All Call.Method this implementation supports. Any method that is returned during parsing + // that is not listed here will cause client validation to error. + CallMethods []string `json:"call_methods,omitempty"` + // BalanceExemptions is an array of BalanceExemption indicating which + // SubAccountIdentifiers may not have a balance populated. This is useful + // for networks like Bitcoin where some UTXOs may not be spendable. + BalanceExemptions []*BalanceExemption `json:"balance_exemptions,omitempty"` + // MempoolCoins is a boolean indicating if the implementation supports + // returning unconfirmed coins from the mempool. If this is true, the + // implementation should return mempool coins in the /account/coins + // response. + MempoolCoins bool `json:"mempool_coins,omitempty"` + // BlockHashSignature indicates whether the implementation supports + // signing the block hash. If this is true, the implementation should + // support signing the block hash in the /construction/payloads + // endpoint. + BlockHashSignature bool `json:"block_hash_signature,omitempty"` + // RestrictedMetadata is a map of restricted metadata keys and their + // descriptions. This is used to indicate which metadata keys are + // restricted and should not be used by clients. + RestrictedMetadata map[string]string `json:"restricted_metadata,omitempty"` +} + +// MarshalJSON overrides the default JSON marshaler to ensure nil slices +// are encoded as empty arrays instead of null. This improves cross-language +// compatibility, as some languages (e.g., JavaScript) may choke on null +// when expecting an array. +// See: https://github.com/coinbase/mesh-sdk-go/issues/62 +func (a *Allow) MarshalJSON() ([]byte, error) { + type Alias Allow + + // Ensure nil slices are converted to empty slices + operationStatuses := a.OperationStatuses + if operationStatuses == nil { + operationStatuses = []*OperationStatus{} + } + + operationTypes := a.OperationTypes + if operationTypes == nil { + operationTypes = []string{} + } + + errors := a.Errors + if errors == nil { + errors = []*Error{} + } + + callMethods := a.CallMethods + if callMethods == nil { + callMethods = []string{} + } + + balanceExemptions := a.BalanceExemptions + if balanceExemptions == nil { + balanceExemptions = []*BalanceExemption{} + } + + return json.Marshal(&struct { + OperationStatuses []*OperationStatus `json:"operation_statuses"` + OperationTypes []string `json:"operation_types"` + Errors []*Error `json:"errors"` + CallMethods []string `json:"call_methods,omitempty"` + BalanceExemptions []*BalanceExemption `json:"balance_exemptions,omitempty"` + *Alias + }{ + OperationStatuses: operationStatuses, + OperationTypes: operationTypes, + Errors: errors, + CallMethods: callMethods, + BalanceExemptions: balanceExemptions, + Alias: (*Alias)(a), + }) } diff --git a/types/nil_slice_test.go b/types/nil_slice_test.go new file mode 100644 index 000000000..6b605e336 --- /dev/null +++ b/types/nil_slice_test.go @@ -0,0 +1,72 @@ +// Copyright 2024 Coinbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestNilSliceMarshaling tests that nil slices are encoded as empty arrays +// instead of null. This is important for cross-language compatibility. +// See: https://github.com/coinbase/mesh-sdk-go/issues/62 +func TestNilSliceMarshaling(t *testing.T) { + // Test Allow type with nil slices + allow := &Allow{ + OperationStatuses: nil, + OperationTypes: nil, + Errors: nil, + CallMethods: nil, + BalanceExemptions: nil, + HistoricalBalanceLookup: false, + } + + j, err := json.Marshal(allow) + assert.NoError(t, err) + + // Verify that nil slices are encoded as "[]" not "null" + assert.Contains(t, string(j), `"operation_statuses":[]`) + assert.Contains(t, string(j), `"operation_types":[]`) + assert.Contains(t, string(j), `"errors":[]`) + assert.Contains(t, string(j), `"call_methods":[]`) + assert.Contains(t, string(j), `"balance_exemptions":[]`) + + // Verify no "null" values for slices + assert.NotContains(t, string(j), `"operation_statuses":null`) + assert.NotContains(t, string(j), `"operation_types":null`) + assert.NotContains(t, string(j), `"errors":null`) +} + +// TestEmptySliceMarshaling tests that empty slices are still encoded correctly +func TestEmptySliceMarshaling(t *testing.T) { + allow := &Allow{ + OperationStatuses: []*OperationStatus{}, + OperationTypes: []string{}, + Errors: []*Error{}, + CallMethods: []string{}, + BalanceExemptions: []*BalanceExemption{}, + HistoricalBalanceLookup: false, + } + + j, err := json.Marshal(allow) + assert.NoError(t, err) + + // Verify that empty slices are encoded as "[]" + assert.Contains(t, string(j), `"operation_statuses":[]`) + assert.Contains(t, string(j), `"operation_types":[]`) + assert.Contains(t, string(j), `"errors":[]`) +}