/*
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 .
*/
/**
* The web3.eth.abi functions let you encode and decode parameters to ABI (Application Binary Interface) for function calls to the EVM (Ethereum Virtual Machine).
*
* For using Web3 ABI functions, first install Web3 package using `npm i web3` or `yarn add web3`.
* After that, Web3 ABI functions will be available.
* ```ts
* import { Web3 } from 'web3';
*
* const web3 = new Web3();
* const encoded = web3.eth.abi.encodeFunctionSignature({
* name: 'myMethod',
* type: 'function',
* inputs: [{
* type: 'uint256',
* name: 'myNumber'
* },{
* type: 'string',
* name: 'myString'
* }]
* });
*
* ```
*
* For using individual package install `web3-eth-abi` package using `npm i web3-eth-abi` or `yarn add web3-eth-abi` and only import required functions.
* This is more efficient approach for building lightweight applications.
* ```ts
* import { encodeFunctionSignature } from 'web3-eth-abi';
*
* const encoded = encodeFunctionSignature({
* name: 'myMethod',
* type: 'function',
* inputs: [{
* type: 'uint256',
* name: 'myNumber'
* },{
* type: 'string',
* name: 'myString'
* }]
* });
*
* ```
*
* @module ABI
*/
// 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 { AbiError } from 'web3-errors';
import { encodeParameters } from './coders/encode.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 AbiError('Cannot encode data: value is not of array type', {
data,
});
}
if (length && data.length !== length) {
throw new AbiError(
`Cannot encode data: expected length of ${length}, but got ${data.length}`,
{
data,
},
);
}
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(encodeParameters(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(field.type)) {
throw new AbiError(`Cannot encode data: missing data for '${field.name}'`, {
data,
field,
});
}
const value = data[field.name];
const [_type, encodedValue] = encodeValue(typedData, field.type, value);
return [
[..._types, _type],
[..._values, encodedValue],
];
},
[['bytes32'], [getTypeHash(typedData, type)]],
);
return encodeParameters(types, values);
};