accounts, signer: implement gnosis safe support (#21593)
* accounts, signer: implement gnosis safe support * common/math: add type for marshalling big to dec * accounts, signer: properly sign gnosis requests * signer, clef: implement account_signGnosisTx * signer: fix auditlog print, change rpc-name (signGnosisTx to signGnosisSafeTx) * signer: pass validation-messages/warnings to the UI for gnonsis-safe txs * signer/core: minor change to validationmessages of typed data
This commit is contained in:
parent
6c8310ebb4
commit
dad26582b6
|
@ -10,6 +10,64 @@ TL;DR: Given a version number MAJOR.MINOR.PATCH, increment the:
|
|||
|
||||
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
|
||||
|
||||
### 6.1.0
|
||||
|
||||
The API-method `account_signGnosisSafeTx` was added. This method takes two parameters,
|
||||
`[address, safeTx]`. The latter, `safeTx`, can be copy-pasted from the gnosis relay. For example:
|
||||
|
||||
```
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "account_signGnosisSafeTx",
|
||||
"params": ["0xfd1c4226bfD1c436672092F4eCbfC270145b7256",
|
||||
{
|
||||
"safe": "0x25a6c4BBd32B2424A9c99aEB0584Ad12045382B3",
|
||||
"to": "0xB372a646f7F05Cc1785018dBDA7EBc734a2A20E2",
|
||||
"value": "20000000000000000",
|
||||
"data": null,
|
||||
"operation": 0,
|
||||
"gasToken": "0x0000000000000000000000000000000000000000",
|
||||
"safeTxGas": 27845,
|
||||
"baseGas": 0,
|
||||
"gasPrice": "0",
|
||||
"refundReceiver": "0x0000000000000000000000000000000000000000",
|
||||
"nonce": 2,
|
||||
"executionDate": null,
|
||||
"submissionDate": "2020-09-15T21:54:49.617634Z",
|
||||
"modified": "2020-09-15T21:54:49.617634Z",
|
||||
"blockNumber": null,
|
||||
"transactionHash": null,
|
||||
"safeTxHash": "0x2edfbd5bc113ff18c0631595db32eb17182872d88d9bf8ee4d8c2dd5db6d95e2",
|
||||
"executor": null,
|
||||
"isExecuted": false,
|
||||
"isSuccessful": null,
|
||||
"ethGasPrice": null,
|
||||
"gasUsed": null,
|
||||
"fee": null,
|
||||
"origin": null,
|
||||
"dataDecoded": null,
|
||||
"confirmationsRequired": null,
|
||||
"confirmations": [
|
||||
{
|
||||
"owner": "0xAd2e180019FCa9e55CADe76E4487F126Fd08DA34",
|
||||
"submissionDate": "2020-09-15T21:54:49.663299Z",
|
||||
"transactionHash": null,
|
||||
"confirmationType": "CONFIRMATION",
|
||||
"signature": "0x95a7250bb645f831c86defc847350e7faff815b2fb586282568e96cc859e39315876db20a2eed5f7a0412906ec5ab57652a6f645ad4833f345bda059b9da2b821c",
|
||||
"signatureType": "EOA"
|
||||
}
|
||||
],
|
||||
"signatures": null
|
||||
}
|
||||
],
|
||||
"id": 67
|
||||
}
|
||||
```
|
||||
|
||||
Not all fields are required, though. This method is really just a UX helper, which massages the
|
||||
input to conform to the `EIP-712` [specification](https://docs.gnosis.io/safe/docs/contracts_tx_execution/#transaction-hash)
|
||||
for the Gnosis Safe, and making the output be directly importable to by a relay service.
|
||||
|
||||
|
||||
### 6.0.0
|
||||
|
||||
|
|
|
@ -67,6 +67,40 @@ func (i *HexOrDecimal256) MarshalText() ([]byte, error) {
|
|||
return []byte(fmt.Sprintf("%#x", (*big.Int)(i))), nil
|
||||
}
|
||||
|
||||
// Decimal256 unmarshals big.Int as a decimal string. When unmarshalling,
|
||||
// it however accepts either "0x"-prefixed (hex encoded) or non-prefixed (decimal)
|
||||
type Decimal256 big.Int
|
||||
|
||||
// NewHexOrDecimal256 creates a new Decimal256
|
||||
func NewDecimal256(x int64) *Decimal256 {
|
||||
b := big.NewInt(x)
|
||||
d := Decimal256(*b)
|
||||
return &d
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||
func (i *Decimal256) UnmarshalText(input []byte) error {
|
||||
bigint, ok := ParseBig256(string(input))
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid hex or decimal integer %q", input)
|
||||
}
|
||||
*i = Decimal256(*bigint)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler.
|
||||
func (i *Decimal256) MarshalText() ([]byte, error) {
|
||||
return []byte(i.String()), nil
|
||||
}
|
||||
|
||||
// String implements Stringer.
|
||||
func (i *Decimal256) String() string {
|
||||
if i == nil {
|
||||
return "0"
|
||||
}
|
||||
return fmt.Sprintf("%#d", (*big.Int)(i))
|
||||
}
|
||||
|
||||
// ParseBig256 parses s as a 256 bit integer in decimal or hexadecimal syntax.
|
||||
// Leading zeros are accepted. The empty string parses as zero.
|
||||
func ParseBig256(s string) (*big.Int, bool) {
|
||||
|
|
|
@ -41,7 +41,7 @@ const (
|
|||
// numberOfAccountsToDerive For hardware wallets, the number of accounts to derive
|
||||
numberOfAccountsToDerive = 10
|
||||
// ExternalAPIVersion -- see extapi_changelog.md
|
||||
ExternalAPIVersion = "6.0.0"
|
||||
ExternalAPIVersion = "6.1.0"
|
||||
// InternalAPIVersion -- see intapi_changelog.md
|
||||
InternalAPIVersion = "7.0.1"
|
||||
)
|
||||
|
@ -62,6 +62,8 @@ type ExternalAPI interface {
|
|||
EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error)
|
||||
// Version info about the APIs
|
||||
Version(ctx context.Context) (string, error)
|
||||
// SignGnosisSafeTransaction signs/confirms a gnosis-safe multisig transaction
|
||||
SignGnosisSafeTx(ctx context.Context, signerAddress common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error)
|
||||
}
|
||||
|
||||
// UIClientAPI specifies what method a UI needs to implement to be able to be used as a
|
||||
|
@ -234,6 +236,7 @@ type (
|
|||
Address common.MixedcaseAddress `json:"address"`
|
||||
Rawdata []byte `json:"raw_data"`
|
||||
Messages []*NameValueType `json:"messages"`
|
||||
Callinfo []ValidationInfo `json:"call_info"`
|
||||
Hash hexutil.Bytes `json:"hash"`
|
||||
Meta Metadata `json:"meta"`
|
||||
}
|
||||
|
@ -581,6 +584,33 @@ func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, meth
|
|||
|
||||
}
|
||||
|
||||
func (api *SignerAPI) SignGnosisSafeTx(ctx context.Context, signerAddress common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) {
|
||||
// Do the usual validations, but on the last-stage transaction
|
||||
args := gnosisTx.ArgsForValidation()
|
||||
msgs, err := api.validator.ValidateTransaction(methodSelector, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If we are in 'rejectMode', then reject rather than show the user warnings
|
||||
if api.rejectMode {
|
||||
if err := msgs.getWarnings(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
typedData := gnosisTx.ToTypedData()
|
||||
signature, preimage, err := api.signTypedData(ctx, signerAddress, typedData, msgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
checkSummedSender, _ := common.NewMixedcaseAddressFromString(signerAddress.Address().Hex())
|
||||
|
||||
gnosisTx.Signature = signature
|
||||
gnosisTx.SafeTxHash = common.BytesToHash(preimage)
|
||||
gnosisTx.Sender = *checkSummedSender // Must be checksumed to be accepted by relay
|
||||
|
||||
return &gnosisTx, nil
|
||||
}
|
||||
|
||||
// Returns the external api version. This method does not require user acceptance. Available methods are
|
||||
// available via enumeration anyway, and this info does not contain user-specific data
|
||||
func (api *SignerAPI) Version(ctx context.Context) (string, error) {
|
||||
|
|
|
@ -18,6 +18,7 @@ package core
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
|
@ -61,13 +62,32 @@ func (l *AuditLogger) SignTransaction(ctx context.Context, args SendTxArgs, meth
|
|||
}
|
||||
|
||||
func (l *AuditLogger) SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) {
|
||||
marshalledData, _ := json.Marshal(data) // can ignore error, marshalling what we just unmarshalled
|
||||
l.log.Info("SignData", "type", "request", "metadata", MetadataFromContext(ctx).String(),
|
||||
"addr", addr.String(), "data", data, "content-type", contentType)
|
||||
"addr", addr.String(), "data", marshalledData, "content-type", contentType)
|
||||
b, e := l.api.SignData(ctx, contentType, addr, data)
|
||||
l.log.Info("SignData", "type", "response", "data", common.Bytes2Hex(b), "error", e)
|
||||
return b, e
|
||||
}
|
||||
|
||||
func (l *AuditLogger) SignGnosisSafeTx(ctx context.Context, addr common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) {
|
||||
sel := "<nil>"
|
||||
if methodSelector != nil {
|
||||
sel = *methodSelector
|
||||
}
|
||||
data, _ := json.Marshal(gnosisTx) // can ignore error, marshalling what we just unmarshalled
|
||||
l.log.Info("SignGnosisSafeTx", "type", "request", "metadata", MetadataFromContext(ctx).String(),
|
||||
"addr", addr.String(), "data", string(data), "selector", sel)
|
||||
res, e := l.api.SignGnosisSafeTx(ctx, addr, gnosisTx, methodSelector)
|
||||
if res != nil {
|
||||
data, _ := json.Marshal(res) // can ignore error, marshalling what we just unmarshalled
|
||||
l.log.Info("SignGnosisSafeTx", "type", "response", "data", string(data), "error", e)
|
||||
} else {
|
||||
l.log.Info("SignGnosisSafeTx", "type", "response", "data", res, "error", e)
|
||||
}
|
||||
return res, e
|
||||
}
|
||||
|
||||
func (l *AuditLogger) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data TypedData) (hexutil.Bytes, error) {
|
||||
l.log.Info("SignTypedData", "type", "request", "metadata", MetadataFromContext(ctx).String(),
|
||||
"addr", addr.String(), "data", data)
|
||||
|
|
|
@ -148,6 +148,13 @@ func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResp
|
|||
|
||||
fmt.Printf("-------- Sign data request--------------\n")
|
||||
fmt.Printf("Account: %s\n", request.Address.String())
|
||||
if len(request.Callinfo) != 0 {
|
||||
fmt.Printf("\nValidation messages:\n")
|
||||
for _, m := range request.Callinfo {
|
||||
fmt.Printf(" * %s : %s\n", m.Typ, m.Message)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
fmt.Printf("messages:\n")
|
||||
for _, nvt := range request.Messages {
|
||||
fmt.Printf("\u00a0\u00a0%v\n", strings.TrimSpace(nvt.Pprint(1)))
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/common/math"
|
||||
)
|
||||
|
||||
// GnosisSafeTx is a type to parse the safe-tx returned by the relayer,
|
||||
// it also conforms to the API required by the Gnosis Safe tx relay service.
|
||||
// See 'SafeMultisigTransaction' on https://safe-transaction.mainnet.gnosis.io/
|
||||
type GnosisSafeTx struct {
|
||||
// These fields are only used on output
|
||||
Signature hexutil.Bytes `json:"signature"`
|
||||
SafeTxHash common.Hash `json:"contractTransactionHash"`
|
||||
Sender common.MixedcaseAddress `json:"sender"`
|
||||
// These fields are used both on input and output
|
||||
Safe common.MixedcaseAddress `json:"safe"`
|
||||
To common.MixedcaseAddress `json:"to"`
|
||||
Value math.Decimal256 `json:"value"`
|
||||
GasPrice math.Decimal256 `json:"gasPrice"`
|
||||
Data *hexutil.Bytes `json:"data"`
|
||||
Operation uint8 `json:"operation"`
|
||||
GasToken common.Address `json:"gasToken"`
|
||||
RefundReceiver common.Address `json:"refundReceiver"`
|
||||
BaseGas big.Int `json:"baseGas"`
|
||||
SafeTxGas big.Int `json:"safeTxGas"`
|
||||
Nonce big.Int `json:"nonce"`
|
||||
InputExpHash common.Hash `json:"safeTxHash"`
|
||||
}
|
||||
|
||||
// ToTypedData converts the tx to a EIP-712 Typed Data structure for signing
|
||||
func (tx *GnosisSafeTx) ToTypedData() TypedData {
|
||||
var data hexutil.Bytes
|
||||
if tx.Data != nil {
|
||||
data = *tx.Data
|
||||
}
|
||||
gnosisTypedData := TypedData{
|
||||
Types: Types{
|
||||
"EIP712Domain": []Type{{Name: "verifyingContract", Type: "address"}},
|
||||
"SafeTx": []Type{
|
||||
{Name: "to", Type: "address"},
|
||||
{Name: "value", Type: "uint256"},
|
||||
{Name: "data", Type: "bytes"},
|
||||
{Name: "operation", Type: "uint8"},
|
||||
{Name: "safeTxGas", Type: "uint256"},
|
||||
{Name: "baseGas", Type: "uint256"},
|
||||
{Name: "gasPrice", Type: "uint256"},
|
||||
{Name: "gasToken", Type: "address"},
|
||||
{Name: "refundReceiver", Type: "address"},
|
||||
{Name: "nonce", Type: "uint256"},
|
||||
},
|
||||
},
|
||||
Domain: TypedDataDomain{
|
||||
VerifyingContract: tx.Safe.Address().Hex(),
|
||||
},
|
||||
PrimaryType: "SafeTx",
|
||||
Message: TypedDataMessage{
|
||||
"to": tx.To.Address().Hex(),
|
||||
"value": tx.Value.String(),
|
||||
"data": data,
|
||||
"operation": fmt.Sprintf("%d", tx.Operation),
|
||||
"safeTxGas": fmt.Sprintf("%#d", &tx.SafeTxGas),
|
||||
"baseGas": fmt.Sprintf("%#d", &tx.BaseGas),
|
||||
"gasPrice": tx.GasPrice.String(),
|
||||
"gasToken": tx.GasToken.Hex(),
|
||||
"refundReceiver": tx.RefundReceiver.Hex(),
|
||||
"nonce": fmt.Sprintf("%d", tx.Nonce.Uint64()),
|
||||
},
|
||||
}
|
||||
return gnosisTypedData
|
||||
}
|
||||
|
||||
// ArgsForValidation returns a SendTxArgs struct, which can be used for the
|
||||
// common validations, e.g. look up 4byte destinations
|
||||
func (tx *GnosisSafeTx) ArgsForValidation() *SendTxArgs {
|
||||
args := &SendTxArgs{
|
||||
From: tx.Safe,
|
||||
To: &tx.To,
|
||||
Gas: hexutil.Uint64(tx.SafeTxGas.Uint64()),
|
||||
GasPrice: hexutil.Big(tx.GasPrice),
|
||||
Value: hexutil.Big(tx.Value),
|
||||
Nonce: hexutil.Uint64(tx.Nonce.Uint64()),
|
||||
Data: tx.Data,
|
||||
Input: nil,
|
||||
}
|
||||
return args
|
||||
}
|
|
@ -125,7 +125,7 @@ var typedDataReferenceTypeRegexp = regexp.MustCompile(`^[A-Z](\w*)(\[\])?$`)
|
|||
//
|
||||
// Note, the produced signature conforms to the secp256k1 curve R, S and V values,
|
||||
// where the V value will be 27 or 28 for legacy reasons, if legacyV==true.
|
||||
func (api *SignerAPI) sign(addr common.MixedcaseAddress, req *SignDataRequest, legacyV bool) (hexutil.Bytes, error) {
|
||||
func (api *SignerAPI) sign(req *SignDataRequest, legacyV bool) (hexutil.Bytes, error) {
|
||||
// We make the request prior to looking up if we actually have the account, to prevent
|
||||
// account-enumeration via the API
|
||||
res, err := api.UI.ApproveSignData(req)
|
||||
|
@ -136,7 +136,7 @@ func (api *SignerAPI) sign(addr common.MixedcaseAddress, req *SignDataRequest, l
|
|||
return nil, ErrRequestDenied
|
||||
}
|
||||
// Look up the wallet containing the requested signer
|
||||
account := accounts.Account{Address: addr.Address()}
|
||||
account := accounts.Account{Address: req.Address.Address()}
|
||||
wallet, err := api.am.Find(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -167,7 +167,7 @@ func (api *SignerAPI) SignData(ctx context.Context, contentType string, addr com
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signature, err := api.sign(addr, req, transformV)
|
||||
signature, err := api.sign(req, transformV)
|
||||
if err != nil {
|
||||
api.UI.ShowError(err.Error())
|
||||
return nil, err
|
||||
|
@ -312,28 +312,47 @@ func cliqueHeaderHashAndRlp(header *types.Header) (hash, rlp []byte, err error)
|
|||
|
||||
// SignTypedData signs EIP-712 conformant typed data
|
||||
// hash = keccak256("\x19${byteVersion}${domainSeparator}${hashStruct(message)}")
|
||||
// It returns
|
||||
// - the signature,
|
||||
// - and/or any error
|
||||
func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, typedData TypedData) (hexutil.Bytes, error) {
|
||||
signature, _, err := api.signTypedData(ctx, addr, typedData, nil)
|
||||
return signature, err
|
||||
}
|
||||
|
||||
// signTypedData is identical to the capitalized version, except that it also returns the hash (preimage)
|
||||
// - the signature preimage (hash)
|
||||
func (api *SignerAPI) signTypedData(ctx context.Context, addr common.MixedcaseAddress,
|
||||
typedData TypedData, validationMessages *ValidationMessages) (hexutil.Bytes, hexutil.Bytes, error) {
|
||||
domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash)))
|
||||
sighash := crypto.Keccak256(rawData)
|
||||
messages, err := typedData.Format()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
req := &SignDataRequest{ContentType: DataTyped.Mime, Rawdata: rawData, Messages: messages, Hash: sighash}
|
||||
signature, err := api.sign(addr, req, true)
|
||||
req := &SignDataRequest{
|
||||
ContentType: DataTyped.Mime,
|
||||
Rawdata: rawData,
|
||||
Messages: messages,
|
||||
Hash: sighash,
|
||||
Address: addr}
|
||||
if validationMessages != nil {
|
||||
req.Callinfo = validationMessages.Messages
|
||||
}
|
||||
signature, err := api.sign(req, true)
|
||||
if err != nil {
|
||||
api.UI.ShowError(err.Error())
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
return signature, nil
|
||||
return signature, sighash, nil
|
||||
}
|
||||
|
||||
// HashStruct generates a keccak256 hash of the encoding of the provided data
|
||||
|
@ -420,8 +439,8 @@ func (typedData *TypedData) EncodeData(primaryType string, data map[string]inter
|
|||
buffer := bytes.Buffer{}
|
||||
|
||||
// Verify extra data
|
||||
if len(typedData.Types[primaryType]) < len(data) {
|
||||
return nil, errors.New("there is extra data provided in the message")
|
||||
if exp, got := len(typedData.Types[primaryType]), len(data); exp < got {
|
||||
return nil, fmt.Errorf("there is extra data provided in the message (%d < %d)", exp, got)
|
||||
}
|
||||
|
||||
// Add typehash
|
||||
|
@ -834,7 +853,11 @@ func (nvt *NameValueType) Pprint(depth int) string {
|
|||
output.WriteString(sublevel)
|
||||
}
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("%q\n", nvt.Value))
|
||||
if nvt.Value != nil {
|
||||
output.WriteString(fmt.Sprintf("%q\n", nvt.Value))
|
||||
} else {
|
||||
output.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return output.String()
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
@ -414,3 +415,119 @@ func TestFuzzerFiles(t *testing.T) {
|
|||
typedData.Format()
|
||||
}
|
||||
}
|
||||
|
||||
var gnosisTypedData = `
|
||||
{
|
||||
"types": {
|
||||
"EIP712Domain": [
|
||||
{ "type": "address", "name": "verifyingContract" }
|
||||
],
|
||||
"SafeTx": [
|
||||
{ "type": "address", "name": "to" },
|
||||
{ "type": "uint256", "name": "value" },
|
||||
{ "type": "bytes", "name": "data" },
|
||||
{ "type": "uint8", "name": "operation" },
|
||||
{ "type": "uint256", "name": "safeTxGas" },
|
||||
{ "type": "uint256", "name": "baseGas" },
|
||||
{ "type": "uint256", "name": "gasPrice" },
|
||||
{ "type": "address", "name": "gasToken" },
|
||||
{ "type": "address", "name": "refundReceiver" },
|
||||
{ "type": "uint256", "name": "nonce" }
|
||||
]
|
||||
},
|
||||
"domain": {
|
||||
"verifyingContract": "0x25a6c4BBd32B2424A9c99aEB0584Ad12045382B3"
|
||||
},
|
||||
"primaryType": "SafeTx",
|
||||
"message": {
|
||||
"to": "0x9eE457023bB3De16D51A003a247BaEaD7fce313D",
|
||||
"value": "20000000000000000",
|
||||
"data": "0x",
|
||||
"operation": 0,
|
||||
"safeTxGas": 27845,
|
||||
"baseGas": 0,
|
||||
"gasPrice": "0",
|
||||
"gasToken": "0x0000000000000000000000000000000000000000",
|
||||
"refundReceiver": "0x0000000000000000000000000000000000000000",
|
||||
"nonce": 3
|
||||
}
|
||||
}`
|
||||
|
||||
var gnosisTx = `
|
||||
{
|
||||
"safe": "0x25a6c4BBd32B2424A9c99aEB0584Ad12045382B3",
|
||||
"to": "0x9eE457023bB3De16D51A003a247BaEaD7fce313D",
|
||||
"value": "20000000000000000",
|
||||
"data": null,
|
||||
"operation": 0,
|
||||
"gasToken": "0x0000000000000000000000000000000000000000",
|
||||
"safeTxGas": 27845,
|
||||
"baseGas": 0,
|
||||
"gasPrice": "0",
|
||||
"refundReceiver": "0x0000000000000000000000000000000000000000",
|
||||
"nonce": 3,
|
||||
"executionDate": null,
|
||||
"submissionDate": "2020-09-15T21:59:23.815748Z",
|
||||
"modified": "2020-09-15T21:59:23.815748Z",
|
||||
"blockNumber": null,
|
||||
"transactionHash": null,
|
||||
"safeTxHash": "0x28bae2bd58d894a1d9b69e5e9fde3570c4b98a6fc5499aefb54fb830137e831f",
|
||||
"executor": null,
|
||||
"isExecuted": false,
|
||||
"isSuccessful": null,
|
||||
"ethGasPrice": null,
|
||||
"gasUsed": null,
|
||||
"fee": null,
|
||||
"origin": null,
|
||||
"dataDecoded": null,
|
||||
"confirmationsRequired": null,
|
||||
"confirmations": [
|
||||
{
|
||||
"owner": "0xAd2e180019FCa9e55CADe76E4487F126Fd08DA34",
|
||||
"submissionDate": "2020-09-15T21:59:28.281243Z",
|
||||
"transactionHash": null,
|
||||
"confirmationType": "CONFIRMATION",
|
||||
"signature": "0x5e562065a0cb15d766dac0cd49eb6d196a41183af302c4ecad45f1a81958d7797753f04424a9b0aa1cb0448e4ec8e189540fbcdda7530ef9b9d95dfc2d36cb521b",
|
||||
"signatureType": "EOA"
|
||||
}
|
||||
],
|
||||
"signatures": null
|
||||
}
|
||||
`
|
||||
|
||||
// TestGnosisTypedData tests the scenario where a user submits a full EIP-712
|
||||
// struct without using the gnosis-specific endpoint
|
||||
func TestGnosisTypedData(t *testing.T) {
|
||||
var td core.TypedData
|
||||
err := json.Unmarshal([]byte(gnosisTypedData), &td)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshalling failed '%v'", err)
|
||||
}
|
||||
_, sighash, err := sign(td)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expSigHash := common.FromHex("0x28bae2bd58d894a1d9b69e5e9fde3570c4b98a6fc5499aefb54fb830137e831f")
|
||||
if !bytes.Equal(expSigHash, sighash) {
|
||||
t.Fatalf("Error, got %x, wanted %x", sighash, expSigHash)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGnosisCustomData tests the scenario where a user submits only the gnosis-safe
|
||||
// specific data, and we fill the TypedData struct on our side
|
||||
func TestGnosisCustomData(t *testing.T) {
|
||||
var tx core.GnosisSafeTx
|
||||
err := json.Unmarshal([]byte(gnosisTx), &tx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var td = tx.ToTypedData()
|
||||
_, sighash, err := sign(td)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expSigHash := common.FromHex("0x28bae2bd58d894a1d9b69e5e9fde3570c4b98a6fc5499aefb54fb830137e831f")
|
||||
if !bytes.Equal(expSigHash, sighash) {
|
||||
t.Fatalf("Error, got %x, wanted %x", sighash, expSigHash)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue