/*
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 { AbiError } from 'web3-errors';
import { isNullish, isUint8Array, leftPad, rightPad, toHex } from 'web3-utils';
import {
AbiInput,
AbiCoderStruct,
AbiFragment,
AbiParameter,
AbiStruct,
AbiEventFragment,
AbiFunctionFragment,
AbiConstructorFragment,
} from 'web3-types';
export const isAbiFragment = (item: unknown): item is AbiFragment =>
!isNullish(item) &&
typeof item === 'object' &&
!isNullish((item as { type: string }).type) &&
['function', 'event', 'constructor', 'error'].includes((item as { type: string }).type);
export const isAbiErrorFragment = (item: unknown): item is AbiEventFragment =>
!isNullish(item) &&
typeof item === 'object' &&
!isNullish((item as { type: string }).type) &&
(item as { type: string }).type === 'error';
export const isAbiEventFragment = (item: unknown): item is AbiEventFragment =>
!isNullish(item) &&
typeof item === 'object' &&
!isNullish((item as { type: string }).type) &&
(item as { type: string }).type === 'event';
export const isAbiFunctionFragment = (item: unknown): item is AbiFunctionFragment =>
!isNullish(item) &&
typeof item === 'object' &&
!isNullish((item as { type: string }).type) &&
(item as { type: string }).type === 'function';
export const isAbiConstructorFragment = (item: unknown): item is AbiConstructorFragment =>
!isNullish(item) &&
typeof item === 'object' &&
!isNullish((item as { type: string }).type) &&
(item as { type: string }).type === 'constructor';
/**
* Check if type is simplified struct format
*/
export const isSimplifiedStructFormat = (
type: string | Partial | Partial,
): type is Omit =>
typeof type === 'object' &&
typeof (type as { components: unknown }).components === 'undefined' &&
typeof (type as { name: unknown }).name === 'undefined';
/**
* Maps the correct tuple type and name when the simplified format in encode/decodeParameter is used
*/
export const mapStructNameAndType = (structName: string): AbiStruct =>
structName.includes('[]')
? { type: 'tuple[]', name: structName.slice(0, -2) }
: { type: 'tuple', name: structName };
/**
* Maps the simplified format in to the expected format of the ABICoder
*/
export const mapStructToCoderFormat = (struct: AbiStruct): Array => {
const components: Array = [];
for (const key of Object.keys(struct)) {
const item = struct[key];
if (typeof item === 'object') {
components.push({
...mapStructNameAndType(key),
components: mapStructToCoderFormat(item as unknown as AbiStruct),
});
} else {
components.push({
name: key,
type: struct[key] as string,
});
}
}
return components;
};
/**
* Map types if simplified format is used
*/
export const mapTypes = (
types: AbiInput[],
): Array> => {
const mappedTypes: Array> = [];
for (const type of types) {
let modifiedType = type;
// Clone object
if (typeof type === 'object') {
modifiedType = { ...type };
}
// Remap `function` type params to bytes24 since Ethers does not
// recognize former type. Solidity docs say `Function` is a bytes24
// encoding the contract address followed by the function selector hash.
if (typeof type === 'object' && type.type === 'function') {
modifiedType = { ...type, type: 'bytes24' };
}
if (isSimplifiedStructFormat(modifiedType)) {
const structName = Object.keys(modifiedType)[0] as unknown as keyof typeof modifiedType;
mappedTypes.push({
...mapStructNameAndType(structName),
components: mapStructToCoderFormat(
modifiedType[structName] as unknown as AbiStruct,
) as unknown as AbiParameter[],
});
} else {
mappedTypes.push(modifiedType);
}
}
return mappedTypes;
};
/**
* returns true if input is a hexstring and is odd-lengthed
*/
export const isOddHexstring = (param: unknown): boolean =>
typeof param === 'string' && /^(-)?0x[0-9a-f]*$/i.test(param) && param.length % 2 === 1;
/**
* format odd-length bytes to even-length
*/
export const formatOddHexstrings = (param: string): string =>
isOddHexstring(param) ? `0x0${param.substring(2)}` : param;
const paramTypeBytes = /^bytes([0-9]*)$/;
const paramTypeBytesArray = /^bytes([0-9]*)\[\]$/;
const paramTypeNumber = /^(u?int)([0-9]*)$/;
const paramTypeNumberArray = /^(u?int)([0-9]*)\[\]$/;
/**
* Handle some formatting of params for backwards compatibility with Ethers V4
*/
export const formatParam = (type: string, _param: unknown): unknown => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
// clone if _param is an object
const param = typeof _param === 'object' && !Array.isArray(_param) ? { ..._param } : _param;
// Format BN to string
if (param instanceof BigInt || typeof param === 'bigint') {
return param.toString(10);
}
if (paramTypeBytesArray.exec(type) || paramTypeNumberArray.exec(type)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
const paramClone = [...(param as Array)];
return paramClone.map(p => formatParam(type.replace('[]', ''), p));
}
// Format correct width for u?int[0-9]*
let match = paramTypeNumber.exec(type);
if (match) {
const size = parseInt(match[2] ? match[2] : '256', 10);
if (size / 8 < (param as { length: number }).length) {
// pad to correct bit width
return leftPad(param as string, size);
}
}
// Format correct length for bytes[0-9]+
match = paramTypeBytes.exec(type);
if (match) {
const hexParam = isUint8Array(param) ? toHex(param) : param;
// format to correct length
const size = parseInt(match[1], 10);
if (size) {
let maxSize = size * 2;
if ((param as string).startsWith('0x')) {
maxSize += 2;
}
// pad to correct length
const paddedParam =
(hexParam as string).length < maxSize
? rightPad(param as string, size * 2)
: hexParam;
return formatOddHexstrings(paddedParam as string);
}
return formatOddHexstrings(hexParam as string);
}
return param;
};
/**
* used to flatten json abi inputs/outputs into an array of type-representing-strings
*/
export const flattenTypes = (
includeTuple: boolean,
puts: ReadonlyArray,
): string[] => {
const types: string[] = [];
puts.forEach(param => {
if (typeof param.components === 'object') {
if (!param.type.startsWith('tuple')) {
throw new AbiError(
`Invalid value given "${param.type}". Error: components found but type is not tuple.`,
);
}
const arrayBracket = param.type.indexOf('[');
const suffix = arrayBracket >= 0 ? param.type.substring(arrayBracket) : '';
const result = flattenTypes(includeTuple, param.components);
if (Array.isArray(result) && includeTuple) {
types.push(`tuple(${result.join(',')})${suffix}`);
} else if (!includeTuple) {
types.push(`(${result.join(',')})${suffix}`);
} else {
types.push(`(${result.join()})`);
}
} else {
types.push(param.type);
}
});
return types;
};
/**
* Should be used to create full function/event name from json abi
* returns a string
*/
export const jsonInterfaceMethodToString = (json: AbiFragment): string => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (isAbiErrorFragment(json) || isAbiEventFragment(json) || isAbiFunctionFragment(json)) {
if (json.name?.includes('(')) {
return json.name;
}
return `${json.name ?? ''}(${flattenTypes(false, json.inputs ?? []).join(',')})`;
}
// Constructor fragment
return `(${flattenTypes(false, json.inputs ?? []).join(',')})`;
};