/** * The Interface class is a low-level class that accepts an * ABI and provides all the necessary functionality to encode * and decode paramaters to and results from methods, events * and errors. * * It also provides several convenience methods to automatically * search and find matching transactions and events to parse them. * * @_subsection api/abi:Interfaces [interfaces] */ import { keccak256 } from 'ethers'; import { id } from 'ethers'; import { concat, dataSlice, getBigInt, getBytes, getBytesCopy, hexlify, zeroPadValue, isHexString, defineProperties, assertArgument, toBeHex, assert, } from 'ethers'; import { AbiCoder } from 'ethers'; import { checkResultErrors, Result } from 'ethers'; import { ConstructorFragment, ErrorFragment, EventFragment, FallbackFragment, Fragment, FunctionFragment, ParamType, } from './fragments.js'; import { Typed } from 'ethers'; import type { BigNumberish, BytesLike, CallExceptionError, CallExceptionTransaction } from 'ethers'; import type { JsonFragment } from 'ethers'; export { checkResultErrors, Result }; /** * When using the [[Interface-parseLog]] to automatically match a Log to its event * for parsing, a **LogDescription** is returned. */ export class LogDescription { /** * The matching fragment for the ``topic0``. */ readonly fragment!: EventFragment; /** * The name of the Event. */ readonly name!: string; /** * The full Event signature. */ readonly signature!: string; /** * The topic hash for the Event. */ readonly topic!: string; /** * The arguments passed into the Event with ``emit``. */ readonly args!: Result; /** * @_ignore: */ constructor(fragment: EventFragment, topic: string, args: Result) { const name = fragment.name, signature = fragment.format(); defineProperties(this, { fragment, name, signature, topic, args, }); } } /** * When using the [[Interface-parseTransaction]] to automatically match * a transaction data to its function for parsing, * a **TransactionDescription** is returned. */ export class TransactionDescription { /** * The matching fragment from the transaction ``data``. */ readonly fragment!: FunctionFragment; /** * The name of the Function from the transaction ``data``. */ readonly name!: string; /** * The arguments passed to the Function from the transaction ``data``. */ readonly args!: Result; /** * The full Function signature from the transaction ``data``. */ readonly signature!: string; /** * The selector for the Function from the transaction ``data``. */ readonly selector!: string; /** * The ``value`` (in wei) from the transaction. */ readonly value!: bigint; /** * @_ignore: */ constructor(fragment: FunctionFragment, selector: string, args: Result, value: bigint) { const name = fragment.name, signature = fragment.format(); defineProperties(this, { fragment, name, args, signature, selector, value, }); } } /** * When using the [[Interface-parseError]] to automatically match an * error for a call result for parsing, an **ErrorDescription** is returned. */ export class ErrorDescription { /** * The matching fragment. */ readonly fragment!: ErrorFragment; /** * The name of the Error. */ readonly name!: string; /** * The arguments passed to the Error with ``revert``. */ readonly args!: Result; /** * The full Error signature. */ readonly signature!: string; /** * The selector for the Error. */ readonly selector!: string; /** * @_ignore: */ constructor(fragment: ErrorFragment, selector: string, args: Result) { const name = fragment.name, signature = fragment.format(); defineProperties(this, { fragment, name, args, signature, selector, }); } } /** * An **Indexed** is used as a value when a value that does not * fit within a topic (i.e. not a fixed-length, 32-byte type). It * is the ``keccak256`` of the value, and used for types such as * arrays, tuples, bytes and strings. */ export class Indexed { /** * The ``keccak256`` of the value logged. */ readonly hash!: null | string; /** * @_ignore: */ readonly _isIndexed!: boolean; /** * Returns ``true`` if %%value%% is an **Indexed**. * * This provides a Type Guard for property access. */ static isIndexed(value: any): value is Indexed { return !!(value && value._isIndexed); } /** * @_ignore: */ constructor(hash: null | string) { defineProperties(this, { hash, _isIndexed: true }); } } type ErrorInfo = { signature: string; inputs: Array; name: string; reason: (...args: Array) => string; }; // https://docs.soliditylang.org/en/v0.8.13/control-structures.html?highlight=panic#panic-via-assert-and-error-via-require const PanicReasons: Record = { '0': 'generic panic', '1': 'assert(false)', '17': 'arithmetic overflow', '18': 'division or modulo by zero', '33': 'enum overflow', '34': 'invalid encoded storage byte array accessed', '49': 'out-of-bounds array access; popping on an empty array', '50': 'out-of-bounds access of an array or bytesN', '65': 'out of memory', '81': 'uninitialized function', }; const BuiltinErrors: Record = { '0x08c379a0': { signature: 'Error(string)', name: 'Error', inputs: ['string'], reason: (message: string) => { return `reverted with reason string ${JSON.stringify(message)}`; }, }, '0x4e487b71': { signature: 'Panic(uint256)', name: 'Panic', inputs: ['uint256'], reason: (code: bigint) => { let reason = 'unknown panic code'; if (code >= 0 && code <= 0xff && PanicReasons[code.toString()]) { reason = PanicReasons[code.toString()]; } return `reverted with panic code 0x${code.toString(16)} (${reason})`; }, }, }; /* function wrapAccessError(property: string, error: Error): Error { const wrap = new Error(`deferred error during ABI decoding triggered accessing ${ property }`); (wrap).error = error; return wrap; } */ /* function checkNames(fragment: Fragment, type: "input" | "output", params: Array): void { params.reduce((accum, param) => { if (param.name) { if (accum[param.name]) { logger.throwArgumentError(`duplicate ${ type } parameter ${ JSON.stringify(param.name) } in ${ fragment.format("full") }`, "fragment", fragment); } accum[param.name] = true; } return accum; }, <{ [ name: string ]: boolean }>{ }); } */ /** * An **InterfaceAbi** may be any supported ABI format. * * A string is expected to be a JSON string, which will be parsed * using ``JSON.parse``. This means that the value **must** be a valid * JSON string, with no stray commas, etc. * * An array may contain any combination of: * - Human-Readable fragments * - Parsed JSON fragment * - [[Fragment]] instances * * A **Human-Readable Fragment** is a string which resembles a Solidity * signature and is introduced in [this blog entry](link-ricmoo-humanreadableabi). * For example, ``function balanceOf(address) view returns (uint)``. * * A **Parsed JSON Fragment** is a JavaScript Object desribed in the * [Solidity documentation](link-solc-jsonabi). */ export type InterfaceAbi = string | ReadonlyArray; /** * An Interface abstracts many of the low-level details for * encoding and decoding the data on the blockchain. * * An ABI provides information on how to encode data to send to * a Contract, how to decode the results and events and how to * interpret revert errors. * * The ABI can be specified by [any supported format](InterfaceAbi). */ export class Interface { /** * All the Contract ABI members (i.e. methods, events, errors, etc). */ readonly fragments!: ReadonlyArray; /** * The Contract constructor. */ readonly deploy!: ConstructorFragment; /** * The Fallback method, if any. */ readonly fallback!: null | FallbackFragment; /** * If receiving ether is supported. */ readonly receive!: boolean; #errors: Map; #events: Map; #functions: Map; // #structs: Map; #abiCoder: AbiCoder; /** * Create a new Interface for the %%fragments%%. */ constructor(fragments: InterfaceAbi) { let abi: ReadonlyArray = []; if (typeof fragments === 'string') { abi = JSON.parse(fragments); } else { abi = fragments; } this.#functions = new Map(); this.#errors = new Map(); this.#events = new Map(); // this.#structs = new Map(); const frags: Array = []; for (const a of abi) { try { frags.push(Fragment.from(a)); } catch (error) { console.log('EE', error); } } defineProperties(this, { fragments: Object.freeze(frags), }); let fallback: null | FallbackFragment = null; let receive = false; this.#abiCoder = this.getAbiCoder(); // Add all fragments by their signature this.fragments.forEach((fragment, index) => { let bucket: Map; switch (fragment.type) { case 'constructor': if (this.deploy) { console.log('duplicate definition - constructor'); return; } //checkNames(fragment, "input", fragment.inputs); defineProperties(this, { deploy: fragment, }); return; case 'fallback': if (fragment.inputs.length === 0) { receive = true; } else { assertArgument( !fallback || (fragment).payable !== fallback.payable, 'conflicting fallback fragments', `fragments[${index}]`, fragment ); fallback = fragment; receive = fallback.payable; } return; case 'function': //checkNames(fragment, "input", fragment.inputs); //checkNames(fragment, "output", (fragment).outputs); bucket = this.#functions; break; case 'event': //checkNames(fragment, "input", fragment.inputs); bucket = this.#events; break; case 'error': bucket = this.#errors; break; default: return; } // Two identical entries; ignore it const signature = fragment.format(); if (bucket.has(signature)) { return; } bucket.set(signature, fragment); }); // If we do not have a constructor add a default if (!this.deploy) { defineProperties(this, { deploy: ConstructorFragment.from('constructor()'), }); } defineProperties(this, { fallback, receive }); } /** * Returns the entire Human-Readable ABI, as an array of * signatures, optionally as %%minimal%% strings, which * removes parameter names and unneceesary spaces. */ format(minimal?: boolean): Array { const format = minimal ? 'minimal' : 'full'; const abi = this.fragments.map((f) => f.format(format)); return abi; } /** * Return the JSON-encoded ABI. This is the format Solidiy * returns. */ formatJson(): string { const abi = this.fragments.map((f) => f.format('json')); // We need to re-bundle the JSON fragments a bit return JSON.stringify(abi.map((j) => JSON.parse(j))); } /** * The ABI coder that will be used to encode and decode binary * data. */ getAbiCoder(): AbiCoder { return AbiCoder.defaultAbiCoder(); } // Find a function definition by any means necessary (unless it is ambiguous) #getFunction(key: string, values: null | Array, forceUnique: boolean): null | FunctionFragment { // Selector if (isHexString(key)) { const selector = key.toLowerCase(); for (const fragment of this.#functions.values()) { if (selector === fragment.selector) { return fragment; } } return null; } // It is a bare name, look up the function (will return null if ambiguous) if (key.indexOf('(') === -1) { const matching: Array = []; for (const [name, fragment] of this.#functions) { if (name.split('(' /* fix:) */)[0] === key) { matching.push(fragment); } } if (values) { const lastValue = values.length > 0 ? values[values.length - 1] : null; let valueLength = values.length; let allowOptions = true; if (Typed.isTyped(lastValue) && lastValue.type === 'overrides') { allowOptions = false; valueLength--; } // Remove all matches that don't have a compatible length. The args // may contain an overrides, so the match may have n or n - 1 parameters for (let i = matching.length - 1; i >= 0; i--) { const inputs = matching[i].inputs.length; if (inputs !== valueLength && (!allowOptions || inputs !== valueLength - 1)) { matching.splice(i, 1); } } // Remove all matches that don't match the Typed signature for (let i = matching.length - 1; i >= 0; i--) { const inputs = matching[i].inputs; for (let j = 0; j < values.length; j++) { // Not a typed value if (!Typed.isTyped(values[j])) { continue; } // We are past the inputs if (j >= inputs.length) { if (values[j].type === 'overrides') { continue; } matching.splice(i, 1); break; } // Make sure the value type matches the input type if (values[j].type !== inputs[j].baseType) { matching.splice(i, 1); break; } } } } // We found a single matching signature with an overrides, but the // last value is something that cannot possibly be an options if (matching.length === 1 && values && values.length !== matching[0].inputs.length) { const lastArg = values[values.length - 1]; if (lastArg == null || Array.isArray(lastArg) || typeof lastArg !== 'object') { matching.splice(0, 1); } } if (matching.length === 0) { return null; } if (matching.length > 1 && forceUnique) { const matchStr = matching.map((m) => JSON.stringify(m.format())).join(', '); assertArgument(false, `ambiguous function description (i.e. matches ${matchStr})`, 'key', key); } return matching[0]; } // Normalize the signature and lookup the function const result = this.#functions.get(FunctionFragment.from(key).format()); if (result) { return result; } return null; } /** * Get the function name for %%key%%, which may be a function selector, * function name or function signature that belongs to the ABI. */ getFunctionName(key: string): string { const fragment = this.#getFunction(key, null, false); assertArgument(fragment, 'no matching function', 'key', key); return fragment.name; } /** * Returns true if %%key%% (a function selector, function name or * function signature) is present in the ABI. * * In the case of a function name, the name may be ambiguous, so * accessing the [[FunctionFragment]] may require refinement. */ hasFunction(key: string): boolean { return !!this.#getFunction(key, null, false); } /** * Get the [[FunctionFragment]] for %%key%%, which may be a function * selector, function name or function signature that belongs to the ABI. * * If %%values%% is provided, it will use the Typed API to handle * ambiguous cases where multiple functions match by name. * * If the %%key%% and %%values%% do not refine to a single function in * the ABI, this will throw. */ getFunction(key: string, values?: Array): null | FunctionFragment { return this.#getFunction(key, values || null, true); } /** * Iterate over all functions, calling %%callback%%, sorted by their name. */ forEachFunction(callback: (func: FunctionFragment, index: number) => void): void { const names = Array.from(this.#functions.keys()); names.sort((a, b) => a.localeCompare(b)); for (let i = 0; i < names.length; i++) { const name = names[i]; callback(this.#functions.get(name), i); } } // Find an event definition by any means necessary (unless it is ambiguous) #getEvent(key: string, values: null | Array, forceUnique: boolean): null | EventFragment { // EventTopic if (isHexString(key)) { const eventTopic = key.toLowerCase(); for (const fragment of this.#events.values()) { if (eventTopic === fragment.topicHash) { return fragment; } } return null; } // It is a bare name, look up the function (will return null if ambiguous) if (key.indexOf('(') === -1) { const matching: EventFragment[] = []; for (const [name, fragment] of this.#events) { if (name.split('(' /* fix:) */)[0] === key) { matching.push(fragment); } } if (values) { // Remove all matches that don't have a compatible length. for (let i = matching.length - 1; i >= 0; i--) { if (matching[i].inputs.length < values.length) { matching.splice(i, 1); } } // Remove all matches that don't match the Typed signature for (let i = matching.length - 1; i >= 0; i--) { const inputs = matching[i].inputs; for (let j = 0; j < values.length; j++) { // Not a typed value if (!Typed.isTyped(values[j])) { continue; } // Make sure the value type matches the input type if (values[j].type !== inputs[j].baseType) { matching.splice(i, 1); break; } } } } if (matching.length === 0) { return null; } if (matching.length > 1 && forceUnique) { const matchStr = matching.map((m) => JSON.stringify(m.format())).join(', '); assertArgument(false, `ambiguous event description (i.e. matches ${matchStr})`, 'key', key); } return matching[0]; } // Normalize the signature and lookup the function const result = this.#events.get(EventFragment.from(key).format()); if (result) { return result; } return null; } /** * Get the event name for %%key%%, which may be a topic hash, * event name or event signature that belongs to the ABI. */ getEventName(key: string): string { const fragment = this.#getEvent(key, null, false); assertArgument(fragment, 'no matching event', 'key', key); return fragment.name; } /** * Returns true if %%key%% (an event topic hash, event name or * event signature) is present in the ABI. * * In the case of an event name, the name may be ambiguous, so * accessing the [[EventFragment]] may require refinement. */ hasEvent(key: string): boolean { return !!this.#getEvent(key, null, false); } /** * Get the [[EventFragment]] for %%key%%, which may be a topic hash, * event name or event signature that belongs to the ABI. * * If %%values%% is provided, it will use the Typed API to handle * ambiguous cases where multiple events match by name. * * If the %%key%% and %%values%% do not refine to a single event in * the ABI, this will throw. */ getEvent(key: string, values?: Array): null | EventFragment { return this.#getEvent(key, values || null, true); } /** * Iterate over all events, calling %%callback%%, sorted by their name. */ forEachEvent(callback: (func: EventFragment, index: number) => void): void { const names = Array.from(this.#events.keys()); names.sort((a, b) => a.localeCompare(b)); for (let i = 0; i < names.length; i++) { const name = names[i]; callback(this.#events.get(name), i); } } /** * Get the [[ErrorFragment]] for %%key%%, which may be an error * selector, error name or error signature that belongs to the ABI. * * If %%values%% is provided, it will use the Typed API to handle * ambiguous cases where multiple errors match by name. * * If the %%key%% and %%values%% do not refine to a single error in * the ABI, this will throw. */ getError(key: string, values?: Array): null | ErrorFragment { if (isHexString(key)) { const selector = key.toLowerCase(); if (BuiltinErrors[selector]) { return ErrorFragment.from(BuiltinErrors[selector].signature); } for (const fragment of this.#errors.values()) { if (selector === fragment.selector) { return fragment; } } return null; } // It is a bare name, look up the function (will return null if ambiguous) if (key.indexOf('(') === -1) { const matching: ErrorFragment[] = []; for (const [name, fragment] of this.#errors) { if (name.split('(' /* fix:) */)[0] === key) { matching.push(fragment); } } if (matching.length === 0) { if (key === 'Error') { return ErrorFragment.from('error Error(string)'); } if (key === 'Panic') { return ErrorFragment.from('error Panic(uint256)'); } return null; } else if (matching.length > 1) { const matchStr = matching.map((m) => JSON.stringify(m.format())).join(', '); assertArgument(false, `ambiguous error description (i.e. ${matchStr})`, 'name', key); } return matching[0]; } // Normalize the signature and lookup the function key = ErrorFragment.from(key).format(); if (key === 'Error(string)') { return ErrorFragment.from('error Error(string)'); } if (key === 'Panic(uint256)') { return ErrorFragment.from('error Panic(uint256)'); } const result = this.#errors.get(key); if (result) { return result; } return null; } /** * Iterate over all errors, calling %%callback%%, sorted by their name. */ forEachError(callback: (func: ErrorFragment, index: number) => void): void { const names = Array.from(this.#errors.keys()); names.sort((a, b) => a.localeCompare(b)); for (let i = 0; i < names.length; i++) { const name = names[i]; callback(this.#errors.get(name), i); } } // Get the 4-byte selector used by Solidity to identify a function /* getSelector(fragment: ErrorFragment | FunctionFragment): string { if (typeof(fragment) === "string") { const matches: Array = [ ]; try { matches.push(this.getFunction(fragment)); } catch (error) { } try { matches.push(this.getError(fragment)); } catch (_) { } if (matches.length === 0) { logger.throwArgumentError("unknown fragment", "key", fragment); } else if (matches.length > 1) { logger.throwArgumentError("ambiguous fragment matches function and error", "key", fragment); } fragment = matches[0]; } return dataSlice(id(fragment.format()), 0, 4); } */ // Get the 32-byte topic hash used by Solidity to identify an event /* getEventTopic(fragment: EventFragment): string { //if (typeof(fragment) === "string") { fragment = this.getEvent(eventFragment); } return id(fragment.format()); } */ _decodeParams(params: ReadonlyArray, data: BytesLike): Result { return this.#abiCoder.decode(params as any, data); } _encodeParams(params: ReadonlyArray, values: ReadonlyArray): string { return this.#abiCoder.encode(params as any, values); } /** * Encodes a ``tx.data`` object for deploying the Contract with * the %%values%% as the constructor arguments. */ encodeDeploy(values?: ReadonlyArray): string { return this._encodeParams(this.deploy.inputs, values || []); } /** * Decodes the result %%data%% (e.g. from an ``eth_call``) for the * specified error (see [[getError]] for valid values for * %%key%%). * * Most developers should prefer the [[parseCallResult]] method instead, * which will automatically detect a ``CALL_EXCEPTION`` and throw the * corresponding error. */ decodeErrorResult(fragment: ErrorFragment | string, data: BytesLike): Result { if (typeof fragment === 'string') { const f = this.getError(fragment); assertArgument(f, 'unknown error', 'fragment', fragment); fragment = f; } assertArgument( dataSlice(data, 0, 4) === fragment.selector, `data signature does not match error ${fragment.name}.`, 'data', data ); return this._decodeParams(fragment.inputs, dataSlice(data, 4)); } /** * Encodes the transaction revert data for a call result that * reverted from the the Contract with the sepcified %%error%% * (see [[getError]] for valid values for %%fragment%%) with the %%values%%. * * This is generally not used by most developers, unless trying to mock * a result from a Contract. */ encodeErrorResult(fragment: ErrorFragment | string, values?: ReadonlyArray): string { if (typeof fragment === 'string') { const f = this.getError(fragment); assertArgument(f, 'unknown error', 'fragment', fragment); fragment = f; } return concat([fragment.selector, this._encodeParams(fragment.inputs, values || [])]); } /** * Decodes the %%data%% from a transaction ``tx.data`` for * the function specified (see [[getFunction]] for valid values * for %%fragment%%). * * Most developers should prefer the [[parseTransaction]] method * instead, which will automatically detect the fragment. */ decodeFunctionData(fragment: FunctionFragment | string, data: BytesLike): Result { if (typeof fragment === 'string') { const f = this.getFunction(fragment); assertArgument(f, 'unknown function', 'fragment', fragment); fragment = f; } assertArgument( dataSlice(data, 0, 4) === fragment.selector, `data signature does not match function ${fragment.name}.`, 'data', data ); return this._decodeParams(fragment.inputs, dataSlice(data, 4)); } /** * Encodes the ``tx.data`` for a transaction that calls the function * specified (see [[getFunction]] for valid values for %%fragment%%) with * the %%values%%. */ encodeFunctionData(fragment: FunctionFragment | string, values?: ReadonlyArray): string { if (typeof fragment === 'string') { const f = this.getFunction(fragment); assertArgument(f, 'unknown function', 'fragment', fragment); fragment = f; } return concat([fragment.selector, this._encodeParams(fragment.inputs, values || [])]); } /** * Decodes the result %%data%% (e.g. from an ``eth_call``) for the * specified function (see [[getFunction]] for valid values for * %%key%%). * * Most developers should prefer the [[parseCallResult]] method instead, * which will automatically detect a ``CALL_EXCEPTION`` and throw the * corresponding error. */ decodeFunctionResult(fragment: FunctionFragment | string, data: BytesLike): Result { if (typeof fragment === 'string') { const f = this.getFunction(fragment); assertArgument(f, 'unknown function', 'fragment', fragment); fragment = f; } let message = 'invalid length for result data'; const bytes = getBytesCopy(data); if (bytes.length % 32 === 0) { try { return this.#abiCoder.decode(fragment.outputs as any, bytes); } catch (error) { message = 'could not decode result data'; } } // Call returned data with no error, but the data is junk assert(false, message, 'BAD_DATA', { value: hexlify(bytes), info: { method: fragment.name, signature: fragment.format() }, }); } makeError(_data: BytesLike, tx: CallExceptionTransaction): CallExceptionError { const data = getBytes(_data, 'data'); const error = AbiCoder.getBuiltinCallException('call', tx, data); // Not a built-in error; try finding a custom error const customPrefix = 'execution reverted (unknown custom error)'; if (error.message.startsWith(customPrefix)) { const selector = hexlify(data.slice(0, 4)); const ef = this.getError(selector); if (ef) { try { const args = this.#abiCoder.decode(ef.inputs as any, data.slice(4)); error.revert = { name: ef.name, signature: ef.format(), args, }; error.reason = error.revert.signature; error.message = `execution reverted: ${error.reason}`; } catch (e) { error.message = `execution reverted (coult not decode custom error)`; } } } // Add the invocation, if available const parsed = this.parseTransaction(tx); if (parsed) { error.invocation = { method: parsed.name, signature: parsed.signature, args: parsed.args, }; } return error; } /** * Encodes the result data (e.g. from an ``eth_call``) for the * specified function (see [[getFunction]] for valid values * for %%fragment%%) with %%values%%. * * This is generally not used by most developers, unless trying to mock * a result from a Contract. */ encodeFunctionResult(fragment: FunctionFragment | string, values?: ReadonlyArray): string { if (typeof fragment === 'string') { const f = this.getFunction(fragment); assertArgument(f, 'unknown function', 'fragment', fragment); fragment = f; } return hexlify(this.#abiCoder.encode(fragment.outputs as any, values || [])); } /* spelunk(inputs: Array, values: ReadonlyArray, processfunc: (type: string, value: any) => Promise): Promise> { const promises: Array> = [ ]; const process = function(type: ParamType, value: any): any { if (type.baseType === "array") { return descend(type.child } if (type. === "address") { } }; const descend = function (inputs: Array, values: ReadonlyArray) { if (inputs.length !== values.length) { throw new Error("length mismatch"); } }; const result: Array = [ ]; values.forEach((value, index) => { if (value == null) { topics.push(null); } else if (param.baseType === "array" || param.baseType === "tuple") { logger.throwArgumentError("filtering with tuples or arrays not supported", ("contract." + param.name), value); } else if (Array.isArray(value)) { topics.push(value.map((value) => encodeTopic(param, value))); } else { topics.push(encodeTopic(param, value)); } }); } */ // Create the filter for the event with search criteria (e.g. for eth_filterLog) encodeFilterTopics(fragment: EventFragment | string, values: ReadonlyArray): Array> { if (typeof fragment === 'string') { const f = this.getEvent(fragment); assertArgument(f, 'unknown event', 'eventFragment', fragment); fragment = f; } assert(values.length <= fragment.inputs.length, `too many arguments for ${fragment.format()}`, 'UNEXPECTED_ARGUMENT', { count: values.length, expectedCount: fragment.inputs.length, }); const topics: Array> = []; if (!fragment.anonymous) { topics.push(fragment.topicHash); } // @TODO: Use the coders for this; to properly support tuples, etc. const encodeTopic = (param: ParamType, value: any): string => { if (param.type === 'string') { return id(value); } else if (param.type === 'bytes') { return keccak256(hexlify(value)); } if (param.type === 'bool' && typeof value === 'boolean') { value = value ? '0x01' : '0x00'; } if (param.type.match(/^u?int/)) { value = toBeHex(value); } // Check addresses are valid if (param.type === 'address') { this.#abiCoder.encode(['address'], [value]); } return zeroPadValue(hexlify(value), 32); //@TOOD should probably be return toHex(value, 32) }; values.forEach((value, index) => { const param = (fragment).inputs[index]; if (!param.indexed) { assertArgument( value == null, 'cannot filter non-indexed parameters; must be null', 'contract.' + param.name, value ); return; } if (value == null) { topics.push(null); } else if (param.baseType === 'array' || param.baseType === 'tuple') { assertArgument(false, 'filtering with tuples or arrays not supported', 'contract.' + param.name, value); } else if (Array.isArray(value)) { topics.push(value.map((value) => encodeTopic(param, value))); } else { topics.push(encodeTopic(param, value)); } }); // Trim off trailing nulls while (topics.length && topics[topics.length - 1] === null) { topics.pop(); } return topics; } encodeEventLog(fragment: EventFragment | string, values: ReadonlyArray): { data: string; topics: Array } { if (typeof fragment === 'string') { const f = this.getEvent(fragment); assertArgument(f, 'unknown event', 'eventFragment', fragment); fragment = f; } const topics: Array = []; const dataTypes: Array = []; const dataValues: Array = []; if (!fragment.anonymous) { topics.push(fragment.topicHash); } assertArgument(values.length === fragment.inputs.length, 'event arguments/values mismatch', 'values', values); fragment.inputs.forEach((param, index) => { const value = values[index]; if (param.indexed) { if (param.type === 'string') { topics.push(id(value)); } else if (param.type === 'bytes') { topics.push(keccak256(value)); } else if (param.baseType === 'tuple' || param.baseType === 'array') { // @TODO throw new Error('not implemented'); } else { topics.push(this.#abiCoder.encode([param.type], [value])); } } else { dataTypes.push(param); dataValues.push(value); } }); return { data: this.#abiCoder.encode(dataTypes as any, dataValues), topics: topics, }; } // Decode a filter for the event and the search criteria decodeEventLog(fragment: EventFragment | string, data: BytesLike, topics?: ReadonlyArray): Result { if (typeof fragment === 'string') { const f = this.getEvent(fragment); assertArgument(f, 'unknown event', 'eventFragment', fragment); fragment = f; } if (topics != null && !fragment.anonymous) { const eventTopic = fragment.topicHash; assertArgument( isHexString(topics[0], 32) && topics[0].toLowerCase() === eventTopic, 'fragment/topic mismatch', 'topics[0]', topics[0] ); topics = topics.slice(1); } const indexed: Array = []; const nonIndexed: Array = []; const dynamic: Array = []; fragment.inputs.forEach((param, index) => { if (param.indexed) { if ( param.type === 'string' || param.type === 'bytes' || param.baseType === 'tuple' || param.baseType === 'array' ) { indexed.push(ParamType.from({ type: 'bytes32', name: param.name })); dynamic.push(true); } else { indexed.push(param); dynamic.push(false); } } else { nonIndexed.push(param); dynamic.push(false); } }); const resultIndexed = topics != null ? this.#abiCoder.decode(indexed as any, concat(topics)) : null; const resultNonIndexed = this.#abiCoder.decode(nonIndexed as any, data, true); //const result: (Array & { [ key: string ]: any }) = [ ]; const values: Array = []; const keys: Array = []; let nonIndexedIndex = 0, indexedIndex = 0; fragment.inputs.forEach((param, index) => { let value: Indexed | null | unknown = null; if (param.indexed) { if (resultIndexed == null) { value = new Indexed(null); } else if (dynamic[index]) { value = new Indexed(resultIndexed[indexedIndex++]); } else { try { value = resultIndexed[indexedIndex++]; } catch (error) { value = error; } } } else { try { value = resultNonIndexed[nonIndexedIndex++]; } catch (error) { value = error; } } values.push(value); keys.push(param.name || null); }); return Result.fromItems(values, keys); } /** * Parses a transaction, finding the matching function and extracts * the parameter values along with other useful function details. * * If the matching function cannot be found, return null. */ parseTransaction(tx: { data: string; value?: BigNumberish }): null | TransactionDescription { const data = getBytes(tx.data, 'tx.data'); const value = getBigInt(tx.value != null ? tx.value : 0, 'tx.value'); const fragment = this.getFunction(hexlify(data.slice(0, 4))); if (!fragment) { return null; } const args = this.#abiCoder.decode(fragment.inputs as any, data.slice(4)); return new TransactionDescription(fragment, fragment.selector, args, value); } parseCallResult(data: BytesLike): Result { throw new Error('@TODO'); } /** * Parses a receipt log, finding the matching event and extracts * the parameter values along with other useful event details. * * If the matching event cannot be found, returns null. */ parseLog(log: { topics: Array; data: string }): null | LogDescription { const fragment = this.getEvent(log.topics[0]); if (!fragment || fragment.anonymous) { return null; } // @TODO: If anonymous, and the only method, and the input count matches, should we parse? // Probably not, because just because it is the only event in the ABI does // not mean we have the full ABI; maybe just a fragment? return new LogDescription(fragment, fragment.topicHash, this.decodeEventLog(fragment, log.data, log.topics)); } /** * Parses a revert data, finding the matching error and extracts * the parameter values along with other useful error details. * * If the matching event cannot be found, returns null. */ parseError(data: BytesLike): null | ErrorDescription { const hexData = hexlify(data); const fragment = this.getError(dataSlice(hexData, 0, 4)); if (!fragment) { return null; } const args = this.#abiCoder.decode(fragment.inputs as any, dataSlice(hexData, 4)); return new ErrorDescription(fragment, fragment.selector, args); } /** * Creates a new [[Interface]] from the ABI %%value%%. * * The %%value%% may be provided as an existing [[Interface]] object, * a JSON-encoded ABI or any Human-Readable ABI format. */ static from(value: InterfaceAbi | Interface): Interface { // Already an Interface, which is immutable if (value instanceof Interface) { return value; } // JSON if (typeof value === 'string') { return new Interface(JSON.parse(value)); } // Maybe an interface from an older version, or from a symlinked copy if (typeof (value).format === 'function') { return new Interface((value).format('json')); } // Array of fragments return new Interface(value); } }