Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions ethereum/eip712/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package eip712

import (
"fmt"
"strconv"
"strings"

"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
Expand Down Expand Up @@ -29,6 +31,11 @@ func createEIP712MessagePayload(data []byte) (eip712MessagePayload, error) {
return eip712MessagePayload{}, err
}

basicPayload, err = stringifyJSONMsgFields(basicPayload)
if err != nil {
return eip712MessagePayload{}, errorsmod.Wrap(err, "failed to stringify JSON message fields")
}

payload, numPayloadMsgs, err := FlattenPayloadMessages(basicPayload)
if err != nil {
return eip712MessagePayload{}, errorsmod.Wrap(err, "failed to flatten payload JSON messages")
Expand All @@ -48,6 +55,62 @@ func createEIP712MessagePayload(data []byte) (eip712MessagePayload, error) {
return messagePayload, nil
}

// stringifyJSONMsgFields converts object- and array-valued "msg" fields to
// strings. Message fields conventionally contain opaque JSON (for example,
// CosmWasm contract messages), whose runtime shape cannot be represented by a
// stable EIP-712 type. Existing string values are left alone so ordinary
// string fields and already-stringified JSON are not changed.
//
// The conversion only affects the derived EIP-712 payload. The protobuf
// transaction still contains the original value used during execution.
func stringifyJSONMsgFields(value gjson.Result) (gjson.Result, error) {
if !value.IsObject() && !value.IsArray() {
return value, nil
}

updated := value.Raw
var iterationErr error
value.ForEach(func(key, child gjson.Result) bool {
path := key.Str
if value.IsArray() {
path = strconv.FormatInt(key.Int(), 10)
} else {
path = escapeSJSONPath(path)
}

if !value.IsArray() && key.Str == "msg" && (child.IsObject() || child.IsArray()) {
updated, iterationErr = sjson.Set(updated, path, child.Raw)
return iterationErr == nil
}

if !child.IsObject() && !child.IsArray() {
return true
}

var transformed gjson.Result
transformed, iterationErr = stringifyJSONMsgFields(child)
if iterationErr != nil {
return false
}

updated, iterationErr = sjson.SetRaw(updated, path, transformed.Raw)
return iterationErr == nil
})
if iterationErr != nil {
return gjson.Result{}, iterationErr
}

return gjson.Parse(updated), nil
}

// escapeSJSONPath escapes object keys so they are treated as literal field
// names rather than SJSON path syntax.
func escapeSJSONPath(path string) string {
path = strings.ReplaceAll(path, `\`, `\\`)
path = strings.ReplaceAll(path, `.`, `\.`)
return strings.ReplaceAll(path, `:`, `\:`)
}

// unmarshalBytesToJSONObject converts a bytestream into
// a JSON object, then makes sure the JSON is an object.
func unmarshalBytesToJSONObject(data []byte) (gjson.Result, error) {
Expand Down
105 changes: 105 additions & 0 deletions ethereum/eip712/message_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package eip712

import (
"testing"

"github.com/ethereum/go-ethereum/signer/core/apitypes"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)

func TestCreateEIP712MessagePayloadStringifiesOpaqueJSONMsg(t *testing.T) {
t.Parallel()

data := []byte(`{
"account_number":"0",
"chain_id":"evm-1",
"fee":{"amount":[],"gas":"200000"},
"memo":"",
"msgs":[{
"type":"wasm/MsgExecuteContract",
"value":{
"contract":"cosmos1contract",
"funds":[],
"msg":{"transfer":{"amount":"10","recipient":"cosmos1recipient"}},
"sender":"cosmos1sender"
}
}],
"sequence":"0"
}`)

payload, err := createEIP712MessagePayload(data)
require.NoError(t, err)

msg := payload.payload.Get("msg0.value.msg")
require.Equal(t, gjson.String, msg.Type)
require.JSONEq(t, `{"transfer":{"amount":"10","recipient":"cosmos1recipient"}}`, msg.Str)

types, err := createEIP712Types(payload)
require.NoError(t, err)
require.Contains(t, types["TypeValue0"], apitypes.Type{Name: "msg", Type: ethString})
require.NotContains(t, types, "TypeValueMsg0")
}

func TestCreateEIP712MessagePayloadHandlesNestedAndArrayJSONMsg(t *testing.T) {
t.Parallel()

data := []byte(`{
"account_number":"0",
"chain_id":"evm-1",
"fee":{"amount":[],"gas":"200000"},
"memo":"",
"msgs":[{
"type":"cosmos.authz.v1beta1/MsgExec",
"value":{"msgs":[{"type":"wasm/MsgExecuteContract","value":{"msg":[{"mint":{}},2]}}]}
}],
"sequence":"0"
}`)

payload, err := createEIP712MessagePayload(data)
require.NoError(t, err)

msg := payload.payload.Get("msg0.value.msgs.0.value.msg")
require.Equal(t, gjson.String, msg.Type)
require.JSONEq(t, `[{"mint":{}},2]`, msg.Str)
}

func TestCreateEIP712MessagePayloadPreservesStringMsg(t *testing.T) {
t.Parallel()

for _, value := range []string{
`"ordinary text"`,
`"{\"already\":\"stringified\"}"`,
} {
data := []byte(`{
"account_number":"0",
"chain_id":"evm-1",
"fee":{"amount":[],"gas":"200000"},
"memo":"",
"msgs":[{"type":"example/Msg","value":{"msg":` + value + `}}],
"sequence":"0"
}`)

original := gjson.ParseBytes(data).Get("msgs.0.value.msg").Str
payload, err := createEIP712MessagePayload(data)
require.NoError(t, err)
require.Equal(t, original, payload.payload.Get("msg0.value.msg").Str)
}
}

func TestCreateEIP712MessagePayloadPreservesTypedObjects(t *testing.T) {
t.Parallel()

data := []byte(`{
"account_number":"0",
"chain_id":"evm-1",
"fee":{"amount":[],"gas":"200000"},
"memo":"",
"msgs":[{"type":"example/Msg","value":{"details":{"enabled":true},"msg":"text"}}],
"sequence":"0"
}`)

payload, err := createEIP712MessagePayload(data)
require.NoError(t, err)
require.True(t, payload.payload.Get("msg0.value.details").IsObject())
}