laconicd-deprecated/ethereum/eip712/eip712.go
yihuang 29d3abcf09
!feat(deps): Upgrade cosmos-sdk to v0.46.0 (#1168)
* Reuse cosmos-sdk client library to create keyring

Extracted from https://github.com/evmos/ethermint/pull/1168
Cleanup cmd code for easier to migration to cosmos-sdk 0.46

* Update cosmos-sdk v0.46

prepare for implementing cosmos-sdk feemarket and tx prioritization

changelog

refactor cmd

use sdkmath

fix lint

fix unit tests

fix unit test genesis

fix unit tests

fix unit test env setup

fix unit tests

fix unit tests

register PrivKey impl

fix extension options

fix lint

fix unit tests

make HandlerOption.Validate private

gofumpt

fix msg response decoding

fix sim test

bump cosmos-sdk version

fix sim test

sdk 46

fix unit test

fix unit tests

update ibc-go
2022-07-28 15:43:49 +02:00

455 lines
9.8 KiB
Go

package eip712
import (
"bytes"
"encoding/json"
"fmt"
"math/big"
"reflect"
"strings"
sdkmath "cosmossdk.io/math"
"golang.org/x/text/cases"
"golang.org/x/text/language"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
)
// ComputeTypedDataHash computes keccak hash of typed data for signing.
func ComputeTypedDataHash(typedData apitypes.TypedData) ([]byte, error) {
domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
if err != nil {
err = sdkerrors.Wrap(err, "failed to pack and hash typedData EIP712Domain")
return nil, err
}
typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message)
if err != nil {
err = sdkerrors.Wrap(err, "failed to pack and hash typedData primary type")
return nil, err
}
rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash)))
return crypto.Keccak256(rawData), nil
}
// WrapTxToTypedData is an ultimate method that wraps Amino-encoded Cosmos Tx JSON data
// into an EIP712-compatible TypedData request.
func WrapTxToTypedData(
cdc codectypes.AnyUnpacker,
chainID uint64,
msg sdk.Msg,
data []byte,
feeDelegation *FeeDelegationOptions,
) (apitypes.TypedData, error) {
txData := make(map[string]interface{})
if err := json.Unmarshal(data, &txData); err != nil {
return apitypes.TypedData{}, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, "failed to JSON unmarshal data")
}
domain := apitypes.TypedDataDomain{
Name: "Cosmos Web3",
Version: "1.0.0",
ChainId: math.NewHexOrDecimal256(int64(chainID)),
VerifyingContract: "cosmos",
Salt: "0",
}
msgTypes, err := extractMsgTypes(cdc, "MsgValue", msg)
if err != nil {
return apitypes.TypedData{}, err
}
if feeDelegation != nil {
feeInfo, ok := txData["fee"].(map[string]interface{})
if !ok {
return apitypes.TypedData{}, sdkerrors.Wrap(sdkerrors.ErrInvalidType, "cannot parse fee from tx data")
}
feeInfo["feePayer"] = feeDelegation.FeePayer.String()
// also patching msgTypes to include feePayer
msgTypes["Fee"] = []apitypes.Type{
{Name: "feePayer", Type: "string"},
{Name: "amount", Type: "Coin[]"},
{Name: "gas", Type: "string"},
}
}
typedData := apitypes.TypedData{
Types: msgTypes,
PrimaryType: "Tx",
Domain: domain,
Message: txData,
}
return typedData, nil
}
type FeeDelegationOptions struct {
FeePayer sdk.AccAddress
}
func extractMsgTypes(cdc codectypes.AnyUnpacker, msgTypeName string, msg sdk.Msg) (apitypes.Types, error) {
rootTypes := apitypes.Types{
"EIP712Domain": {
{
Name: "name",
Type: "string",
},
{
Name: "version",
Type: "string",
},
{
Name: "chainId",
Type: "uint256",
},
{
Name: "verifyingContract",
Type: "string",
},
{
Name: "salt",
Type: "string",
},
},
"Tx": {
{Name: "account_number", Type: "string"},
{Name: "chain_id", Type: "string"},
{Name: "fee", Type: "Fee"},
{Name: "memo", Type: "string"},
{Name: "msgs", Type: "Msg[]"},
{Name: "sequence", Type: "string"},
// Note timeout_height was removed because it was not getting filled with the legacyTx
// {Name: "timeout_height", Type: "string"},
},
"Fee": {
{Name: "amount", Type: "Coin[]"},
{Name: "gas", Type: "string"},
},
"Coin": {
{Name: "denom", Type: "string"},
{Name: "amount", Type: "string"},
},
"Msg": {
{Name: "type", Type: "string"},
{Name: "value", Type: msgTypeName},
},
msgTypeName: {},
}
if err := walkFields(cdc, rootTypes, msgTypeName, msg); err != nil {
return nil, err
}
return rootTypes, nil
}
const typeDefPrefix = "_"
func walkFields(cdc codectypes.AnyUnpacker, typeMap apitypes.Types, rootType string, in interface{}) (err error) {
defer doRecover(&err)
t := reflect.TypeOf(in)
v := reflect.ValueOf(in)
for {
if t.Kind() == reflect.Ptr ||
t.Kind() == reflect.Interface {
t = t.Elem()
v = v.Elem()
continue
}
break
}
return traverseFields(cdc, typeMap, rootType, typeDefPrefix, t, v)
}
type cosmosAnyWrapper struct {
Type string `json:"type"`
Value interface{} `json:"value"`
}
func traverseFields(
cdc codectypes.AnyUnpacker,
typeMap apitypes.Types,
rootType string,
prefix string,
t reflect.Type,
v reflect.Value,
) error {
n := t.NumField()
if prefix == typeDefPrefix {
if len(typeMap[rootType]) == n {
return nil
}
} else {
typeDef := sanitizeTypedef(prefix)
if len(typeMap[typeDef]) == n {
return nil
}
}
for i := 0; i < n; i++ {
var field reflect.Value
if v.IsValid() {
field = v.Field(i)
}
fieldType := t.Field(i).Type
fieldName := jsonNameFromTag(t.Field(i).Tag)
if fieldType == cosmosAnyType {
any, ok := field.Interface().(*codectypes.Any)
if !ok {
return sdkerrors.Wrapf(sdkerrors.ErrPackAny, "%T", field.Interface())
}
anyWrapper := &cosmosAnyWrapper{
Type: any.TypeUrl,
}
if err := cdc.UnpackAny(any, &anyWrapper.Value); err != nil {
return sdkerrors.Wrap(err, "failed to unpack Any in msg struct")
}
fieldType = reflect.TypeOf(anyWrapper)
field = reflect.ValueOf(anyWrapper)
// then continue as normal
}
for {
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
if field.IsValid() {
field = field.Elem()
}
continue
}
if fieldType.Kind() == reflect.Interface {
fieldType = reflect.TypeOf(field.Interface())
continue
}
if field.Kind() == reflect.Ptr {
field = field.Elem()
continue
}
break
}
var isCollection bool
if fieldType.Kind() == reflect.Array || fieldType.Kind() == reflect.Slice {
if field.Len() == 0 {
// skip empty collections from type mapping
continue
}
fieldType = fieldType.Elem()
field = field.Index(0)
isCollection = true
}
for {
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
if field.IsValid() {
field = field.Elem()
}
continue
}
if fieldType.Kind() == reflect.Interface {
fieldType = reflect.TypeOf(field.Interface())
continue
}
if field.Kind() == reflect.Ptr {
field = field.Elem()
continue
}
break
}
fieldPrefix := fmt.Sprintf("%s.%s", prefix, fieldName)
ethTyp := typToEth(fieldType)
if len(ethTyp) > 0 {
if prefix == typeDefPrefix {
typeMap[rootType] = append(typeMap[rootType], apitypes.Type{
Name: fieldName,
Type: ethTyp,
})
} else {
typeDef := sanitizeTypedef(prefix)
typeMap[typeDef] = append(typeMap[typeDef], apitypes.Type{
Name: fieldName,
Type: ethTyp,
})
}
continue
}
if fieldType.Kind() == reflect.Struct {
var fieldTypedef string
if isCollection {
fieldTypedef = sanitizeTypedef(fieldPrefix) + "[]"
} else {
fieldTypedef = sanitizeTypedef(fieldPrefix)
}
if prefix == typeDefPrefix {
typeMap[rootType] = append(typeMap[rootType], apitypes.Type{
Name: fieldName,
Type: fieldTypedef,
})
} else {
typeDef := sanitizeTypedef(prefix)
typeMap[typeDef] = append(typeMap[typeDef], apitypes.Type{
Name: fieldName,
Type: fieldTypedef,
})
}
if err := traverseFields(cdc, typeMap, rootType, fieldPrefix, fieldType, field); err != nil {
return err
}
continue
}
}
return nil
}
func jsonNameFromTag(tag reflect.StructTag) string {
jsonTags := tag.Get("json")
parts := strings.Split(jsonTags, ",")
return parts[0]
}
// _.foo_bar.baz -> TypeFooBarBaz
//
// this is needed for Geth's own signing code which doesn't
// tolerate complex type names
func sanitizeTypedef(str string) string {
buf := new(bytes.Buffer)
parts := strings.Split(str, ".")
caser := cases.Title(language.English, cases.NoLower)
for _, part := range parts {
if part == "_" {
buf.WriteString("Type")
continue
}
subparts := strings.Split(part, "_")
for _, subpart := range subparts {
buf.WriteString(caser.String(subpart))
}
}
return buf.String()
}
var (
hashType = reflect.TypeOf(common.Hash{})
addressType = reflect.TypeOf(common.Address{})
bigIntType = reflect.TypeOf(big.Int{})
cosmIntType = reflect.TypeOf(sdkmath.Int{})
cosmosAnyType = reflect.TypeOf(&codectypes.Any{})
)
// typToEth supports only basic types and arrays of basic types.
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md
func typToEth(typ reflect.Type) string {
const str = "string"
switch typ.Kind() {
case reflect.String:
return str
case reflect.Bool:
return "bool"
case reflect.Int:
return "int64"
case reflect.Int8:
return "int8"
case reflect.Int16:
return "int16"
case reflect.Int32:
return "int32"
case reflect.Int64:
return "int64"
case reflect.Uint:
return "uint64"
case reflect.Uint8:
return "uint8"
case reflect.Uint16:
return "uint16"
case reflect.Uint32:
return "uint32"
case reflect.Uint64:
return "uint64"
case reflect.Slice:
ethName := typToEth(typ.Elem())
if len(ethName) > 0 {
return ethName + "[]"
}
case reflect.Array:
ethName := typToEth(typ.Elem())
if len(ethName) > 0 {
return ethName + "[]"
}
case reflect.Ptr:
if typ.Elem().ConvertibleTo(bigIntType) ||
typ.Elem().ConvertibleTo(cosmIntType) {
return str
}
case reflect.Struct:
if typ.ConvertibleTo(hashType) ||
typ.ConvertibleTo(addressType) ||
typ.ConvertibleTo(bigIntType) ||
typ.ConvertibleTo(cosmIntType) {
return str
}
}
return ""
}
func doRecover(err *error) {
if r := recover(); r != nil {
if e, ok := r.(error); ok {
e = sdkerrors.Wrap(e, "panicked with error")
*err = e
return
}
*err = fmt.Errorf("%v", r)
}
}