Skip to content

Commit

Permalink
feat: use abitype for event and method names (#51)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
VGLoic authored Oct 18, 2022
1 parent 9748504 commit 32c3b46
Show file tree
Hide file tree
Showing 10 changed files with 96 additions and 39 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
```
Expand Down
2 changes: 2 additions & 0 deletions examples/react-apps/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions examples/react-apps/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion examples/react-apps/src/constants/storage-contract.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const ABI = [
export const ABI = <const>[
{
inputs: [],
stateMutability: "nonpayable",
Expand Down
39 changes: 31 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"format": "prettier src examples/react-apps/src --write"
},
"dependencies": {
"abitype": "^0.1.6",
"ethers": "^5.5.4"
},
"devDependencies": {
Expand Down
43 changes: 30 additions & 13 deletions src/testing-utils/contract-utils.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -20,19 +27,22 @@ type TxOptions = {

type ConditionCache = Record<string, MockCondition>;

export class ContractUtils {
type AbiType = ReadonlyArray<AbiEvent | AbiFunction | AbiError>;

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 = {};
}
Expand All @@ -54,7 +64,9 @@ export class ContractUtils {
* ```
*/
public mockCall(
functionName: string,
functionName: TAbi extends AbiType
? ExtractAbiFunctionNames<TAbi, "view" | "pure">
: string,
values: readonly any[] | undefined,
callOptions: CallOptions = {},
mockOptions: MockOptions = {}
Expand Down Expand Up @@ -113,7 +125,9 @@ export class ContractUtils {
* ```
*/
public mockTransaction(
functionName: string,
functionName: TAbi extends AbiType
? ExtractAbiFunctionNames<TAbi, "nonpayable" | "payable">
: string,
txOptions: TxOptions = {},
mockOptions: MockOptions = {}
) {
Expand Down Expand Up @@ -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<TAbi> : string,
allValues: unknown[][]
) {
const blockNumberMock =
this.mockManager.findUnconditionalPersistentMock("eth_blockNumber");
if (!blockNumberMock) {
Expand Down Expand Up @@ -294,7 +311,7 @@ export class ContractUtils {
* ```
*/
public mockEmitLog(
eventName: string,
eventName: TAbi extends AbiType ? ExtractAbiEventNames<TAbi> : string,
values: unknown[],
subscriptionId?: string,
logOverrides?: Partial<Log>
Expand Down Expand Up @@ -364,7 +381,7 @@ export class ContractUtils {
* @returns The log for the event
*/
public generateMockLog(
eventName: string,
eventName: TAbi extends AbiType ? ExtractAbiEventNames<TAbi> : string,
values: unknown[],
logOverrides?: Partial<Log>
): Log {
Expand Down
4 changes: 2 additions & 2 deletions src/testing-utils/ens-utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <const>[
{
inputs: [{ internalType: "contract ENS", name: "_old", type: "address" }],
payable: false,
Expand Down Expand Up @@ -236,7 +236,7 @@ export const ENS_REGISTRY_WITH_FALLBACK_ABI = [
},
];

export const PUBLIC_RESOLVER_ABI = [
export const PUBLIC_RESOLVER_ABI = <const>[
{
inputs: [{ internalType: "contract ENS", name: "_ens", type: "address" }],
payable: false,
Expand Down
17 changes: 11 additions & 6 deletions src/testing-utils/ens-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -66,7 +66,7 @@ export class EnsUtils {
persistent: true,
});
this.publicResolverUtils.mockCall(
"addr(bytes32)",
"addr(bytes32)" as any,
[ethers.constants.AddressZero],
undefined,
{ persistent: true }
Expand Down Expand Up @@ -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,
}
);
}

/**
Expand Down
19 changes: 13 additions & 6 deletions src/testing-utils/testing-utils.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<TAbi>(this.mockManager, abi, contractAddress);
}

/**
Expand Down

0 comments on commit 32c3b46

Please sign in to comment.