From d1bf4634fee7223a541afa4bc38cec883e385d59 Mon Sep 17 00:00:00 2001 From: codehans <94654388+codehans@users.noreply.github.com> Date: Tue, 4 Aug 2026 12:17:32 +0100 Subject: [PATCH] stringify json in 712 --- ethereum/eip712/message.go | 63 +++++++++++++++++++ ethereum/eip712/message_test.go | 105 ++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 ethereum/eip712/message_test.go diff --git a/ethereum/eip712/message.go b/ethereum/eip712/message.go index 294489dbba..92eb7bb246 100644 --- a/ethereum/eip712/message.go +++ b/ethereum/eip712/message.go @@ -2,6 +2,8 @@ package eip712 import ( "fmt" + "strconv" + "strings" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -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") @@ -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) { diff --git a/ethereum/eip712/message_test.go b/ethereum/eip712/message_test.go new file mode 100644 index 0000000000..f53f708a26 --- /dev/null +++ b/ethereum/eip712/message_test.go @@ -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()) +}