diff --git a/docs/MIGRATING.md b/docs/MIGRATING.md index 7c6a6be34..95d4bcdec 100644 --- a/docs/MIGRATING.md +++ b/docs/MIGRATING.md @@ -33,6 +33,15 @@ whether the given string is a valid address. This was previously done internally using separate calls to `CanonicalizeAddress` and `HumanizeAddress` but can be done more efficiently using a single call. +- The IBC `TransferMsg` now includes an optional `Memo` field. +- `SubMsgResponse` now has an additional `MsgResponses` field, mirroring the + Cosmos SDK +- The types `Events`, `EventAttributes`, `Delegations`, `IBCChannels`, + `Validators`, `MsgResponses` and `Coins` were replaced with a generic + `Array[C]` type. This new type is a wrapper around a `[]C`. One difference to + the old behavior is that the new type will unmarshal to an empty slice when + the JSON value is `null` or `[]`. Previously, both cases resulted in a `nil` + value. ## Renamings diff --git a/ibc_test.go b/ibc_test.go index fb8a76a5b..4212bc19e 100644 --- a/ibc_test.go +++ b/ibc_test.go @@ -96,7 +96,7 @@ func TestIBCHandshake(t *testing.T) { // instantiate it with this store store := api.NewLookup(gasMeter1) goapi := api.NewMockAPI() - balance := types.Coins{} + balance := types.Array[types.Coin]{} querier := api.DefaultQuerier(api.MOCK_CONTRACT_ADDR, balance) // instantiate @@ -169,7 +169,7 @@ func TestIBCPacketDispatch(t *testing.T) { // instantiate it with this store store := api.NewLookup(gasMeter1) goapi := api.NewMockAPI() - balance := types.Coins{} + balance := types.Array[types.Coin]{} querier := api.DefaultQuerier(api.MOCK_CONTRACT_ADDR, balance) // instantiate @@ -210,9 +210,9 @@ func TestIBCPacketDispatch(t *testing.T) { ID: id, Result: types.SubMsgResult{ Ok: &types.SubMsgResponse{ - Events: types.Events{{ + Events: types.Array[types.Event]{{ Type: "instantiate", - Attributes: types.EventAttributes{ + Attributes: types.Array[types.EventAttribute]{ { Key: "_contract_address", Value: REFLECT_ADDR, @@ -249,7 +249,7 @@ func TestIBCPacketDispatch(t *testing.T) { Msgs: []types.CosmosMsg{{ Bank: &types.BankMsg{Send: &types.SendMsg{ ToAddress: "my-friend", - Amount: types.Coins{types.NewCoin(12345678, "uatom")}, + Amount: types.Array[types.Coin]{types.NewCoin(12345678, "uatom")}, }}, }}, }, diff --git a/internal/api/api_test.go b/internal/api/api_test.go index dc0921b4c..cc3f25e5b 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -24,7 +24,7 @@ func TestValidateAddressFailure(t *testing.T) { // instantiate it with this store store := NewLookup(gasMeter) api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Coins{types.NewCoin(100, "ATOM")}) + querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Array[types.Coin]{types.NewCoin(100, "ATOM")}) env := MockEnvBin(t) info := MockInfoBin(t, "creator") diff --git a/internal/api/iterator_test.go b/internal/api/iterator_test.go index 3585460fd..bf7a34635 100644 --- a/internal/api/iterator_test.go +++ b/internal/api/iterator_test.go @@ -31,7 +31,7 @@ func setupQueueContractWithData(t *testing.T, cache Cache, values ...int) queueD // instantiate it with this store store := NewLookup(gasMeter1) api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Coins{types.NewCoin(100, "ATOM")}) + querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Array[types.Coin]{types.NewCoin(100, "ATOM")}) env := MockEnvBin(t) info := MockInfoBin(t, "creator") msg := []byte(`{}`) diff --git a/internal/api/lib_test.go b/internal/api/lib_test.go index 5fe32790f..3d8725128 100644 --- a/internal/api/lib_test.go +++ b/internal/api/lib_test.go @@ -292,7 +292,7 @@ func TestGetMetrics(t *testing.T) { igasMeter := types.GasMeter(gasMeter) store := NewLookup(gasMeter) api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Coins{types.NewCoin(100, "ATOM")}) + querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Array[types.Coin]{types.NewCoin(100, "ATOM")}) env := MockEnvBin(t) info := MockInfoBin(t, "creator") msg1 := []byte(`{"verifier": "fred", "beneficiary": "bob"}`) @@ -397,7 +397,7 @@ func TestInstantiate(t *testing.T) { // instantiate it with this store store := NewLookup(gasMeter) api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Coins{types.NewCoin(100, "ATOM")}) + querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Array[types.Coin]{types.NewCoin(100, "ATOM")}) env := MockEnvBin(t) info := MockInfoBin(t, "creator") msg := []byte(`{"verifier": "fred", "beneficiary": "bob"}`) @@ -424,7 +424,7 @@ func TestExecute(t *testing.T) { // instantiate it with this store store := NewLookup(gasMeter1) api := NewMockAPI() - balance := types.Coins{types.NewCoin(250, "ATOM")} + balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} querier := DefaultQuerier(MOCK_CONTRACT_ADDR, balance) env := MockEnvBin(t) info := MockInfoBin(t, "creator") @@ -489,7 +489,7 @@ func TestExecutePanic(t *testing.T) { // instantiate it with this store store := NewLookup(gasMeter1) api := NewMockAPI() - balance := types.Coins{types.NewCoin(250, "ATOM")} + balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} querier := DefaultQuerier(MOCK_CONTRACT_ADDR, balance) env := MockEnvBin(t) info := MockInfoBin(t, "creator") @@ -518,7 +518,7 @@ func TestExecuteUnreachable(t *testing.T) { // instantiate it with this store store := NewLookup(gasMeter1) api := NewMockAPI() - balance := types.Coins{types.NewCoin(250, "ATOM")} + balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} querier := DefaultQuerier(MOCK_CONTRACT_ADDR, balance) env := MockEnvBin(t) info := MockInfoBin(t, "creator") @@ -702,7 +702,7 @@ func TestExecuteUserErrorsInApiCalls(t *testing.T) { igasMeter1 := types.GasMeter(gasMeter1) // instantiate it with this store store := NewLookup(gasMeter1) - balance := types.Coins{types.NewCoin(250, "ATOM")} + balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} querier := DefaultQuerier(MOCK_CONTRACT_ADDR, balance) env := MockEnvBin(t) info := MockInfoBin(t, "creator") @@ -733,7 +733,7 @@ func TestMigrate(t *testing.T) { // instantiate it with this store store := NewLookup(gasMeter) api := NewMockAPI() - balance := types.Coins{types.NewCoin(250, "ATOM")} + balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} querier := DefaultQuerier(MOCK_CONTRACT_ADDR, balance) env := MockEnvBin(t) info := MockInfoBin(t, "creator") @@ -778,7 +778,7 @@ func TestMultipleInstances(t *testing.T) { igasMeter1 := types.GasMeter(gasMeter1) store1 := NewLookup(gasMeter1) api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Coins{types.NewCoin(100, "ATOM")}) + querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Array[types.Coin]{types.NewCoin(100, "ATOM")}) env := MockEnvBin(t) info := MockInfoBin(t, "regen") msg := []byte(`{"verifier": "fred", "beneficiary": "bob"}`) @@ -832,7 +832,7 @@ func TestSudo(t *testing.T) { // instantiate it with this store store := NewLookup(gasMeter1) api := NewMockAPI() - balance := types.Coins{types.NewCoin(250, "ATOM")} + balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} querier := DefaultQuerier(MOCK_CONTRACT_ADDR, balance) env := MockEnvBin(t) info := MockInfoBin(t, "creator") @@ -862,7 +862,7 @@ func TestSudo(t *testing.T) { require.NotNil(t, dispatch.Bank.Send, "%#v", dispatch) send := dispatch.Bank.Send assert.Equal(t, "community-pool", send.ToAddress) - expectedPayout := types.Coins{types.NewCoin(700, "gold")} + expectedPayout := types.Array[types.Coin]{types.NewCoin(700, "gold")} assert.Equal(t, expectedPayout, send.Amount) } @@ -891,7 +891,7 @@ func TestDispatchSubmessage(t *testing.T) { ID: id, Msg: types.CosmosMsg{Bank: &types.BankMsg{Send: &types.SendMsg{ ToAddress: "friend", - Amount: types.Coins{types.NewCoin(1, "token")}, + Amount: types.Array[types.Coin]{types.NewCoin(1, "token")}, }}}, ReplyOn: types.ReplyAlways, } @@ -940,9 +940,9 @@ func TestReplyAndQuery(t *testing.T) { var id uint64 = 1234 data := []byte("foobar") - events := types.Events{{ + events := types.Array[types.Event]{{ Type: "message", - Attributes: types.EventAttributes{{ + Attributes: types.Array[types.EventAttribute]{{ Key: "signer", Value: "caller-addr", }}, @@ -1067,7 +1067,7 @@ func TestQuery(t *testing.T) { igasMeter1 := types.GasMeter(gasMeter1) store := NewLookup(gasMeter1) api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Coins{types.NewCoin(100, "ATOM")}) + querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Array[types.Coin]{types.NewCoin(100, "ATOM")}) env := MockEnvBin(t) info := MockInfoBin(t, "creator") msg := []byte(`{"verifier": "fred", "beneficiary": "bob"}`) @@ -1110,7 +1110,7 @@ func TestHackatomQuerier(t *testing.T) { igasMeter := types.GasMeter(gasMeter) store := NewLookup(gasMeter) api := NewMockAPI() - initBalance := types.Coins{types.NewCoin(1234, "ATOM"), types.NewCoin(65432, "ETH")} + initBalance := types.Array[types.Coin]{types.NewCoin(1234, "ATOM"), types.NewCoin(65432, "ETH")} querier := DefaultQuerier("foobar", initBalance) // make a valid query to the other address @@ -1153,7 +1153,7 @@ func TestCustomReflectQuerier(t *testing.T) { igasMeter := types.GasMeter(gasMeter) store := NewLookup(gasMeter) api := NewMockAPI() - initBalance := types.Coins{types.NewCoin(1234, "ATOM")} + initBalance := types.Array[types.Coin]{types.NewCoin(1234, "ATOM")} querier := DefaultQuerier(MOCK_CONTRACT_ADDR, initBalance) // we need this to handle the custom requests from the reflect contract innerQuerier := querier.(*MockQuerier) diff --git a/internal/api/mocks.go b/internal/api/mocks.go index dbc5eecc4..84fcd7e65 100644 --- a/internal/api/mocks.go +++ b/internal/api/mocks.go @@ -412,8 +412,8 @@ type MockQuerier struct { var _ types.Querier = &MockQuerier{} -func DefaultQuerier(contractAddr string, coins types.Coins) types.Querier { - balances := map[string]types.Coins{ +func DefaultQuerier(contractAddr string, coins types.Array[types.Coin]) types.Querier { + balances := map[string]types.Array[types.Coin]{ contractAddr: coins, } return &MockQuerier{ @@ -449,11 +449,11 @@ func (q MockQuerier) GasConsumed() uint64 { } type BankQuerier struct { - Balances map[string]types.Coins + Balances map[string]types.Array[types.Coin] } -func NewBankQuerier(balances map[string]types.Coins) BankQuerier { - bal := make(map[string]types.Coins, len(balances)) +func NewBankQuerier(balances map[string]types.Array[types.Coin]) BankQuerier { + bal := make(map[string]types.Array[types.Coin], len(balances)) for k, v := range balances { dst := make([]types.Coin, len(v)) copy(dst, v) @@ -540,7 +540,7 @@ func (q ReflectCustom) Query(request json.RawMessage) ([]byte, error) { func TestBankQuerierAllBalances(t *testing.T) { addr := "foobar" - balance := types.Coins{types.NewCoin(12345678, "ATOM"), types.NewCoin(54321, "ETH")} + balance := types.Array[types.Coin]{types.NewCoin(12345678, "ATOM"), types.NewCoin(54321, "ETH")} q := DefaultQuerier(addr, balance) // query existing account @@ -576,7 +576,7 @@ func TestBankQuerierAllBalances(t *testing.T) { func TestBankQuerierBalance(t *testing.T) { addr := "foobar" - balance := types.Coins{types.NewCoin(12345678, "ATOM"), types.NewCoin(54321, "ETH")} + balance := types.Array[types.Coin]{types.NewCoin(12345678, "ATOM"), types.NewCoin(54321, "ETH")} q := DefaultQuerier(addr, balance) // query existing account with matching denom diff --git a/lib_test.go b/lib_test.go index 57572adcf..4d7a4c957 100644 --- a/lib_test.go +++ b/lib_test.go @@ -139,7 +139,7 @@ func TestHappyPath(t *testing.T) { // instantiate it with this store store := api.NewLookup(gasMeter1) goapi := api.NewMockAPI() - balance := types.Coins{types.NewCoin(250, "ATOM")} + balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} querier := api.DefaultQuerier(api.MOCK_CONTRACT_ADDR, balance) // instantiate @@ -184,7 +184,7 @@ func TestEnv(t *testing.T) { // instantiate it with this store store := api.NewLookup(gasMeter1) goapi := api.NewMockAPI() - balance := types.Coins{types.NewCoin(250, "ATOM")} + balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} querier := api.DefaultQuerier(api.MOCK_CONTRACT_ADDR, balance) // instantiate @@ -264,7 +264,7 @@ func TestGetMetrics(t *testing.T) { // instantiate it with this store store := api.NewLookup(gasMeter1) goapi := api.NewMockAPI() - balance := types.Coins{types.NewCoin(250, "ATOM")} + balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} querier := api.DefaultQuerier(api.MOCK_CONTRACT_ADDR, balance) env := api.MockEnv() diff --git a/types/env.go b/types/env.go index 8ad0bbbe1..b175ab1e6 100644 --- a/types/env.go +++ b/types/env.go @@ -39,5 +39,5 @@ type MessageInfo struct { // Bech32 encoded sdk.AccAddress executing the contract Sender HumanAddress `json:"sender"` // Amount of funds send to the contract along with this message - Funds Coins `json:"funds"` + Funds Array[Coin] `json:"funds"` } diff --git a/types/env_test.go b/types/env_test.go index 2cc092cab..4bae5038a 100644 --- a/types/env_test.go +++ b/types/env_test.go @@ -34,10 +34,14 @@ func TestMessageInfoHandlesMissingCoins(t *testing.T) { require.NoError(t, err) // we can unmarshal it properly into struct + expected := MessageInfo{ + Sender: "baz", + Funds: []Coin{}, + } var recover MessageInfo err = json.Unmarshal(bz, &recover) require.NoError(t, err) - assert.Equal(t, info, recover) + assert.Equal(t, expected, recover) // make sure "funds":[] is in JSON var raw map[string]json.RawMessage diff --git a/types/ibc_test.go b/types/ibc_test.go index 73da7584b..8c17b558d 100644 --- a/types/ibc_test.go +++ b/types/ibc_test.go @@ -92,3 +92,28 @@ func TestIbcTimeoutDeserialization(t *testing.T) { Timestamp: 0, }, timeout4) } + +func TestIbcReceiveResponseDeserialization(t *testing.T) { + var err error + + // without acknowledgement + var resp IBCReceiveResponse + err = json.Unmarshal([]byte(`{"acknowledgement":null,"messages":[],"attributes":[],"events":[]}`), &resp) + require.NoError(t, err) + assert.Equal(t, IBCReceiveResponse{ + Acknowledgement: nil, + Messages: []SubMsg{}, + Attributes: []EventAttribute{}, + Events: []Event{}, + }, resp) + + // with acknowledgement + err = json.Unmarshal([]byte(`{"acknowledgement":"YWNr","messages":[],"attributes":[],"events":[]}`), &resp) + require.NoError(t, err) + assert.Equal(t, IBCReceiveResponse{ + Acknowledgement: []byte("ack"), + Messages: []SubMsg{}, + Attributes: []EventAttribute{}, + Events: []Event{}, + }, resp) +} diff --git a/types/msg.go b/types/msg.go index 6a6323443..016faaea0 100644 --- a/types/msg.go +++ b/types/msg.go @@ -31,61 +31,9 @@ type Response struct { Events []Event `json:"events"` } -// Events must encode empty array as [] -type Events []Event - -// MarshalJSON ensures that we get [] for empty arrays -func (e Events) MarshalJSON() ([]byte, error) { - if len(e) == 0 { - return []byte("[]"), nil - } - var raw []Event = e - return json.Marshal(raw) -} - -// UnmarshalJSON ensures that we get [] for empty arrays -func (e *Events) UnmarshalJSON(data []byte) error { - // make sure we deserialize [] back to null - if string(data) == "[]" || string(data) == "null" { - return nil - } - var raw []Event - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - *e = raw - return nil -} - type Event struct { - Type string `json:"type"` - Attributes EventAttributes `json:"attributes"` -} - -// EventAttributes must encode empty array as [] -type EventAttributes []EventAttribute - -// MarshalJSON ensures that we get [] for empty arrays -func (a EventAttributes) MarshalJSON() ([]byte, error) { - if len(a) == 0 { - return []byte("[]"), nil - } - var raw []EventAttribute = a - return json.Marshal(raw) -} - -// UnmarshalJSON ensures that we get [] for empty arrays -func (a *EventAttributes) UnmarshalJSON(data []byte) error { - // make sure we deserialize [] back to null - if string(data) == "[]" || string(data) == "null" { - return nil - } - var raw []EventAttribute - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - *a = raw - return nil + Type string `json:"type"` + Attributes Array[EventAttribute] `json:"attributes"` } // EventAttribute @@ -154,15 +102,15 @@ type BankMsg struct { // SendMsg contains instructions for a Cosmos-SDK/SendMsg // It has a fixed interface here and should be converted into the proper SDK format before dispatching type SendMsg struct { - ToAddress string `json:"to_address"` - Amount Coins `json:"amount"` + ToAddress string `json:"to_address"` + Amount Array[Coin] `json:"amount"` } // BurnMsg will burn the given coins from the contract's account. // There is no Cosmos SDK message that performs this, but it can be done by calling the bank keeper. // Important if a contract controls significant token supply that must be retired. type BurnMsg struct { - Amount Coins `json:"amount"` + Amount Array[Coin] `json:"amount"` } type IBCMsg struct { @@ -277,6 +225,7 @@ type TransferMsg struct { ToAddress string `json:"to_address"` Amount Coin `json:"amount"` Timeout IBCTimeout `json:"timeout"` + Memo string `json:"memo,omitempty"` } type SendPacketMsg struct { @@ -335,7 +284,7 @@ type WithdrawDelegatorRewardMsg struct { // `depositor` is automatically filled with the current contract's address type FundCommunityPoolMsg struct { // Amount is the list of coins to be send to the community pool - Amount Coins `json:"amount"` + Amount Array[Coin] `json:"amount"` } // AnyMsg is encoded the same way as a protobof [Any](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/any.proto). @@ -369,7 +318,7 @@ type ExecuteMsg struct { // as `userMsg` when calling `Handle` on the above-defined contract Msg []byte `json:"msg"` // Send is an optional amount of coins this contract sends to the called contract - Funds Coins `json:"funds"` + Funds Array[Coin] `json:"funds"` } // InstantiateMsg will create a new contract instance from a previously uploaded CodeID. @@ -381,7 +330,7 @@ type InstantiateMsg struct { // as `userMsg` when calling `Instantiate` on a new contract with the above-defined CodeID Msg []byte `json:"msg"` // Send is an optional amount of coins this contract sends to the called contract - Funds Coins `json:"funds"` + Funds Array[Coin] `json:"funds"` // Label is optional metadata to be stored with a contract instance. Label string `json:"label"` // Admin (optional) may be set here to allow future migrations from this address @@ -397,7 +346,7 @@ type Instantiate2Msg struct { // as `userMsg` when calling `Instantiate` on a new contract with the above-defined CodeID Msg []byte `json:"msg"` // Send is an optional amount of coins this contract sends to the called contract - Funds Coins `json:"funds"` + Funds Array[Coin] `json:"funds"` // Label is optional metadata to be stored with a contract instance. Label string `json:"label"` // Admin (optional) may be set here to allow future migrations from this address diff --git a/types/msg_test.go b/types/msg_test.go index cd8601505..f56915928 100644 --- a/types/msg_test.go +++ b/types/msg_test.go @@ -26,7 +26,7 @@ func TestWasmMsgInstantiateSerialization(t *testing.T) { require.Equal(t, "", msg.Instantiate.Admin) require.Equal(t, uint64(7897), msg.Instantiate.CodeID) require.Equal(t, []byte(`{"claim":{}}`), msg.Instantiate.Msg) - require.Equal(t, Coins{ + require.Equal(t, Array[Coin]{ {"stones", "321"}, }, msg.Instantiate.Funds) require.Equal(t, "my instance", msg.Instantiate.Label) @@ -47,9 +47,7 @@ func TestWasmMsgInstantiateSerialization(t *testing.T) { require.Equal(t, "king", msg.Instantiate.Admin) require.Equal(t, uint64(7897), msg.Instantiate.CodeID) require.Equal(t, []byte(`{"claim":{}}`), msg.Instantiate.Msg) - require.Equal(t, Coins{ - {"stones", "321"}, - }, msg.Instantiate.Funds) + require.Equal(t, Array[Coin]{}, msg.Instantiate.Funds) require.Equal(t, "my instance", msg.Instantiate.Label) } @@ -70,7 +68,7 @@ func TestWasmMsgInstantiate2Serialization(t *testing.T) { require.Equal(t, "", msg.Instantiate2.Admin) require.Equal(t, uint64(7897), msg.Instantiate2.CodeID) require.Equal(t, []byte(`{"claim":{}}`), msg.Instantiate2.Msg) - require.Equal(t, Coins{ + require.Equal(t, Array[Coin]{ {"stones", "321"}, }, msg.Instantiate2.Funds) require.Equal(t, "my instance", msg.Instantiate2.Label) @@ -161,5 +159,5 @@ func TestMsgFundCommunityPoolSerialization(t *testing.T) { err := json.Unmarshal(document, &msg) require.NoError(t, err) - require.Equal(t, Coins{{"adenom", "300"}, {"bdenom", "400"}}, msg.FundCommunityPool.Amount) + require.Equal(t, Array[Coin]{{"adenom", "300"}, {"bdenom", "400"}}, msg.FundCommunityPool.Amount) } diff --git a/types/queries.go b/types/queries.go index 32ee88dbe..748003bee 100644 --- a/types/queries.go +++ b/types/queries.go @@ -143,7 +143,7 @@ type AllBalancesQuery struct { // AllBalancesResponse is the expected response to AllBalancesQuery type AllBalancesResponse struct { - Amount Coins `json:"amount"` + Amount Array[Coin] `json:"amount"` } type DenomMetadataQuery struct { @@ -192,59 +192,7 @@ type ListChannelsQuery struct { } type ListChannelsResponse struct { - Channels IBCChannels `json:"channels"` -} - -// IBCChannels must JSON encode empty array as [] (not null) for consistency with Rust parser -type IBCChannels []IBCChannel - -// MarshalJSON ensures that we get [] for empty arrays -func (e IBCChannels) MarshalJSON() ([]byte, error) { - if len(e) == 0 { - return []byte("[]"), nil - } - var raw []IBCChannel = e - return json.Marshal(raw) -} - -// UnmarshalJSON ensures that we get [] for empty arrays -func (e *IBCChannels) UnmarshalJSON(data []byte) error { - // make sure we deserialize [] back to null - if string(data) == "[]" || string(data) == "null" { - return nil - } - var raw []IBCChannel - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - *e = raw - return nil -} - -// IBCEndpoints must JSON encode empty array as [] (not null) for consistency with Rust parser -type IBCEndpoints []IBCEndpoint - -// MarshalJSON ensures that we get [] for empty arrays -func (e IBCEndpoints) MarshalJSON() ([]byte, error) { - if len(e) == 0 { - return []byte("[]"), nil - } - var raw []IBCEndpoint = e - return json.Marshal(raw) -} - -// UnmarshalJSON ensures that we get [] for empty arrays -func (e *IBCEndpoints) UnmarshalJSON(data []byte) error { - // make sure we deserialize [] back to null - if string(data) == "[]" || string(data) == "null" { - return nil - } - var raw []IBCEndpoint - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - *e = raw - return nil + Channels Array[IBCChannel] `json:"channels"` } type ChannelQuery struct { @@ -270,33 +218,7 @@ type AllValidatorsQuery struct{} // AllValidatorsResponse is the expected response to AllValidatorsQuery type AllValidatorsResponse struct { - Validators Validators `json:"validators"` -} - -// Validators must JSON encode empty array as [] -type Validators []Validator - -// MarshalJSON ensures that we get [] for empty arrays -func (v Validators) MarshalJSON() ([]byte, error) { - if len(v) == 0 { - return []byte("[]"), nil - } - var raw []Validator = v - return json.Marshal(raw) -} - -// UnmarshalJSON ensures that we get [] for empty arrays -func (v *Validators) UnmarshalJSON(data []byte) error { - // make sure we deserialize [] back to null - if string(data) == "[]" || string(data) == "null" { - return nil - } - var raw []Validator - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - *v = raw - return nil + Validators Array[Validator] `json:"validators"` } type ValidatorQuery struct { @@ -330,32 +252,7 @@ type DelegationQuery struct { // AllDelegationsResponse is the expected response to AllDelegationsQuery type AllDelegationsResponse struct { - Delegations Delegations `json:"delegations"` -} - -type Delegations []Delegation - -// MarshalJSON ensures that we get [] for empty arrays -func (d Delegations) MarshalJSON() ([]byte, error) { - if len(d) == 0 { - return []byte("[]"), nil - } - var raw []Delegation = d - return json.Marshal(raw) -} - -// UnmarshalJSON ensures that we get [] for empty arrays -func (d *Delegations) UnmarshalJSON(data []byte) error { - // make sure we deserialize [] back to null - if string(data) == "[]" || string(data) == "null" { - return nil - } - var raw []Delegation - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - *d = raw - return nil + Delegations Array[Delegation] `json:"delegations"` } type Delegation struct { @@ -417,17 +314,17 @@ type DelegatorValidatorsResponse struct { Validators []string `json:"validators"` } -// DelegationResponse is the expected response to DelegationsQuery +// DelegationResponse is the expected response to Array[Delegation]Query type DelegationResponse struct { Delegation *FullDelegation `json:"delegation,omitempty"` } type FullDelegation struct { - Delegator string `json:"delegator"` - Validator string `json:"validator"` - Amount Coin `json:"amount"` - AccumulatedRewards Coins `json:"accumulated_rewards"` - CanRedelegate Coin `json:"can_redelegate"` + Delegator string `json:"delegator"` + Validator string `json:"validator"` + Amount Coin `json:"amount"` + AccumulatedRewards Array[Coin] `json:"accumulated_rewards"` + CanRedelegate Coin `json:"can_redelegate"` } type BondedDenomResponse struct { diff --git a/types/queries_test.go b/types/queries_test.go index 2ec87f68e..8dd52dc8d 100644 --- a/types/queries_test.go +++ b/types/queries_test.go @@ -9,19 +9,19 @@ import ( ) func TestDelegationWithEmptyArray(t *testing.T) { - var del Delegations + var del Array[Delegation] bz, err := json.Marshal(&del) require.NoError(t, err) assert.Equal(t, string(bz), `[]`) - var redel Delegations + var redel Array[Delegation] err = json.Unmarshal(bz, &redel) require.NoError(t, err) - assert.Nil(t, redel) + assert.Equal(t, Array[Delegation]{}, redel) } func TestDelegationWithData(t *testing.T) { - del := Delegations{{ + del := Array[Delegation]{{ Validator: "foo", Delegator: "bar", Amount: NewCoin(123, "stake"), @@ -29,26 +29,26 @@ func TestDelegationWithData(t *testing.T) { bz, err := json.Marshal(&del) require.NoError(t, err) - var redel Delegations + var redel Array[Delegation] err = json.Unmarshal(bz, &redel) require.NoError(t, err) assert.Equal(t, redel, del) } func TestValidatorWithEmptyArray(t *testing.T) { - var val Validators + var val Array[Validator] bz, err := json.Marshal(&val) require.NoError(t, err) assert.Equal(t, string(bz), `[]`) - var reval Validators + var reval Array[Validator] err = json.Unmarshal(bz, &reval) require.NoError(t, err) - assert.Nil(t, reval) + assert.Equal(t, Array[Validator]{}, reval) } func TestValidatorWithData(t *testing.T) { - val := Validators{{ + val := Array[Validator]{{ Address: "1234567890", Commission: "0.05", MaxCommission: "0.1", @@ -57,7 +57,7 @@ func TestValidatorWithData(t *testing.T) { bz, err := json.Marshal(&val) require.NoError(t, err) - var reval Validators + var reval Array[Validator] err = json.Unmarshal(bz, &reval) require.NoError(t, err) assert.Equal(t, reval, val) diff --git a/types/submessages.go b/types/submessages.go index 93eda1808..faaa14a53 100644 --- a/types/submessages.go +++ b/types/submessages.go @@ -81,6 +81,12 @@ type SubMsgResult struct { // with full Cosmos SDK events. // This mirrors Rust's SubMsgResponse. type SubMsgResponse struct { - Events Events `json:"events"` - Data []byte `json:"data,omitempty"` + Events Array[Event] `json:"events"` + Data []byte `json:"data,omitempty"` + MsgResponses Array[MsgResponse] `json:"msg_responses"` +} + +type MsgResponse struct { + TypeURL string `json:"type_url"` + Value []byte `json:"value"` } diff --git a/types/submessages_test.go b/types/submessages_test.go index 336ce1cdc..0c9ab5401 100644 --- a/types/submessages_test.go +++ b/types/submessages_test.go @@ -13,10 +13,10 @@ func TestReplySerialization(t *testing.T) { ID: 75, Result: SubMsgResult{ Ok: &SubMsgResponse{ - Events: []Event{ + Events: Array[Event]{ { Type: "hi", - Attributes: []EventAttribute{ + Attributes: Array[EventAttribute]{ { Key: "si", Value: "claro", @@ -25,10 +25,30 @@ func TestReplySerialization(t *testing.T) { }, }, Data: []byte{0x3f, 0x00, 0xaa, 0x5c, 0xab}, + MsgResponses: Array[MsgResponse]{ + MsgResponse{ + TypeURL: "/cosmos.bank.v1beta1.MsgSendResponse", + Value: []byte{}, + }, + }, }, }, } serialized, err := json.Marshal(&reply1) require.NoError(t, err) - require.Equal(t, `{"gas_used":4312324,"id":75,"result":{"ok":{"events":[{"type":"hi","attributes":[{"key":"si","value":"claro"}]}],"data":"PwCqXKs="}}}`, string(serialized)) + require.Equal(t, `{"gas_used":4312324,"id":75,"result":{"ok":{"events":[{"type":"hi","attributes":[{"key":"si","value":"claro"}]}],"data":"PwCqXKs=","msg_responses":[{"type_url":"/cosmos.bank.v1beta1.MsgSendResponse","value":""}]}}}`, string(serialized)) +} + +func TestSubMsgResponseSerialization(t *testing.T) { + response := SubMsgResponse{} + document, err := json.Marshal(response) + require.NoError(t, err) + require.Equal(t, `{"events":[],"msg_responses":[]}`, string(document)) + + // we really only care about marshal, but let's test unmarshal too + document2 := []byte(`{}`) + var response2 SubMsgResponse + err = json.Unmarshal(document2, &response2) + require.NoError(t, err) + require.Equal(t, response, response2) } diff --git a/types/types.go b/types/types.go index b98765b21..a8cca6dd6 100644 --- a/types/types.go +++ b/types/types.go @@ -55,7 +55,7 @@ type CanonicalAddress = []byte // Coin is a string representation of the sdk.Coin type (more portable than sdk.Int) type Coin struct { Denom string `json:"denom"` // type, eg. "ATOM" - Amount string `json:"amount"` // string encoing of decimal value, eg. "12.3456" + Amount string `json:"amount"` // string encoding of decimal value, eg. "12.3456" } func NewCoin(amount uint64, denom string) Coin { @@ -65,32 +65,6 @@ func NewCoin(amount uint64, denom string) Coin { } } -// Coins handles properly serializing empty amounts -type Coins []Coin - -// MarshalJSON ensures that we get [] for empty arrays -func (c Coins) MarshalJSON() ([]byte, error) { - if len(c) == 0 { - return []byte("[]"), nil - } - var d []Coin = c - return json.Marshal(d) -} - -// UnmarshalJSON ensures that we get [] for empty arrays -func (c *Coins) UnmarshalJSON(data []byte) error { - // make sure we deserialize [] back to null - if string(data) == "[]" || string(data) == "null" { - return nil - } - var d []Coin - if err := json.Unmarshal(data, &d); err != nil { - return err - } - *c = d - return nil -} - // Replicating the cosmos-sdk bank module Metadata type type DenomMetadata struct { Description string `json:"description"` @@ -209,3 +183,35 @@ type Metrics struct { // Cumulative size of all elements in memory cache (in bytes) SizeMemoryCache uint64 } + +// Array is a wrapper around a slice that ensures that we get "[]" JSON for nil values. +// When unmarshaling, we get an empty slice for "[]" and "null". +// +// This is needed for fields that are "Vec" on the Rust side because `null` values +// will result in an error there. +// Using this on a field with an "Option>" type on the Rust side would +// never result in a "None" value on the Rust side, making the "Option" pointless. +type Array[C any] []C + +// MarshalJSON ensures that we get "[]" for nil arrays +func (a Array[C]) MarshalJSON() ([]byte, error) { + if len(a) == 0 { + return []byte("[]"), nil + } + var raw []C = a + return json.Marshal(raw) +} + +// UnmarshalJSON ensures that we get an empty slice for "[]" and "null" +func (a *Array[C]) UnmarshalJSON(data []byte) error { + var raw []C + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + // make sure we deserialize [] back to empty slice + if len(raw) == 0 { + raw = []C{} + } + *a = raw + return nil +} diff --git a/types/types_test.go b/types/types_test.go index af1d472e5..172a3ca9c 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -84,3 +84,53 @@ func TestInt64JSON(t *testing.T) { err = json.Unmarshal([]byte(`""`), &i) require.EqualError(t, err, "cannot unmarshal \"\" into Int64, failed to parse integer") } + +func TestArraySerialization(t *testing.T) { + var arr Array[string] + + // unmarshal empty + err := json.Unmarshal([]byte(`[]`), &arr) + require.NoError(t, err) + require.Equal(t, Array[string]{}, arr) + err = json.Unmarshal([]byte(`[ ]`), &arr) + require.NoError(t, err) + require.Equal(t, Array[string]{}, arr) + err = json.Unmarshal([]byte(` []`), &arr) + require.NoError(t, err) + require.Equal(t, Array[string]{}, arr) + + // unmarshal null + err = json.Unmarshal([]byte(`null`), &arr) + require.NoError(t, err) + require.Equal(t, Array[string]{}, arr) + + // unmarshal filled + err = json.Unmarshal([]byte(`["a","b"]`), &arr) + require.NoError(t, err) + require.Equal(t, Array[string]{"a", "b"}, arr) + + // marshal filled + bz, err := json.Marshal(arr) + require.NoError(t, err) + require.Equal(t, `["a","b"]`, string(bz)) + + // marshal null + bz, err = json.Marshal(Array[string](nil)) + require.NoError(t, err) + require.Equal(t, `[]`, string(bz)) + + // marshal empty + bz, err = json.Marshal(Array[uint64]{}) + require.NoError(t, err) + require.Equal(t, `[]`, string(bz)) + + // unmarshal elements + var arr2 Array[uint64] + err = json.Unmarshal([]byte(`[1,2]`), &arr2) + require.NoError(t, err) + require.Equal(t, Array[uint64]{1, 2}, arr2) + // unmarshal null into the same pointer + err = json.Unmarshal([]byte(`null`), &arr2) + require.NoError(t, err) + require.Equal(t, Array[uint64]{}, arr2) +}