From d7d371c51c5ecb13072a8997a700db2c1f0ffabd Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Thu, 19 Dec 2024 11:24:27 -0500 Subject: [PATCH] ethcoder: typed data marshaljson (#155) --- ethcoder/typed_data.go | 233 ------------------------- ethcoder/typed_data_json.go | 337 ++++++++++++++++++++++++++++++++++++ ethcoder/typed_data_test.go | 121 ++++++++++++- 3 files changed, 456 insertions(+), 235 deletions(-) create mode 100644 ethcoder/typed_data_json.go diff --git a/ethcoder/typed_data.go b/ethcoder/typed_data.go index f57c014..d193688 100644 --- a/ethcoder/typed_data.go +++ b/ethcoder/typed_data.go @@ -2,10 +2,8 @@ package ethcoder import ( "bytes" - "encoding/json" "fmt" "math/big" - "slices" "sort" "strings" @@ -269,234 +267,3 @@ func (t *TypedData) EncodeDigest() ([]byte, error) { } return digest, nil } - -func TypedDataFromJSON(typedDataJSON string) (*TypedData, error) { - var typedData TypedData - err := json.Unmarshal([]byte(typedDataJSON), &typedData) - if err != nil { - return nil, err - } - return &typedData, nil -} - -func (t *TypedData) UnmarshalJSON(data []byte) error { - // Intermediary structure to decode message field - type TypedDataRaw struct { - Types TypedDataTypes `json:"types"` - PrimaryType string `json:"primaryType"` - Domain struct { - Name string `json:"name,omitempty"` - Version string `json:"version,omitempty"` - ChainID interface{} `json:"chainId,omitempty"` - VerifyingContract *common.Address `json:"verifyingContract,omitempty"` - Salt *common.Hash `json:"salt,omitempty"` - } `json:"domain"` - Message map[string]interface{} `json:"message"` - } - - // Json decoder with json.Number support, so that we can decode big.Int values - dec := json.NewDecoder(bytes.NewReader(data)) - dec.UseNumber() - - var raw TypedDataRaw - if err := dec.Decode(&raw); err != nil { - return err - } - - // Ensure the "EIP712Domain" type is defined. In case its not defined - // we will add it to the types map - _, ok := raw.Types["EIP712Domain"] - if !ok { - raw.Types["EIP712Domain"] = []TypedDataArgument{} - if raw.Domain.Name != "" { - raw.Types["EIP712Domain"] = append(raw.Types["EIP712Domain"], TypedDataArgument{Name: "name", Type: "string"}) - } - if raw.Domain.Version != "" { - raw.Types["EIP712Domain"] = append(raw.Types["EIP712Domain"], TypedDataArgument{Name: "version", Type: "string"}) - } - if raw.Domain.ChainID != nil { - raw.Types["EIP712Domain"] = append(raw.Types["EIP712Domain"], TypedDataArgument{Name: "chainId", Type: "uint256"}) - } - if raw.Domain.VerifyingContract != nil { - raw.Types["EIP712Domain"] = append(raw.Types["EIP712Domain"], TypedDataArgument{Name: "verifyingContract", Type: "address"}) - } - if raw.Domain.Salt != nil { - raw.Types["EIP712Domain"] = append(raw.Types["EIP712Domain"], TypedDataArgument{Name: "salt", Type: "bytes32"}) - } - } - - // Ensure primary type is defined - if raw.PrimaryType == "" { - // detect primary type if its unspecified - primaryType, err := typedDataDetectPrimaryType(raw.Types.Map(), raw.Message) - if err != nil { - return err - } - raw.PrimaryType = primaryType - } - _, ok = raw.Types[raw.PrimaryType] - if !ok { - return fmt.Errorf("primary type '%s' is not defined", raw.PrimaryType) - } - - // Decode the domain, which is mostly decooded except the chainId is an interface{} type - // because the value may be a number of a hex encoded number. We want it in a big.Int. - domain := TypedDataDomain{ - Name: raw.Domain.Name, - Version: raw.Domain.Version, - ChainID: nil, - VerifyingContract: raw.Domain.VerifyingContract, - Salt: raw.Domain.Salt, - } - if raw.Domain.ChainID != nil { - chainID := big.NewInt(0) - if val, ok := raw.Domain.ChainID.(float64); ok { - chainID.SetInt64(int64(val)) - } else if val, ok := raw.Domain.ChainID.(json.Number); ok { - chainID.SetString(val.String(), 10) - } else if val, ok := raw.Domain.ChainID.(string); ok { - if strings.HasPrefix(val, "0x") { - chainID.SetString(val[2:], 16) - } else { - chainID.SetString(val, 10) - } - } - domain.ChainID = chainID - } - - // Decode the raw message into Go runtime types - message, err := typedDataDecodeRawMessageMap(raw.Types.Map(), raw.PrimaryType, raw.Message) - if err != nil { - return err - } - - t.Types = raw.Types - t.PrimaryType = raw.PrimaryType - t.Domain = domain - - m, ok := message.(map[string]interface{}) - if !ok { - return fmt.Errorf("resulting message is not a map") - } - t.Message = m - - return nil -} - -func typedDataDetectPrimaryType(typesMap map[string]map[string]string, message map[string]interface{}) (string, error) { - // If there are only two types, and one is the EIP712Domain, then the other is the primary type - if len(typesMap) == 2 { - _, ok := typesMap["EIP712Domain"] - if ok { - for typ := range typesMap { - if typ == "EIP712Domain" { - continue - } - return typ, nil - } - } - } - - // Otherwise search for the primary type by looking for the first type that has a message field keys - messageKeys := []string{} - for k := range message { - messageKeys = append(messageKeys, k) - } - sort.Strings(messageKeys) - - for typ := range typesMap { - if typ == "EIP712Domain" { - continue - } - if len(typesMap[typ]) != len(messageKeys) { - continue - } - - typKeys := []string{} - for k := range typesMap[typ] { - typKeys = append(typKeys, k) - } - sort.Strings(typKeys) - - if !slices.Equal(messageKeys, typKeys) { - continue - } - return typ, nil - } - - return "", fmt.Errorf("no primary type found") -} - -func typedDataDecodeRawMessageMap(typesMap map[string]map[string]string, primaryType string, data interface{}) (interface{}, error) { - // Handle array types - if arr, ok := data.([]interface{}); ok { - results := make([]interface{}, len(arr)) - for i, item := range arr { - decoded, err := typedDataDecodeRawMessageMap(typesMap, primaryType, item) - if err != nil { - return nil, err - } - results[i] = decoded - } - return results, nil - } - - // Handle primitive directly - message, ok := data.(map[string]interface{}) - if !ok { - return typedDataDecodePrimitiveValue(primaryType, data) - } - - currentType, ok := typesMap[primaryType] - if !ok { - return nil, fmt.Errorf("type %s is not defined", primaryType) - } - - processedMessage := make(map[string]interface{}) - for k, v := range message { - typ, ok := currentType[k] - if !ok { - return nil, fmt.Errorf("message field '%s' is missing type definition on '%s'", k, primaryType) - } - - // Extract base type and check if it's an array - baseType := typ - isArray := false - if idx := strings.Index(typ, "["); idx != -1 { - baseType = typ[:idx] - isArray = true - } - - // Process value based on whether it's a custom or primitive type - if _, isCustomType := typesMap[baseType]; isCustomType { - decoded, err := typedDataDecodeRawMessageMap(typesMap, baseType, v) - if err != nil { - return nil, err - } - processedMessage[k] = decoded - } else { - var decoded interface{} - var err error - if isArray { - decoded, err = typedDataDecodeRawMessageMap(typesMap, baseType, v) - } else { - decoded, err = typedDataDecodePrimitiveValue(baseType, v) - } - if err != nil { - return nil, fmt.Errorf("failed to decode field '%s': %w", k, err) - } - processedMessage[k] = decoded - } - } - - return processedMessage, nil -} - -func typedDataDecodePrimitiveValue(typ string, value interface{}) (interface{}, error) { - val := fmt.Sprintf("%v", value) - out, err := ABIUnmarshalStringValuesAny([]string{typ}, []any{val}) - if err != nil { - return nil, fmt.Errorf("typedDataDecodePrimitiveValue: %w", err) - } - return out[0], nil -} diff --git a/ethcoder/typed_data_json.go b/ethcoder/typed_data_json.go new file mode 100644 index 0000000..6d8645b --- /dev/null +++ b/ethcoder/typed_data_json.go @@ -0,0 +1,337 @@ +package ethcoder + +import ( + "bytes" + "encoding/json" + "fmt" + "math/big" + "slices" + "sort" + "strings" + + "github.com/0xsequence/ethkit/go-ethereum/common" +) + +func TypedDataFromJSON(typedDataJSON string) (*TypedData, error) { + var typedData TypedData + err := json.Unmarshal([]byte(typedDataJSON), &typedData) + if err != nil { + return nil, err + } + return &typedData, nil +} + +func (t *TypedData) MarshalJSON() ([]byte, error) { + type TypedDataJSON struct { + Types TypedDataTypes `json:"types"` + PrimaryType string `json:"primaryType"` + Domain TypedDataDomain `json:"domain"` + Message map[string]interface{} `json:"message"` + } + + encodedMessage, err := t.jsonEncodeMessageValues(t.PrimaryType, t.Message) + if err != nil { + return nil, err + } + + return json.Marshal(TypedDataJSON{ + Types: t.Types, + PrimaryType: t.PrimaryType, + Domain: t.Domain, + Message: encodedMessage, + }) +} + +func (t *TypedData) jsonEncodeMessageValues(typeName string, message map[string]interface{}) (map[string]interface{}, error) { + typeFields, ok := t.Types[typeName] + if !ok { + return nil, fmt.Errorf("type '%s' not found in types", typeName) + } + + encodedMessage := make(map[string]interface{}) + + for _, field := range typeFields { + val, exists := message[field.Name] + if !exists { + continue + } + + // Handle arrays + if strings.HasSuffix(field.Type, "[]") { + baseType := field.Type[:len(field.Type)-2] + if arr, ok := val.([]interface{}); ok { + encodedArr := make([]interface{}, len(arr)) + for i, item := range arr { + encoded, err := t.jsonEncodeValue(baseType, item) + if err != nil { + return nil, err + } + encodedArr[i] = encoded + } + encodedMessage[field.Name] = encodedArr + continue + } + } + + // Handle single values + encoded, err := t.jsonEncodeValue(field.Type, val) + if err != nil { + return nil, err + } + encodedMessage[field.Name] = encoded + } + + return encodedMessage, nil +} + +func (t *TypedData) jsonEncodeValue(fieldType string, value interface{}) (interface{}, error) { + // Handle bytes/bytes32 + if strings.HasPrefix(fieldType, "bytes") { + switch v := value.(type) { + case []byte: + return "0x" + common.Bytes2Hex(v), nil + case [8]byte: + return "0x" + common.Bytes2Hex(v[:]), nil + case [16]byte: + return "0x" + common.Bytes2Hex(v[:]), nil + case [24]byte: + return "0x" + common.Bytes2Hex(v[:]), nil + case [32]byte: + return "0x" + common.Bytes2Hex(v[:]), nil + } + return value, nil + } + + // Handle nested custom types + if _, isCustomType := t.Types[fieldType]; isCustomType { + if nestedMsg, ok := value.(map[string]interface{}); ok { + return t.jsonEncodeMessageValues(fieldType, nestedMsg) + } + return nil, fmt.Errorf("value for custom type '%s' is not a map", fieldType) + } + + // Return primitive values as-is + return value, nil +} + +func (t *TypedData) UnmarshalJSON(data []byte) error { + // Intermediary structure to decode message field + type TypedDataRaw struct { + Types TypedDataTypes `json:"types"` + PrimaryType string `json:"primaryType"` + Domain struct { + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` + ChainID interface{} `json:"chainId,omitempty"` + VerifyingContract *common.Address `json:"verifyingContract,omitempty"` + Salt *common.Hash `json:"salt,omitempty"` + } `json:"domain"` + Message map[string]interface{} `json:"message"` + } + + // Json decoder with json.Number support, so that we can decode big.Int values + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + + var raw TypedDataRaw + if err := dec.Decode(&raw); err != nil { + return err + } + + // Ensure the "EIP712Domain" type is defined. In case its not defined + // we will add it to the types map + _, ok := raw.Types["EIP712Domain"] + if !ok { + raw.Types["EIP712Domain"] = []TypedDataArgument{} + if raw.Domain.Name != "" { + raw.Types["EIP712Domain"] = append(raw.Types["EIP712Domain"], TypedDataArgument{Name: "name", Type: "string"}) + } + if raw.Domain.Version != "" { + raw.Types["EIP712Domain"] = append(raw.Types["EIP712Domain"], TypedDataArgument{Name: "version", Type: "string"}) + } + if raw.Domain.ChainID != nil { + raw.Types["EIP712Domain"] = append(raw.Types["EIP712Domain"], TypedDataArgument{Name: "chainId", Type: "uint256"}) + } + if raw.Domain.VerifyingContract != nil { + raw.Types["EIP712Domain"] = append(raw.Types["EIP712Domain"], TypedDataArgument{Name: "verifyingContract", Type: "address"}) + } + if raw.Domain.Salt != nil { + raw.Types["EIP712Domain"] = append(raw.Types["EIP712Domain"], TypedDataArgument{Name: "salt", Type: "bytes32"}) + } + } + + // Ensure primary type is defined + if raw.PrimaryType == "" { + // detect primary type if its unspecified + primaryType, err := typedDataDetectPrimaryType(raw.Types.Map(), raw.Message) + if err != nil { + return err + } + raw.PrimaryType = primaryType + } + _, ok = raw.Types[raw.PrimaryType] + if !ok { + return fmt.Errorf("primary type '%s' is not defined", raw.PrimaryType) + } + + // Decode the domain, which is mostly decooded except the chainId is an interface{} type + // because the value may be a number of a hex encoded number. We want it in a big.Int. + domain := TypedDataDomain{ + Name: raw.Domain.Name, + Version: raw.Domain.Version, + ChainID: nil, + VerifyingContract: raw.Domain.VerifyingContract, + Salt: raw.Domain.Salt, + } + if raw.Domain.ChainID != nil { + chainID := big.NewInt(0) + if val, ok := raw.Domain.ChainID.(float64); ok { + chainID.SetInt64(int64(val)) + } else if val, ok := raw.Domain.ChainID.(json.Number); ok { + chainID.SetString(val.String(), 10) + } else if val, ok := raw.Domain.ChainID.(string); ok { + if strings.HasPrefix(val, "0x") { + chainID.SetString(val[2:], 16) + } else { + chainID.SetString(val, 10) + } + } + domain.ChainID = chainID + } + + // Decode the raw message into Go runtime types + message, err := typedDataDecodeRawMessageMap(raw.Types.Map(), raw.PrimaryType, raw.Message) + if err != nil { + return err + } + + t.Types = raw.Types + t.PrimaryType = raw.PrimaryType + t.Domain = domain + + m, ok := message.(map[string]interface{}) + if !ok { + return fmt.Errorf("resulting message is not a map") + } + t.Message = m + + return nil +} + +func typedDataDetectPrimaryType(typesMap map[string]map[string]string, message map[string]interface{}) (string, error) { + // If there are only two types, and one is the EIP712Domain, then the other is the primary type + if len(typesMap) == 2 { + _, ok := typesMap["EIP712Domain"] + if ok { + for typ := range typesMap { + if typ == "EIP712Domain" { + continue + } + return typ, nil + } + } + } + + // Otherwise search for the primary type by looking for the first type that has a message field keys + messageKeys := []string{} + for k := range message { + messageKeys = append(messageKeys, k) + } + sort.Strings(messageKeys) + + for typ := range typesMap { + if typ == "EIP712Domain" { + continue + } + if len(typesMap[typ]) != len(messageKeys) { + continue + } + + typKeys := []string{} + for k := range typesMap[typ] { + typKeys = append(typKeys, k) + } + sort.Strings(typKeys) + + if !slices.Equal(messageKeys, typKeys) { + continue + } + return typ, nil + } + + return "", fmt.Errorf("no primary type found") +} + +func typedDataDecodeRawMessageMap(typesMap map[string]map[string]string, primaryType string, data interface{}) (interface{}, error) { + // Handle array types + if arr, ok := data.([]interface{}); ok { + results := make([]interface{}, len(arr)) + for i, item := range arr { + decoded, err := typedDataDecodeRawMessageMap(typesMap, primaryType, item) + if err != nil { + return nil, err + } + results[i] = decoded + } + return results, nil + } + + // Handle primitive directly + message, ok := data.(map[string]interface{}) + if !ok { + return typedDataDecodePrimitiveValue(primaryType, data) + } + + currentType, ok := typesMap[primaryType] + if !ok { + return nil, fmt.Errorf("type %s is not defined", primaryType) + } + + processedMessage := make(map[string]interface{}) + for k, v := range message { + typ, ok := currentType[k] + if !ok { + return nil, fmt.Errorf("message field '%s' is missing type definition on '%s'", k, primaryType) + } + + // Extract base type and check if it's an array + baseType := typ + isArray := false + if idx := strings.Index(typ, "["); idx != -1 { + baseType = typ[:idx] + isArray = true + } + + // Process value based on whether it's a custom or primitive type + if _, isCustomType := typesMap[baseType]; isCustomType { + decoded, err := typedDataDecodeRawMessageMap(typesMap, baseType, v) + if err != nil { + return nil, err + } + processedMessage[k] = decoded + } else { + var decoded interface{} + var err error + if isArray { + decoded, err = typedDataDecodeRawMessageMap(typesMap, baseType, v) + } else { + decoded, err = typedDataDecodePrimitiveValue(baseType, v) + } + if err != nil { + return nil, fmt.Errorf("failed to decode field '%s': %w", k, err) + } + processedMessage[k] = decoded + } + } + + return processedMessage, nil +} + +func typedDataDecodePrimitiveValue(typ string, value interface{}) (interface{}, error) { + val := fmt.Sprintf("%v", value) + out, err := ABIUnmarshalStringValuesAny([]string{typ}, []any{val}) + if err != nil { + return nil, fmt.Errorf("typedDataDecodePrimitiveValue: %w", err) + } + return out[0], nil +} diff --git a/ethcoder/typed_data_test.go b/ethcoder/typed_data_test.go index 59dab73..8ed67d9 100644 --- a/ethcoder/typed_data_test.go +++ b/ethcoder/typed_data_test.go @@ -1,6 +1,7 @@ package ethcoder_test import ( + "encoding/json" "math/big" "testing" @@ -204,6 +205,17 @@ func TestTypedDataFromJSON(t *testing.T) { valid, err := ethwallet.ValidateEthereumSignature(wallet.Address().Hex(), typedDataEncodedOut, ethSigedTypedDataHex) require.NoError(t, err) require.True(t, valid) + + // test MarshalJSON by encoding, then comparing digests + jsonOut, err := json.Marshal(typedData) + require.NoError(t, err) + + typedData2, err := ethcoder.TypedDataFromJSON(string(jsonOut)) + require.NoError(t, err) + + digest2, err := typedData2.EncodeDigest() + require.NoError(t, err) + require.Equal(t, digest, digest2) } func TestTypedDataFromJSONPart2(t *testing.T) { @@ -293,6 +305,17 @@ func TestTypedDataFromJSONPart2(t *testing.T) { valid, err := ethwallet.ValidateEthereumSignature(wallet.Address().Hex(), typedDataEncodedOut, ethSigedTypedDataHex) require.NoError(t, err) require.True(t, valid) + + // test MarshalJSON by encoding, then comparing digests + jsonOut, err := json.Marshal(typedData) + require.NoError(t, err) + + typedData2, err := ethcoder.TypedDataFromJSON(string(jsonOut)) + require.NoError(t, err) + + digest2, err := typedData2.EncodeDigest() + require.NoError(t, err) + require.Equal(t, digest, digest2) } func TestTypedDataFromJSONPart3(t *testing.T) { @@ -356,6 +379,20 @@ func TestTypedDataFromJSONPart3(t *testing.T) { valid, err := ethwallet.ValidateEthereumSignature(wallet.Address().Hex(), typedDataEncodedOut, ethSigedTypedDataHex) require.NoError(t, err) require.True(t, valid) + + // test MarshalJSON by encoding, then comparing digests + jsonOut, err := json.Marshal(typedData) + require.NoError(t, err) + + typedData2, err := ethcoder.TypedDataFromJSON(string(jsonOut)) + require.NoError(t, err) + + digest, err := typedData.EncodeDigest() + require.NoError(t, err) + + digest2, err := typedData2.EncodeDigest() + require.NoError(t, err) + require.Equal(t, digest, digest2) } func TestTypedDataFromJSONPart4(t *testing.T) { @@ -415,6 +452,20 @@ func TestTypedDataFromJSONPart4(t *testing.T) { valid, err := ethwallet.ValidateEthereumSignature(wallet.Address().Hex(), typedDataEncodedOut, ethSigedTypedDataHex) require.NoError(t, err) require.True(t, valid) + + // test MarshalJSON by encoding, then comparing digests + jsonOut, err := json.Marshal(typedData) + require.NoError(t, err) + + typedData2, err := ethcoder.TypedDataFromJSON(string(jsonOut)) + require.NoError(t, err) + + digest, err := typedData.EncodeDigest() + require.NoError(t, err) + + digest2, err := typedData2.EncodeDigest() + require.NoError(t, err) + require.Equal(t, digest, digest2) } func TestTypedDataFromJSONPart5(t *testing.T) { @@ -437,7 +488,7 @@ func TestTypedDataFromJSONPart5(t *testing.T) { "domain": { "name": "EIP712Example", "version": "1", - "chainId": "0x0f", + "chainId": "0x0fffffffffffffffffffffffffffffff", "verifyingContract": "0xc0ffee254729296a45a3885639AC7E10F9d54979", "salt": "0x70736575646f2d72616e646f6d2076616c756500000000000000000000000000" }, @@ -452,7 +503,40 @@ func TestTypedDataFromJSONPart5(t *testing.T) { typedData, err := ethcoder.TypedDataFromJSON(typedDataJson) require.NoError(t, err) - require.Equal(t, typedData.Domain.ChainID.Int64(), int64(15)) + require.Equal(t, "21267647932558653966460912964485513215", typedData.Domain.ChainID.String()) + + // Sign and validate + wallet, err := ethwallet.NewWalletFromMnemonic("dose weasel clever culture letter volume endorse used harvest ripple circle install") + require.NoError(t, err) + + ethSigedTypedData, typedDataEncodedOut, err := wallet.SignTypedData(typedData) + ethSigedTypedDataHex := ethcoder.HexEncode(ethSigedTypedData) + require.NoError(t, err) + + // NOTE: this signature and above method has been compared against ethers v6 test + require.Equal(t, + "0xade97a2c5dc7fedcbab1d5c9cf66974abb99f4e7d206e57a59d22b2003e962bd1eaef8243ab670dd6343d127ceea8f3b955288e85ca0c2acdd55c60f10244d3c1c", + ethSigedTypedDataHex, + ) + + // recover / validate signature + valid, err := ethwallet.ValidateEthereumSignature(wallet.Address().Hex(), typedDataEncodedOut, ethSigedTypedDataHex) + require.NoError(t, err) + require.True(t, valid) + + // test MarshalJSON by encoding, then comparing digests + jsonOut, err := json.Marshal(typedData) + require.NoError(t, err) + + typedData2, err := ethcoder.TypedDataFromJSON(string(jsonOut)) + require.NoError(t, err) + + digest, err := typedData.EncodeDigest() + require.NoError(t, err) + + digest2, err := typedData2.EncodeDigest() + require.NoError(t, err) + require.Equal(t, digest, digest2) } func TestTypedDataFromJSONPart6(t *testing.T) { @@ -613,4 +697,37 @@ func TestTypedDataFromJSONPart6(t *testing.T) { typedData, err := ethcoder.TypedDataFromJSON(typedDataJson) require.NoError(t, err) require.NotNil(t, typedData) + + // Sign and validate + wallet, err := ethwallet.NewWalletFromMnemonic("dose weasel clever culture letter volume endorse used harvest ripple circle install") + require.NoError(t, err) + + ethSigedTypedData, typedDataEncodedOut, err := wallet.SignTypedData(typedData) + ethSigedTypedDataHex := ethcoder.HexEncode(ethSigedTypedData) + require.NoError(t, err) + + // NOTE: this signature and above method has been compared against ethers v6 test + require.Equal(t, + "0xcd9d2f3e124e1ebcd870fb63023619a94a604353e9e91428e927117063f991a003cb5713cb1a2bb36ffdb0339b2c4973ce40024948ec08f1174d382b0c458dfe1c", + ethSigedTypedDataHex, + ) + + // recover / validate signature + valid, err := ethwallet.ValidateEthereumSignature(wallet.Address().Hex(), typedDataEncodedOut, ethSigedTypedDataHex) + require.NoError(t, err) + require.True(t, valid) + + // test MarshalJSON by encoding, then comparing digests + jsonOut, err := json.Marshal(typedData) + require.NoError(t, err) + + typedData2, err := ethcoder.TypedDataFromJSON(string(jsonOut)) + require.NoError(t, err) + + digest, err := typedData.EncodeDigest() + require.NoError(t, err) + + digest2, err := typedData2.EncodeDigest() + require.NoError(t, err) + require.Equal(t, digest, digest2) }