From 992c7eef074f8aac4d761dbce70b0842aeb55726 Mon Sep 17 00:00:00 2001 From: Richard Moore Date: Sat, 1 Oct 2022 01:17:15 -0400 Subject: [PATCH] Simplified Fragment API --- src.ts/abi/fragments.ts | 376 +++++++++++++++++++++------------------- src.ts/abi/interface.ts | 24 +-- 2 files changed, 211 insertions(+), 189 deletions(-) diff --git a/src.ts/abi/fragments.ts b/src.ts/abi/fragments.ts index 54a146463..a26c8a8b1 100644 --- a/src.ts/abi/fragments.ts +++ b/src.ts/abi/fragments.ts @@ -376,7 +376,7 @@ function consumeMutability(tokens: TokenString): string { // ...a parameter list, returning the ParamType list function consumeParams(tokens: TokenString, allowIndexed?: boolean): Array { - return tokens.popParams().map((t) => ParamType.fromTokens(t, allowIndexed)); + return tokens.popParams().map((t) => ParamType.from(t, allowIndexed)); } // ...a gas limit, returning a BigNumber or null if none @@ -433,7 +433,13 @@ export type FragmentWalkFunc = (type: string, value: any) => any; export type FragmentWalkAsyncFunc = (type: string, value: any) => any | Promise; const internal = Symbol.for("_ethers_internal"); + const ParamTypeInternal = "_ParamTypeInternal"; +const ErrorFragmentInternal = "_ErrorInternal"; +const EventFragmentInternal = "_EventInternal"; +const ConstructorFragmentInternal = "_ConstructorInternal"; +const FunctionFragmentInternal = "_FunctionInternal"; +const StructFragmentInternal = "_StructInternal"; export class ParamType { @@ -640,8 +646,51 @@ export class ParamType { static from(obj: any, allowIndexed?: boolean): ParamType { if (ParamType.isParamType(obj)) { return obj; } - if (typeof(obj) === "string") { return ParamType.fromTokens(lex(obj), allowIndexed); } - if (obj instanceof TokenString) { return ParamType.fromTokens(obj, allowIndexed); } + + if (typeof(obj) === "string") { + return ParamType.from(lex(obj), allowIndexed); + + } else if (obj instanceof TokenString) { + let type = "", baseType = ""; + let comps: null | Array = null; + + if (consumeKeywords(obj, setify([ "tuple" ])).has("tuple") || obj.peekType("OPEN_PAREN")) { + // Tuple + baseType = "tuple"; + comps = obj.popParams().map((t) => ParamType.from(t)); + type = `tuple(${ comps.map((c) => c.format()).join(",") })`; + } else { + // Normal + type = verifyBasicType(obj.popType("TYPE")); + baseType = type; + } + + // Check for Array + let arrayChildren: null | ParamType = null; + let arrayLength: null | number = null; + + while (obj.length && obj.peekType("BRACKET")) { + const bracket = obj.pop(); //arrays[i]; + arrayChildren = new ParamType(_guard, "", type, baseType, null, comps, arrayLength, arrayChildren); + arrayLength = bracket.value; + type += bracket.text; + baseType = "array"; + comps = null; + } + + let indexed = null; + const keywords = consumeKeywords(obj, KwModifiers); + if (keywords.has("indexed")) { + if (!allowIndexed) { throw new Error(""); } + indexed = true; + } + + const name = (obj.peekType("ID") ? obj.pop().text: ""); + + if (obj.length) { throw new Error("leftover tokens"); } + + return new ParamType(_guard, name, type, baseType, indexed, comps, arrayLength, arrayChildren); + } const name = obj.name; if (name && (typeof(name) !== "string" || !name.match(regexIdentifier))) { @@ -681,52 +730,6 @@ export class ParamType { return new ParamType(_guard, name, type, type, indexed, null, null, null); } - static fromObject(obj: any, allowIndexed?: boolean): ParamType { - throw new Error("@TODO"); - } - - static fromTokens(tokens: TokenString, allowIndexed?: boolean): ParamType { - let type = "", baseType = ""; - let comps: null | Array = null; - - if (consumeKeywords(tokens, setify([ "tuple" ])).has("tuple") || tokens.peekType("OPEN_PAREN")) { - // Tuple - baseType = "tuple"; - comps = tokens.popParams().map((t) => ParamType.from(t)); - type = `tuple(${ comps.map((c) => c.format()).join(",") })`; - } else { - // Normal - type = verifyBasicType(tokens.popType("TYPE")); - baseType = type; - } - - // Check for Array - let arrayChildren: null | ParamType = null; - let arrayLength: null | number = null; - - while (tokens.length && tokens.peekType("BRACKET")) { - const bracket = tokens.pop(); //arrays[i]; - arrayChildren = new ParamType(_guard, "", type, baseType, null, comps, arrayLength, arrayChildren); - arrayLength = bracket.value; - type += bracket.text; - baseType = "array"; - comps = null; - } - - let indexed = null; - const keywords = consumeKeywords(tokens, KwModifiers); - if (keywords.has("indexed")) { - if (!allowIndexed) { throw new Error(""); } - indexed = true; - } - - const name = (tokens.peekType("ID") ? tokens.pop().text: ""); - - if (tokens.length) { throw new Error("leftover tokens"); } - - return new ParamType(_guard, name, type, baseType, indexed, comps, arrayLength, arrayChildren); - } - static isParamType(value: any): value is ParamType { return (value && value[internal] === ParamTypeInternal); } @@ -747,82 +750,60 @@ export abstract class Fragment { abstract format(format?: FormatType): string; static from(obj: any): Fragment { - if (typeof(obj) === "string") { return this.fromString(obj); } - if (obj instanceof TokenString) { return this.fromTokens(obj); } - if (typeof(obj) === "object") { return this.fromObject(obj); } + if (typeof(obj) === "string") { + try { + Fragment.from(JSON.parse(obj)); + } catch (e) { } + + return Fragment.from(lex(obj)); + } + + if (obj instanceof TokenString) { + const type = obj.popKeyword(KwTypes); + + switch (type) { + case "constructor": return ConstructorFragment.from(obj); + case "error": return ErrorFragment.from(obj); + case "event": return EventFragment.from(obj); + case "function": return FunctionFragment.from(obj); + case "struct": return StructFragment.from(obj); + } + + throw new Error(`unsupported type: ${ type }`); + } + + if (typeof(obj) === "object") { + switch (obj.type) { + case "constructor": return ConstructorFragment.from(obj); + case "error": return ErrorFragment.from(obj); + case "event": return EventFragment.from(obj); + case "function": return FunctionFragment.from(obj); + case "struct": return StructFragment.from(obj); + } + throw new Error(`not implemented yet: ${ obj.type }`); + } + throw new Error(`unsupported type: ${ obj }`); } - static fromObject(obj: any): Fragment { - switch (obj.type) { - case "constructor": return ConstructorFragment.fromObject(obj); - case "error": return ErrorFragment.fromObject(obj); - case "event": return EventFragment.fromObject(obj); - case "function": return FunctionFragment.fromObject(obj); - case "struct": return StructFragment.fromObject(obj); - } - throw new Error(`not implemented yet: ${ obj.type }`); - } - - static fromString(text: string): Fragment { - try { - Fragment.from(JSON.parse(text)); - } catch (e) { } - - return Fragment.fromTokens(lex(text)); - } - - static fromTokens(tokens: TokenString): Fragment { - const type = tokens.popKeyword(KwTypes); - - switch (type) { - case "constructor": return ConstructorFragment.fromTokens(tokens); - case "error": return ErrorFragment.fromTokens(tokens); - case "event": return EventFragment.fromTokens(tokens); - case "function": return FunctionFragment.fromTokens(tokens); - case "struct": return StructFragment.fromTokens(tokens); - } - - throw new Error(`unsupported type: ${ type }`); - } - - /* - static fromTokens(tokens: TokenString): Fragment { - const assertDone = () => { - if (tokens.length) { throw new Error(`unexpected tokens: ${ tokens.toString() }`); } - }); - - const type = (tokens.length && tokens.peek().type === "KEYWORD") ? tokens.peek().text: "unknown"; - - const name = consumeName("error", tokens); - const inputs = consumeParams(tokens, type === "event"); - - switch (type) { - case "event": case "struct": - assertDone(); - } - - } - */ - static isConstructor(value: any): value is ConstructorFragment { - return (value && value.type === "constructor"); + return ConstructorFragment.isFragment(value); } static isError(value: any): value is ErrorFragment { - return (value && value.type === "error"); + return ErrorFragment.isFragment(value); } static isEvent(value: any): value is EventFragment { - return (value && value.type === "event"); + return EventFragment.isFragment(value); } static isFunction(value: any): value is FunctionFragment { - return (value && value.type === "function"); + return FunctionFragment.isFragment(value); } static isStruct(value: any): value is StructFragment { - return (value && value.type === "struct"); + return StructFragment.isFragment(value); } } @@ -831,6 +812,9 @@ export abstract class NamedFragment extends Fragment { constructor(guard: any, type: FragmentType, name: string, inputs: ReadonlyArray) { super(guard, type, inputs); + if (typeof(name) !== "string" || !name.match(regexIdentifier)) { + throwArgumentError("invalid identifier", "name", name); + } inputs = Object.freeze(inputs.slice()); defineProperties(this, { name }); } @@ -843,6 +827,7 @@ function joinParams(format: FormatType, params: ReadonlyArray): strin export class ErrorFragment extends NamedFragment { constructor(guard: any, name: string, inputs: ReadonlyArray) { super(guard, "error", name, inputs); + Object.defineProperty(this, internal, { value: ErrorFragmentInternal }); } get selector(): string { @@ -864,21 +849,26 @@ export class ErrorFragment extends NamedFragment { return result.join(" "); } - static fromObject(obj: any): ErrorFragment { + static from(obj: any): ErrorFragment { + if (ErrorFragment.isFragment(obj)) { return obj; } + + if (typeof(obj) === "string") { + return ErrorFragment.from(lex(obj)); + + } else if (obj instanceof TokenString) { + const name = consumeName("error", obj); + const inputs = consumeParams(obj); + consumeEoi(obj); + + return new ErrorFragment(_guard, name, inputs); + } + return new ErrorFragment(_guard, obj.name, - obj.inputs ? obj.inputs.map(ParamType.fromObject): [ ]); + obj.inputs ? obj.inputs.map(ParamType.from): [ ]); } - static fromString(text: string): ErrorFragment { - return ErrorFragment.fromTokens(lex(text)); - } - - static fromTokens(tokens: TokenString): ErrorFragment { - const name = consumeName("error", tokens); - const inputs = consumeParams(tokens); - consumeEoi(tokens); - - return new ErrorFragment(_guard, name, inputs); + static isFragment(value: any): value is ErrorFragment { + return (value && value[internal] === ErrorFragmentInternal); } } @@ -888,6 +878,7 @@ export class EventFragment extends NamedFragment { constructor(guard: any, name: string, inputs: ReadonlyArray, anonymous: boolean) { super(guard, "event", name, inputs); + Object.defineProperty(this, internal, { value: EventFragmentInternal }); defineProperties(this, { anonymous }); } @@ -912,22 +903,27 @@ export class EventFragment extends NamedFragment { return result.join(" "); } - static fromObject(obj: any): EventFragment { + static from(obj: any): EventFragment { + if (EventFragment.isFragment(obj)) { return obj; } + + if (typeof(obj) === "string") { + return EventFragment.from(lex(obj)); + + } else if (obj instanceof TokenString) { + const name = consumeName("event", obj); + const inputs = consumeParams(obj, true); + const anonymous = !!consumeKeywords(obj, setify([ "anonymous" ])).has("anonymous"); + consumeEoi(obj); + + return new EventFragment(_guard, name, inputs, anonymous); + } + return new EventFragment(_guard, obj.name, - obj.inputs ? obj.inputs.map(ParamType.fromObject): [ ], !!obj.anonymous); + obj.inputs ? obj.inputs.map(ParamType.from): [ ], !!obj.anonymous); } - static fromString(text: string): EventFragment { - return EventFragment.fromTokens(lex(text)); - } - - static fromTokens(tokens: TokenString): EventFragment { - const name = consumeName("event", tokens); - const inputs = consumeParams(tokens, true); - const anonymous = !!consumeKeywords(tokens, setify([ "anonymous" ])).has("anonymous"); - consumeEoi(tokens); - - return new EventFragment(_guard, name, inputs, anonymous); + static isFragment(value: any): value is EventFragment { + return (value && value[internal] === EventFragmentInternal); } } @@ -938,6 +934,7 @@ export class ConstructorFragment extends Fragment { constructor(guard: any, type: FragmentType, inputs: ReadonlyArray, payable: boolean, gas: null | bigint) { super(guard, type, inputs); + Object.defineProperty(this, internal, { value: ConstructorFragmentInternal }); defineProperties(this, { payable, gas }); } @@ -964,24 +961,29 @@ export class ConstructorFragment extends Fragment { return result.join(" "); } - static fromObject(obj: any): ConstructorFragment { + static from(obj: any): ConstructorFragment { + if (ConstructorFragment.isFragment(obj)) { return obj; } + + if (typeof(obj) === "string") { + return ConstructorFragment.from(lex(obj)); + + } else if (obj instanceof TokenString) { + consumeKeywords(obj, setify([ "constructor" ])); + const inputs = consumeParams(obj); + const payable = !!consumeKeywords(obj, setify([ "payable" ])).has("payable"); + const gas = consumeGas(obj); + consumeEoi(obj); + + return new ConstructorFragment(_guard, "constructor", inputs, payable, gas); + } + return new ConstructorFragment(_guard, "constructor", - obj.inputs ? obj.inputs.map(ParamType.fromObject): [ ], + obj.inputs ? obj.inputs.map(ParamType.from): [ ], !!obj.payable, (obj.gas != null) ? obj.gas: null); } - static fromString(text: string): ConstructorFragment { - return ConstructorFragment.fromTokens(lex(text)); - } - - static fromTokens(tokens: TokenString): ConstructorFragment { - consumeKeywords(tokens, setify([ "constructor" ])); - const inputs = consumeParams(tokens); - const payable = !!consumeKeywords(tokens, setify([ "payable" ])).has("payable"); - const gas = consumeGas(tokens); - consumeEoi(tokens); - - return new ConstructorFragment(_guard, "constructor", inputs, payable, gas); + static isFragment(value: any): value is ConstructorFragment { + return (value && value[internal] === ConstructorFragmentInternal); } } @@ -995,6 +997,7 @@ export class FunctionFragment extends NamedFragment { constructor(guard: any, name: string, stateMutability: string, inputs: ReadonlyArray, outputs: ReadonlyArray, gas: null | bigint) { super(guard, "function", name, inputs); + Object.defineProperty(this, internal, { value: FunctionFragmentInternal }); outputs = Object.freeze(outputs.slice()); const constant = (stateMutability === "view" || stateMutability === "pure"); const payable = (stateMutability === "payable"); @@ -1040,51 +1043,68 @@ export class FunctionFragment extends NamedFragment { return result.join(" "); } - static fromObject(obj: any): FunctionFragment { + static from(obj: any): FunctionFragment { + if (FunctionFragment.isFragment(obj)) { return obj; } + + if (typeof(obj) === "string") { + return FunctionFragment.from(lex(obj)); + + } else if (obj instanceof TokenString) { + const name = consumeName("function", obj); + const inputs = consumeParams(obj); + const mutability = consumeMutability(obj); + + let outputs: Array = [ ]; + if (consumeKeywords(obj, setify([ "returns" ])).has("returns")) { + outputs = consumeParams(obj); + } + + const gas = consumeGas(obj); + + consumeEoi(obj); + + return new FunctionFragment(_guard, name, mutability, inputs, outputs, gas); + } + // @TODO: verifyState for stateMutability + return new FunctionFragment(_guard, obj.name, obj.stateMutability, - obj.inputs ? obj.inputs.map(ParamType.fromObject): [ ], - obj.outputs ? obj.outputs.map(ParamType.fromObject): [ ], + obj.inputs ? obj.inputs.map(ParamType.from): [ ], + obj.outputs ? obj.outputs.map(ParamType.from): [ ], (obj.gas != null) ? obj.gas: null); } - static fromString(text: string): FunctionFragment { - return FunctionFragment.fromTokens(lex(text)); - } - - static fromTokens(tokens: TokenString): FunctionFragment { - const name = consumeName("function", tokens); - const inputs = consumeParams(tokens); - const mutability = consumeMutability(tokens); - - let outputs: Array = [ ]; - if (consumeKeywords(tokens, setify([ "returns" ])).has("returns")) { - outputs = consumeParams(tokens); - } - - const gas = consumeGas(tokens); - - consumeEoi(tokens); - - return new FunctionFragment(_guard, name, mutability, inputs, outputs, gas); + static isFragment(value: any): value is FunctionFragment { + return (value && value[internal] === FunctionFragmentInternal); } } export class StructFragment extends NamedFragment { + constructor(guard: any, name: string, inputs: ReadonlyArray) { + super(guard, "struct", name, inputs); + Object.defineProperty(this, internal, { value: StructFragmentInternal }); + } + format(): string { throw new Error("@TODO"); } - static fromString(text: string): StructFragment { - return StructFragment.fromTokens(lex(text)); + static from(obj: any): StructFragment { + if (typeof(obj) === "string") { + return StructFragment.from(lex(obj)); + + } else if (obj instanceof TokenString) { + const name = consumeName("struct", obj); + const inputs = consumeParams(obj); + consumeEoi(obj); + return new StructFragment(_guard, name, inputs); + } + + return new StructFragment(_guard, obj.name, obj.inputs ? obj.inputs.map(ParamType.from): [ ]); } - static fromTokens(tokens: TokenString): StructFragment { - const name = consumeName("struct", tokens); - const inputs = consumeParams(tokens); - consumeEoi(tokens); - - return new StructFragment(_guard, "struct", name, inputs); + static isFragment(value: any): value is FunctionFragment { + return (value && value[internal] === StructFragmentInternal); } } diff --git a/src.ts/abi/interface.ts b/src.ts/abi/interface.ts index 326693091..d2f9055e0 100644 --- a/src.ts/abi/interface.ts +++ b/src.ts/abi/interface.ts @@ -182,7 +182,9 @@ export class Interface { for (const a of abi) { try { frags.push(Fragment.from(a)); - } catch (error) { } + } catch (error) { + console.log("EE", error); + } } defineProperties(this, { @@ -233,7 +235,7 @@ export class Interface { // If we do not have a constructor add a default if (!this.deploy) { defineProperties(this, { - deploy: ConstructorFragment.fromString("constructor()") + deploy: ConstructorFragment.from("constructor()") }); } } @@ -350,7 +352,7 @@ export class Interface { } // Normalize the signature and lookup the function - const result = this.#functions.get(FunctionFragment.fromString(key).format()); + const result = this.#functions.get(FunctionFragment.from(key).format()); if (result) { return result; } return throwArgumentError("no matching function", "signature", key); @@ -433,7 +435,7 @@ export class Interface { } // Normalize the signature and lookup the function - const result = this.#events.get(EventFragment.fromString(key).format()); + const result = this.#events.get(EventFragment.from(key).format()); if (result) { return result; } return throwArgumentError("no matching event", "signature", key); @@ -476,7 +478,7 @@ export class Interface { const selector = key.toLowerCase(); if (BuiltinErrors[selector]) { - return ErrorFragment.fromString(BuiltinErrors[selector].signature); + return ErrorFragment.from(BuiltinErrors[selector].signature); } for (const fragment of this.#errors.values()) { @@ -493,8 +495,8 @@ export class Interface { } if (matching.length === 0) { - if (key === "Error") { return ErrorFragment.fromString("error Error(string)"); } - if (key === "Panic") { return ErrorFragment.fromString("error Panic(uint256)"); } + if (key === "Error") { return ErrorFragment.from("error Error(string)"); } + if (key === "Panic") { return ErrorFragment.from("error Panic(uint256)"); } throwArgumentError("no matching error", "name", key); } else if (matching.length > 1) { // @TODO: refine by Typed @@ -505,9 +507,9 @@ export class Interface { } // Normalize the signature and lookup the function - key = ErrorFragment.fromString(key).format() - if (key === "Error(string)") { return ErrorFragment.fromString("error Error(string)"); } - if (key === "Panic(uint256)") { return ErrorFragment.fromString("error Panic(uint256)"); } + 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; } @@ -891,7 +893,7 @@ export class Interface { eventFragment.inputs.forEach((param, index) => { if (param.indexed) { if (param.type === "string" || param.type === "bytes" || param.baseType === "tuple" || param.baseType === "array") { - indexed.push(ParamType.fromObject({ type: "bytes32", name: param.name })); + indexed.push(ParamType.from({ type: "bytes32", name: param.name })); dynamic.push(true); } else { indexed.push(param);