0xweb
Version:
Contract package manager and other web3 tools
253 lines (224 loc) • 8.35 kB
text/typescript
import type { TAbiItem, TAbiInput, TAbiOutput } from '@dequanto/types/TAbi';
import { $require } from './$require';
import { $str } from '@dequanto/solidity/utils/$str';
interface IParameter {
name?: string
type?: string
components?: IParameter[]
}
export namespace $abiParser {
export function getReturnType (abi: TAbiItem): 'array' | 'object' | 'uint256' | 'boolean' | 'buffer' | string {
let outputs = abi.outputs;
if (outputs == null || outputs.length == 0) {
return 'uint256';
}
if (outputs.length === 1 && !outputs[0].name) {
return outputs[0].type;
}
let hasKeys = outputs.every(x => Boolean(x.name));
if (hasKeys) {
return 'object';
}
return 'array';
}
export function getReturnTypeFromTypes (outputs: TAbiOutput[]): 'array' | 'object' | 'uint256' | 'boolean' | 'buffer' | string {
if (outputs == null || outputs.length == 0) {
return 'uint256';
}
if (outputs.length === 1 && !outputs[0].name) {
return outputs[0].type;
}
let hasKeys = outputs.every(x => Boolean(x.name));
if (hasKeys) {
return 'object';
}
return 'array';
}
const methodRgx = /^((?<type>function|event)\s+)?(?<methodName>\w+)\s*\((?<params>[^)]+)?\)\s*((:|returns)(?<return>.+))?$/;
const rgxMethodName = /^((?<type>function|event)\s+)?(?<methodName>\w+)/;
const rgxMethodReturn = /((:|returns)(?<return>.+))?$/;
const rgxArguments = /^\(.?\)$/;
const rgxModifiers = /(?<=\))[\s\w]+$/
/**
* foo(uint256):address
* function foo(uint256): (address account, uint256 value)
* function foo(uint256) returns (address)
*/
export function parseMethod (methodAbi: string): TAbiItem {
let matchMethodName = rgxMethodName.exec(methodAbi);
$require.notNull(matchMethodName, `Method name in abi ${methodAbi} is not valid. Expect like 'foo(uint256):address`);
let matchReturn = rgxMethodReturn.exec(methodAbi);
let fnName = matchMethodName.groups.methodName;
let fnParams = $str.removeRgxMatches(methodAbi, matchMethodName, matchReturn).trim();
$require.notNull(rgxArguments.test(fnParams), `Method arguments in abi ${methodAbi} is not valid. Expect like 'foo(uint256):address`);
let stateMutability = void 0;
let fnModifiers = rgxModifiers.exec(fnParams);
if (fnModifiers) {
let str = fnModifiers[0]
stateMutability = /\b(view|pure)\b/.exec(str)?.[1] ?? void 0;
fnParams = $str.removeRgxMatches(fnParams, fnModifiers);
}
// Remove trailing '()'
fnParams = fnParams.substring(1, fnParams.length - 1);
let outputs = parseArguments(matchReturn.groups.return?.trim() ?? '');
let inputs = Parse.parametersLine(fnParams)
let isSig = /^0x[A-F\d]{8}$/i.test(fnName);
return <TAbiItem> {
constant: false,
payable: false,
stateMutability,
name: fnName,
signature: isSig ? fnName : void 0,
inputs: inputs,
outputs: outputs,
type: matchMethodName.groups.type ?? 'function',
};
}
// uint256
// address[]
// (uint256, uint256)
// (uint256 foo, uint256 bar)
// (uint256 foo, uint256 bar)[]
// ((uint256 foo, uint256 bar) foo, uint256 bar)
export function parseArguments (line: string): TAbiInput[] {
line = line?.trim();
if (!line || line === '()') {
return [];
}
let c = line[0];
if (c === '{') {
throw new Error(`${line} is not supported, use (...) or [...] declarations`);
}
if (c === '[' || c === '(') {
let end = Parse.goToClosing(line, 0, c);
let parametersLine = line.substring(1, end);
let isArray = line.endsWith('[]');
let params = Parse.parametersLine(parametersLine);
if (isArray) {
return [
{
name: '',
type: `tuple[]`,
components: params
}
];
}
let delimiter = line.indexOf(',', end);
if (delimiter === -1) {
delimiter = line.length;
}
let tupleName = line.substring(end + 1, delimiter).trim();
if (tupleName) {
let type = 'tuple';
// e.g. tupleName "[] users" from (uint256 foo, uint256 bar)[] users
let arrMatch = /\[(\d+)?\]/.exec(tupleName);
if (arrMatch != null) {
type = type + arrMatch[0];
tupleName = tupleName.substring(arrMatch[0].length).trim();
}
return [
{
name: tupleName,
type,
components: params
}
];
}
// if (params.length === 1) {
// line = params[0].type;
// params = [
// {
// name: "",
// type: line,
// },
// ];
// }
return params;
}
// if (c === '{') {
// let params = line.substring(1, line.length - 1);
// outputs = params.split(',').map(x => x.trim()).map(param => {
// let [_type, _name] = param.split(/[\s+:]/).map(x => x.trim()).filter(Boolean);
// return {
// name: _name ?? '',
// type: _type.trim()
// };
// });
// }
let name = '';
let type = line;
let match = /^(?<type>.+)\s+(?<name>[\w_$]+)$/.exec(line);
if (match) {
name = match.groups.name;
type = match.groups.type;
let trimmed = type.replace(/\b(memory|calldata)\b/, '').trim();
if (trimmed !== '') {
type = trimmed;
}
}
return [
{
name,
type,
},
];
}
}
namespace Parse {
// uint256 foo, uint bar, address qux
export function parametersLine (paramsStr: string) {
let arr = splitByDelimiter(paramsStr, ',')
return arr.map(param => {
// `(uint256 foo, uint256 bar)` -> single params
// `(uint256 foo, uint256 bar) param` -> tuple
let params = $abiParser.parseArguments(param);
if (param.startsWith('(') && param.endsWith(')')) {
return {
name: null,
type: 'tuple',
components: params
} as any as TAbiInput;
};
return params[0];
});
}
export function splitByDelimiter (line: string, delimiter: string): string[]{
let parts = [];
let start = 0;
for (let i = 0; i < line.length; i++) {
let c = line[i];
if (c === delimiter) {
parts.push(line.substring(start, i).trim());
start = i + 1;
continue;
}
if (c === '(') {
i = goToClosing(line, i, c);
continue;
}
}
// final part
parts.push(line.substring(start).trim());
return parts.filter(Boolean);
}
export function goToClosing (str: string, startI: number, openChar: string, closeChar?: string) {
closeChar = closeChar ?? CLOSE_CHARS[openChar];
let count = 0;
for (let i = startI; i < str.length; i++) {
if (str[i] === openChar) {
count++;
}
if (str[i] === closeChar) {
count--;
}
if (count === 0) {
return i;
}
}
throw new Error(`Unmatched closing chars ${openChar} ${closeChar} in ${str}`);
}
const CLOSE_CHARS = {
'[': ']',
'(': ')'
};
}