From 32c3b46aa7afe9b6bf0ed991a6fcfd4b86a02825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Vincent-Genod?= <37535451+VGLoic@users.noreply.github.com> Date: Tue, 18 Oct 2022 09:01:35 +0200 Subject: [PATCH] feat: use abitype for event and method names (#51) * feat: use abitype for event and method names * feat: optionally provide constant type * feat: remove narrow utils * feat: remove commented code * feat: remove commented code * feat: update README --- README.md | 4 +- examples/react-apps/package-lock.json | 2 + examples/react-apps/package.json | 4 +- .../src/constants/storage-contract.ts | 2 +- package-lock.json | 39 +++++++++++++---- package.json | 1 + src/testing-utils/contract-utils.ts | 43 +++++++++++++------ src/testing-utils/ens-utils/constants.ts | 4 +- src/testing-utils/ens-utils/index.ts | 17 +++++--- src/testing-utils/testing-utils.ts | 19 +++++--- 10 files changed, 96 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 1b19bd4..b5cd5b2 100644 --- a/README.md +++ b/README.md @@ -166,8 +166,10 @@ testingUtils.mockRequestAccounts(["0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf"]) Most of the application interacts with deployed contracts, interactions are generally more complex with contracts, hence a dedicated object has been created for it. The testing utils expose a `generateContractUtils` method, it allows to generate high level utils for contract interactions based on their ABI. + +It is advised to *freeze* the ABI using `as const` [feature](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#readonly-and-const) of TypeScript. By doing so, types will be generated for event and method names when using the contract testing utils. Refer to [ABIType](https://github.com/wagmi-dev/abitype) for additional informations. ```ts -const abi = [...]; +const abi = [...] as const; // An address may be optionally given as second argument, advised in case of multiple similar contracts const contractTestingUtils = testingUtils.generateContractUtils(abi); ``` diff --git a/examples/react-apps/package-lock.json b/examples/react-apps/package-lock.json index 8ae7d1a..6b7fde8 100644 --- a/examples/react-apps/package-lock.json +++ b/examples/react-apps/package-lock.json @@ -50,6 +50,7 @@ "dev": true, "license": "MIT", "dependencies": { + "abitype": "^0.1.6", "ethers": "^5.5.4" }, "devDependencies": { @@ -77373,6 +77374,7 @@ "@types/jest": "^27.5.0", "@typescript-eslint/eslint-plugin": "^4.12.0", "@typescript-eslint/parser": "^4.12.0", + "abitype": "^0.1.6", "babel-eslint": "^10.1.0", "eslint": "^7.17.0", "eslint-config-prettier": "^7.1.0", diff --git a/examples/react-apps/package.json b/examples/react-apps/package.json index 4f68364..cabcc67 100644 --- a/examples/react-apps/package.json +++ b/examples/react-apps/package.json @@ -11,8 +11,6 @@ "hardhat:node": "hardhat node" }, "dependencies": { - "@web3-react/core": "6.1.9", - "@web3-react/injected-connector": "6.0.7", "@tanstack/react-location": "^3.6.1", "@testing-library/jest-dom": "^5.16.2", "@testing-library/react": "^12.1.3", @@ -22,6 +20,8 @@ "@types/react": "^17.0.39", "@types/react-dom": "^17.0.13", "@walletconnect/web3-provider": "^1.7.3", + "@web3-react/core": "6.1.9", + "@web3-react/injected-connector": "6.0.7", "ethers": "^5.6.6", "install": "^0.13.0", "npm": "^8.5.2", diff --git a/examples/react-apps/src/constants/storage-contract.ts b/examples/react-apps/src/constants/storage-contract.ts index 9155a41..3f67622 100644 --- a/examples/react-apps/src/constants/storage-contract.ts +++ b/examples/react-apps/src/constants/storage-contract.ts @@ -1,4 +1,4 @@ -export const ABI = [ +export const ABI = [ { inputs: [], stateMutability: "nonpayable", diff --git a/package-lock.json b/package-lock.json index d356e18..8739407 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1-semantic-release", "license": "MIT", "dependencies": { + "abitype": "^0.1.6", "ethers": "^5.5.4" }, "devDependencies": { @@ -2763,6 +2764,24 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/abitype": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.1.6.tgz", + "integrity": "sha512-cdOzP4mi5pH/N3o5ly8wlw7HIqdFJVkS0RSw5p7BTvwB1gFXOqPlQJ/ZKTSCsGqd8EITJcpNuZ2h7hPX2mKECA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wagmi-dev" + } + ], + "engines": { + "node": ">=16", + "pnpm": ">=7" + }, + "peerDependencies": { + "typescript": ">=4.7.4" + } + }, "node_modules/acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -12663,10 +12682,9 @@ } }, "node_modules/typescript": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", - "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", - "dev": true, + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14926,6 +14944,12 @@ "eslint-visitor-keys": "^2.0.0" } }, + "abitype": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.1.6.tgz", + "integrity": "sha512-cdOzP4mi5pH/N3o5ly8wlw7HIqdFJVkS0RSw5p7BTvwB1gFXOqPlQJ/ZKTSCsGqd8EITJcpNuZ2h7hPX2mKECA==", + "requires": {} + }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -22290,10 +22314,9 @@ "dev": true }, "typescript": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", - "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", - "dev": true + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==" }, "uglify-js": { "version": "3.14.4", diff --git a/package.json b/package.json index 9a7dac7..ac7ff70 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "format": "prettier src examples/react-apps/src --write" }, "dependencies": { + "abitype": "^0.1.6", "ethers": "^5.5.4" }, "devDependencies": { diff --git a/src/testing-utils/contract-utils.ts b/src/testing-utils/contract-utils.ts index 14ecbdf..50a2336 100644 --- a/src/testing-utils/contract-utils.ts +++ b/src/testing-utils/contract-utils.ts @@ -1,11 +1,18 @@ import { ethers, EventFilter } from "ethers"; -import { Fragment, Interface, JsonFragment } from "@ethersproject/abi"; +import { Interface, JsonFragment, Fragment } from "@ethersproject/abi"; import { BigNumber } from "@ethersproject/bignumber"; import { Transaction } from "@ethersproject/transactions"; import { ContractReceipt } from "@ethersproject/contracts"; import { Log } from "@ethersproject/abstract-provider"; import { MockManager } from "../mock-manager"; import { MockCondition, MockOptions } from "../types"; +import { + AbiError, + AbiEvent, + AbiFunction, + ExtractAbiEventNames, + ExtractAbiFunctionNames, +} from "abitype"; type CallOptions = { contractAddress?: string; @@ -20,19 +27,22 @@ type TxOptions = { type ConditionCache = Record; -export class ContractUtils { +type AbiType = ReadonlyArray; + +export class ContractUtils< + TAbi extends readonly ( + | (JsonFragment | Fragment) + | (AbiEvent | AbiFunction | AbiError) + )[] +> { private mockManager: MockManager; private contractInterface: Interface; public address?: string; private conditionCache: ConditionCache; - constructor( - mockManager: MockManager, - abi: readonly (string | JsonFragment | Fragment)[], - address?: string - ) { + constructor(mockManager: MockManager, abi: TAbi, address?: string) { this.mockManager = mockManager; - this.contractInterface = new Interface(abi); + this.contractInterface = new Interface(abi as readonly JsonFragment[]); this.address = address; this.conditionCache = {}; } @@ -54,7 +64,9 @@ export class ContractUtils { * ``` */ public mockCall( - functionName: string, + functionName: TAbi extends AbiType + ? ExtractAbiFunctionNames + : string, values: readonly any[] | undefined, callOptions: CallOptions = {}, mockOptions: MockOptions = {} @@ -113,7 +125,9 @@ export class ContractUtils { * ``` */ public mockTransaction( - functionName: string, + functionName: TAbi extends AbiType + ? ExtractAbiFunctionNames + : string, txOptions: TxOptions = {}, mockOptions: MockOptions = {} ) { @@ -248,7 +262,10 @@ export class ContractUtils { * contractTestingUtils.mockGetLogs("ValueUpdated", [["0"], ["12"]]); * ``` */ - public mockGetLogs(eventName: string, allValues: unknown[][]) { + public mockGetLogs( + eventName: TAbi extends AbiType ? ExtractAbiEventNames : string, + allValues: unknown[][] + ) { const blockNumberMock = this.mockManager.findUnconditionalPersistentMock("eth_blockNumber"); if (!blockNumberMock) { @@ -294,7 +311,7 @@ export class ContractUtils { * ``` */ public mockEmitLog( - eventName: string, + eventName: TAbi extends AbiType ? ExtractAbiEventNames : string, values: unknown[], subscriptionId?: string, logOverrides?: Partial @@ -364,7 +381,7 @@ export class ContractUtils { * @returns The log for the event */ public generateMockLog( - eventName: string, + eventName: TAbi extends AbiType ? ExtractAbiEventNames : string, values: unknown[], logOverrides?: Partial ): Log { diff --git a/src/testing-utils/ens-utils/constants.ts b/src/testing-utils/ens-utils/constants.ts index cc7f152..aac6233 100644 --- a/src/testing-utils/ens-utils/constants.ts +++ b/src/testing-utils/ens-utils/constants.ts @@ -3,7 +3,7 @@ export const ENS_REGISTRY_WITH_FALLBACK_ADDRESS = export const PUBLIC_RESOLVER_ADDRESS = "0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41"; -export const ENS_REGISTRY_WITH_FALLBACK_ABI = [ +export const ENS_REGISTRY_WITH_FALLBACK_ABI = [ { inputs: [{ internalType: "contract ENS", name: "_old", type: "address" }], payable: false, @@ -236,7 +236,7 @@ export const ENS_REGISTRY_WITH_FALLBACK_ABI = [ }, ]; -export const PUBLIC_RESOLVER_ABI = [ +export const PUBLIC_RESOLVER_ABI = [ { inputs: [{ internalType: "contract ENS", name: "_ens", type: "address" }], payable: false, diff --git a/src/testing-utils/ens-utils/index.ts b/src/testing-utils/ens-utils/index.ts index 151a79c..51dbde6 100644 --- a/src/testing-utils/ens-utils/index.ts +++ b/src/testing-utils/ens-utils/index.ts @@ -9,8 +9,8 @@ import { } from "./constants"; export class EnsUtils { - private ensRegistryWithFallbackUtils: ContractUtils; - private publicResolverUtils: ContractUtils; + private ensRegistryWithFallbackUtils; + private publicResolverUtils; constructor(mockManager: MockManager) { this.ensRegistryWithFallbackUtils = new ContractUtils( @@ -66,7 +66,7 @@ export class EnsUtils { persistent: true, }); this.publicResolverUtils.mockCall( - "addr(bytes32)", + "addr(bytes32)" as any, [ethers.constants.AddressZero], undefined, { persistent: true } @@ -112,9 +112,14 @@ export class EnsUtils { this.publicResolverUtils.mockCall("supportsInterface", [false], undefined, { persistent: true, }); - this.publicResolverUtils.mockCall("addr(bytes32)", [address], callValues, { - persistent: true, - }); + this.publicResolverUtils.mockCall( + "addr(bytes32)" as any, + [address], + callValues, + { + persistent: true, + } + ); } /** diff --git a/src/testing-utils/testing-utils.ts b/src/testing-utils/testing-utils.ts index c3079ad..ada082e 100644 --- a/src/testing-utils/testing-utils.ts +++ b/src/testing-utils/testing-utils.ts @@ -1,10 +1,12 @@ -import { Fragment, JsonFragment } from "@ethersproject/abi"; import { ethers } from "ethers"; import { MockManager } from "../mock-manager"; import { ContractUtils } from "./contract-utils"; import { MockCondition, MockOptions } from "../types"; import { Provider } from "../providers"; import { EnsUtils } from "./ens-utils"; +import { AbiError, AbiEvent, AbiFunction } from "abitype"; +import { Fragment } from "ethers/lib/utils"; +import { JsonFragment } from "@ethersproject/abi"; export class LowLevelTestingUtils { private mockManager: MockManager; @@ -316,11 +318,16 @@ export class TestingUtils { * const erc20TestingUtils = testingUtils.generateContractUtils(ERC20_ABI); * ``` */ - public generateContractUtils( - abi: (string | JsonFragment | Fragment)[], - contractAddress?: string - ) { - return new ContractUtils(this.mockManager, abi, contractAddress); + public generateContractUtils< + TAbi extends readonly ( + | Fragment + | JsonFragment + | AbiEvent + | AbiFunction + | AbiError + )[] + >(abi: TAbi, contractAddress?: string) { + return new ContractUtils(this.mockManager, abi, contractAddress); } /**