From 32f605eb7bde81a774427b297d02d5d4e6ee92cb Mon Sep 17 00:00:00 2001 From: Richard Moore Date: Fri, 27 Jan 2023 17:27:29 -0500 Subject: [PATCH] Interface returns null instead of throws on missing fragment. --- src.ts/abi/interface.ts | 157 ++++++++++++++++++++++-------------- src.ts/contract/contract.ts | 33 ++++++-- 2 files changed, 124 insertions(+), 66 deletions(-) diff --git a/src.ts/abi/interface.ts b/src.ts/abi/interface.ts index 873ef4a2a..5ec9708e7 100644 --- a/src.ts/abi/interface.ts +++ b/src.ts/abi/interface.ts @@ -292,7 +292,7 @@ export class Interface { } // Find a function definition by any means necessary (unless it is ambiguous) - #getFunction(key: string, values: null | Array, forceUnique: boolean): FunctionFragment { + #getFunction(key: string, values: null | Array, forceUnique: boolean): null | FunctionFragment { // Selector if (isHexString(key)) { @@ -300,7 +300,7 @@ export class Interface { for (const fragment of this.#functions.values()) { if (selector === fragment.selector) { return fragment; } } - assertArgument(false, "no matching function", "selector", key); + return null; } // It is a bare name, look up the function (will return null if ambiguous) @@ -361,11 +361,11 @@ export class Interface { } } - assertArgument(matching.length !== 0, "no matching function", "name", key); + if (matching.length === 0) { return null; } if (matching.length > 1 && forceUnique) { const matchStr = matching.map((m) => JSON.stringify(m.format())).join(", "); - assertArgument(false, `multiple matching functions (i.e. ${ matchStr })`, "name", key); + assertArgument(false, `ambiguous function description (i.e. matches ${ matchStr })`, "key", key); } return matching[0]; @@ -375,7 +375,7 @@ export class Interface { const result = this.#functions.get(FunctionFragment.from(key).format()); if (result) { return result; } - assertArgument(false, "no matching function", "signature", key); + return null; } /** @@ -383,7 +383,9 @@ export class Interface { * function name or function signature that belongs to the ABI. */ getFunctionName(key: string): string { - return (this.#getFunction(key, null, false)).name; + const fragment = this.#getFunction(key, null, false); + assertArgument(fragment, "no matching function", "key", key); + return fragment.name; } /** @@ -396,8 +398,8 @@ export class Interface { * If the %%key%% and %%values%% do not refine to a single function in * the ABI, this will throw. */ - getFunction(key: string, values?: Array): FunctionFragment { - return this.#getFunction(key, values || null, true) + getFunction(key: string, values?: Array): null | FunctionFragment { + return this.#getFunction(key, values || null, true); } /** @@ -414,7 +416,7 @@ export class Interface { // Find an event definition by any means necessary (unless it is ambiguous) - #getEvent(key: string, values: null | Array, forceUnique: boolean): EventFragment { + #getEvent(key: string, values: null | Array, forceUnique: boolean): null | EventFragment { // EventTopic if (isHexString(key)) { @@ -422,7 +424,7 @@ export class Interface { for (const fragment of this.#events.values()) { if (eventTopic === fragment.topicHash) { return fragment; } } - assertArgument(false, "no matching event", "eventTopic", key); + return null; } // It is a bare name, look up the function (will return null if ambiguous) @@ -456,10 +458,11 @@ export class Interface { } } - assertArgument(matching.length > 0, "no matching event", "name", key); + if (matching.length === 0) { return null; } + if (matching.length > 1 && forceUnique) { - // @TODO: refine by Typed - assertArgument(false, "multiple matching events", "name", key); + const matchStr = matching.map((m) => JSON.stringify(m.format())).join(", "); + assertArgument(false, `ambiguous event description (i.e. matches ${ matchStr })`, "key", key); } return matching[0]; @@ -469,7 +472,7 @@ export class Interface { const result = this.#events.get(EventFragment.from(key).format()); if (result) { return result; } - assertArgument(false, "no matching event", "signature", key); + return null; } /** @@ -477,7 +480,10 @@ export class Interface { * event name or event signature that belongs to the ABI. */ getEventName(key: string): string { - return (this.#getEvent(key, null, false)).name; + const fragment = this.#getEvent(key, null, false); + assertArgument(fragment, "no matching event", "key", key); + + return fragment.name; } /** @@ -490,7 +496,7 @@ export class Interface { * If the %%key%% and %%values%% do not refine to a single event in * the ABI, this will throw. */ - getEvent(key: string, values?: Array): EventFragment { + getEvent(key: string, values?: Array): null | EventFragment { return this.#getEvent(key, values || null, true) } @@ -516,7 +522,7 @@ export class Interface { * If the %%key%% and %%values%% do not refine to a single error in * the ABI, this will throw. */ - getError(key: string, values?: Array): ErrorFragment { + getError(key: string, values?: Array): null | ErrorFragment { if (isHexString(key)) { const selector = key.toLowerCase(); @@ -527,7 +533,8 @@ export class Interface { for (const fragment of this.#errors.values()) { if (selector === fragment.selector) { return fragment; } } - assertArgument(false, "no matching error", "selector", key); + + return null; } // It is a bare name, look up the function (will return null if ambiguous) @@ -540,10 +547,10 @@ export class Interface { if (matching.length === 0) { if (key === "Error") { return ErrorFragment.from("error Error(string)"); } if (key === "Panic") { return ErrorFragment.from("error Panic(uint256)"); } - assertArgument(false, "no matching error", "name", key); + return null; } else if (matching.length > 1) { - // @TODO: refine by Typed - assertArgument(false, "multiple matching errors", "name", key); + const matchStr = matching.map((m) => JSON.stringify(m.format())).join(", "); + assertArgument(false, `ambiguous error description (i.e. ${ matchStr })`, "name", key); } return matching[0]; @@ -557,7 +564,7 @@ export class Interface { const result = this.#errors.get(key); if (result) { return result; } - assertArgument(false, "no matching error", "signature", key); + return null; } /** @@ -629,7 +636,11 @@ export class Interface { * corresponding error. */ decodeErrorResult(fragment: ErrorFragment | string, data: BytesLike): Result { - if (typeof(fragment) === "string") { fragment = this.getError(fragment); } + 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); @@ -640,13 +651,17 @@ export class Interface { /** * 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 %%key%%) with the %%values%%. + * (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(key: ErrorFragment | string, values?: ReadonlyArray): string { - const fragment = (typeof(key) === "string") ? this.getError(key): key; + 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, @@ -657,13 +672,17 @@ export class Interface { /** * Decodes the %%data%% from a transaction ``tx.data`` for * the function specified (see [[getFunction]] for valid values - * for %%key%%). + * for %%fragment%%). * * Most developers should prefer the [[parseTransaction]] method * instead, which will automatically detect the fragment. */ - decodeFunctionData(key: FunctionFragment | string, data: BytesLike): Result { - const fragment = (typeof(key) === "string") ? this.getFunction(key): key; + 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); @@ -673,11 +692,15 @@ export class Interface { /** * Encodes the ``tx.data`` for a transaction that calls the function - * specified (see [[getFunction]] for valid values for %%key%%) with + * specified (see [[getFunction]] for valid values for %%fragment%%) with * the %%values%%. */ - encodeFunctionData(key: FunctionFragment | string, values?: ReadonlyArray): string { - const fragment = (typeof(key) === "string") ? this.getFunction(key): key; + 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, @@ -695,7 +718,11 @@ export class Interface { * corresponding error. */ decodeFunctionResult(fragment: FunctionFragment | string, data: BytesLike): Result { - if (typeof(fragment) === "string") { fragment = this.getFunction(fragment); } + if (typeof(fragment) === "string") { + const f = this.getFunction(fragment); + assertArgument(f, "unknown function", "fragment", fragment); + fragment = f; + } let message = "invalid length for result data"; @@ -725,8 +752,8 @@ export class Interface { const selector = hexlify(data.slice(0, 4)); error.message = "execution reverted (unknown custom error)"; - try { - const ef = this.getError(selector); + const ef = this.getError(selector); + if (ef) { try { error.revert = { name: ef.name, @@ -738,8 +765,6 @@ export class Interface { } catch (e) { error.message = `execution reverted (coult not decode custom error)` } - } catch (error) { - console.log(error); // @TODO: remove } } @@ -759,13 +784,17 @@ export class Interface { /** * Encodes the result data (e.g. from an ``eth_call``) for the * specified function (see [[getFunction]] for valid values - * for %%key%%) with %%values%%. + * for %%fragment%%) with %%values%%. * * This is generally not used by most developers, unless trying to mock * a result from a Contract. */ - encodeFunctionResult(key: FunctionFragment | string, values?: ReadonlyArray): string { - const fragment = (typeof(key) === "string") ? this.getFunction(key): key; + 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, values || [ ])); } /* @@ -799,16 +828,18 @@ export class Interface { } */ // Create the filter for the event with search criteria (e.g. for eth_filterLog) - encodeFilterTopics(eventFragment: EventFragment | string, values: ReadonlyArray): Array> { - if (typeof(eventFragment) === "string") { - eventFragment = this.getEvent(eventFragment); + 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 <= eventFragment.inputs.length, `too many arguments for ${ eventFragment.format() }`, - "UNEXPECTED_ARGUMENT", { count: values.length, expectedCount: eventFragment.inputs.length }) + 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 (!eventFragment.anonymous) { topics.push(eventFragment.topicHash); } + 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 => { @@ -834,7 +865,7 @@ export class Interface { values.forEach((value, index) => { - const param = (eventFragment).inputs[index]; + const param = (fragment).inputs[index]; if (!param.indexed) { assertArgument(value == null, @@ -861,9 +892,11 @@ export class Interface { return topics; } - encodeEventLog(eventFragment: EventFragment | string, values: ReadonlyArray): { data: string, topics: Array } { - if (typeof(eventFragment) === "string") { - eventFragment = this.getEvent(eventFragment); + 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 = [ ]; @@ -871,14 +904,14 @@ export class Interface { const dataTypes: Array = [ ]; const dataValues: Array = [ ]; - if (!eventFragment.anonymous) { - topics.push(eventFragment.topicHash); + if (!fragment.anonymous) { + topics.push(fragment.topicHash); } - assertArgument(values.length === eventFragment.inputs.length, + assertArgument(values.length === fragment.inputs.length, "event arguments/values mismatch", "values", values); - eventFragment.inputs.forEach((param, index) => { + fragment.inputs.forEach((param, index) => { const value = values[index]; if (param.indexed) { if (param.type === "string") { @@ -904,13 +937,15 @@ export class Interface { } // Decode a filter for the event and the search criteria - decodeEventLog(eventFragment: EventFragment | string, data: BytesLike, topics?: ReadonlyArray): Result { - if (typeof(eventFragment) === "string") { - eventFragment = this.getEvent(eventFragment); + 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 && !eventFragment.anonymous) { - const eventTopic = eventFragment.topicHash; + 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); @@ -920,7 +955,7 @@ export class Interface { const nonIndexed: Array = []; const dynamic: Array = []; - eventFragment.inputs.forEach((param, index) => { + 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 })); @@ -942,7 +977,7 @@ export class Interface { const values: Array = [ ]; const keys: Array = [ ]; let nonIndexedIndex = 0, indexedIndex = 0; - eventFragment.inputs.forEach((param, index) => { + fragment.inputs.forEach((param, index) => { let value = null; if (param.indexed) { if (resultIndexed == null) { diff --git a/src.ts/contract/contract.ts b/src.ts/contract/contract.ts index fc7eeeec4..d7e402c93 100644 --- a/src.ts/contract/contract.ts +++ b/src.ts/contract/contract.ts @@ -195,11 +195,19 @@ class WrappedMethod = Array, R = any, D extends R | Co // Only works on non-ambiguous keys (refined fragment is always non-ambiguous) get fragment(): FunctionFragment { - return this._contract.interface.getFunction(this._key); + const fragment = this._contract.interface.getFunction(this._key); + assert(fragment, "no matching fragment", "UNSUPPORTED_OPERATION", { + operation: "fragment" + }); + return fragment; } getFragment(...args: ContractMethodArgs): FunctionFragment { - return this._contract.interface.getFunction(this._key, args); + const fragment = this._contract.interface.getFunction(this._key, args); + assert(fragment, "no matching fragment", "UNSUPPORTED_OPERATION", { + operation: "fragment" + }); + return fragment; } async populateTransaction(...args: ContractMethodArgs): Promise { @@ -299,11 +307,23 @@ class WrappedEvent = Array> extends _WrappedEventBase( // Only works on non-ambiguous keys get fragment(): EventFragment { - return this._contract.interface.getEvent(this._key); + const fragment = this._contract.interface.getEvent(this._key); + + assert(fragment, "no matching fragment", "UNSUPPORTED_OPERATION", { + operation: "fragment" + }); + + return fragment; } getFragment(...args: ContractEventArgs): EventFragment { - return this._contract.interface.getEvent(this._key, args); + const fragment = this._contract.interface.getEvent(this._key, args); + + assert(fragment, "no matching fragment", "UNSUPPORTED_OPERATION", { + operation: "fragment" + }); + + return fragment; } }; @@ -355,7 +375,9 @@ async function getSubInfo(contract: BaseContract, event: ContractEventName): Pro if (Array.isArray(event)) { const topicHashify = function(name: string): string { if (isHexString(name, 32)) { return name; } - return contract.interface.getEvent(name).topicHash; + const fragment = contract.interface.getEvent(name); + assertArgument(fragment, "unknown fragment", "name", name); + return fragment.topicHash; } // Array of Topics and Names; e.g. `[ "0x1234...89ab", "Transfer(address)" ]` @@ -375,6 +397,7 @@ async function getSubInfo(contract: BaseContract, event: ContractEventName): Pro } else { // Name or Signature; e.g. `"Transfer", `"Transfer(address)"` fragment = contract.interface.getEvent(event); + assertArgument(fragment, "unknown fragment", "event", event); topics = [ fragment.topicHash ]; }