Skip to content

Commit

Permalink
feat(vlocalchain): use jsonp.Marshaller with OrigName: false for …
Browse files Browse the repository at this point in the history
…response marshalling (#9447)

closes: #9445 

## Description

Uses `jsonp.Marshaller` with `OrigName: false` for response marshalling
for `vlocalchain` queries and transactions to ensure responses sent to
the JS side of the bridge are camelCase instead of snake_case.

### Security Considerations


### Scaling Considerations


### Documentation Considerations


### Testing Considerations

Starts with new tests that have multiword response keys (e.g.
`completion_time`/`completionTime`,
`validator_address`/`validatorAddress`) to demonstrate current behavior.

### Upgrade Considerations

`vlocalchain` has yet to be released, but this is important to land in
the initial iteration to avoid future breaking changes.
  • Loading branch information
mergify[bot] committed Jun 4, 2024
2 parents 96dbb12 + bacefd1 commit a446a00
Show file tree
Hide file tree
Showing 5 changed files with 366 additions and 26 deletions.
38 changes: 38 additions & 0 deletions golang/cosmos/vm/proto_json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package vm

import (
"encoding/json"

"github.com/gogo/protobuf/jsonpb"
"github.com/gogo/protobuf/proto"
)

// We need jsonpb for its access to the global registry.
var marshaller = jsonpb.Marshaler{EmitDefaults: true}

func ProtoJSONMarshal(val interface{}) ([]byte, error) {
if pm, ok := val.(proto.Message); ok {
var s string
s, err := marshaller.MarshalToString(pm)
return []byte(s), err
}

// Marshal a non-proto value to JSON.
return json.Marshal(val)
}

// ProtoJSONMarshalSlice marshals a slice of proto messages and non-proto values to
// a single JSON byte slice.
func ProtoJSONMarshalSlice(vals []interface{}) ([]byte, error) {
var err error
jsonSlice := make([]json.RawMessage, len(vals))
for i, val := range vals {
jsonSlice[i], err = ProtoJSONMarshal(val)
if err != nil {
return nil, err
}
}

// Marshal the JSON array to a single JSON byte slice.
return json.Marshal(jsonSlice)
}
103 changes: 103 additions & 0 deletions golang/cosmos/vm/proto_json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package vm_test

import (
"bytes"
"strings"
"testing"

"github.com/Agoric/agoric-sdk/golang/cosmos/vm"

sdk "github.com/cosmos/cosmos-sdk/types"

banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)

func TestProtoJSONMarshal(t *testing.T) {
accAddr, err := sdk.AccAddressFromHexUnsafe("0123456789")
if err != nil {
panic(err)
}
valAddr, err := sdk.ValAddressFromHex("9876543210")
if err != nil {
panic(err)
}
coin := sdk.NewInt64Coin("uatom", 1234567)

testCases := []struct {
name string
create func() interface{}
expected string
}{
{
"nil",
func() interface{} {
return nil
},
`null`,
},
{
"primitive number",
func() interface{} {
return 12345
},
`12345`,
},
{
"MsgDelegate",
func() interface{} {
return stakingtypes.NewMsgDelegate(accAddr, valAddr, coin)
},
`{"delegatorAddress":"cosmos1qy352eufjjmc9c","validatorAddress":"cosmosvaloper1npm9gvss52mlmk","amount":{"denom":"uatom","amount":"1234567"}}`,
},
{
"QueryDenomOwnersResponse",
func() interface{} {
return &banktypes.QueryDenomOwnersResponse{
DenomOwners: []*banktypes.DenomOwner{
{
Address: accAddr.String(),
Balance: coin,
},
{
Address: valAddr.String(),
Balance: coin.Add(coin),
},
},
}
},
`{"denomOwners":[{"address":"cosmos1qy352eufjjmc9c","balance":{"denom":"uatom","amount":"1234567"}},{"address":"cosmosvaloper1npm9gvss52mlmk","balance":{"denom":"uatom","amount":"2469134"}}],"pagination":null}`,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
val := tc.create()
bz, err := vm.ProtoJSONMarshal(val)
if err != nil {
t.Errorf("ProtoJSONMarshal of %q failed %v", val, err)
}
if !bytes.Equal(bz, []byte(tc.expected)) {
t.Errorf("ProtoJSONMarshal of %q returned %q, expected %q", val, string(bz), tc.expected)
}
})
}

t.Run("all in a slice", func(t *testing.T) {
vals := make([]interface{}, len(testCases))
expectedJson := make([]string, len(testCases))
for i, tc := range testCases {
vals[i] = tc.create()
expectedJson[i] = tc.expected
}
bz, err := vm.ProtoJSONMarshalSlice(vals)
if err != nil {
t.Errorf("ProtoJSONMarshalSlice of %q failed %v", vals, err)
}

expected := "[" + strings.Join(expectedJson, ",") + "]"
if !bytes.Equal(bz, []byte(expected)) {
t.Errorf("ProtoJSONMarshalSlice of %q returned %q, expected %q", vals, string(bz), expected)
}
})
}
67 changes: 66 additions & 1 deletion golang/cosmos/x/vlocalchain/keeper/keeper_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
package keeper

