diff --git a/CHANGELOG.md b/CHANGELOG.md index 956f5c62f64..7db914cfc86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1783,6 +1783,24 @@ If there are any bugs, improvements, optimizations or any new feature proposal f - `RpcErrorMessages` that contains mapping for standard RPC Errors and their messages. (#6230) +#### web3-eth + +- A `rpc_method_wrapper` (`signTypedData`) for the rpc calls `eth_signTypedData` and `eth_signTypedData_v4` (#6286) +- A `signTypedData` method to the `Web3Eth` class (#6286) + +#### web3-eth-abi + +- A `getEncodedEip712Data` method that takes an EIP-712 typed data object and returns the encoded data with the option to also keccak256 hash it (#6286) + +#### web3-rpc-methods + +- A `signTypedData` method to `eth_rpc_methods` for the rpc calls `eth_signTypedData` and `eth_signTypedData_v4` (#6286) + +#### web3-types + +- `eth_signTypedData` and `eth_signTypedData_v4` to `web3_eth_execution_api` (#6286) +- `Eip712TypeDetails` and `Eip712TypedData` to `eth_types` (#6286) + #### web3-validator - Added `json-schema` as a main json schema type (#6264) diff --git a/packages/web3-eth-abi/CHANGELOG.md b/packages/web3-eth-abi/CHANGELOG.md index 20d0819bccd..4cc31c0f92c 100644 --- a/packages/web3-eth-abi/CHANGELOG.md +++ b/packages/web3-eth-abi/CHANGELOG.md @@ -125,3 +125,7 @@ Documentation: - Dependencies updated ## [Unreleased] + +### Added + +- A `getEncodedEip712Data` method that takes an EIP-712 typed data object and returns the encoded data with the option to also keccak256 hash it (#6286) diff --git a/packages/web3-eth-abi/src/eip_712.ts b/packages/web3-eth-abi/src/eip_712.ts new file mode 100644 index 00000000000..948591bb474 --- /dev/null +++ b/packages/web3-eth-abi/src/eip_712.ts @@ -0,0 +1,201 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +/** + * @note This code was taken from: https://github.com/Mrtenz/eip-712/tree/master + */ + +import { Eip712TypedData } from 'web3-types'; +import { isNullish, keccak256 } from 'web3-utils'; + +import ethersAbiCoder from './ethers_abi_coder.js'; + +const TYPE_REGEX = /^\w+/; +const ARRAY_REGEX = /^(.*)\[([0-9]*?)]$/; + +/** + * Get the dependencies of a struct type. If a struct has the same dependency multiple times, it's only included once + * in the resulting array. + */ +const getDependencies = ( + typedData: Eip712TypedData, + type: string, + dependencies: string[] = [], +): string[] => { + const match = type.match(TYPE_REGEX)!; + const actualType = match[0]; + if (dependencies.includes(actualType)) { + return dependencies; + } + + if (!typedData.types[actualType]) { + return dependencies; + } + + return [ + actualType, + ...typedData.types[actualType].reduce( + (previous, _type) => [ + ...previous, + ...getDependencies(typedData, _type.type, previous).filter( + dependency => !previous.includes(dependency), + ), + ], + [], + ), + ]; +}; + +/** + * Encode a type to a string. All dependant types are alphabetically sorted. + * + * @param {TypedData} typedData + * @param {string} type + * @param {Options} [options] + * @return {string} + */ +const encodeType = (typedData: Eip712TypedData, type: string): string => { + const [primary, ...dependencies] = getDependencies(typedData, type); + // eslint-disable-next-line @typescript-eslint/require-array-sort-compare + const types = [primary, ...dependencies.sort()]; + + return types + .map( + dependency => + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `${dependency}(${typedData.types[dependency].map( + _type => `${_type.type} ${_type.name}`, + )})`, + ) + .join(''); +}; + +/** + * Get a type string as hash. + */ +const getTypeHash = (typedData: Eip712TypedData, type: string) => + keccak256(encodeType(typedData, type)); + +/** + * Get encoded data as a hash. The data should be a key -> value object with all the required values. All dependant + * types are automatically encoded. + */ +const getStructHash = ( + typedData: Eip712TypedData, + type: string, + data: Record, + // eslint-disable-next-line no-use-before-define +): string => keccak256(encodeData(typedData, type, data)); + +/** + * Get the EIP-191 encoded message to sign, from the typedData object. If `hash` is enabled, the message will be hashed + * with Keccak256. + */ +export const getMessage = (typedData: Eip712TypedData, hash?: boolean): string => { + const EIP_191_PREFIX = '1901'; + const message = `0x${EIP_191_PREFIX}${getStructHash( + typedData, + 'EIP712Domain', + typedData.domain as Record, + ).substring(2)}${getStructHash(typedData, typedData.primaryType, typedData.message).substring( + 2, + )}`; + + if (hash) { + return keccak256(message); + } + + return message; +}; + +/** + * Encodes a single value to an ABI serialisable string, number or Buffer. Returns the data as tuple, which consists of + * an array of ABI compatible types, and an array of corresponding values. + */ +const encodeValue = ( + typedData: Eip712TypedData, + type: string, + data: unknown, +): [string, string | Uint8Array | number] => { + const match = type.match(ARRAY_REGEX); + + // Checks for array types + if (match) { + const arrayType = match[1]; + const length = Number(match[2]) || undefined; + + if (!Array.isArray(data)) { + throw new Error('Cannot encode data: value is not of array type'); + } + + if (length && data.length !== length) { + throw new Error( + `Cannot encode data: expected length of ${length}, but got ${data.length}`, + ); + } + + const encodedData = data.map(item => encodeValue(typedData, arrayType, item)); + const types = encodedData.map(item => item[0]); + const values = encodedData.map(item => item[1]); + + return ['bytes32', keccak256(ethersAbiCoder.encode(types, values))]; + } + + if (typedData.types[type]) { + return ['bytes32', getStructHash(typedData, type, data as Record)]; + } + + // Strings and arbitrary byte arrays are hashed to bytes32 + if (type === 'string') { + return ['bytes32', keccak256(data as string)]; + } + + if (type === 'bytes') { + return ['bytes32', keccak256(data as string)]; + } + + return [type, data as string]; +}; + +/** + * Encode the data to an ABI encoded Buffer. The data should be a key -> value object with all the required values. All + * dependant types are automatically encoded. + */ +const encodeData = ( + typedData: Eip712TypedData, + type: string, + data: Record, +): string => { + const [types, values] = typedData.types[type].reduce<[string[], unknown[]]>( + ([_types, _values], field) => { + if (isNullish(data[field.name]) || isNullish(data[field.name])) { + throw new Error(`Cannot encode data: missing data for '${field.name}'`); + } + + const value = data[field.name]; + const [_type, encodedValue] = encodeValue(typedData, field.type, value); + + return [ + [..._types, _type], + [..._values, encodedValue], + ]; + }, + [['bytes32'], [getTypeHash(typedData, type)]], + ); + + return ethersAbiCoder.encode(types, values); +}; diff --git a/packages/web3-eth-abi/src/index.ts b/packages/web3-eth-abi/src/index.ts index 6a8754e486a..baa57947847 100644 --- a/packages/web3-eth-abi/src/index.ts +++ b/packages/web3-eth-abi/src/index.ts @@ -25,3 +25,4 @@ export * from './api/logs_api.js'; export * from './api/parameters_api.js'; export * from './utils.js'; export * from './decode_contract_error_data.js'; +export { getMessage as getEncodedEip712Data } from './eip_712.js'; diff --git a/packages/web3-eth-abi/test/fixtures/get_encoded_eip712_data.ts b/packages/web3-eth-abi/test/fixtures/get_encoded_eip712_data.ts new file mode 100644 index 00000000000..ff7474aa33f --- /dev/null +++ b/packages/web3-eth-abi/test/fixtures/get_encoded_eip712_data.ts @@ -0,0 +1,758 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +import { Eip712TypedData } from 'web3-types'; + +/** + * string is the test title + * Eip712TypedData is the entire EIP-712 typed data object + * boolean is whether the EIP-712 encoded data is keccak256 hashed + * string is the encoded data expected to be returned by getEncodedEip712Data + */ +export const testData: [string, Eip712TypedData, boolean | undefined, string][] = [ + [ + 'should get encoded message without hashing, hash = undefined', + { + 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!', + }, + }, + undefined, + '0x1901f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090fc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e', + ], + [ + 'should get encoded message without hashing, hash = false', + { + 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!', + }, + }, + false, + '0x1901f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090fc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e', + ], + [ + 'should get the hashed encoded message, hash = true', + { + 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!', + }, + }, + true, + '0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2', + ], + [ + 'should get encoded message with array types', + { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + ArrayData: [ + { + name: 'array1', + type: 'string[]', + }, + { + name: 'array2', + type: 'address[]', + }, + { + name: 'array3', + type: 'uint256[]', + }, + ], + }, + primaryType: 'ArrayData', + domain: { + name: 'Array Data', + version: '1', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + message: { + array1: ['string', 'string2', 'string3'], + array2: [ + '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + ], + array3: [123456, 654321, 42], + }, + }, + false, + '0x1901928e4773f1f7243172cd0dd213906be49eb9d275e09c8bd0575921c51ba00058596a0bafab67b5b49cfe99456c50dd5b6294b1383e4f17c6e5c3c14afee96ac3', + ], + [ + 'should get encoded message with array types', + { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + ArrayData: [ + { + name: 'array1', + type: 'string[]', + }, + { + name: 'array2', + type: 'address[]', + }, + { + name: 'array3', + type: 'uint256[]', + }, + ], + }, + primaryType: 'ArrayData', + domain: { + name: 'Array Data', + version: '1', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + message: { + array1: ['string', 'string2', 'string3'], + array2: [ + '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + ], + array3: [123456, 654321, 42], + }, + }, + true, + '0x3e4d581a408c8c2fa8775017c26e0127df030593d83a8202e6c19b3380bde3da', + ], + [ + 'should get encoded message with fixed array', + { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + ArrayData: [ + { + name: 'array1', + type: 'string[]', + }, + { + name: 'array2', + type: 'address[3]', + }, + { + name: 'array3', + type: 'uint256[]', + }, + ], + }, + primaryType: 'ArrayData', + domain: { + name: 'Array Data', + version: '1', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + message: { + array1: ['string', 'string2', 'string3'], + array2: [ + '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + ], + array3: [123456, 654321, 42], + }, + }, + false, + '0x1901928e4773f1f7243172cd0dd213906be49eb9d275e09c8bd0575921c51ba00058b068b45d685c16bc9ef637106b4fd3a4fb9aa259f53218491a3d9eb65b1b574c', + ], + [ + 'should get encoded message with fixed array', + { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + ArrayData: [ + { + name: 'array1', + type: 'string[]', + }, + { + name: 'array2', + type: 'address[3]', + }, + { + name: 'array3', + type: 'uint256[]', + }, + ], + }, + primaryType: 'ArrayData', + domain: { + name: 'Array Data', + version: '1', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + message: { + array1: ['string', 'string2', 'string3'], + array2: [ + '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + ], + array3: [123456, 654321, 42], + }, + }, + true, + '0x133d00e67f2390ce846a631aeb6718a674a3923f5320b79b6d3e2f5bf146319e', + ], + [ + 'should get encoded message with bytes32', + { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + ArrayData: [ + { + name: 'bytes32', + type: 'bytes32', + }, + ], + }, + primaryType: 'ArrayData', + domain: { + name: 'Array Data', + version: '1', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + message: { + bytes32: '0x133d00e67f2390ce846a631aeb6718a674a3923f5320b79b6d3e2f5bf146319e', + }, + }, + false, + '0x1901928e4773f1f7243172cd0dd213906be49eb9d275e09c8bd0575921c51ba000587c9d26380d51aac5dc2ff6f794d1c043ea4259bb42068f70f79d2e4849133ac3', + ], + [ + 'should get encoded message with bytes32', + { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + ArrayData: [ + { + name: 'bytes32', + type: 'bytes', + }, + ], + }, + primaryType: 'ArrayData', + domain: { + name: 'Array Data', + version: '1', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + message: { + bytes32: '0x133d00e67f2390ce846a631aeb6718a674a3923f5320b79b6d3e2f5bf146319e', + }, + }, + false, + '0x1901928e4773f1f7243172cd0dd213906be49eb9d275e09c8bd0575921c51ba00058353ed034fd1df0cd409a19133f4a89f5e99ddc735ad3fbb767d0bb72c97ef175', + ], + [ + 'should get encoded message with bytes32', + { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + ArrayData: [ + { + name: 'bytes32', + type: 'bytes32', + }, + ], + }, + primaryType: 'ArrayData', + domain: { + name: 'Array Data', + version: '1', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + message: { + bytes32: '0x133d00e67f2390ce846a631aeb6718a674a3923f5320b79b6d3e2f5bf146319e', + }, + }, + true, + '0xa6cd048c02ef3cb70feee1bd9795decbbc8b431b976dfc86e3b09e55e0d2a3f3', + ], +]; + +/** + * string is the test title + * Eip712TypedData is the entire EIP-712 typed data object + * boolean is whether the EIP-712 encoded data is keccak256 hashed + * string is the encoded data expected to be returned by getEncodedEip712Data + */ +export const erroneousTestData: [string, Eip712TypedData, boolean | undefined, Error][] = [ + [ + 'should throw error: Cannot encode data: value is not of array type', + { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + ArrayData: [ + { + name: 'array1', + type: 'string[]', + }, + { + name: 'array2', + type: 'address[]', + }, + { + name: 'array3', + type: 'uint256[]', + }, + ], + }, + primaryType: 'ArrayData', + domain: { + name: 'Array Data', + version: '1', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + message: { + array1: ['string', 'string2', 'string3'], + array2: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + array3: [123456, 654321, 42], + }, + }, + false, + new Error('Cannot encode data: value is not of array type'), + ], + [ + 'should throw error: Cannot encode data: expected length of 3, but got 1', + { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + ArrayData: [ + { + name: 'array1', + type: 'string[]', + }, + { + name: 'array2', + type: 'address[3]', + }, + { + name: 'array3', + type: 'uint256[]', + }, + ], + }, + primaryType: 'ArrayData', + domain: { + name: 'Array Data', + version: '1', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + message: { + array1: ['string', 'string2', 'string3'], + array2: ['0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'], + array3: [123456, 654321, 42], + }, + }, + false, + new Error('Cannot encode data: expected length of 3, but got 1'), + ], + [ + "should throw error: Cannot encode data: missing data for 'array3'", + { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + ArrayData: [ + { + name: 'array1', + type: 'string[]', + }, + { + name: 'array2', + type: 'address[]', + }, + { + name: 'array3', + type: 'uint256[]', + }, + ], + }, + primaryType: 'ArrayData', + domain: { + name: 'Array Data', + version: '1', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + message: { + array1: ['string', 'string2', 'string3'], + array2: ['0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'], + array3: undefined, + }, + }, + false, + new Error("Cannot encode data: missing data for 'array3'"), + ], +]; diff --git a/packages/web3-eth-abi/test/unit/get_encoded_eip712_data.test.ts b/packages/web3-eth-abi/test/unit/get_encoded_eip712_data.test.ts new file mode 100644 index 00000000000..d40c5f25511 --- /dev/null +++ b/packages/web3-eth-abi/test/unit/get_encoded_eip712_data.test.ts @@ -0,0 +1,29 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +import { getEncodedEip712Data } from '../../src/index'; +import { erroneousTestData, testData } from '../fixtures/get_encoded_eip712_data'; + +describe('getEncodedEip712Data', () => { + it.each(testData)('%s', (_, typedData, hashEncodedData, expectedResponse) => { + const encodedMessage = getEncodedEip712Data(typedData, hashEncodedData); + expect(encodedMessage).toBe(expectedResponse); + }); + + it.each(erroneousTestData)('%s', (_, typedData, hashEncodedData, expectedError) => { + expect(() => getEncodedEip712Data(typedData, hashEncodedData)).toThrowError(expectedError); + }); +}); diff --git a/packages/web3-eth/CHANGELOG.md b/packages/web3-eth/CHANGELOG.md index 37151b39dd6..abefb0b7e65 100644 --- a/packages/web3-eth/CHANGELOG.md +++ b/packages/web3-eth/CHANGELOG.md @@ -163,6 +163,11 @@ Documentation: ## [Unreleased] +### Added + +- A `rpc_method_wrapper` (`signTypedData`) for the rpc calls `eth_signTypedData` and `eth_signTypedData_v4` (#6286) +- A `signTypedData` method to the `Web3Eth` class (#6286) + ### Fixed - Missing `blockHeaderSchema` properties causing some properties to not appear in response of `newHeads` subscription (#6243) diff --git a/packages/web3-eth/src/rpc_method_wrappers.ts b/packages/web3-eth/src/rpc_method_wrappers.ts index 246cbf67ca0..1d7d1915405 100644 --- a/packages/web3-eth/src/rpc_method_wrappers.ts +++ b/packages/web3-eth/src/rpc_method_wrappers.ts @@ -47,6 +47,7 @@ import { TransactionWithFromAndToLocalWalletIndex, TransactionForAccessList, AccessListResult, + Eip712TypedData, } from 'web3-types'; import { Web3Context, Web3PromiEvent } from 'web3-core'; import { format, hexToBytes, bytesToUint8Array, numberToHex } from 'web3-utils'; @@ -1125,3 +1126,24 @@ export async function createAccessList( return format(accessListResultSchema, response, returnFormat); } + +/** + * View additional documentations here: {@link Web3Eth.signTypedData} + * @param web3Context ({@link Web3Context}) Web3 configuration object that contains things such as the provider, request manager, wallet, etc. + */ +export async function signTypedData( + web3Context: Web3Context, + address: Address, + typedData: Eip712TypedData, + useLegacy: boolean, + returnFormat: ReturnFormat, +) { + const response = await ethRpcMethods.signTypedData( + web3Context.requestManager, + address, + typedData, + useLegacy, + ); + + return format({ format: 'bytes' }, response, returnFormat); +} diff --git a/packages/web3-eth/src/web3_eth.ts b/packages/web3-eth/src/web3_eth.ts index cd1c57bcc2b..c4b3b6a92f8 100644 --- a/packages/web3-eth/src/web3_eth.ts +++ b/packages/web3-eth/src/web3_eth.ts @@ -37,6 +37,7 @@ import { TransactionForAccessList, DataFormat, DEFAULT_RETURN_FORMAT, + Eip712TypedData, } from 'web3-types'; import { isSupportedProvider, Web3Context, Web3ContextInitOptions } from 'web3-core'; import { TransactionNotFound } from 'web3-errors'; @@ -1534,6 +1535,24 @@ export class Web3Eth extends Web3Context( + address: Address, + typedData: Eip712TypedData, + useLegacy = false, + returnFormat: ReturnFormat = DEFAULT_RETURN_FORMAT as ReturnFormat, + ) { + return rpcMethodsWrappers.signTypedData(this, address, typedData, useLegacy, returnFormat); + } + /** * Lets you subscribe to specific events in the blockchain. * diff --git a/packages/web3-eth/test/integration/web3_eth/sign_typed_data.test.ts b/packages/web3-eth/test/integration/web3_eth/sign_typed_data.test.ts new file mode 100644 index 00000000000..741690bc47f --- /dev/null +++ b/packages/web3-eth/test/integration/web3_eth/sign_typed_data.test.ts @@ -0,0 +1,206 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +import { getEncodedEip712Data } from 'web3-eth-abi'; +import { ecrecover, toUint8Array } from 'web3-eth-accounts'; +import { bytesToHex, hexToNumber, keccak256 } from 'web3-utils'; + +import Web3Eth from '../../../src'; +import { + closeOpenConnection, + createTempAccount, + getSystemTestBackend, + getSystemTestProvider, + itIf, +} from '../../fixtures/system_test_utils'; + +describe('Web3Eth.signTypedData', () => { + let web3Eth: Web3Eth; + let tempAcc: { address: string; privateKey: string }; + + beforeAll(async () => { + web3Eth = new Web3Eth(getSystemTestProvider()); + tempAcc = await createTempAccount(); + }); + + afterAll(async () => { + await closeOpenConnection(web3Eth); + }); + + itIf(getSystemTestBackend() === 'ganache')( + 'should sign the typed data, return the signature, and recover the correct ETH address', + async () => { + const typedData = { + 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!', + }, + }; + const encodedTypedDataHash = getEncodedEip712Data(typedData, true); + const signature = await web3Eth.signTypedData(tempAcc.address, typedData); + const r = toUint8Array(signature.slice(0, 66)); + const s = toUint8Array(`0x${signature.slice(66, 130)}`); + const v = BigInt(hexToNumber(`0x${signature.slice(130, 132)}`)); + const recoveredPublicKey = bytesToHex( + ecrecover(toUint8Array(encodedTypedDataHash), v, r, s), + ); + + const recoveredAddress = `0x${keccak256(bytesToHex(recoveredPublicKey)).slice(-40)}`; + // eslint-disable-next-line jest/no-standalone-expect + expect(recoveredAddress).toBe(tempAcc.address); + }, + ); + + itIf(getSystemTestBackend() === 'ganache')( + 'should sign the typed data (using legacy RPC method), return the signature, and recover the correct ETH address', + async () => { + const typedData = { + 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!', + }, + }; + const encodedTypedDataHash = getEncodedEip712Data(typedData, true); + const signature = await web3Eth.signTypedData(tempAcc.address, typedData, true); + const r = toUint8Array(signature.slice(0, 66)); + const s = toUint8Array(`0x${signature.slice(66, 130)}`); + const v = BigInt(hexToNumber(`0x${signature.slice(130, 132)}`)); + const recoveredPublicKey = bytesToHex( + ecrecover(toUint8Array(encodedTypedDataHash), v, r, s), + ); + + const recoveredAddress = `0x${keccak256(bytesToHex(recoveredPublicKey)).slice(-40)}`; + // eslint-disable-next-line jest/no-standalone-expect + expect(recoveredAddress).toBe(tempAcc.address); + }, + ); +}); diff --git a/packages/web3-eth/test/unit/rpc_method_wrappers/fixtures/sign_typed_data.ts b/packages/web3-eth/test/unit/rpc_method_wrappers/fixtures/sign_typed_data.ts new file mode 100644 index 00000000000..b01f33f698e --- /dev/null +++ b/packages/web3-eth/test/unit/rpc_method_wrappers/fixtures/sign_typed_data.ts @@ -0,0 +1,100 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +import { Address, Eip712TypedData } from 'web3-types'; + +const address = '0x407d73d8a49eeb85d32cf465507dd71d507100c1'; + +const typedData = { + 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!', + }, +}; + +export const mockRpcResponse = + '0xf326421b6b34e1e59a8a34c986861e8790a9402a9e51e012718872cd51dad4e23c590bd170be23c51cff4b44d8d4eba54120431ca6a04940098dae62d97677da1c'; + +/** + * Array consists of: + * - Test title + * - Input parameters: + * - address + * - message + */ +type TestData = [string, [Address, Eip712TypedData, boolean]]; +export const testData: TestData[] = [ + ['useLegacy = false', [address, typedData, false]], + ['useLegacy = true', [address, typedData, true]], +]; diff --git a/packages/web3-eth/test/unit/rpc_method_wrappers/sign_typed_data.test.ts b/packages/web3-eth/test/unit/rpc_method_wrappers/sign_typed_data.test.ts new file mode 100644 index 00000000000..8256beb7f78 --- /dev/null +++ b/packages/web3-eth/test/unit/rpc_method_wrappers/sign_typed_data.test.ts @@ -0,0 +1,64 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +import { Web3Context } from 'web3-core'; +import { DEFAULT_RETURN_FORMAT, FMT_BYTES, FMT_NUMBER, Web3EthExecutionAPI } from 'web3-types'; +import { ethRpcMethods } from 'web3-rpc-methods'; +import { format } from 'web3-utils'; + +import { signTypedData } from '../../../src/rpc_method_wrappers'; +import { testData, mockRpcResponse } from './fixtures/sign_typed_data'; + +jest.mock('web3-rpc-methods'); + +describe('signTypedData', () => { + let web3Context: Web3Context; + + beforeAll(() => { + web3Context = new Web3Context('http://127.0.0.1:8545'); + }); + + it.each(testData)( + `should call rpcMethods.signTypedData with expected parameters\nTitle: %s\nInput parameters: %s\n`, + async (_, inputParameters) => { + await signTypedData(web3Context, ...inputParameters, DEFAULT_RETURN_FORMAT); + expect(ethRpcMethods.signTypedData).toHaveBeenCalledWith( + web3Context.requestManager, + ...inputParameters, + ); + }, + ); + + it.each(testData)( + `should format mockRpcResponse using provided return format\nTitle: %s\nInput parameters: %s\n`, + async (_, inputParameters) => { + const expectedReturnFormat = { number: FMT_NUMBER.STR, bytes: FMT_BYTES.UINT8ARRAY }; + const expectedFormattedResult = format( + { format: 'bytes' }, + mockRpcResponse, + expectedReturnFormat, + ); + (ethRpcMethods.signTypedData as jest.Mock).mockResolvedValueOnce(mockRpcResponse); + + const result = await signTypedData( + web3Context, + ...inputParameters, + expectedReturnFormat, + ); + expect(result).toStrictEqual(expectedFormattedResult); + }, + ); +}); diff --git a/packages/web3-rpc-methods/CHANGELOG.md b/packages/web3-rpc-methods/CHANGELOG.md index 56772ed7164..e30d8e79193 100644 --- a/packages/web3-rpc-methods/CHANGELOG.md +++ b/packages/web3-rpc-methods/CHANGELOG.md @@ -103,3 +103,7 @@ Documentation: - Rpc method `getPastLogs` accept blockHash as a parameter https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getlogs (#6181) ## [Unreleased] + +### Added + +- A `signTypedData` method to `eth_rpc_methods` for the rpc calls `eth_signTypedData` and `eth_signTypedData_v4` (#6286) diff --git a/packages/web3-rpc-methods/src/eth_rpc_methods.ts b/packages/web3-rpc-methods/src/eth_rpc_methods.ts index 5209302903a..2a027733dfb 100644 --- a/packages/web3-rpc-methods/src/eth_rpc_methods.ts +++ b/packages/web3-rpc-methods/src/eth_rpc_methods.ts @@ -28,6 +28,7 @@ import { Uint256, Web3EthExecutionAPI, } from 'web3-types'; +import { Eip712TypedData } from 'web3-types/src/eth_types'; import { validator } from 'web3-validator'; export async function getProtocolVersion(requestManager: Web3RequestManager) { @@ -575,3 +576,18 @@ export async function createAccessList( params: [transaction, blockNumber], }); } + +export async function signTypedData( + requestManager: Web3RequestManager, + address: Address, + typedData: Eip712TypedData, + useLegacy = false, +): Promise { + // TODO Add validation for typedData + validator.validate(['address'], [address]); + + return requestManager.send({ + method: `eth_signTypedData${useLegacy ? '' : '_v4'}`, + params: [address, typedData], + }); +} diff --git a/packages/web3-rpc-methods/test/unit/eth_rpc_methods/fixtures/sign_typed_data.ts b/packages/web3-rpc-methods/test/unit/eth_rpc_methods/fixtures/sign_typed_data.ts new file mode 100644 index 00000000000..37a5ee3171e --- /dev/null +++ b/packages/web3-rpc-methods/test/unit/eth_rpc_methods/fixtures/sign_typed_data.ts @@ -0,0 +1,98 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +import { Address, Eip712TypedData } from 'web3-types'; + +const address = '0x407d73d8a49eeb85d32cf465507dd71d507100c1'; + +const typedData = { + 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!', + }, +}; + +/** + * Array consists of: + * - Test title + * - Input parameters: + * - address + * - message + */ +type TestData = [string, [Address, Eip712TypedData, boolean | undefined]]; +export const testData: TestData[] = [ + ['useLegacy = undefined', [address, typedData, undefined]], + ['useLegacy = false', [address, typedData, false]], + ['useLegacy = true', [address, typedData, true]], +]; diff --git a/packages/web3-rpc-methods/test/unit/eth_rpc_methods/sign_typed_data.test.ts b/packages/web3-rpc-methods/test/unit/eth_rpc_methods/sign_typed_data.test.ts new file mode 100644 index 00000000000..a9674a8d381 --- /dev/null +++ b/packages/web3-rpc-methods/test/unit/eth_rpc_methods/sign_typed_data.test.ts @@ -0,0 +1,54 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +import { Web3RequestManager } from 'web3-core'; +import { validator } from 'web3-validator'; + +import { ethRpcMethods } from '../../../src/index'; +import { testData } from './fixtures/sign_typed_data'; + +jest.mock('web3-validator'); + +describe('signTypedData', () => { + let requestManagerSendSpy: jest.Mock; + let requestManager: Web3RequestManager; + + beforeAll(() => { + requestManager = new Web3RequestManager('http://127.0.0.1:8545'); + requestManagerSendSpy = jest.fn(); + requestManager.send = requestManagerSendSpy; + }); + + it.each(testData)( + 'should call requestManager.send with signTypedData method and expect parameters\n Title: %s\n Input parameters: %s', + async (_, inputParameters) => { + await ethRpcMethods.signTypedData(requestManager, ...inputParameters); + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: `eth_signTypedData${inputParameters[2] ? '' : '_v4'}`, + params: [inputParameters[0], inputParameters[1]], + }); + }, + ); + + it.each(testData)( + 'should call validator.validate with expected params\n Title: %s\n Input parameters: %s', + async (_, inputParameters) => { + const validatorSpy = jest.spyOn(validator, 'validate'); + await ethRpcMethods.signTypedData(requestManager, ...inputParameters); + expect(validatorSpy).toHaveBeenCalledWith(['address'], [inputParameters[0]]); + }, + ); +}); diff --git a/packages/web3-types/CHANGELOG.md b/packages/web3-types/CHANGELOG.md index 3607abfae24..da55982895e 100644 --- a/packages/web3-types/CHANGELOG.md +++ b/packages/web3-types/CHANGELOG.md @@ -141,3 +141,8 @@ Documentation: - type `Filter` includes `blockHash` (#6206) ## [Unreleased] + +### Added + +- `eth_signTypedData` and `eth_signTypedData_v4` to `web3_eth_execution_api` (#6286) +- `Eip712TypeDetails` and `Eip712TypedData` to `eth_types` (#6286) diff --git a/packages/web3-types/src/apis/web3_eth_execution_api.ts b/packages/web3-types/src/apis/web3_eth_execution_api.ts index 161e73f8d39..fde1250b4a3 100644 --- a/packages/web3-types/src/apis/web3_eth_execution_api.ts +++ b/packages/web3-types/src/apis/web3_eth_execution_api.ts @@ -19,6 +19,8 @@ import { AccountObject, Address, BlockNumberOrTag, + Eip712TypedData, + HexString256Bytes, HexString32Bytes, TransactionInfo, Uint, @@ -41,4 +43,18 @@ export type Web3EthExecutionAPI = EthExecutionAPI & { storageKeys: HexString32Bytes[], blockNumber: BlockNumberOrTag, ) => AccountObject; + + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md + eth_signTypedData: ( + address: Address, + typedData: Eip712TypedData, + useLegacy: true, + ) => HexString256Bytes; + + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md + eth_signTypedData_v4: ( + address: Address, + typedData: Eip712TypedData, + useLegacy: false | undefined, + ) => HexString256Bytes; }; diff --git a/packages/web3-types/src/eth_types.ts b/packages/web3-types/src/eth_types.ts index 6a9a4aa8e88..2865428b092 100644 --- a/packages/web3-types/src/eth_types.ts +++ b/packages/web3-types/src/eth_types.ts @@ -496,3 +496,17 @@ export interface AccountObject { readonly accountProof: Bytes[]; readonly storageProof: StorageProof[]; } + +export interface Eip712TypeDetails { + name: string; + type: string; +} +export interface Eip712TypedData { + readonly types: { + EIP712Domain: Eip712TypeDetails[]; + [key: string]: Eip712TypeDetails[]; + }; + readonly primaryType: string; + readonly domain: Record; + readonly message: Record; +}