UNPKG

0xweb

Version:

Contract package manager and other web3 tools

507 lines (452 loc) 17.4 kB
import alot from 'alot'; import type { ArrayTypeName, ContractDefinition, ElementaryTypeName, Mapping, StructDefinition, TypeName, UserDefinedTypeName } from '@solidity-parser/parser/dist/src/ast-types'; import { $require } from '@dequanto/utils/$require'; import { $abiParser } from '@dequanto/utils/$abiParser'; import { $types } from './utils/$types'; import { $abiType } from '@dequanto/utils/$abiType'; import { SourceFile } from './SlotsParser/SourceFile'; import { Ast } from './SlotsParser/Ast'; import { ISlotsParserOption, ISlotVarDefinition } from './SlotsParser/models'; import type { TAbiInput } from '@dequanto/types/TAbi'; const SLOT_SIZE = 256; export namespace SlotsParser { export async function slotsFromAbi (abiDef: string | TAbiInput[]): Promise<ISlotVarDefinition[]> { let inputs = typeof abiDef === 'string' ? $abiParser.parseArguments(abiDef) : abiDef; let slotsDef = await alot(inputs) .mapAsync(async input => { let util = TypeAbiUtil.get(input) if (util) { return { slot: null as number, position: null as number, name: input.name, size: await util.sizeOf(), type: await util.serialize(), }; } throw new Error(`Unknown var ${input.name} ${input.type}`); }) .toArrayAsync(); slotsDef = applyPositions(slotsDef); return slotsDef; } export async function slots (source: { path: string, code?: string }, contractName?: string, opts?: ISlotsParserOption): Promise<ISlotVarDefinition[]> { const sourceFile = new SourceFile(source.path, source.code, opts?.files); const chain = await sourceFile.getContractInheritanceChain(contractName); return await extractSlots(chain, opts); } async function extractSlots(inheritanceChain: { contract: ContractDefinition | StructDefinition, file: SourceFile }[], opts?: ISlotsParserOption) { let slotsDef = await alot(inheritanceChain) .mapManyAsync(async (item, i) => { let slots = await extractSlotsSingle({ ...item }, opts); return slots; }) .toArrayAsync({ threads: 1 }); // remove duplicates, take the first declaration. (sorting is last..first) alot(slotsDef) .groupBy(x => x.name) .filter(x => x.values.length > 1) .forEach(group => { group.values.reverse().slice(1).forEach((slot, i) => { slot.name += ''.padStart(i + 1, '$'); }); }) .toArray(); let offset = { slot: 0, position: 0 }; slotsDef = applyPositions(slotsDef, offset); return slotsDef; } async function extractSlotsSingle (contract: TypeUtil.ITypeCtx, opts?: { withConstants?: boolean withImmutables?: boolean }) { let vars = Ast.isStructDefinition(contract.contract) ? contract.contract.members : alot(Ast.getVariableDeclarations(contract.contract)) .map($var => { if ($var.isDeclaredConst && opts?.withConstants !== true) { return null; } if (($var as any /** not in typings */).isImmutable && opts?.withImmutables !== true) { return null; } return $var; }) .filter(x => x != null) .toArray(); let slotsDef = await alot(vars) .mapAsync(async v => { let util = TypeUtil.get(v.typeName, contract); if (util) { let $var = { slot: null as number, position: null as number, name: v.name, size: await util.sizeOf(), type: await util.serialize(), } as ISlotVarDefinition; if (v.isDeclaredConst) { $var.memory = 'constant'; if (v.expression) { try { $var.value = Ast.evaluate(v.expression); } catch (error) { console.error('Skipped', error); } } } if ((v as any /** not in typings */).isImmutable) { $var.memory = 'immutable'; } return $var; } throw new Error(`Unknown var ${v.name} ${v.typeName.type}`); }) .toArrayAsync(); return slotsDef; } function applyPositions ($vars: ISlotVarDefinition[], offset?: { slot: number, position: number }) { offset ??= { slot: 0, position: 0 }; $vars.forEach($var => { if ($var.memory === 'constant' || $var.memory === 'immutable') { // skip calculation for static variables return; } if ($var.size === Infinity) { if (offset.position > 0) { // was previously moved further in a slot, so just take the next slot offset.position = 0; offset.slot += 1; } $var.slot = offset.slot; $var.position = 0; // move to the start of the next slot offset.slot += 1; offset.position = 0; return; } if ($var.size <= SLOT_SIZE - offset.position && TypeUtil.isComplexType($var.type) === false) { $var.slot = offset.slot; $var.position = offset.position; offset.position += $var.size; return; } if (offset.position > 0) { offset.slot += 1; offset.position = 0; // > moves to next slot } $var.slot = offset.slot; $var.position = offset.position; let slots = Math.floor($var.size / SLOT_SIZE); offset.slot += slots; offset.position = $var.size % SLOT_SIZE; }); return $vars; } } namespace TypeUtil { export interface ITypeUtil { sizeOf(): Promise<number> serialize(): Promise<string> } export interface ITypeCtx { contract: ContractDefinition | StructDefinition contractBase?: ContractDefinition[] file: SourceFile }; export function get(type: TypeName, ctx: ITypeCtx): ITypeUtil { if (type.type === 'ElementaryTypeName') { return new ElementaryTypeNameUtil(type); } if (type.type === 'ArrayTypeName') { return new ArrayTypeNameUtil(type, ctx); } if (type.type === 'UserDefinedTypeName') { return new UserDefinedTypeNameUtil(type, ctx) } if (type.type === 'Mapping') { return new MappingUtil(type, ctx) } throw new Error(`Unknow type ${type.type}`); } export function isComplexType (type: string) { return type.endsWith(']') || type.endsWith(')'); } class ElementaryTypeNameUtil implements ITypeUtil { constructor (public type: ElementaryTypeName) { } async sizeOf () { return $types.sizeOf(this.type.name) } async serialize () { return this.type.name; } } class ArrayTypeNameUtil implements ITypeUtil { constructor (public type: ArrayTypeName, public ctx: ITypeCtx) { } async sizeOf () { let type = this.type; let length = this.length(); if (length === Infinity) { return length; } let single = await get(type.baseTypeName, this.ctx).sizeOf(); if (single === Infinity) { return single; } return single * length; } async serialize () { let len = this.length(); let t = await get(this.type.baseTypeName, this.ctx).serialize(); return `${t}[${len === Infinity ? '' : len}]`; } private length () { return Number((this.type.length as any)?.number ?? Infinity) } } class UserDefinedTypeNameUtil implements ITypeUtil { constructor (public type: UserDefinedTypeName, public ctx: ITypeCtx) { } // Calculate the SLOTs size of the enum excluding any dynamic values that are stored in offset async sizeOf () { let definition = await this.getDefinition(); if (definition.type === 'ContractDefinition') { return $types.sizeOf('address'); } if (definition.type === 'EnumDefinition') { let count = Math.ceil(definition.members.length / 256); return $types.sizeOf(`uint${ 8 * count }`) } if (definition.type === 'TypeDefinition') { let x = get(definition.definition, this.ctx); return x.sizeOf(); } let ctx = { ...this.ctx, contract: definition.parent, }; let members = definition.members.map(x => get(x.typeName, ctx)); let position = 0; let size = 0; for (let member of members) { let sizeOf = await member.sizeOf(); if (sizeOf === Infinity) { // dynamic value occupies a slot as pointer to the offset size += SLOT_SIZE; position = 0; continue; } if (sizeOf > SLOT_SIZE - position) { // next slot if (position > 0) { // include empty memory in current slot size += SLOT_SIZE - position; } size += sizeOf; position = sizeOf < SLOT_SIZE ? sizeOf : 0; continue; } // pack into current slot size += sizeOf; position += sizeOf; } //--let sizes = await alot(members).sumAsync(x => x.sizeOf()); return size; } async serialize () { let definition = await this.getDefinition(); if (definition.type === 'ContractDefinition') { return 'address'; } if (definition.type === 'EnumDefinition') { return 'enum'; } if (definition.type === 'TypeDefinition') { let x = get(definition.definition, this.ctx); return x.serialize(); } let ctx = { ...this.ctx, contract: definition.parent, }; let members = await alot(definition.members).mapAsync(async x => { let util = get(x.typeName, ctx); let type = await util.serialize(); return `${type} ${x.name}`; }).toArrayAsync(); return `(${members.join(', ')})`; } private async getDefinition () { let name = this.type.namePath; // Search inside the contract let typeDef = Ast.getUserDefinedType(this.ctx.contract, name); if (typeDef) { return typeDef; } // Search in the source file typeDef = await this.ctx.file.getUserDefinedType(name); if (typeDef) { return typeDef; } if (Ast.isContractDefinition(this.ctx.contract)) { // Search inside the base contracts let baseContracts = this.ctx.contract.baseContracts; if (baseContracts?.length > 0) { typeDef = await alot(baseContracts) .mapAsync(async $base => { let namePath = $base.baseName?.namePath; if (namePath == null) { return null; } let contract = await this.ctx.file.getUserDefinedType(namePath); if (contract == null || contract.type !== 'ContractDefinition') { return null; } return Ast.getUserDefinedType(contract, name); }) .firstAsync(x => x != null); if (typeDef) { return typeDef; } } } $require.notNull(typeDef, `UserDefined Type not resolved ${name} in ${this.ctx.file?.path}`); return typeDef; } } class MappingUtil implements ITypeUtil { constructor (public type: Mapping, public ctx: ITypeCtx) { } async sizeOf () { return Infinity; } async serialize () { let keyType = get(this.type.keyType, this.ctx); let valueType = get(this.type.valueType, this.ctx); let [ key, value ] = await Promise.all([ keyType.serialize(), valueType.serialize()]) return `mapping(${key} => ${value})`; } } } namespace TypeAbiUtil { interface ITypeUtil { sizeOf(): Promise<number> serialize(): Promise<string> } interface ITypeCtx { contract: ContractDefinition file: SourceFile }; export function get(input: TAbiInput): ITypeUtil { if (/\]$/.test(input.type)) { return new ArrayTypeNameUtil(input); } if (/tuple|mapping|(\]$)/.test(input.type) === false) { return new ElementaryTypeNameUtil(input); } if (/tuple/.test(input.type)) { return new UserDefinedTypeNameUtil(input) } if (/mapping/.test(input.type)) { return new MappingUtil(input) } throw new Error(`Unknown type ${input.type} for ${input.name}`); } class ElementaryTypeNameUtil implements ITypeUtil { constructor (public type: TAbiInput) { } async sizeOf () { return $types.sizeOf(this.type.type) } async serialize () { return this.type.type; } } class ArrayTypeNameUtil implements ITypeUtil { constructor (public input: TAbiInput) { } async sizeOf () { let length = this.length(); if (length === Infinity) { return length; } let baseType = $abiType.array.getBaseType(this.input.type); let single = await get({ type: baseType, name: '', components: this.input.components }).sizeOf(); if (single === Infinity) { return single; } return single * length; } async serialize () { if (this.input.components?.length > 0) { let struct = new UserDefinedTypeNameUtil(this.input); let str = await struct.serialize(); if (str.startsWith('(') === false) { str = `(${str})`; } let arrSfx = /\[.+$/.exec(this.input.type)[0]; let serialized = str + arrSfx return serialized; } return this.input.type; } private length () { return $abiType.array.getLength(this.input.type) } } class UserDefinedTypeNameUtil implements ITypeUtil { constructor (public input: TAbiInput) { } async sizeOf () { let components = this.input.components; if (components == null) { throw new Error(`Components not defined for the tuple: ${this.input.name}`); } let members = components.map(x => get(x)); let sizes = await alot(members).sumAsync(x => x.sizeOf()); return sizes; } async serialize () { let components = this.input.components; if (components == null) { throw new Error(`Components not defined for the tuple: ${this.input.name}`); } let members = await alot(components).mapAsync(async x => { let util = get(x); let type = await util.serialize(); return `${type} ${x.name}`.trim(); }).toArrayAsync(); return `(${members.join(', ')})`; } } class MappingUtil implements ITypeUtil { constructor (public input: TAbiInput) { } async sizeOf () { return Infinity; } async serialize () { return this.input.type; } } }