import "testing"
import (
"testing"

"github.com/stretchr/testify/require"

"github.com/Agoric/agoric-sdk/golang/cosmos/app/params"
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
)


func TestKeeper_ParseRequestTypeURL(t *testing.T) {
testCases := []struct {
Expand Down Expand Up @@ -30,3 +39,59 @@ func TestKeeper_ParseRequestTypeURL(t *testing.T) {
})
}
}

func TestKeeper_DeserializeTxMessages(t *testing.T) {
encodingConfig := params.MakeEncodingConfig()
cdc := encodingConfig.Marshaler

banktypes.RegisterInterfaces(encodingConfig.InterfaceRegistry)

keeper := NewKeeper(cdc, nil, nil, nil)

expectedMsgSend := []sdk.Msg{
&banktypes.MsgSend{
FromAddress: "cosmos1abc",
ToAddress: "cosmos1xyz",
Amount: sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(100))),
},
}

testCases := []struct {
name string
json string
expected []sdk.Msg
wantErr bool
}{
{
name: "camelCase keys",
json: `{"messages":[{"@type":"/cosmos.bank.v1beta1.MsgSend","fromAddress":"cosmos1abc","toAddress":"cosmos1xyz","amount":[{"denom":"stake","amount":"100"}]}]}`,
expected: expectedMsgSend,
wantErr: false,
},
{
name: "snake_case keys",
json: `{"messages":[{"@type":"/cosmos.bank.v1beta1.MsgSend","from_address":"cosmos1abc","to_address":"cosmos1xyz","amount":[{"denom":"stake","amount":"100"}]}]}`,
expected: expectedMsgSend,
wantErr: false,
},
{
name: "misspelled key",
json: `{"messages":[{"@type":"/cosmos.bank.v1beta1.MsgSend","from_addresss":"cosmos1abc","to_address":"cosmos1xyz","amount":[{"denom":"stake","amount":"100"}]}]}`,
expected: expectedMsgSend,
wantErr: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
msgs, err := keeper.DeserializeTxMessages([]byte(tc.json))

if tc.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tc.expected, msgs)
}
})
}
}
47 changes: 26 additions & 21 deletions golang/cosmos/x/vlocalchain/vlocalchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/gogo/protobuf/jsonpb"

"github.com/Agoric/agoric-sdk/golang/cosmos/vm"
"github.com/Agoric/agoric-sdk/golang/cosmos/x/vlocalchain/keeper"
Expand Down Expand Up @@ -56,28 +55,36 @@ func (h portHandler) Receive(cctx context.Context, str string) (ret string, err
return
}

// We need jsonpb for its access to the global registry.
marshaller := jsonpb.Marshaler{EmitDefaults: true, OrigName: true}

var s string
resps := make([]json.RawMessage, len(qms))
var errs []error
resps := make([]interface{}, len(qms))
for i, qm := range qms {
var qr *types.QueryResponse
qr, err = h.keeper.Query(cctx, qm)
if err != nil {
return
}
if s, err = marshaller.MarshalToString(qr); err != nil {
return
if err == nil {
// Only fill out the response if the query was successful.
resps[i] = qr
} else {
errs = append(errs, err) // Accumulate errors
resps[i] = &types.QueryResponse{Error: err.Error()}
}
resps[i] = []byte(s)
}

var bz []byte
if bz, err = json.Marshal(resps); err != nil {
return
bz, err := vm.ProtoJSONMarshalSlice(resps)
if err != nil {
return "", err
}
ret = string(bz)

switch len(errs) {
case 0:
err = nil
case 1:
err = errs[0]
case len(resps):
err = fmt.Errorf("all queries in batch failed: %v", errs)
default:
// Let them inspect the individual errors manually.
}
return string(bz), err

case "VLOCALCHAIN_EXECUTE_TX":
origCtx := sdk.UnwrapSDKContext(cctx)
Expand All @@ -97,11 +104,9 @@ func (h portHandler) Receive(cctx context.Context, str string) (ret string, err
return
}

var bz []byte
if bz, err = json.Marshal(resps); err != nil {
return
}
ret = string(bz)
// Marshal the responses to proto3 JSON.
bz, e := vm.ProtoJSONMarshalSlice(resps)
return string(bz), e
default:
err = fmt.Errorf("unrecognized message type %s", msg.Type)
}
Expand Down
Loading

0 comments on commit a446a00

Please sign in to comment.