Interface returns null instead of throws on missing fragment.

This commit is contained in:
Richard Moore 2023-01-27 17:27:29 -05:00
parent c2a2edc9d8
commit 32f605eb7b
2 changed files with 124 additions and 66 deletions

@ -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<any | Typed>, forceUnique: boolean): FunctionFragment {
#getFunction(key: string, values: null | Array<any | Typed>, 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<any | Typed>): FunctionFragment {
return this.#getFunction(key, values || null, true)
getFunction(key: string, values?: Array<any | Typed>): 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<null | any | Typed>, forceUnique: boolean): EventFragment {
#getEvent(key: string, values: null | Array<null | any | Typed>, 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<any | Typed>): EventFragment {
getEvent(key: string, values?: Array<any | Typed>): 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<any | Typed>): ErrorFragment {
getError(key: string, values?: Array<any | Typed>): 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<any>): string {
const fragment = (typeof(key) === "string") ? this.getError(key): key;
encodeErrorResult(fragment: ErrorFragment | string, values?: ReadonlyArray<any>): 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<any>): string {
const fragment = (typeof(key) === "string") ? this.getFunction(key): key;
encodeFunctionData(fragment: FunctionFragment | string, values?: ReadonlyArray<any>): 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);
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<any>): string {
const fragment = (typeof(key) === "string") ? this.getFunction(key): key;
encodeFunctionResult(fragment: FunctionFragment | string, values?: ReadonlyArray<any>): 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<any>): Array<null | string | Array<string>> {
if (typeof(eventFragment) === "string") {
eventFragment = this.getEvent(eventFragment);
encodeFilterTopics(fragment: EventFragment | string, values: ReadonlyArray<any>): Array<null | string | Array<string>> {
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<null | string | Array<string>> = [];
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>eventFragment).inputs[index];
const param = (<EventFragment>fragment).inputs[index];
if (!param.indexed) {
assertArgument(value == null,
@ -861,9 +892,11 @@ export class Interface {
return topics;
}
encodeEventLog(eventFragment: EventFragment | string, values: ReadonlyArray<any>): { data: string, topics: Array<string> } {
if (typeof(eventFragment) === "string") {
eventFragment = this.getEvent(eventFragment);
encodeEventLog(fragment: EventFragment | string, values: ReadonlyArray<any>): { data: string, topics: Array<string> } {
if (typeof(fragment) === "string") {
const f = this.getEvent(fragment);
assertArgument(f, "unknown event", "eventFragment", fragment);
fragment = f;
}
const topics: Array<string> = [ ];
@ -871,14 +904,14 @@ export class Interface {
const dataTypes: Array<ParamType> = [ ];
const dataValues: Array<string> = [ ];
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<string>): Result {
if (typeof(eventFragment) === "string") {
eventFragment = this.getEvent(eventFragment);
decodeEventLog(fragment: EventFragment | string, data: BytesLike, topics?: ReadonlyArray<string>): 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<ParamType> = [];
const dynamic: Array<boolean> = [];
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<any> = [ ];
const keys: Array<null | string> = [ ];
let nonIndexedIndex = 0, indexedIndex = 0;
eventFragment.inputs.forEach((param, index) => {
fragment.inputs.forEach((param, index) => {
let value = null;
if (param.indexed) {
if (resultIndexed == null) {

@ -195,11 +195,19 @@ class WrappedMethod<A extends Array<any> = Array<any>, 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<A>): 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<A>): Promise<ContractTransaction> {
@ -299,11 +307,23 @@ class WrappedEvent<A extends Array<any> = Array<any>> 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<A>): 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 ];
}