Skip to content

Commit

Permalink
Implement EIP712 RPC Wrapper Methods and getEncodedEip712Data Util (#…
Browse files Browse the repository at this point in the history
…6286)

* Add Eip712TypeDetails and Eip712TypedData interfaces

* Init signTypedData rpc method and test

* Init getEncodedEip712Message and test

* Init signTypedData methods and tests

* Add return type to signTypedData

* Add eth_signTypedData and eth_signTypedData_v4 to web3_eth_execution_api

* Add itIf to signTypedData integration test

* Replace use of Buffer.from with bytesToHex

* Add additional test cases

* Rename getEncodedEip712Message to getEncodedEip712Data

* Update CHANGELOGs and renamed some files

* Update packages/web3-rpc-methods/test/unit/eth_rpc_methods/sign_typed_data.test.ts
  • Loading branch information
spacesailor24 authored Jul 20, 2023
1 parent 7d0a91d commit 933ef51
Show file tree
Hide file tree
Showing 19 changed files with 1,634 additions and 0 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions packages/web3-eth-abi/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
201 changes: 201 additions & 0 deletions packages/web3-eth-abi/src/eip_712.ts
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

/**
* @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<string[]>(
(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<string, unknown>,
// 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<string, unknown>,
).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<string, unknown>)];
}

// 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, unknown>,
): 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);
};
1 change: 1 addition & 0 deletions packages/web3-eth-abi/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Loading

0 comments on commit 933ef51

Please sign in to comment.