UNPKG

0xweb

Version:

Contract package manager and other web3 tools

315 lines (285 loc) 10.3 kB
import alot from 'alot'; import { keccak_256 } from '@noble/hashes/sha3'; import type { TAbiItem } from '@dequanto/types/TAbi'; import { ITxLogItem } from '@dequanto/txs/receipt/ITxLogItem'; import { TBufferLike } from '@dequanto/models/TBufferLike'; import { $abiUtils } from './$abiUtils'; import { $abiParser } from './$abiParser'; import { $require } from './$require'; import { $buffer } from './$buffer'; import { TEth } from '@dequanto/models/TEth'; import { $abiCoder } from '@dequanto/abi/$abiCoder'; import { $logger } from './$logger'; import { EvmBytecode } from '@dequanto/evm/EvmBytecode'; import { $bytecode } from '@dequanto/evm/utils/$bytecode'; import { $hex } from './$hex'; import { $is } from './$is'; export namespace $contract { export function keccak256(str: string | TBufferLike, output: 'buffer'): Uint8Array export function keccak256(str: string | TBufferLike, output?: 'hex'): TEth.Hex export function keccak256(str: string | TBufferLike, output?: 'buffer' | 'hex'): Uint8Array | TEth.Hex { if (str == null || str === '0x') { return `0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`; } let input; if (typeof str === 'string') { input = $is.Hex(str) ? $buffer.fromHex(str) : $buffer.fromString(str); } else { input = str; } let hashBytes = keccak_256(input); if (output === 'buffer') { return hashBytes } return $buffer.toHex(hashBytes); } export function normalizeArgs(args: any[]) { return args.map(val => { if (val?._isBigNumber) { return BigInt(val.toString()); } if (Array.isArray(val)) { return normalizeArgs(val); } return val; }) } function normalizeValue(val: any) { if (val == null || typeof val !== 'object') { return val; } if (val?._isBigNumber) { return BigInt(val.toString()); } return val; } export function extractLogsForAbi(tx: TEth.TxReceipt, abiItem: string | TAbiItem): ITxLogItem[] { let topicHash = $abiUtils.getMethodHash(abiItem); let logs = tx .logs .filter(log => { return log.topics[0] === topicHash; }) .map(log => { return parseLogWithAbi(log, abiItem); }) return logs; } export function parseInputData(inputHex: string, abis: TAbiItem[]) { if (abis == null || abis.length === 0 || inputHex == null) { return null; } let str = inputHex.substring(2); if (str === '') { return null; } let methodHex = `0x${str.substring(0, 8)}`; let bytesHex = `0x${str.substring(8)}`; let abi = abis.find(abi => { let sig = $abiUtils.getMethodSignature(abi); return sig === methodHex; }); if (abi == null) { return null; } let params: any = $abiCoder.decode(abi.inputs, bytesHex); let asObject = abi.inputs.every(x => x.name != null); if (asObject) { params = alot(abi.inputs).map((x, i) => { return { key: x.name, value: params[i] } }).toDictionary(x => x.key, x => x.value); } return { method: abi.name, params }; } export function decodeDeploymentArguments(input: TEth.Hex, ctorAbi: TAbiItem) { let { arguments: hex } = $bytecode.parseContractCreation(input); if ($hex.isEmpty(hex)) { return null; } let decoded = $abiUtils.decode(ctorAbi.inputs, hex); return { encoded: hex, ...decoded }; } export function parseDeploymentBytecode(input: TEth.Hex) { let evm = new EvmBytecode(input, { withConstructorCode: true }); let opcodes = evm.getOpcodes(); let codeSize = opcodes.findIndex(x => x.name === 'CODESIZE'); if (codeSize === -1) { return { bytecode: input, arguments: '0x', }; } let prev = opcodes[codeSize - 1]; $require.True(/PUSH/.test(prev.name), `PUSH expected but got ${prev.name}`); let codeSizeValue = $buffer.toBigInt(prev.pushData) * 2n; let inputCursor = 2 /*0x*/ + Number(codeSizeValue); return { bytecode: input.substring(0, inputCursor), arguments: '0x' + input.substring(inputCursor), }; } export function decodeCustomError(errorDataHex: string | { type, params } | any, abiArr: TAbiItem[]) { if (errorDataHex == null) { return null; } if (typeof errorDataHex !== 'string') { let isUnknown = errorDataHex.type === 'Unknown' && typeof errorDataHex.params === 'string'; if (isUnknown === false) { return errorDataHex; } errorDataHex = errorDataHex.params; } if (errorDataHex.startsWith('0x')) { let arr = abiArr?.length === 0 ? store.getFlattened() : abiArr; let errors = [ ...(arr.filter(x => (x as any).type === 'error')), $abiParser.parseMethod(`Error(string)`), $abiParser.parseMethod(`Panic(uint256)`), ]; let parsed = $contract.parseInputData(errorDataHex, errors); if (parsed) { return { type: parsed.method, params: parsed.params, } } } return { type: 'Unknown', params: errorDataHex }; } export function parseLogWithAbi(log: TEth.Log, mix: TAbiItem[] | TAbiItem | string): ITxLogItem { let abiItem: TAbiItem; if (typeof mix === 'string') { abiItem = $abiParser.parseMethod(mix); } else if (Array.isArray(mix)) { abiItem = mix.find(x => { let topic = log.topics[0]; let sig = $abiUtils.getTopicSignature(x); return topic === sig; }); if (abiItem == null) { $logger.log(`Abi not found for ${log.topics[0]} within ${mix.map(x => x.name)}`); return <any>{ event: 'Unknown', ...log, } } } else { abiItem = mix; } let inputs = abiItem.inputs.slice(); // Move indexed inputs forward inputs.sort((a, b) => { if (a.indexed === b.indexed) { return 0; } if (b.indexed === true && a.indexed !== true) { return 1; } return -1; }); let args = log.topics.slice(1).map((bytes, i) => { let type = inputs.shift(); if (type.indexed && $abiUtils.isDynamicType(type.type)) { // Dynamic types are stored as keccak256 hashes return { name: type.name, value: bytes }; } let val = $abiCoder.decode([type], bytes); return { name: type.name, value: val[0] } }); if (inputs.length > 0) { let values = $abiCoder.decode(inputs, log.data); args.push(...values.map((val, i) => { return { name: inputs[i].name, value: val }; })); } let params = args.reduce((agr, arg) => { agr[arg.name] = arg.value; return agr; }, {}); return { id: log.blockNumber * 100_000 + log.logIndex, blockNumber: log.blockNumber, logIndex: log.logIndex, transactionHash: log.transactionHash, address: log.address, event: abiItem.name, arguments: args, params: params, }; } export function formatCall(call: { method?, type?, params?}) { let method = call.method ?? call.type ?? 'Unknown'; let paramsStr = ''; if (call.params != null) { if (typeof call.params === 'string') { paramsStr = call.params; } else { let arr = []; for (let key in call.params) { let value = call.params[key]; if (value != null && typeof value === 'object') { value = formatJson(value); } arr.push(`${key}=${value}`); } paramsStr = arr.join(', '); } } return `${method}(${paramsStr})`; } export function formatCallFromAbi(abi: TAbiItem, params: any[]) { let method = abi.name; let paramsItems = []; let count = Math.max(abi.inputs.length, params.length); for (let i = 0; i < count; i++) { let key = abi.inputs[i]?.name; let value = params[i]; let valueStr = value != null && typeof value === 'object' ? formatJson(value) : String(value); if (key == null) { paramsItems.push(valueStr); continue; } paramsItems.push(`${key}=${valueStr}`); } let paramsStr = paramsItems.join(', '); return `${method}(${paramsStr})`; } export namespace store { const knownContracts = [] as { abi: TAbiItem[] }[]; export function getFlattened() { return alot(knownContracts).mapMany(x => x.abi).toArray(); } export function register(contract: { abi: TAbiItem[] }) { knownContracts.push(contract) } } function formatJson(json) { let str = JSON.stringify(json, null, 2); str = str.replace(/"([\w]+)":/g, '$1:'); return str; } }