0xweb
Version:
Contract package manager and other web3 tools
278 lines (242 loc) • 9.17 kB
text/typescript
import { $types } from '@dequanto/solidity/utils/$types';
import alot from 'alot';
import { type TAbiInput } from '@dequanto/types/TAbi';
import { $require } from './$require';
export namespace $abiType {
export function getTsTypeFromDefinition (type: string) {
type = type.trim();
if ($types.isArray(type)) {
let baseType = array.getBaseType(type);
let baseTsType = getTsTypeFromDefinition(baseType);
return `${baseTsType}[${array.serializeLength(type)}]`;
}
if (type.startsWith('(')) {
let entries = [] as { ts: string, name: string }[];
for (let i = 1; i < type.length; i++) {
i = Parse.skipWhitespace(type, i);
let c = type[i];
let start = i;
let tsTypeEnd: number;
if (c === '(') {
i = 1 + Parse.goToClosing(type, i + 1, c);
tsTypeEnd = i;
} else if (c === 'm' && type.substring(i, i + 'mapping'.length) === 'mapping') {
i = i + 'mapping'.length;
i = Parse.skipWhitespace(type, i);
let c = type[i];
if (c !== '(') {
throw new Error(`Expect "(" after the mapping keyword. Got ${type.substring(i, i + 10)} ...`);
}
i = 1 + Parse.goToClosing(type, i + 1, c);
tsTypeEnd = i;
} else {
i = Parse.goToTypeBoundary(type, i);
tsTypeEnd = i;
}
i = Parse.skipWhitespace(type, i);
c = type[i];
if (c === '[') {
i = 1 + Parse.goToClosing(type, i + 1, c);
tsTypeEnd = i;
}
let solType = type.substring(start, i);
let tsType = getTsTypeFromDefinition(solType)
let name: string = null;
i = Parse.skipWhitespace(type, i);
c = type[i];
if (/[\w_$]/.test(c)) {
start = i;
i = Parse.goToTypeBoundary(type, i);
name = type.substring(start, i);
}
entries.push({ ts: tsType, name: name });
i = Parse.skipWhitespace(type, i);
c = type[i];
if (c === ')') {
break;
}
if (c === ',') {
i++;
continue;
}
console.log(`Invalid character starting at: ${type.substring(i)}`);
}
let isArray = entries.every(x => x.name == null);
if (isArray) {
return `[ ${entries.map(x => x.ts).join(', ')} ]`
}
let keys = entries.map(x => `${x.name}: ${x.ts}`).join(', ');
return `{ ${keys} }`;
}
if ($types.isMapping(type)) {
let valueType = $abiType.mapping.getValueType(type);
let keyType = $abiType.mapping.getKeyType(type);
let keyTsType = getTsTypeFromDefinition(keyType);
let valueTsType = getTsTypeFromDefinition(valueType);
// keyTsType could be bigint, Buffer, etc, lets use the string or number only
return `Record<string | number, ${valueTsType}>`;
}
return getTsType(type);
}
export function getTsType ($abiType: TAbiInput['type'], $abi?: { name?, components? }) {
let rgxArray = /\[(?<size>\d+)?\]$/
let isArray = rgxArray.test($abiType);
if (isArray) {
return getTsType($abiType.replace(rgxArray, ''), $abi) + '[]';
}
// // -fix subArrays
// let rgxSubType = /\[\]\[\d+\]$/
// if (rgxSubType.test($abiType)) {
// $abiType = $abiType.replace(rgxSubType, '');
// }
let abiType =$abiType;
let tsType = AbiTsTypes[abiType];
if (tsType == null) {
let byRgx = alot(AbiTsTypesRgx).map(definition => ({ match: definition.rgx.exec(abiType), definition })).first(x => x.match != null);
if (byRgx) {
let { match, definition } = byRgx;
tsType = definition.fromMatch?.(match) ?? definition.type;
}
}
if (tsType == null && abiType === 'tuple') {
let components = $abi?.components as { type, name }[];
if (components == null) {
throw new Error(`Components undefined for tuple ${$abi?.name ?? ''}`);
}
let fields = components.map(x => {
return `${x.name}: ${getTsType(x.type, x)}`;
}).join(', ');
tsType = `{ ${fields} }`;
}
if (tsType == null) {
throw new Error(`Unknown abi type in return: "${abiType}"`);
}
return tsType;
};
export namespace array {
// uint256[2] bytes32[] ...
export function getBaseType (arrayType: string) {
let baseType = arrayType.replace(/\[\d*\]\s*$/, '');
$require.notEq(baseType, arrayType, `${arrayType} is not valid array declaration`);
return baseType;
}
export function getLength (arrayType: string) {
let match = /\[(?<len>\d+)?\]$/.exec(arrayType);
$require.notNull(match, `${arrayType} is not valid array declaration`);
return Number(match.groups.len ?? Infinity);
}
export function serializeLength (arrayType: string) {
let match = /\[(?<len>\d+)?\]$/.exec(arrayType);
return match?.groups?.len ?? '';
}
}
export namespace mapping {
export function getKeyType (mappingType: string) {
let mid = mappingType.indexOf('=>');
$require.True(mid > -1, `Invalid mapping type: ${mappingType}`);
let str = mappingType.substring(0, mid);
return str.replace(/mapping\s*\(/, '').trim();
}
export function getValueType (mappingType: string) {
let mid = mappingType.indexOf('=>');
$require.True(mid > -1, `Invalid mapping type: ${mappingType}`);
let closed = 0;
for (let i = mid + 2; i < mappingType.length; i++) {
let c = mappingType[i];
if (c === '(') {
closed++;
continue;
}
if (c === ')') {
closed -= 1;
if (closed === -1) {
return mappingType.substring(mid + 2, i).trim();
}
}
}
throw new Error(`Mapping value was not extracted from ${mappingType}`);
}
// export function isMapping (type: string) {
// return type.startsWith('mapping');
// }
}
const AbiTsTypes = {
'enum': 'number',
'uint': 'number',
'int': 'number',
'bool': 'boolean',
'bytes': 'TEth.Hex',
'bytes4': 'TEth.Hex',
'bytes32': 'TEth.Hex',
'bytes64': 'TEth.Hex',
'bytes128': 'TEth.Hex',
'bytes256': 'TEth.Hex',
'address': 'TAddress',
'string': 'string',
};
const AbiTsTypesRgx = [
{
rgx: /u?int(?<bits>\d+)?/,
fromMatch (match: RegExpMatchArray) {
let bits = Number(match.groups.bits ?? 256);
if (bits > 64) {
return 'bigint';
}
return 'number';
},
type: null,
},
{
rgx: /bytes(?<bits>\d+)?/,
fromMatch (match: RegExpMatchArray) {
return 'TEth.Hex';
},
type: null,
},
// {
// rgx: /uint\d+/,
// type: 'bigint',
// }
];
}
namespace Parse {
export function skipWhitespace (str: string, _i: number) {
let i = _i;
for (; i < str.length; i++) {
if (str.charCodeAt(i) > 32) {
return i;
}
}
return i;
}
export function goToClosing (str: string, startI: number, openChar: string, closeChar?: string) {
closeChar = closeChar ?? CLOSE_CHARS[openChar];
let count = 1;
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 goToTypeBoundary(type: string, i: number): number {
let rgx = /[\w_$]/;
for (; i < type.length; i++) {
if (rgx.test(type[i])) {
continue;
}
break;
}
return i;
}
}