Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improved eip712 support #149

Merged
merged 11 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
135 changes: 126 additions & 9 deletions ethcoder/typed_data.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package ethcoder

import (
"bytes"
"encoding/json"
"fmt"
"math/big"
"sort"
Expand Down Expand Up @@ -192,30 +194,145 @@ func (t *TypedData) encodeData(primaryType string, data map[string]interface{})
return encodedData, nil
}

func (t *TypedData) EncodeDigest() ([]byte, error) {
EIP191_HEADER := "0x1901"
// 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 (0x1901 + domain + hashStruct(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 {
// Intermediary structure to decode message field
type TypedDataRaw struct {
Types TypedDataTypes `json:"types"`
PrimaryType string `json:"primaryType"`
Domain TypedDataDomain `json:"domain"`
Message map[string]interface{} `json:"message"`
}

// Json decoder with json.Number support
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"})
}
}

// 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)
}
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)

typ, ok := primaryDomainTypeMap[k]
if !ok {
return fmt.Errorf("type %s is not defined", k)
}
// fmt.Println("===> typ", k, typ)

// ...
customType, ok := raw.Types[typ]
if ok {
val := fmt.Sprintf("%v", v)
_ = customType
_ = val
// 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)
}
processedMessage[k] = out[0]
}
}
hashBytes := crypto.Keccak256(hashPack)

return hashBytes, nil
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
}
152 changes: 149 additions & 3 deletions ethcoder/typed_data_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package ethcoder_test

import (
"fmt"
"math/big"
"testing"

"github.com/0xsequence/ethkit/ethcoder"
"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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -135,10 +139,152 @@ 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
}
}`

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)
}

// 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 := `{
"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)
}
Loading
Loading