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 = /^((?function|event)\s+)?(?\w+)\s*\((?[^)]+)?\)\s*((:|returns)(?.+))?$/; const rgxMethodName = /^((?function|event)\s+)?(?\w+)/; const rgxMethodReturn = /((:|returns)(?.+))?$/; 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 { constant: false, payable: false, stateMutability, name: fnName, signature: isSig ? fnName : void 0, inputs: inputs, outputs: outputs, type: matchMethodName.groups.type ?? 'function', }; } /** * Parses Solidity struct declarations into a tuple ABI component. * Supports multiple declarations so later structs can reference earlier ones, * and returns the ABI component for the last struct in the input. */ export function parseStruct (struct: string): TAbiItem { let structs = Parse.structs(struct); $require.gt(structs.length, 0, `No structs found in ${struct}`); let byName = new Map(structs.map(x => [ x.name, x ])); let cache = new Map(); function toTuple (name: string): TAbiInput { let cached = cache.get(name); if (cached != null) { return cached; } let def = byName.get(name); $require.notNull(def, `Struct ${name} is not found`); let tuple: TAbiInput = { name: '', type: 'tuple', internalType: `struct ${name}`, components: [] }; cache.set(name, tuple); tuple.components = Parse.splitByDelimiter(def.body, ';') .map(field => $abiParser.parseArguments(field)[0]) .map(resolveStructs); return tuple; } function resolveStructs (param: TAbiInput): TAbiInput { let { type, array } = Parse.extractArray(param.type); if (byName.has(type) === false) { if (param.components != null) { return { ...param, components: param.components.map(resolveStructs) }; } return param; } let tuple = toTuple(type); return { name: param.name, type: `tuple${array}`, internalType: `struct ${type}${array}`, components: tuple.components }; } let last = structs[structs.length - 1]; return { ...toTuple(last.name), name: last.name } as any as TAbiItem; } // 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 = /^(?.+)\s+(?[\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 = { '[': ']', '(': ')', '{': '}' }; export function structs (source: string) { let rgx = /\bstruct\s+(?[\w_$]+)\s*\{/g; let arr: { name: string, body: string }[] = []; let match: RegExpExecArray; while ((match = rgx.exec(source)) != null) { let openI = source.indexOf('{', match.index); let closeI = goToClosing(source, openI, '{'); arr.push({ name: match.groups.name, body: source.substring(openI + 1, closeI).trim() }); rgx.lastIndex = closeI + 1; } return arr; } export function extractArray (type: string) { let array = ''; while (true) { let match = /^(?.+?)(?\[[^\]]*\])$/.exec(type); if (match == null) { return { type, array }; } type = match.groups.type; array = `${match.groups.array}${array}`; } } }