From b26e31cad2510febc03fe231301aa8a0553cd779 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Thu, 12 Dec 2024 15:47:58 -0500 Subject: [PATCH 01/11] improved eip712 support --- ethcoder/typed_data.go | 132 +++++++++++++++++++++++++++++++++--- ethcoder/typed_data_test.go | 74 +++++++++++++++++++- ethwallet/ethwallet.go | 39 +++++++++-- ethwallet/utils.go | 30 ++++++-- 4 files changed, 252 insertions(+), 23 deletions(-) diff --git a/ethcoder/typed_data.go b/ethcoder/typed_data.go index 509b0b9e..10266bad 100644 --- a/ethcoder/typed_data.go +++ b/ethcoder/typed_data.go @@ -1,6 +1,8 @@ package ethcoder import ( + "bytes" + "encoding/json" "fmt" "math/big" "sort" @@ -192,30 +194,142 @@ func (t *TypedData) encodeData(primaryType string, data map[string]interface{}) return encodedData, nil } -func (t *TypedData) EncodeDigest() ([]byte, error) { - EIP191_HEADER := "0x1901" +// EncodeDigest returns the digest of the typed data and the fully encoded +// EIP712 message. +// +// NOTE: +// * the digest is the hash of the fully encoded EIP712 message +// * the encoded message is the fully encoded EIP712 message +func (t *TypedData) Encode() ([]byte, []byte, error) { + EIP191_HEADER := "0x1901" // EIP191 for typed data eip191Header, err := HexDecode(EIP191_HEADER) if err != nil { - return nil, err + return nil, nil, err } // Prepare hash struct for the domain domainHash, err := t.HashStruct("EIP712Domain", t.Domain.Map()) if err != nil { - return nil, err + return nil, nil, err } // Prepare hash struct for the message object messageHash, err := t.HashStruct(t.PrimaryType, t.Message) if err != nil { - return nil, err + return nil, nil, err } - hashPack, err := SolidityPack([]string{"bytes", "bytes32", "bytes32"}, []interface{}{eip191Header, domainHash, messageHash}) + encodedMessage, err := SolidityPack([]string{"bytes", "bytes32", "bytes32"}, []interface{}{eip191Header, domainHash, messageHash}) if err != nil { - return []byte{}, err + return nil, nil, err + } + + digest := crypto.Keccak256(encodedMessage) + + return digest, encodedMessage, 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 { + // Create an intermediate structure using json.Number + type TypedDataRaw struct { + Types TypedDataTypes `json:"types"` + PrimaryType string `json:"primaryType"` + Domain TypedDataDomain `json:"domain"` + Message map[string]interface{} `json:"message"` + } + + // Create a decoder that will preserve number strings + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + + // First unmarshal into the intermediate structure + var raw TypedDataRaw + if err := dec.Decode(&raw); err != nil { + return err } - hashBytes := crypto.Keccak256(hashPack) - return hashBytes, nil + // .. + primaryDomainType, ok := raw.Types[raw.PrimaryType] + if !ok { + return fmt.Errorf("primary type %s is not defined", raw.PrimaryType) + } + primaryDomainTypeMap := typedDataTypeMap(primaryDomainType) + fmt.Println("===> primaryDomainType", primaryDomainTypeMap) + + // Process the Message map to convert values to desired types + processedMessage := make(map[string]interface{}) + for k, v := range raw.Message { + fmt.Println("===> k", k, "v", v) + + typ, ok := primaryDomainTypeMap[k] + if !ok { + return fmt.Errorf("type %s is not defined", k) + } + fmt.Println("===> typ", k, typ) + + // TODO: its possible that the type is a struct, and we need to do another call to get the typedData map, etc + + switch val := v.(type) { + case json.Number: + // TODO: we will check the domain, etc......... + + if typ == "uint8" { + num, err := val.Int64() + if err != nil { + return fmt.Errorf("failed to parse uint8 value %s, because %w", val, err) + } + // TODO: is this okay ... int64 to uint8 ..???... + processedMessage[k] = uint8(num) + } else { + // Try parsing as big.Int first + if n, ok := new(big.Int).SetString(string(val), 10); ok { + processedMessage[k] = n + } else { + // If it's not a valid integer, keep the original value + processedMessage[k] = v + } + } + + case string: + if typ == "address" { + addr := common.HexToAddress(val) + processedMessage[k] = addr + } else if len(val) > 2 && (val[:2] == "0x" || val[:2] == "0X") { + // Convert hex strings to *big.Int + n := new(big.Int) + n.SetString(val[2:], 16) + processedMessage[k] = n + } else { + processedMessage[k] = val + } + + default: + // TODO: prob needs to be recursive.. cuz might be some array or object .. + return fmt.Errorf("unsupported type %T for value %v", v, v) + } + } + + t.Types = raw.Types + t.PrimaryType = raw.PrimaryType + t.Domain = raw.Domain + t.Message = processedMessage + + return nil +} + +func typedDataTypeMap(typ []TypedDataArgument) map[string]string { + m := map[string]string{} + for _, arg := range typ { + m[arg.Name] = arg.Type + } + return m } diff --git a/ethcoder/typed_data_test.go b/ethcoder/typed_data_test.go index 5b6e3aee..b99f5c5b 100644 --- a/ethcoder/typed_data_test.go +++ b/ethcoder/typed_data_test.go @@ -1,6 +1,7 @@ package ethcoder_test import ( + "fmt" "math/big" "testing" @@ -8,6 +9,7 @@ import ( "github.com/0xsequence/ethkit/ethwallet" "github.com/0xsequence/ethkit/go-ethereum/common" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTypedDataTypes(t *testing.T) { @@ -74,16 +76,18 @@ func TestTypedDataCase1(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f", ethcoder.HexEncode(domainHash)) - digest, err := typedData.EncodeDigest() + digest, _, err := typedData.Encode() assert.NoError(t, err) assert.Equal(t, "0x0a94cf6625e5860fc4f330d75bcd0c3a4737957d2321d1a024540ab5320fe903", ethcoder.HexEncode(digest)) - // fmt.Println("===> digest", HexEncode(digest)) + fmt.Println("===> digest", ethcoder.HexEncode(digest)) // lets sign it.. wallet, err := ethwallet.NewWalletFromMnemonic("dose weasel clever culture letter volume endorse used harvest ripple circle install") assert.NoError(t, err) + // TODO: this is wrong.. we need wallet.SignTypedData(digest).. or wallet.SignData(digest) wherre digest is fully encoded with prefix, etc. + ethSigedTypedData, err := wallet.SignMessage([]byte(digest)) ethSigedTypedDataHex := ethcoder.HexEncode(ethSigedTypedData) assert.NoError(t, err) @@ -135,10 +139,74 @@ func TestTypedDataCase2(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f", ethcoder.HexEncode(domainHash)) - digest, err := typedData.EncodeDigest() + digest, _, err := typedData.Encode() assert.NoError(t, err) assert.Equal(t, "0x2218fda59750be7bb9e5dfb2b49e4ec000dc2542862c5826f1fe980d6d727e95", ethcoder.HexEncode(digest)) // fmt.Println("===> digest", HexEncode(digest)) } + +func TestTypedDataFromJSON(t *testing.T) { + typedDataJson := `{ + "types": { + "EIP712Domain": [ + {"name": "name", "type": "string"}, + {"name": "version", "type": "string"}, + {"name": "chainId", "type": "uint256"}, + {"name": "verifyingContract", "type": "address"} + ], + "Person": [ + {"name": "name", "type": "string"}, + {"name": "wallet", "type": "address"}, + {"name": "count", "type": "uint8"} + ] + }, + "primaryType": "Person", + "domain": { + "name": "Ether Mail", + "version": "1", + "chainId": 1, + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + }, + "message": { + "name": "Bob", + "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + "count": 4 + } + }` + + // TODO: lets write ethers v6 test to ensure this is working the exact same way... + + typedData, err := ethcoder.TypedDataFromJSON(typedDataJson) + assert.NoError(t, err) + + domainHash, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) + assert.NoError(t, err) + assert.Equal(t, "0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f", ethcoder.HexEncode(domainHash)) + + digest, typedDataEncoded, err := typedData.Encode() + assert.NoError(t, err) + assert.Equal(t, "0x2218fda59750be7bb9e5dfb2b49e4ec000dc2542862c5826f1fe980d6d727e95", ethcoder.HexEncode(digest)) + assert.Equal(t, "0x1901f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090ff5117e79519388f3d62844df1325ebe783523d9db9762c50fa78a60400a20b5b", ethcoder.HexEncode(typedDataEncoded)) + + // Sign and validate + wallet, err := ethwallet.NewWalletFromMnemonic("dose weasel clever culture letter volume endorse used harvest ripple circle install") + assert.NoError(t, err) + + ethSigedTypedData, typedDataEncodedOut, err := wallet.SignTypedData(typedData) + ethSigedTypedDataHex := ethcoder.HexEncode(ethSigedTypedData) + require.NoError(t, err) + require.Equal(t, typedDataEncoded, typedDataEncodedOut) + + // NOTE: this signature and above method has been compared against ethers v6 test + assert.Equal(t, + "0x296c98bed8f3fd7ea96f55ca8148b4d092cbada953c8d9205b2fff759461ab4e6d6db0b78833b954684900530caeee9aaef8e42dfd8439a3fa107e910b57e2cc1b", + ethSigedTypedDataHex, + ) + + // recover / validate signature + valid, err := ethwallet.ValidateEthereumSignature(wallet.Address().Hex(), typedDataEncodedOut, ethSigedTypedDataHex) + assert.NoError(t, err) + assert.True(t, valid) +} diff --git a/ethwallet/ethwallet.go b/ethwallet/ethwallet.go index 7804313f..8f331133 100644 --- a/ethwallet/ethwallet.go +++ b/ethwallet/ethwallet.go @@ -7,6 +7,7 @@ import ( "fmt" "math/big" + "github.com/0xsequence/ethkit/ethcoder" "github.com/0xsequence/ethkit/ethrpc" "github.com/0xsequence/ethkit/ethtxn" "github.com/0xsequence/ethkit/go-ethereum/accounts" @@ -254,6 +255,11 @@ func (w *Wallet) SignTx(tx *types.Transaction, chainID *big.Int) (*types.Transac return signedTx, nil } +// SignMessage signs a message with EIP-191 prefix with the wallet's private key. +// +// This is the same as SignData, but it adds the prefix "Ethereum Signed Message:\n" to +// the message and encodes the length of the message in the prefix. In case the message +// already has the prefix, it will not be added again. func (w *Wallet) SignMessage(message []byte) ([]byte, error) { message191 := []byte("\x19Ethereum Signed Message:\n") if !bytes.HasPrefix(message, message191) { @@ -263,21 +269,40 @@ func (w *Wallet) SignMessage(message []byte) ([]byte, error) { } else { message191 = message } + return w.SignData(message191) +} - h := crypto.Keccak256(message191) - - sig, err := crypto.Sign(h, w.hdnode.PrivateKey()) +// SignTypedData signs a typed data with EIP-712 prefix with the wallet's private key. +// It returns the signature and the digest of the typed data. +func (w *Wallet) SignTypedData(typedData *ethcoder.TypedData) ([]byte, []byte, error) { + _, encodedData, err := typedData.Encode() if err != nil { - return []byte{}, err + return []byte{}, []byte{}, err } - sig[64] += 27 - return sig, nil + sig, err := w.SignData(encodedData) + if err != nil { + return []byte{}, []byte{}, err + } + return sig, encodedData, nil } +// SignData signs a message with the wallet's private key. +// +// This is the same as SignMessage, but it does not add the EIP-191 prefix. +// Please be careful with this method as it can be used to sign arbitrary data, but +// its helpful for signing typed data as defined by EIP-712. func (w *Wallet) SignData(data []byte) ([]byte, error) { + // NOTE: this is commended out for now, in case we use this method anywhere + // without expecting the data to be EIP191 prefixed. + // + // extra protection to ensure the input data is EIP191 prefixed + // if !(data[0] == 0x19 && (data[1] == 0x00 || data[1] == 0x01 || data[1] == 0x45)) { + // return nil, fmt.Errorf("invalid EIP191 input data") + // } + + // hash the data and sign it with the wallet's private key h := crypto.Keccak256(data) - sig, err := crypto.Sign(h, w.hdnode.PrivateKey()) if err != nil { return []byte{}, err diff --git a/ethwallet/utils.go b/ethwallet/utils.go index d189efcd..f5caa4a7 100644 --- a/ethwallet/utils.go +++ b/ethwallet/utils.go @@ -75,15 +75,37 @@ func IsValid191Signature(address common.Address, message, signature []byte) (boo return false, fmt.Errorf("signature is not of proper length") } - message191 := []byte("\x19Ethereum Signed Message:\n") - if !bytes.HasPrefix(message, message191) { + // Ensure EIP191 signature + var message191 []byte + personalSignPrefix := []byte("\x19Ethereum Signed Message:\n") + + if message[0] == 0x19 { + if message[1] == 0x45 { + // EIP191 for "Ethereum Signed Message" prefix + if !bytes.HasPrefix(message, personalSignPrefix) { + mlen := fmt.Sprintf("%d", len(message)) + message191 = append(personalSignPrefix, []byte(mlen)...) + message191 = append(message191, message...) + } else { + message191 = message + } + } else if message[1] == 0x01 { + // EIP191 for typed data + message191 = message + } + } + + // auto-prefix if message wasn't previously prefixed + if len(message191) == 0 { + // Message is not a EIP191, so we will automatically add the EIP191 prefix + // assuming its a message scheme. + message191 = personalSignPrefix mlen := fmt.Sprintf("%d", len(message)) message191 = append(message191, []byte(mlen)...) message191 = append(message191, message...) - } else { - message191 = message } + // Recovery the address from the signature sig := make([]byte, 65) copy(sig, signature) From 67c0d82ecf6feca865beecd820ff86be845531f1 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Thu, 12 Dec 2024 15:48:55 -0500 Subject: [PATCH 02/11] update --- ethcoder/typed_data_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/ethcoder/typed_data_test.go b/ethcoder/typed_data_test.go index b99f5c5b..44fb4341 100644 --- a/ethcoder/typed_data_test.go +++ b/ethcoder/typed_data_test.go @@ -176,8 +176,6 @@ func TestTypedDataFromJSON(t *testing.T) { } }` - // TODO: lets write ethers v6 test to ensure this is working the exact same way... - typedData, err := ethcoder.TypedDataFromJSON(typedDataJson) assert.NoError(t, err) From 17b32b414b07c468fbafbf1e00218bfcb9b65286 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Thu, 12 Dec 2024 15:56:35 -0500 Subject: [PATCH 03/11] update --- ethcoder/typed_data.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ethcoder/typed_data.go b/ethcoder/typed_data.go index 10266bad..d8427b5f 100644 --- a/ethcoder/typed_data.go +++ b/ethcoder/typed_data.go @@ -194,12 +194,11 @@ func (t *TypedData) encodeData(primaryType string, data map[string]interface{}) return encodedData, nil } -// EncodeDigest returns the digest of the typed data and the fully encoded -// EIP712 message. +// Encode returns the digest of the typed data and the fully encoded EIP712 typed data message. // // NOTE: // * the digest is the hash of the fully encoded EIP712 message -// * the encoded message is the fully encoded EIP712 message +// * the encoded message is the fully encoded EIP712 message (0x1901 + domain + hashStruct(message)) func (t *TypedData) Encode() ([]byte, []byte, error) { EIP191_HEADER := "0x1901" // EIP191 for typed data eip191Header, err := HexDecode(EIP191_HEADER) From 76c1637f12873c2bb88961bbf38d395a755a1afc Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Thu, 12 Dec 2024 17:31:32 -0500 Subject: [PATCH 04/11] update --- ethcoder/typed_data.go | 22 ++++++++++++++++++++++ ethcoder/typed_data_test.go | 20 ++++++++++---------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/ethcoder/typed_data.go b/ethcoder/typed_data.go index d8427b5f..4a6a52ea 100644 --- a/ethcoder/typed_data.go +++ b/ethcoder/typed_data.go @@ -256,6 +256,28 @@ func (t *TypedData) UnmarshalJSON(data []byte) error { 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"}) + } + } + // .. primaryDomainType, ok := raw.Types[raw.PrimaryType] if !ok { diff --git a/ethcoder/typed_data_test.go b/ethcoder/typed_data_test.go index 44fb4341..a425784d 100644 --- a/ethcoder/typed_data_test.go +++ b/ethcoder/typed_data_test.go @@ -177,20 +177,20 @@ func TestTypedDataFromJSON(t *testing.T) { }` typedData, err := ethcoder.TypedDataFromJSON(typedDataJson) - assert.NoError(t, err) + require.NoError(t, err) domainHash, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) - assert.NoError(t, err) - assert.Equal(t, "0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f", ethcoder.HexEncode(domainHash)) + require.NoError(t, err) + require.Equal(t, "0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f", ethcoder.HexEncode(domainHash)) digest, typedDataEncoded, err := typedData.Encode() - assert.NoError(t, err) - assert.Equal(t, "0x2218fda59750be7bb9e5dfb2b49e4ec000dc2542862c5826f1fe980d6d727e95", ethcoder.HexEncode(digest)) - assert.Equal(t, "0x1901f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090ff5117e79519388f3d62844df1325ebe783523d9db9762c50fa78a60400a20b5b", ethcoder.HexEncode(typedDataEncoded)) + require.NoError(t, err) + require.Equal(t, "0x2218fda59750be7bb9e5dfb2b49e4ec000dc2542862c5826f1fe980d6d727e95", ethcoder.HexEncode(digest)) + require.Equal(t, "0x1901f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090ff5117e79519388f3d62844df1325ebe783523d9db9762c50fa78a60400a20b5b", ethcoder.HexEncode(typedDataEncoded)) // Sign and validate wallet, err := ethwallet.NewWalletFromMnemonic("dose weasel clever culture letter volume endorse used harvest ripple circle install") - assert.NoError(t, err) + require.NoError(t, err) ethSigedTypedData, typedDataEncodedOut, err := wallet.SignTypedData(typedData) ethSigedTypedDataHex := ethcoder.HexEncode(ethSigedTypedData) @@ -198,13 +198,13 @@ func TestTypedDataFromJSON(t *testing.T) { require.Equal(t, typedDataEncoded, typedDataEncodedOut) // NOTE: this signature and above method has been compared against ethers v6 test - assert.Equal(t, + require.Equal(t, "0x296c98bed8f3fd7ea96f55ca8148b4d092cbada953c8d9205b2fff759461ab4e6d6db0b78833b954684900530caeee9aaef8e42dfd8439a3fa107e910b57e2cc1b", ethSigedTypedDataHex, ) // recover / validate signature valid, err := ethwallet.ValidateEthereumSignature(wallet.Address().Hex(), typedDataEncodedOut, ethSigedTypedDataHex) - assert.NoError(t, err) - assert.True(t, valid) + require.NoError(t, err) + require.True(t, valid) } From 371a4c3127dc71447cfda819b609e9b6da302a72 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Thu, 12 Dec 2024 18:49:02 -0500 Subject: [PATCH 05/11] update --- ethcoder/typed_data.go | 68 +++++++++++-------------------- ethcoder/typed_data_test.go | 79 +++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 44 deletions(-) diff --git a/ethcoder/typed_data.go b/ethcoder/typed_data.go index 4a6a52ea..82a0e3f8 100644 --- a/ethcoder/typed_data.go +++ b/ethcoder/typed_data.go @@ -238,7 +238,7 @@ func TypedDataFromJSON(typedDataJSON string) (*TypedData, error) { } func (t *TypedData) UnmarshalJSON(data []byte) error { - // Create an intermediate structure using json.Number + // Intermediary structure to decode message field type TypedDataRaw struct { Types TypedDataTypes `json:"types"` PrimaryType string `json:"primaryType"` @@ -246,11 +246,10 @@ func (t *TypedData) UnmarshalJSON(data []byte) error { Message map[string]interface{} `json:"message"` } - // Create a decoder that will preserve number strings + // Json decoder with json.Number support dec := json.NewDecoder(bytes.NewReader(data)) dec.UseNumber() - // First unmarshal into the intermediate structure var raw TypedDataRaw if err := dec.Decode(&raw); err != nil { return err @@ -278,15 +277,18 @@ func (t *TypedData) UnmarshalJSON(data []byte) error { } } - // .. + // Check primary type is defined + if raw.PrimaryType == "" { + return fmt.Errorf("primary type is required") + } primaryDomainType, ok := raw.Types[raw.PrimaryType] if !ok { - return fmt.Errorf("primary type %s is not defined", raw.PrimaryType) + return fmt.Errorf("primary type '%s' is not defined", raw.PrimaryType) } primaryDomainTypeMap := typedDataTypeMap(primaryDomainType) fmt.Println("===> primaryDomainType", primaryDomainTypeMap) - // Process the Message map to convert values to desired types + // Decode the message map into the typedData struct processedMessage := make(map[string]interface{}) for k, v := range raw.Message { fmt.Println("===> k", k, "v", v) @@ -297,45 +299,23 @@ func (t *TypedData) UnmarshalJSON(data []byte) error { } fmt.Println("===> typ", k, typ) - // TODO: its possible that the type is a struct, and we need to do another call to get the typedData map, etc - - switch val := v.(type) { - case json.Number: - // TODO: we will check the domain, etc......... - - if typ == "uint8" { - num, err := val.Int64() - if err != nil { - return fmt.Errorf("failed to parse uint8 value %s, because %w", val, err) - } - // TODO: is this okay ... int64 to uint8 ..???... - processedMessage[k] = uint8(num) - } else { - // Try parsing as big.Int first - if n, ok := new(big.Int).SetString(string(val), 10); ok { - processedMessage[k] = n - } else { - // If it's not a valid integer, keep the original value - processedMessage[k] = v - } - } - - case string: - if typ == "address" { - addr := common.HexToAddress(val) - processedMessage[k] = addr - } else if len(val) > 2 && (val[:2] == "0x" || val[:2] == "0X") { - // Convert hex strings to *big.Int - n := new(big.Int) - n.SetString(val[2:], 16) - processedMessage[k] = n - } else { - processedMessage[k] = val + // ... + customType, ok := raw.Types[typ] + if ok { + val := fmt.Sprintf("%v", v) + fmt.Println("===> customType", customType, val) + // processedMessage[k] = val + + // ............ + // .. + + } else { + val := fmt.Sprintf("%v", v) + out, err := ABIUnmarshalStringValuesAny([]string{typ}, []any{val}) + if err != nil { + return fmt.Errorf("failed to unmarshal string value for type %s with argument name %s, because %w", typ, k, err) } - - default: - // TODO: prob needs to be recursive.. cuz might be some array or object .. - return fmt.Errorf("unsupported type %T for value %v", v, v) + processedMessage[k] = out[0] } } diff --git a/ethcoder/typed_data_test.go b/ethcoder/typed_data_test.go index a425784d..fa85b572 100644 --- a/ethcoder/typed_data_test.go +++ b/ethcoder/typed_data_test.go @@ -208,3 +208,82 @@ func TestTypedDataFromJSON(t *testing.T) { require.NoError(t, err) require.True(t, valid) } + +func TestTypedDataFromJSONPart2(t *testing.T) { + // NOTE: we omit the EIP712Domain type definition because it will + // automatically be added by the library if its not specified + typedDataJson := `{ + "types": { + "Person": [ + { "name": "name", "type": "string" }, + { "name": "wallets", "type": "address[]" } + ], + "Mail": [ + { "name": "from", "type": "Person" }, + { "name": "to", "type": "Person[]" }, + { "name": "contents", "type": "string" } + ], + "Group": [ + { "name": "name", "type": "string" }, + { "name": "members", "type": "Person[]" } + ] + }, + "domain": { + "name": "Ether Mail", + "version": "1", + "chainId": 1, + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + }, + "primaryType": "Mail", + "message": { + "from": { + "name": "Cow", + "wallets": [ + "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF" + ] + }, + "to": [{ + "name": "Bob", + "wallets": [ + "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + "0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57", + "0xB0B0b0b0b0b0B000000000000000000000000000" + ] + }], + "contents": "Hello, Bob!" + } + }` + + typedData, err := ethcoder.TypedDataFromJSON(typedDataJson) + require.NoError(t, err) + + domainHash, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) + require.NoError(t, err) + require.Equal(t, "0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f", ethcoder.HexEncode(domainHash)) + + digest, typedDataEncoded, err := typedData.Encode() + require.NoError(t, err) + require.Equal(t, "0x2218fda59750be7bb9e5dfb2b49e4ec000dc2542862c5826f1fe980d6d727e95", ethcoder.HexEncode(digest)) + require.Equal(t, "0x1901f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090ff5117e79519388f3d62844df1325ebe783523d9db9762c50fa78a60400a20b5b", ethcoder.HexEncode(typedDataEncoded)) + + // 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) + require.Equal(t, typedDataEncoded, typedDataEncodedOut) + + // NOTE: this signature and above method has been compared against ethers v6 test + require.Equal(t, + "0x296c98bed8f3fd7ea96f55ca8148b4d092cbada953c8d9205b2fff759461ab4e6d6db0b78833b954684900530caeee9aaef8e42dfd8439a3fa107e910b57e2cc1b", + ethSigedTypedDataHex, + ) + + // recover / validate signature + valid, err := ethwallet.ValidateEthereumSignature(wallet.Address().Hex(), typedDataEncodedOut, ethSigedTypedDataHex) + require.NoError(t, err) + require.True(t, valid) +} From 3c5c73fe1c9b93c8b075547f0108aee340b78f65 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Thu, 12 Dec 2024 20:48:26 -0500 Subject: [PATCH 06/11] update --- ethcoder/typed_data.go | 10 ++++++---- ethcoder/typed_data_test.go | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ethcoder/typed_data.go b/ethcoder/typed_data.go index 82a0e3f8..9b029cf4 100644 --- a/ethcoder/typed_data.go +++ b/ethcoder/typed_data.go @@ -286,24 +286,26 @@ func (t *TypedData) UnmarshalJSON(data []byte) error { return fmt.Errorf("primary type '%s' is not defined", raw.PrimaryType) } primaryDomainTypeMap := typedDataTypeMap(primaryDomainType) - fmt.Println("===> primaryDomainType", primaryDomainTypeMap) + // fmt.Println("===> primaryDomainType", primaryDomainTypeMap) // Decode the message map into the typedData struct processedMessage := make(map[string]interface{}) for k, v := range raw.Message { - fmt.Println("===> k", k, "v", v) + // fmt.Println("===> k", k, "v", v) typ, ok := primaryDomainTypeMap[k] if !ok { return fmt.Errorf("type %s is not defined", k) } - fmt.Println("===> typ", k, typ) + // fmt.Println("===> typ", k, typ) // ... customType, ok := raw.Types[typ] if ok { val := fmt.Sprintf("%v", v) - fmt.Println("===> customType", customType, val) + _ = customType + _ = val + // fmt.Println("===> customType", customType, val) // processedMessage[k] = val // ............ diff --git a/ethcoder/typed_data_test.go b/ethcoder/typed_data_test.go index fa85b572..d0490d77 100644 --- a/ethcoder/typed_data_test.go +++ b/ethcoder/typed_data_test.go @@ -209,7 +209,8 @@ func TestTypedDataFromJSON(t *testing.T) { require.True(t, valid) } -func TestTypedDataFromJSONPart2(t *testing.T) { +// TODO +func XTestTypedDataFromJSONPart2(t *testing.T) { // NOTE: we omit the EIP712Domain type definition because it will // automatically be added by the library if its not specified typedDataJson := `{ From e54992b80c554fd1c533b5c324548152c0e50fe7 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Fri, 13 Dec 2024 09:50:53 -0500 Subject: [PATCH 07/11] nested typed data support, wip --- ethcoder/typed_data.go | 141 ++++++++++++++++++++++++++---------- ethcoder/typed_data_test.go | 14 +++- 2 files changed, 115 insertions(+), 40 deletions(-) diff --git a/ethcoder/typed_data.go b/ethcoder/typed_data.go index 9b029cf4..1ca629a8 100644 --- a/ethcoder/typed_data.go +++ b/ethcoder/typed_data.go @@ -6,6 +6,7 @@ import ( "fmt" "math/big" "sort" + "strings" "github.com/0xsequence/ethkit/go-ethereum/common" "github.com/0xsequence/ethkit/go-ethereum/crypto" @@ -64,6 +65,18 @@ func (t TypedDataTypes) EncodeType(primaryType string) (string, error) { return s, nil } +func (t TypedDataTypes) Map() map[string]map[string]string { + out := map[string]map[string]string{} + for k, v := range t { + m := make(map[string]string, len(v)) + for _, arg := range v { + m[arg.Name] = arg.Type + } + out[k] = m + } + return out +} + func (t TypedDataTypes) TypeHash(primaryType string) ([]byte, error) { encodeType, err := t.EncodeType(primaryType) if err != nil { @@ -112,6 +125,7 @@ func (t *TypedData) HashStruct(primaryType string, data map[string]interface{}) } encodedData, err := t.encodeData(primaryType, data) if err != nil { + panic(err) return nil, err } v, err := SolidityPack([]string{"bytes32", "bytes"}, []interface{}{BytesToBytes32(typeHash), encodedData}) @@ -228,6 +242,15 @@ func (t *TypedData) Encode() ([]byte, []byte, error) { return digest, encodedMessage, nil } +// EncodeDigest returns the digest of the typed data message. +func (t *TypedData) EncodeDigest() ([]byte, error) { + digest, _, err := t.Encode() + if err != nil { + return nil, err + } + return digest, nil +} + func TypedDataFromJSON(typedDataJSON string) (*TypedData, error) { var typedData TypedData err := json.Unmarshal([]byte(typedDataJSON), &typedData) @@ -246,7 +269,7 @@ func (t *TypedData) UnmarshalJSON(data []byte) error { Message map[string]interface{} `json:"message"` } - // Json decoder with json.Number support + // Json decoder with json.Number support, so that we can decode big.Int values dec := json.NewDecoder(bytes.NewReader(data)) dec.UseNumber() @@ -277,62 +300,104 @@ func (t *TypedData) UnmarshalJSON(data []byte) error { } } - // Check primary type is defined + // Ensure primary type is defined if raw.PrimaryType == "" { return fmt.Errorf("primary type is required") } - primaryDomainType, ok := raw.Types[raw.PrimaryType] + _, ok = raw.Types[raw.PrimaryType] if !ok { return fmt.Errorf("primary type '%s' is not defined", raw.PrimaryType) } - primaryDomainTypeMap := typedDataTypeMap(primaryDomainType) - // fmt.Println("===> primaryDomainType", primaryDomainTypeMap) - // Decode the message map into the typedData struct - processedMessage := make(map[string]interface{}) - for k, v := range raw.Message { - // fmt.Println("===> k", k, "v", v) + // Decode the raw message into Go runtime types + message, err := typedDataDecodeRawMessageMap(raw.Types.Map(), raw.PrimaryType, raw.Message) + if err != nil { + return err + } - typ, ok := primaryDomainTypeMap[k] - if !ok { - return fmt.Errorf("type %s is not defined", k) + t.Types = raw.Types + t.PrimaryType = raw.PrimaryType + t.Domain = raw.Domain + + m, ok := message.(map[string]interface{}) + if !ok { + return fmt.Errorf("resulting message is not a map") + } + t.Message = m + + return nil +} + +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 } - // fmt.Println("===> typ", k, typ) + return results, nil + } - // ... - customType, ok := raw.Types[typ] - if ok { - val := fmt.Sprintf("%v", v) - _ = customType - _ = val - // fmt.Println("===> customType", customType, val) - // processedMessage[k] = val + // 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 { - val := fmt.Sprintf("%v", v) - out, err := ABIUnmarshalStringValuesAny([]string{typ}, []any{val}) + var decoded interface{} + var err error + if isArray { + decoded, err = typedDataDecodeRawMessageMap(typesMap, baseType, v) + } else { + decoded, err = typedDataDecodePrimitiveValue(baseType, v) + } if err != nil { - return fmt.Errorf("failed to unmarshal string value for type %s with argument name %s, because %w", typ, k, err) + return nil, fmt.Errorf("failed to decode field '%s': %w", k, err) } - processedMessage[k] = out[0] + processedMessage[k] = decoded } } - t.Types = raw.Types - t.PrimaryType = raw.PrimaryType - t.Domain = raw.Domain - t.Message = processedMessage - - return nil + return processedMessage, nil } -func typedDataTypeMap(typ []TypedDataArgument) map[string]string { - m := map[string]string{} - for _, arg := range typ { - m[arg.Name] = arg.Type +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, err } - return m + return out[0], nil } diff --git a/ethcoder/typed_data_test.go b/ethcoder/typed_data_test.go index d0490d77..bbccabd2 100644 --- a/ethcoder/typed_data_test.go +++ b/ethcoder/typed_data_test.go @@ -8,6 +8,7 @@ import ( "github.com/0xsequence/ethkit/ethcoder" "github.com/0xsequence/ethkit/ethwallet" "github.com/0xsequence/ethkit/go-ethereum/common" + "github.com/davecgh/go-spew/spew" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -209,8 +210,7 @@ func TestTypedDataFromJSON(t *testing.T) { require.True(t, valid) } -// TODO -func XTestTypedDataFromJSONPart2(t *testing.T) { +func TestTypedDataFromJSONPart2(t *testing.T) { // NOTE: we omit the EIP712Domain type definition because it will // automatically be added by the library if its not specified typedDataJson := `{ @@ -259,15 +259,25 @@ func XTestTypedDataFromJSONPart2(t *testing.T) { typedData, err := ethcoder.TypedDataFromJSON(typedDataJson) require.NoError(t, err) + spew.Dump(typedData) + domainHash, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) require.NoError(t, err) require.Equal(t, "0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f", ethcoder.HexEncode(domainHash)) + fromArg, ok := typedData.Message["from"].(map[string]interface{}) + require.True(t, ok) + personHash, err := typedData.HashStruct("Person", fromArg) + require.NoError(t, err) + require.Equal(t, "0x12345", ethcoder.HexEncode(personHash)) + digest, typedDataEncoded, err := typedData.Encode() require.NoError(t, err) require.Equal(t, "0x2218fda59750be7bb9e5dfb2b49e4ec000dc2542862c5826f1fe980d6d727e95", ethcoder.HexEncode(digest)) require.Equal(t, "0x1901f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090ff5117e79519388f3d62844df1325ebe783523d9db9762c50fa78a60400a20b5b", ethcoder.HexEncode(typedDataEncoded)) + return + // Sign and validate wallet, err := ethwallet.NewWalletFromMnemonic("dose weasel clever culture letter volume endorse used harvest ripple circle install") require.NoError(t, err) From eebd3750367ce46e8fc6d036cc9fc9d2456b257e Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Fri, 13 Dec 2024 10:53:24 -0500 Subject: [PATCH 08/11] update --- ethcoder/typed_data.go | 112 +++++++++++++++++++++--------------- ethcoder/typed_data_test.go | 24 ++++---- 2 files changed, 78 insertions(+), 58 deletions(-) diff --git a/ethcoder/typed_data.go b/ethcoder/typed_data.go index 1ca629a8..02359ee3 100644 --- a/ethcoder/typed_data.go +++ b/ethcoder/typed_data.go @@ -33,16 +33,21 @@ func (t TypedDataTypes) EncodeType(primaryType string) (string, error) { s := primaryType + "(" for i, arg := range args { - _, ok := t[arg.Type] - if ok { + baseType := arg.Type + if strings.Index(baseType, "[") > 0 { + baseType = baseType[:strings.Index(baseType, "[")] + } + + if _, ok := t[baseType]; ok { set := false for _, v := range subTypes { - if v == arg.Type { + if v == baseType { set = true + break } } if !set { - subTypes = append(subTypes, arg.Type) + subTypes = append(subTypes, baseType) } } @@ -125,7 +130,6 @@ func (t *TypedData) HashStruct(primaryType string, data map[string]interface{}) } encodedData, err := t.encodeData(primaryType, data) if err != nil { - panic(err) return nil, err } v, err := SolidityPack([]string{"bytes32", "bytes"}, []interface{}{BytesToBytes32(typeHash), encodedData}) @@ -144,68 +148,82 @@ func (t *TypedData) encodeData(primaryType string, data map[string]interface{}) return nil, fmt.Errorf("encoding failed for type %s, expecting %d arguments but received %d data values", primaryType, len(args), len(data)) } - abiTypes := []string{} - abiValues := []interface{}{} + encodedTypes := make([]string, len(args)) + encodedValues := make([]interface{}, len(args)) - for _, arg := range args { + for i, arg := range args { dataValue, ok := data[arg.Name] if !ok { return nil, fmt.Errorf("data value missing for type %s with argument name %s", primaryType, arg.Name) } - switch arg.Type { - case "bytes", "string": - var bytesValue []byte - if v, ok := dataValue.([]byte); ok { - bytesValue = v - } else if v, ok := dataValue.(string); ok { - bytesValue = []byte(v) - } else { - return nil, fmt.Errorf("data value invalid for type %s with argument name %s", primaryType, arg.Name) - } - abiTypes = append(abiTypes, "bytes32") - abiValues = append(abiValues, BytesToBytes32(Keccak256(bytesValue))) - - default: - dataValueString, isString := dataValue.(string) - if isString { - v, err := ABIUnmarshalStringValues([]string{arg.Type}, []string{dataValueString}) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal string value for type %s with argument name %s, because %w", primaryType, arg.Name, err) - } - abiValues = append(abiValues, v[0]) - } else { - abiValues = append(abiValues, dataValue) + encValue, err := t.encodeValue(arg.Type, dataValue) + if err != nil { + return nil, fmt.Errorf("failed to encode %s: %w", arg.Name, err) + } + encodedTypes[i] = "bytes" + encodedValues[i] = encValue + } + + return SolidityPack(encodedTypes, encodedValues) +} + +// encodeValue handles the recursive encoding of values according to their types +func (t *TypedData) encodeValue(typ string, value interface{}) ([]byte, error) { + // Handle arrays + if strings.HasSuffix(typ, "[]") { + baseType := typ[:len(typ)-2] + values, ok := value.([]interface{}) + if !ok { + return nil, fmt.Errorf("expected array for type %s", typ) + } + + encodedValues := make([][]byte, len(values)) + for i, val := range values { + encoded, err := t.encodeValue(baseType, val) + if err != nil { + return nil, fmt.Errorf("failed to encode array element %d: %w", i, err) } - abiTypes = append(abiTypes, arg.Type) + encodedValues[i] = encoded } + + // For arrays, we concatenate the encoded values and hash the result + concat := bytes.Join(encodedValues, nil) + return Keccak256(concat), nil } - if len(args) != len(abiTypes) || len(args) != len(abiValues) { - return nil, fmt.Errorf("argument encoding failed to encode all values") + // Handle bytes and string + if typ == "bytes" || typ == "string" { + var bytesValue []byte + if v, ok := value.([]byte); ok { + bytesValue = v + } else if v, ok := value.(string); ok { + bytesValue = []byte(v) + } else { + return nil, fmt.Errorf("invalid value for type %s", typ) + } + return Keccak256(bytesValue), nil } - // NOTE: each part must be bytes32 - var err error - encodedTypes := make([]string, len(args)) - encodedValues := make([]interface{}, len(args)) - for i := 0; i < len(args); i++ { - pack, err := SolidityPack([]string{abiTypes[i]}, []interface{}{abiValues[i]}) - if err != nil { - return nil, err + // Handle custom struct types + if _, isCustomType := t.Types[typ]; isCustomType { + mapVal, ok := value.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid value for custom type %s", typ) } - encodedValues[i], err = PadZeros(pack, 32) + encoded, err := t.HashStruct(typ, mapVal) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to encode custom type %s: %w", typ, err) } - encodedTypes[i] = "bytes" + return PadZeros(encoded, 32) } - encodedData, err := SolidityPack(encodedTypes, encodedValues) + // Handle primitive types + packed, err := SolidityPack([]string{typ}, []interface{}{value}) if err != nil { return nil, err } - return encodedData, nil + return PadZeros(packed, 32) } // Encode returns the digest of the typed data and the fully encoded EIP712 typed data message. diff --git a/ethcoder/typed_data_test.go b/ethcoder/typed_data_test.go index bbccabd2..5ae0b9d4 100644 --- a/ethcoder/typed_data_test.go +++ b/ethcoder/typed_data_test.go @@ -223,10 +223,6 @@ func TestTypedDataFromJSONPart2(t *testing.T) { { "name": "from", "type": "Person" }, { "name": "to", "type": "Person[]" }, { "name": "contents", "type": "string" } - ], - "Group": [ - { "name": "name", "type": "string" }, - { "name": "members", "type": "Person[]" } ] }, "domain": { @@ -265,18 +261,24 @@ func TestTypedDataFromJSONPart2(t *testing.T) { require.NoError(t, err) require.Equal(t, "0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f", ethcoder.HexEncode(domainHash)) + personTypeHash, err := typedData.Types.TypeHash("Person") + require.NoError(t, err) + require.Equal(t, "0xfabfe1ed996349fc6027709802be19d047da1aa5d6894ff5f6486d92db2e6860", ethcoder.HexEncode(personTypeHash)) + fromArg, ok := typedData.Message["from"].(map[string]interface{}) require.True(t, ok) - personHash, err := typedData.HashStruct("Person", fromArg) + personHashStruct, err := typedData.HashStruct("Person", fromArg) require.NoError(t, err) - require.Equal(t, "0x12345", ethcoder.HexEncode(personHash)) + require.Equal(t, "0x9b4846dd48b866f0ac54d61b9b21a9e746f921cefa4ee94c4c0a1c49c774f67f", ethcoder.HexEncode(personHashStruct)) - digest, typedDataEncoded, err := typedData.Encode() + mailHashStruct, err := typedData.HashStruct("Mail", typedData.Message) require.NoError(t, err) - require.Equal(t, "0x2218fda59750be7bb9e5dfb2b49e4ec000dc2542862c5826f1fe980d6d727e95", ethcoder.HexEncode(digest)) - require.Equal(t, "0x1901f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090ff5117e79519388f3d62844df1325ebe783523d9db9762c50fa78a60400a20b5b", ethcoder.HexEncode(typedDataEncoded)) + require.Equal(t, "0xeb4221181ff3f1a83ea7313993ca9218496e424604ba9492bb4052c03d5c3df8", ethcoder.HexEncode(mailHashStruct)) - return + digest, typedDataEncoded, err := typedData.Encode() + require.NoError(t, err) + require.Equal(t, "0xa85c2e2b118698e88db68a8105b794a8cc7cec074e89ef991cb4f5f533819cc2", ethcoder.HexEncode(digest)) + require.Equal(t, "0x1901f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090feb4221181ff3f1a83ea7313993ca9218496e424604ba9492bb4052c03d5c3df8", ethcoder.HexEncode(typedDataEncoded)) // Sign and validate wallet, err := ethwallet.NewWalletFromMnemonic("dose weasel clever culture letter volume endorse used harvest ripple circle install") @@ -289,7 +291,7 @@ func TestTypedDataFromJSONPart2(t *testing.T) { // NOTE: this signature and above method has been compared against ethers v6 test require.Equal(t, - "0x296c98bed8f3fd7ea96f55ca8148b4d092cbada953c8d9205b2fff759461ab4e6d6db0b78833b954684900530caeee9aaef8e42dfd8439a3fa107e910b57e2cc1b", + "0xafd9e7d3b912a9ca989b622837ab92a8616446e6a517c486de5745dda166152f2d40f1d62593da438a65b58deacfdfbbeb7bbce2a12056815b19c678c563cc311c", ethSigedTypedDataHex, ) From a92a21b99d30370787311bce2dad7ca0394900b9 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Fri, 13 Dec 2024 13:08:37 -0500 Subject: [PATCH 09/11] update --- ethcoder/typed_data_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/ethcoder/typed_data_test.go b/ethcoder/typed_data_test.go index 5ae0b9d4..9ea218b7 100644 --- a/ethcoder/typed_data_test.go +++ b/ethcoder/typed_data_test.go @@ -8,7 +8,6 @@ import ( "github.com/0xsequence/ethkit/ethcoder" "github.com/0xsequence/ethkit/ethwallet" "github.com/0xsequence/ethkit/go-ethereum/common" - "github.com/davecgh/go-spew/spew" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -255,8 +254,6 @@ func TestTypedDataFromJSONPart2(t *testing.T) { typedData, err := ethcoder.TypedDataFromJSON(typedDataJson) require.NoError(t, err) - spew.Dump(typedData) - domainHash, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) require.NoError(t, err) require.Equal(t, "0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f", ethcoder.HexEncode(domainHash)) From c2c69528891f2a2657cda12511f0a953d004b081 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Fri, 13 Dec 2024 13:10:07 -0500 Subject: [PATCH 10/11] update --- ethcoder/typed_data.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ethcoder/typed_data.go b/ethcoder/typed_data.go index 02359ee3..7c0222be 100644 --- a/ethcoder/typed_data.go +++ b/ethcoder/typed_data.go @@ -171,8 +171,8 @@ func (t *TypedData) encodeData(primaryType string, data map[string]interface{}) // encodeValue handles the recursive encoding of values according to their types func (t *TypedData) encodeValue(typ string, value interface{}) ([]byte, error) { // Handle arrays - if strings.HasSuffix(typ, "[]") { - baseType := typ[:len(typ)-2] + if strings.Index(typ, "[") > 0 { + baseType := typ[:strings.Index(typ, "[")] values, ok := value.([]interface{}) if !ok { return nil, fmt.Errorf("expected array for type %s", typ) From 933b997088627465e7b5e9453a84010a4aab69bb Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Fri, 13 Dec 2024 13:15:21 -0500 Subject: [PATCH 11/11] update --- ethcoder/typed_data_test.go | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/ethcoder/typed_data_test.go b/ethcoder/typed_data_test.go index 9ea218b7..2f3d1d57 100644 --- a/ethcoder/typed_data_test.go +++ b/ethcoder/typed_data_test.go @@ -1,7 +1,6 @@ package ethcoder_test import ( - "fmt" "math/big" "testing" @@ -66,9 +65,8 @@ func TestTypedDataCase1(t *testing.T) { VerifyingContract: &verifyingContract, }, Message: map[string]interface{}{ - "name": "Bob", - // "wallet": common.HexToAddress("0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"), // NOTE: passing common.Address object works too - "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + "name": "Bob", + "wallet": common.HexToAddress("0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"), }, } @@ -80,25 +78,23 @@ func TestTypedDataCase1(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "0x0a94cf6625e5860fc4f330d75bcd0c3a4737957d2321d1a024540ab5320fe903", ethcoder.HexEncode(digest)) - fmt.Println("===> digest", ethcoder.HexEncode(digest)) + // fmt.Println("===> digest", ethcoder.HexEncode(digest)) // lets sign it.. wallet, err := ethwallet.NewWalletFromMnemonic("dose weasel clever culture letter volume endorse used harvest ripple circle install") assert.NoError(t, err) - // TODO: this is wrong.. we need wallet.SignTypedData(digest).. or wallet.SignData(digest) wherre digest is fully encoded with prefix, etc. - - ethSigedTypedData, err := wallet.SignMessage([]byte(digest)) + ethSigedTypedData, encodedTypeData, err := wallet.SignTypedData(typedData) ethSigedTypedDataHex := ethcoder.HexEncode(ethSigedTypedData) assert.NoError(t, err) assert.Equal(t, - "0x842ed2d5c3bf97c4977ee84e600fec7d0f9c5e21d4090b5035a3ea650ec6127d18053e4aafb631de26eb3fd5d61e4a6f2d6a106ee8e3d8d5cb0c4571d06798741b", + "0x07cc7c723b24733e11494438927012ec9b086e8edcb06022231710988ff7e54c45b0bb8911b1e06d322eb24b919f2a479e3062fee75ce57c1f7d7fc16c371fa81b", ethSigedTypedDataHex, ) // recover / validate signature - valid, err := ethwallet.ValidateEthereumSignature(wallet.Address().Hex(), digest, ethSigedTypedDataHex) + valid, err := ethwallet.ValidateEthereumSignature(wallet.Address().Hex(), encodedTypeData, ethSigedTypedDataHex) assert.NoError(t, err) assert.True(t, valid) } @@ -128,9 +124,8 @@ func TestTypedDataCase2(t *testing.T) { VerifyingContract: &verifyingContract, }, Message: map[string]interface{}{ - "name": "Bob", - // "wallet": common.HexToAddress("0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"), // NOTE: passing common.Address object works too - "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + "name": "Bob", + "wallet": common.HexToAddress("0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"), "count": uint8(4), }, } @@ -144,7 +139,6 @@ func TestTypedDataCase2(t *testing.T) { assert.Equal(t, "0x2218fda59750be7bb9e5dfb2b49e4ec000dc2542862c5826f1fe980d6d727e95", ethcoder.HexEncode(digest)) // fmt.Println("===> digest", HexEncode(digest)) - } func TestTypedDataFromJSON(t *testing.T) {