Sign typed data implementation (#1250)
* Implement EIP 712 * Cover majority of cases with tests * Add solidity and signer tests and cover integer edges case * Add thin api to sign type data using selected status account * All integers are extended to int256 and marshalled into big.Int * Document how deps works * Fix linter * Fix errors test * Add validation tests * Unmarshal every atomic type in separate functions
This commit is contained in:
parent
ee3c05c79b
commit
db786ef1d2
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/big"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
gethcommon "github.com/ethereum/go-ethereum/common"
|
gethcommon "github.com/ethereum/go-ethereum/common"
|
||||||
|
@ -22,6 +23,7 @@ import (
|
||||||
"github.com/status-im/status-go/services/rpcfilters"
|
"github.com/status-im/status-go/services/rpcfilters"
|
||||||
"github.com/status-im/status-go/services/shhext/chat"
|
"github.com/status-im/status-go/services/shhext/chat"
|
||||||
"github.com/status-im/status-go/services/shhext/chat/crypto"
|
"github.com/status-im/status-go/services/shhext/chat/crypto"
|
||||||
|
"github.com/status-im/status-go/services/typeddata"
|
||||||
"github.com/status-im/status-go/signal"
|
"github.com/status-im/status-go/signal"
|
||||||
"github.com/status-im/status-go/transactions"
|
"github.com/status-im/status-go/transactions"
|
||||||
)
|
)
|
||||||
|
@ -258,6 +260,20 @@ func (b *StatusBackend) Recover(rpcParams personal.RecoverParams) (gethcommon.Ad
|
||||||
return b.personalAPI.Recover(rpcParams)
|
return b.personalAPI.Recover(rpcParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SignTypedData accepts data and password. Gets verified account and signs typed data.
|
||||||
|
func (b *StatusBackend) SignTypedData(typed typeddata.TypedData, password string) (hexutil.Bytes, error) {
|
||||||
|
account, err := b.getVerifiedAccount(password)
|
||||||
|
if err != nil {
|
||||||
|
return hexutil.Bytes{}, err
|
||||||
|
}
|
||||||
|
chain := new(big.Int).SetUint64(b.StatusNode().Config().NetworkID)
|
||||||
|
sig, err := typeddata.Sign(typed, account.AccountKey.PrivateKey, chain)
|
||||||
|
if err != nil {
|
||||||
|
return hexutil.Bytes{}, err
|
||||||
|
}
|
||||||
|
return hexutil.Bytes(sig), err
|
||||||
|
}
|
||||||
|
|
||||||
func (b *StatusBackend) getVerifiedAccount(password string) (*account.SelectedExtKey, error) {
|
func (b *StatusBackend) getVerifiedAccount(password string) (*account.SelectedExtKey, error) {
|
||||||
selectedAccount, err := b.accountManager.SelectedAccount()
|
selectedAccount, err := b.accountManager.SelectedAccount()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/status-im/status-go/params"
|
"github.com/status-im/status-go/params"
|
||||||
"github.com/status-im/status-go/profiling"
|
"github.com/status-im/status-go/profiling"
|
||||||
"github.com/status-im/status-go/services/personal"
|
"github.com/status-im/status-go/services/personal"
|
||||||
|
"github.com/status-im/status-go/services/typeddata"
|
||||||
"github.com/status-im/status-go/signal"
|
"github.com/status-im/status-go/signal"
|
||||||
"github.com/status-im/status-go/transactions"
|
"github.com/status-im/status-go/transactions"
|
||||||
"gopkg.in/go-playground/validator.v9"
|
"gopkg.in/go-playground/validator.v9"
|
||||||
|
@ -349,6 +350,22 @@ func SendTransaction(txArgsJSON, password *C.char) *C.char {
|
||||||
return C.CString(prepareJSONResponseWithCode(hash.String(), err, code))
|
return C.CString(prepareJSONResponseWithCode(hash.String(), err, code))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SignTypedData unmarshall data into TypedData, validate it and signs with selected account,
|
||||||
|
// if password matches selected account.
|
||||||
|
//export SignTypedData
|
||||||
|
func SignTypedData(data, password *C.char) *C.char {
|
||||||
|
var typed typeddata.TypedData
|
||||||
|
err := json.Unmarshal([]byte(C.GoString(data)), &typed)
|
||||||
|
if err != nil {
|
||||||
|
return C.CString(prepareJSONResponseWithCode(nil, err, codeFailedParseParams))
|
||||||
|
}
|
||||||
|
if err := typed.Validate(); err != nil {
|
||||||
|
return C.CString(prepareJSONResponseWithCode(nil, err, codeFailedParseParams))
|
||||||
|
}
|
||||||
|
result, err := statusBackend.SignTypedData(typed, C.GoString(password))
|
||||||
|
return C.CString(prepareJSONResponse(result.String(), err))
|
||||||
|
}
|
||||||
|
|
||||||
//StartCPUProfile runs pprof for cpu
|
//StartCPUProfile runs pprof for cpu
|
||||||
//export StartCPUProfile
|
//export StartCPUProfile
|
||||||
func StartCPUProfile(dataDir *C.char) *C.char {
|
func StartCPUProfile(dataDir *C.char) *C.char {
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
EIP712 example smart contract
|
||||||
|
=============================
|
||||||
|
|
||||||
|
example.sol is taken from https://github.com/ethereum/EIPs/blob/master/assets/eip-712/Example.sol
|
||||||
|
and slightly modified in order to read results from golang bindings.
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,98 @@
|
||||||
|
pragma solidity ^0.4.24;
|
||||||
|
|
||||||
|
contract Example {
|
||||||
|
|
||||||
|
struct EIP712Domain {
|
||||||
|
string name;
|
||||||
|
string version;
|
||||||
|
uint256 chainId;
|
||||||
|
address verifyingContract;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Person {
|
||||||
|
string name;
|
||||||
|
address wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Mail {
|
||||||
|
Person from;
|
||||||
|
Person to;
|
||||||
|
string contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256(
|
||||||
|
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes32 constant PERSON_TYPEHASH = keccak256(
|
||||||
|
"Person(string name,address wallet)"
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes32 constant MAIL_TYPEHASH = keccak256(
|
||||||
|
"Mail(Person from,Person to,string contents)Person(string name,address wallet)"
|
||||||
|
);
|
||||||
|
|
||||||
|
Mail internal mail = Mail({
|
||||||
|
from: Person({
|
||||||
|
name: "Cow",
|
||||||
|
wallet: 0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826
|
||||||
|
}),
|
||||||
|
to: Person({
|
||||||
|
name: "Bob",
|
||||||
|
wallet: 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB
|
||||||
|
}),
|
||||||
|
contents: "Hello, Bob!"
|
||||||
|
});
|
||||||
|
|
||||||
|
bytes32 public DOMAIN_SEPARATOR;
|
||||||
|
bytes32 public MAIL;
|
||||||
|
|
||||||
|
constructor () public {
|
||||||
|
DOMAIN_SEPARATOR = hash(EIP712Domain({
|
||||||
|
name: "Ether Mail",
|
||||||
|
version: '1',
|
||||||
|
chainId: 1,
|
||||||
|
// verifyingContract: this
|
||||||
|
verifyingContract: 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC
|
||||||
|
}));
|
||||||
|
MAIL = hash(mail);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hash(EIP712Domain eip712Domain) internal pure returns (bytes32) {
|
||||||
|
return keccak256(abi.encode(
|
||||||
|
EIP712DOMAIN_TYPEHASH,
|
||||||
|
keccak256(bytes(eip712Domain.name)),
|
||||||
|
keccak256(bytes(eip712Domain.version)),
|
||||||
|
eip712Domain.chainId,
|
||||||
|
eip712Domain.verifyingContract
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hash(Person person) internal pure returns (bytes32) {
|
||||||
|
return keccak256(abi.encode(
|
||||||
|
PERSON_TYPEHASH,
|
||||||
|
keccak256(bytes(person.name)),
|
||||||
|
person.wallet
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hash(Mail mail) internal pure returns (bytes32) {
|
||||||
|
return keccak256(abi.encode(
|
||||||
|
MAIL_TYPEHASH,
|
||||||
|
hash(mail.from),
|
||||||
|
hash(mail.to),
|
||||||
|
keccak256(bytes(mail.contents))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function verify(uint8 v, bytes32 r, bytes32 s) public returns (bool) {
|
||||||
|
// Note: we need to use `encodePacked` here instead of `encode`.
|
||||||
|
bytes32 digest = keccak256(abi.encodePacked(
|
||||||
|
"\x19\x01",
|
||||||
|
DOMAIN_SEPARATOR,
|
||||||
|
hash(mail)
|
||||||
|
));
|
||||||
|
require(ecrecover(digest, v, r, s) == msg.sender);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,180 @@
|
||||||
|
package typeddata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
bytes32Type, _ = abi.NewType("bytes32")
|
||||||
|
int256Type, _ = abi.NewType("int256")
|
||||||
|
)
|
||||||
|
|
||||||
|
// deps runs breadth-first traversal starting from target and collects all
|
||||||
|
// found composite dependencies types into result slice. target always will be first
|
||||||
|
// in the result array. all other dependencies are sorted alphabetically.
|
||||||
|
// for example: Z{c C, a A} A{c C} and the target is Z.
|
||||||
|
// result would be Z, A, B, C
|
||||||
|
func deps(target string, types Types) []string {
|
||||||
|
unique := map[string]struct{}{}
|
||||||
|
unique[target] = struct{}{}
|
||||||
|
visited := []string{target}
|
||||||
|
deps := []string{}
|
||||||
|
for len(visited) > 0 {
|
||||||
|
current := visited[0]
|
||||||
|
fields := types[current]
|
||||||
|
for i := range fields {
|
||||||
|
f := fields[i]
|
||||||
|
if _, defined := types[f.Type]; defined {
|
||||||
|
if _, exist := unique[f.Type]; !exist {
|
||||||
|
visited = append(visited, f.Type)
|
||||||
|
unique[f.Type] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visited = visited[1:]
|
||||||
|
deps = append(deps, current)
|
||||||
|
}
|
||||||
|
sort.Slice(deps[1:], func(i, j int) bool {
|
||||||
|
return deps[1:][i] < deps[1:][j]
|
||||||
|
})
|
||||||
|
return deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func typeString(target string, types Types) string {
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
for _, dep := range deps(target, types) {
|
||||||
|
b.WriteString(dep)
|
||||||
|
b.WriteString("(")
|
||||||
|
fields := types[dep]
|
||||||
|
first := true
|
||||||
|
for i := range fields {
|
||||||
|
if !first {
|
||||||
|
b.WriteString(",")
|
||||||
|
} else {
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
f := fields[i]
|
||||||
|
b.WriteString(f.Type)
|
||||||
|
b.WriteString(" ")
|
||||||
|
b.WriteString(f.Name)
|
||||||
|
}
|
||||||
|
b.WriteString(")")
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func typeHash(target string, types Types) (rst common.Hash) {
|
||||||
|
return crypto.Keccak256Hash([]byte(typeString(target, types)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashStruct(target string, data map[string]json.RawMessage, types Types) (rst common.Hash, err error) {
|
||||||
|
fields := types[target]
|
||||||
|
typeh := typeHash(target, types)
|
||||||
|
args := abi.Arguments{{Type: bytes32Type}}
|
||||||
|
vals := []interface{}{typeh}
|
||||||
|
for i := range fields {
|
||||||
|
f := fields[i]
|
||||||
|
val, typ, err := toABITypeAndValue(f, data, types)
|
||||||
|
if err != nil {
|
||||||
|
return rst, err
|
||||||
|
}
|
||||||
|
vals = append(vals, val)
|
||||||
|
args = append(args, abi.Argument{Name: f.Name, Type: typ})
|
||||||
|
}
|
||||||
|
packed, err := args.Pack(vals...)
|
||||||
|
if err != nil {
|
||||||
|
return rst, err
|
||||||
|
}
|
||||||
|
return crypto.Keccak256Hash(packed), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toABITypeAndValue(f Field, data map[string]json.RawMessage, types Types) (val interface{}, typ abi.Type, err error) {
|
||||||
|
if f.Type == "string" {
|
||||||
|
var str string
|
||||||
|
if err = json.Unmarshal(data[f.Name], &str); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return crypto.Keccak256Hash([]byte(str)), bytes32Type, nil
|
||||||
|
} else if f.Type == "bytes" {
|
||||||
|
var bytes hexutil.Bytes
|
||||||
|
if err = json.Unmarshal(data[f.Name], &bytes); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return crypto.Keccak256Hash(bytes), bytes32Type, nil
|
||||||
|
} else if _, exist := types[f.Type]; exist {
|
||||||
|
var obj map[string]json.RawMessage
|
||||||
|
if err = json.Unmarshal(data[f.Name], &obj); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val, err = hashStruct(f.Type, obj, types)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return val, bytes32Type, nil
|
||||||
|
}
|
||||||
|
return atomicType(f, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func atomicType(f Field, data map[string]json.RawMessage) (val interface{}, typ abi.Type, err error) {
|
||||||
|
typ, err = abi.NewType(f.Type)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if typ.T == abi.SliceTy || typ.T == abi.ArrayTy || typ.T == abi.FunctionTy {
|
||||||
|
return val, typ, errors.New("arrays, slices and functions are not supported")
|
||||||
|
} else if typ.T == abi.FixedBytesTy {
|
||||||
|
return toFixedBytes(f, data[f.Name])
|
||||||
|
} else if typ.T == abi.AddressTy {
|
||||||
|
val, err = toAddress(f, data[f.Name])
|
||||||
|
} else if typ.T == abi.IntTy || typ.T == abi.UintTy {
|
||||||
|
return toInt(f, data[f.Name])
|
||||||
|
} else if typ.T == abi.BoolTy {
|
||||||
|
val, err = toBool(f, data[f.Name])
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("type %s is not supported", f.Type)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func toFixedBytes(f Field, data json.RawMessage) (rst [32]byte, typ abi.Type, err error) {
|
||||||
|
var bytes hexutil.Bytes
|
||||||
|
if err = json.Unmarshal(data, &bytes); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
typ = bytes32Type
|
||||||
|
rst = [32]byte{}
|
||||||
|
// reduce the length to the advertised size
|
||||||
|
if len(bytes) > typ.Size {
|
||||||
|
bytes = bytes[:typ.Size]
|
||||||
|
}
|
||||||
|
copy(rst[:], bytes)
|
||||||
|
return rst, typ, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toInt(f Field, data json.RawMessage) (val *big.Int, typ abi.Type, err error) {
|
||||||
|
var rst big.Int
|
||||||
|
if err = json.Unmarshal(data, &rst); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return &rst, int256Type, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toAddress(f Field, data json.RawMessage) (rst common.Address, err error) {
|
||||||
|
err = json.Unmarshal(data, &rst)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func toBool(f Field, data json.RawMessage) (rst bool, err error) {
|
||||||
|
err = json.Unmarshal(data, &rst)
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,273 @@
|
||||||
|
package typeddata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math/big"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTypeString(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
description string
|
||||||
|
typeString string
|
||||||
|
types Types
|
||||||
|
target string
|
||||||
|
}
|
||||||
|
for _, tc := range []testCase{
|
||||||
|
{
|
||||||
|
"WithoutDeps",
|
||||||
|
"Person(string name,address wallet)",
|
||||||
|
Types{"Person": []Field{{Name: "name", Type: "string"}, {Name: "wallet", Type: "address"}}},
|
||||||
|
"Person",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SingleDep",
|
||||||
|
"Mail(Person from,Person to)Person(string name,address wallet)",
|
||||||
|
Types{
|
||||||
|
"Person": []Field{{Name: "name", Type: "string"}, {Name: "wallet", Type: "address"}},
|
||||||
|
"Mail": []Field{{Name: "from", Type: "Person"}, {Name: "to", Type: "Person"}},
|
||||||
|
},
|
||||||
|
"Mail",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"DepsOrdered",
|
||||||
|
"Z(A a,B b)A(string name)B(string name)",
|
||||||
|
Types{
|
||||||
|
"A": []Field{{Name: "name", Type: "string"}},
|
||||||
|
"B": []Field{{Name: "name", Type: "string"}},
|
||||||
|
"Z": []Field{{Name: "a", Type: "A"}, {Name: "b", Type: "B"}},
|
||||||
|
},
|
||||||
|
"Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"RecursiveDepsIgnored",
|
||||||
|
"Z(A a)A(Z z)",
|
||||||
|
Types{
|
||||||
|
"A": []Field{{Name: "z", Type: "Z"}},
|
||||||
|
"Z": []Field{{Name: "a", Type: "A"}},
|
||||||
|
},
|
||||||
|
"Z",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
|
require.Equal(t, tc.typeString, typeString(tc.target, tc.types))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeData(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
description string
|
||||||
|
message map[string]json.RawMessage
|
||||||
|
types Types
|
||||||
|
target string
|
||||||
|
result func(testCase) common.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes32, _ := abi.NewType("bytes32")
|
||||||
|
addr, _ := abi.NewType("address")
|
||||||
|
boolT, _ := abi.NewType("bool")
|
||||||
|
|
||||||
|
for _, tc := range []testCase{
|
||||||
|
{
|
||||||
|
"HexAddressConvertedToBytes",
|
||||||
|
map[string]json.RawMessage{"wallet": json.RawMessage(`"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"`)},
|
||||||
|
Types{"A": []Field{{Name: "wallet", Type: "address"}}},
|
||||||
|
"A",
|
||||||
|
func(tc testCase) common.Hash {
|
||||||
|
args := abi.Arguments{{Type: bytes32}, {Type: addr}}
|
||||||
|
typehash := typeHash(tc.target, tc.types)
|
||||||
|
var data common.Address
|
||||||
|
assert.NoError(t, json.Unmarshal(tc.message["wallet"], &data))
|
||||||
|
packed, _ := args.Pack(typehash, data)
|
||||||
|
return crypto.Keccak256Hash(packed)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"StringHashed",
|
||||||
|
map[string]json.RawMessage{"name": json.RawMessage(`"AAA"`)},
|
||||||
|
Types{"A": []Field{{Name: "name", Type: "string"}}},
|
||||||
|
"A",
|
||||||
|
func(tc testCase) common.Hash {
|
||||||
|
args := abi.Arguments{{Type: bytes32}, {Type: bytes32}}
|
||||||
|
typehash := typeHash(tc.target, tc.types)
|
||||||
|
var data string
|
||||||
|
assert.NoError(t, json.Unmarshal(tc.message["name"], &data))
|
||||||
|
packed, _ := args.Pack(typehash, crypto.Keccak256Hash([]byte(data)))
|
||||||
|
return crypto.Keccak256Hash(packed)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"BytesHashed",
|
||||||
|
map[string]json.RawMessage{"name": json.RawMessage(`"0x010203"`)}, // []byte{1,2,3}
|
||||||
|
Types{"A": []Field{{Name: "name", Type: "bytes"}}},
|
||||||
|
"A",
|
||||||
|
func(tc testCase) common.Hash {
|
||||||
|
args := abi.Arguments{{Type: bytes32}, {Type: bytes32}}
|
||||||
|
typehash := typeHash(tc.target, tc.types)
|
||||||
|
var data hexutil.Bytes
|
||||||
|
assert.NoError(t, json.Unmarshal(tc.message["name"], &data))
|
||||||
|
packed, _ := args.Pack(typehash, crypto.Keccak256Hash(data))
|
||||||
|
return crypto.Keccak256Hash(packed)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"FixedBytesAsIs",
|
||||||
|
map[string]json.RawMessage{"name": json.RawMessage(`"0x010203"`)}, // []byte{1,2,3}
|
||||||
|
Types{"A": []Field{{Name: "name", Type: "bytes32"}}},
|
||||||
|
"A",
|
||||||
|
func(tc testCase) common.Hash {
|
||||||
|
args := abi.Arguments{{Type: bytes32}, {Type: bytes32}}
|
||||||
|
typehash := typeHash(tc.target, tc.types)
|
||||||
|
var data hexutil.Bytes
|
||||||
|
assert.NoError(t, json.Unmarshal(tc.message["name"], &data))
|
||||||
|
rst := [32]byte{}
|
||||||
|
copy(rst[:], data)
|
||||||
|
packed, _ := args.Pack(typehash, rst)
|
||||||
|
return crypto.Keccak256Hash(packed)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"BoolAsIs",
|
||||||
|
map[string]json.RawMessage{"flag": json.RawMessage("true")},
|
||||||
|
Types{"A": []Field{{Name: "flag", Type: "bool"}}},
|
||||||
|
"A",
|
||||||
|
func(tc testCase) common.Hash {
|
||||||
|
args := abi.Arguments{{Type: bytes32}, {Type: boolT}}
|
||||||
|
typehash := typeHash(tc.target, tc.types)
|
||||||
|
var data bool
|
||||||
|
assert.NoError(t, json.Unmarshal(tc.message["flag"], &data))
|
||||||
|
packed, _ := args.Pack(typehash, data)
|
||||||
|
return crypto.Keccak256Hash(packed)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Int32Uint32AsIs",
|
||||||
|
map[string]json.RawMessage{"I": json.RawMessage("-10"), "UI": json.RawMessage("10")},
|
||||||
|
Types{"A": []Field{{Name: "I", Type: "int32"}, {Name: "UI", Type: "uint32"}}},
|
||||||
|
"A",
|
||||||
|
func(tc testCase) common.Hash {
|
||||||
|
args := abi.Arguments{{Type: bytes32}, {Type: int256Type}, {Type: int256Type}}
|
||||||
|
typehash := typeHash(tc.target, tc.types)
|
||||||
|
packed, _ := args.Pack(typehash, big.NewInt(-10), big.NewInt(10))
|
||||||
|
return crypto.Keccak256Hash(packed)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SignedUnsignedIntegersBiggerThen64",
|
||||||
|
map[string]json.RawMessage{
|
||||||
|
"i128": json.RawMessage("1"),
|
||||||
|
"i256": json.RawMessage("1"),
|
||||||
|
"ui128": json.RawMessage("1"),
|
||||||
|
"ui256": json.RawMessage("1"),
|
||||||
|
},
|
||||||
|
Types{"A": []Field{
|
||||||
|
{Name: "i128", Type: "int128"}, {Name: "i256", Type: "int256"},
|
||||||
|
{Name: "ui128", Type: "uint128"}, {Name: "ui256", Type: "uint256"},
|
||||||
|
}},
|
||||||
|
"A",
|
||||||
|
func(tc testCase) common.Hash {
|
||||||
|
intBig, _ := abi.NewType("int128")
|
||||||
|
uintBig, _ := abi.NewType("uint128")
|
||||||
|
args := abi.Arguments{{Type: bytes32},
|
||||||
|
{Type: intBig}, {Type: intBig}, {Type: uintBig}, {Type: uintBig}}
|
||||||
|
typehash := typeHash(tc.target, tc.types)
|
||||||
|
val := big.NewInt(1)
|
||||||
|
packed, _ := args.Pack(typehash, val, val, val, val)
|
||||||
|
return crypto.Keccak256Hash(packed)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CompositeTypesAreRecursivelyEncoded",
|
||||||
|
map[string]json.RawMessage{"a": json.RawMessage(`{"name":"AAA"}`)},
|
||||||
|
Types{"A": []Field{{Name: "name", Type: "string"}}, "Z": []Field{{Name: "a", Type: "A"}}},
|
||||||
|
"Z",
|
||||||
|
func(tc testCase) common.Hash {
|
||||||
|
args := abi.Arguments{{Type: bytes32}, {Type: bytes32}}
|
||||||
|
zhash := typeHash(tc.target, tc.types)
|
||||||
|
ahash := typeHash("A", tc.types)
|
||||||
|
var A map[string]string
|
||||||
|
assert.NoError(t, json.Unmarshal(tc.message["a"], &A))
|
||||||
|
apacked, _ := args.Pack(ahash, crypto.Keccak256Hash([]byte(A["name"])))
|
||||||
|
packed, _ := args.Pack(zhash, crypto.Keccak256Hash(apacked))
|
||||||
|
return crypto.Keccak256Hash(packed)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
|
encoded, err := hashStruct(tc.target, tc.message, tc.types)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.result(tc), encoded)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeDataErrors(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
description string
|
||||||
|
message map[string]json.RawMessage
|
||||||
|
types Types
|
||||||
|
target string
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []testCase{
|
||||||
|
{
|
||||||
|
"FailedUnmxarshalAsAString",
|
||||||
|
map[string]json.RawMessage{"a": json.RawMessage("1")},
|
||||||
|
Types{"A": []Field{{Name: "name", Type: "string"}}},
|
||||||
|
"A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"FailedUnmarshalToHexBytesToABytes",
|
||||||
|
map[string]json.RawMessage{"a": {1, 2, 3}},
|
||||||
|
Types{"A": []Field{{Name: "name", Type: "bytes"}}},
|
||||||
|
"A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CompositeTypeIsNotAnObject",
|
||||||
|
map[string]json.RawMessage{"a": json.RawMessage(`"AAA"`)},
|
||||||
|
Types{"A": []Field{{Name: "name", Type: "string"}}, "Z": []Field{{Name: "a", Type: "A"}}},
|
||||||
|
"Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CompositeTypesFailed",
|
||||||
|
map[string]json.RawMessage{"a": json.RawMessage(`{"name":10}`)},
|
||||||
|
Types{"A": []Field{{Name: "name", Type: "string"}}, "Z": []Field{{Name: "a", Type: "A"}}},
|
||||||
|
"Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ArraysNotSupported",
|
||||||
|
map[string]json.RawMessage{"a": json.RawMessage("[1,2]")},
|
||||||
|
Types{"A": []Field{{Name: "name", Type: "int8[2]"}}},
|
||||||
|
"A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SlicesNotSupported",
|
||||||
|
map[string]json.RawMessage{"a": json.RawMessage("[1,2]")},
|
||||||
|
Types{"A": []Field{{Name: "name", Type: "int[]"}}},
|
||||||
|
"A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"FailedToUnmarshalInteger",
|
||||||
|
map[string]json.RawMessage{"a": json.RawMessage("x00x")},
|
||||||
|
Types{"A": []Field{{Name: "name", Type: "uint256"}}},
|
||||||
|
"A",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
|
encoded, err := hashStruct(tc.target, tc.message, tc.types)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, common.Hash{}, encoded)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package typeddata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// x19 to avoid collision with rlp encode. x01 version byte defined in EIP-191
|
||||||
|
messagePadding = []byte{0x19, 0x01}
|
||||||
|
)
|
||||||
|
|
||||||
|
func encodeData(typed TypedData) (rst common.Hash, err error) {
|
||||||
|
domainSeparator, err := hashStruct(eip712Domain, typed.Domain, typed.Types)
|
||||||
|
if err != nil {
|
||||||
|
return rst, err
|
||||||
|
}
|
||||||
|
primary, err := hashStruct(typed.PrimaryType, typed.Message, typed.Types)
|
||||||
|
if err != nil {
|
||||||
|
return rst, err
|
||||||
|
}
|
||||||
|
return crypto.Keccak256Hash(messagePadding, domainSeparator[:], primary[:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign TypedData with a given private key. Verify that chainId in the typed data matches currently selected chain.
|
||||||
|
func Sign(typed TypedData, prv *ecdsa.PrivateKey, chain *big.Int) ([]byte, error) {
|
||||||
|
if err := typed.ValidateChainID(chain); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hash, err := encodeData(typed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sig, err := crypto.Sign(hash[:], prv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sig[64] += 27
|
||||||
|
return sig, nil
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
package typeddata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"math"
|
||||||
|
"math/big"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
|
||||||
|
"github.com/ethereum/go-ethereum/core"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/status-im/status-go/services/typeddata/eip712example"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
fromWallet = `
|
||||||
|
{
|
||||||
|
"name": "Cow",
|
||||||
|
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
toWallet = `
|
||||||
|
{
|
||||||
|
"name": "Bob",
|
||||||
|
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestChainIDValidation(t *testing.T) {
|
||||||
|
chain := big.NewInt(10)
|
||||||
|
type testCase struct {
|
||||||
|
description string
|
||||||
|
domain map[string]json.RawMessage
|
||||||
|
}
|
||||||
|
for _, tc := range []testCase{
|
||||||
|
{
|
||||||
|
"ChainIDMismatch",
|
||||||
|
map[string]json.RawMessage{chainIDKey: json.RawMessage("1")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ChainIDNotAnInt",
|
||||||
|
map[string]json.RawMessage{chainIDKey: json.RawMessage(`"aa"`)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NoChainIDKey",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
|
typed := TypedData{Domain: tc.domain}
|
||||||
|
_, err := Sign(typed, nil, chain)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInteroparableWithSolidity(t *testing.T) {
|
||||||
|
key, _ := crypto.GenerateKey()
|
||||||
|
testaddr := crypto.PubkeyToAddress(key.PublicKey)
|
||||||
|
genesis := core.GenesisAlloc{
|
||||||
|
testaddr: {Balance: new(big.Int).SetInt64(math.MaxInt64)},
|
||||||
|
}
|
||||||
|
backend := backends.NewSimulatedBackend(genesis, math.MaxInt64)
|
||||||
|
opts := bind.NewKeyedTransactor(key)
|
||||||
|
_, _, example, err := eip712example.DeployExample(opts, backend)
|
||||||
|
require.NoError(t, err)
|
||||||
|
backend.Commit()
|
||||||
|
|
||||||
|
domainSol, err := example.DOMAINSEPARATOR(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
mailSol, err := example.MAIL(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mytypes := Types{
|
||||||
|
eip712Domain: []Field{
|
||||||
|
{Name: "name", Type: "string"},
|
||||||
|
{Name: "version", Type: "string"},
|
||||||
|
{Name: "chainId", Type: "uint256"},
|
||||||
|
{Name: "verifyingContract", Type: "address"},
|
||||||
|
},
|
||||||
|
"Person": []Field{
|
||||||
|
{Name: "name", Type: "string"},
|
||||||
|
{Name: "wallet", Type: "address"},
|
||||||
|
},
|
||||||
|
"Mail": []Field{
|
||||||
|
{Name: "from", Type: "Person"},
|
||||||
|
{Name: "to", Type: "Person"},
|
||||||
|
{Name: "contents", Type: "string"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
domain := map[string]json.RawMessage{
|
||||||
|
"name": json.RawMessage(`"Ether Mail"`),
|
||||||
|
"version": json.RawMessage(`"1"`),
|
||||||
|
"chainId": json.RawMessage("1"),
|
||||||
|
"verifyingContract": json.RawMessage(`"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"`),
|
||||||
|
}
|
||||||
|
msg := map[string]json.RawMessage{
|
||||||
|
"from": json.RawMessage(fromWallet),
|
||||||
|
"to": json.RawMessage(toWallet),
|
||||||
|
"contents": json.RawMessage(`"Hello, Bob!"`),
|
||||||
|
}
|
||||||
|
typed := TypedData{
|
||||||
|
Types: mytypes,
|
||||||
|
PrimaryType: "Mail",
|
||||||
|
Domain: domain,
|
||||||
|
Message: msg,
|
||||||
|
}
|
||||||
|
|
||||||
|
domainHash, err := hashStruct(eip712Domain, typed.Domain, typed.Types)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, domainSol[:], domainHash[:])
|
||||||
|
|
||||||
|
mailHash, err := hashStruct(typed.PrimaryType, typed.Message, typed.Types)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, mailSol[:], mailHash[:])
|
||||||
|
|
||||||
|
signature, err := Sign(typed, key, big.NewInt(1))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, signature, 65)
|
||||||
|
|
||||||
|
r := [32]byte{}
|
||||||
|
copy(r[:], signature[:32])
|
||||||
|
s := [32]byte{}
|
||||||
|
copy(s[:], signature[32:64])
|
||||||
|
v := signature[64]
|
||||||
|
tx, err := example.Verify(opts, v, r, s)
|
||||||
|
require.NoError(t, err)
|
||||||
|
backend.Commit()
|
||||||
|
receipt, err := bind.WaitMined(context.TODO(), backend, tx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status)
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package typeddata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
eip712Domain = "EIP712Domain"
|
||||||
|
chainIDKey = "chainId"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Types define fields for each composite type.
|
||||||
|
type Types map[string][]Field
|
||||||
|
|
||||||
|
// Field stores name and solidity type of the field.
|
||||||
|
type Field struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks that both name and type are not empty.
|
||||||
|
func (f Field) Validate() error {
|
||||||
|
if len(f.Name) == 0 {
|
||||||
|
return errors.New("`name` is required")
|
||||||
|
}
|
||||||
|
if len(f.Type) == 0 {
|
||||||
|
return errors.New("`type` is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypedData defines typed data according to eip-712.
|
||||||
|
type TypedData struct {
|
||||||
|
Types Types `json:"types"`
|
||||||
|
PrimaryType string `json:"primaryType"`
|
||||||
|
Domain map[string]json.RawMessage `json:"domain"`
|
||||||
|
Message map[string]json.RawMessage `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that required fields are defined.
|
||||||
|
// This method doesn't check if dependencies of the main type are defined, it will be validated
|
||||||
|
// when type string is computed.
|
||||||
|
func (t TypedData) Validate() error {
|
||||||
|
if _, exist := t.Types[eip712Domain]; !exist {
|
||||||
|
return fmt.Errorf("`%s` must be in `types`", eip712Domain)
|
||||||
|
}
|
||||||
|
if t.PrimaryType == "" {
|
||||||
|
return errors.New("`primaryType` is required")
|
||||||
|
}
|
||||||
|
if _, exist := t.Types[t.PrimaryType]; !exist {
|
||||||
|
return fmt.Errorf("primary type `%s` not defined in types", t.PrimaryType)
|
||||||
|
}
|
||||||
|
if t.Domain == nil {
|
||||||
|
return errors.New("`domain` is required")
|
||||||
|
}
|
||||||
|
if t.Message == nil {
|
||||||
|
return errors.New("`message` is required")
|
||||||
|
}
|
||||||
|
for typ := range t.Types {
|
||||||
|
fields := t.Types[typ]
|
||||||
|
for i := range fields {
|
||||||
|
if err := fields[i].Validate(); err != nil {
|
||||||
|
return fmt.Errorf("field %d from type `%s` is invalid: %v", i, typ, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateChainID accept chain as big integer and verifies if typed data belongs to the same chain.
|
||||||
|
func (t TypedData) ValidateChainID(chain *big.Int) error {
|
||||||
|
if _, exist := t.Domain[chainIDKey]; !exist {
|
||||||
|
return fmt.Errorf("domain misses chain key %s", chainIDKey)
|
||||||
|
}
|
||||||
|
var chainID int64
|
||||||
|
if err := json.Unmarshal(t.Domain[chainIDKey], &chainID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if chainID != chain.Int64() {
|
||||||
|
return fmt.Errorf("chainId %d doesn't match selected chain %s", chainID, chain)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
package typeddata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnmarshalFull(t *testing.T) {
|
||||||
|
data := `
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Mail": [
|
||||||
|
{
|
||||||
|
"name": "from",
|
||||||
|
"type": "Person"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "to",
|
||||||
|
"type": "Person"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "contents",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"primaryType": "Mail",
|
||||||
|
"domain": {
|
||||||
|
"name": "Ether Mail",
|
||||||
|
"version": "1",
|
||||||
|
"chainId": 1,
|
||||||
|
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"from": {
|
||||||
|
"name": "Cow",
|
||||||
|
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"name": "Bob",
|
||||||
|
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
|
||||||
|
},
|
||||||
|
"contents": "Hello, Bob!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
var typed TypedData
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(data), &typed))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateField(t *testing.T) {
|
||||||
|
f := Field{}
|
||||||
|
require.EqualError(t, f.Validate(), "`name` is required")
|
||||||
|
f.Name = "name"
|
||||||
|
require.EqualError(t, f.Validate(), "`type` is required")
|
||||||
|
f.Type = "type"
|
||||||
|
require.NoError(t, f.Validate())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateTypedData(t *testing.T) {
|
||||||
|
d := TypedData{Types: Types{}}
|
||||||
|
require.EqualError(t, d.Validate(), "`EIP712Domain` must be in `types`")
|
||||||
|
d.Types[eip712Domain] = []Field{}
|
||||||
|
require.EqualError(t, d.Validate(), "`primaryType` is required")
|
||||||
|
d.PrimaryType = "primary"
|
||||||
|
d.Types[d.PrimaryType] = []Field{}
|
||||||
|
require.EqualError(t, d.Validate(), "`domain` is required")
|
||||||
|
d.Domain = map[string]json.RawMessage{}
|
||||||
|
require.EqualError(t, d.Validate(), "`message` is required")
|
||||||
|
d.Message = map[string]json.RawMessage{}
|
||||||
|
require.NoError(t, d.Validate())
|
||||||
|
d.Types[d.PrimaryType] = append(d.Types[d.PrimaryType], Field{Name: "name"})
|
||||||
|
require.EqualError(t, d.Validate(), "field 0 from type `primary` is invalid: `type` is required")
|
||||||
|
d.Types[d.PrimaryType][0].Type = "tttt"
|
||||||
|
require.NoError(t, d.Validate())
|
||||||
|
}
|
Loading…
Reference in New Issue