import { Contract, ContractInterface, ethers } from "ethers"; import { shallowCopy } from "ethers/lib/utils"; function NoEventException(value: any) { this.value = value; this.message = "No event found!"; // eslint-disable-next-line func-names this.toString = function () { return this.value + this.message; }; } const cache: { [key: string]: { contract: ethers.Contract; proxy: any; }; } = {}; type InterceptorFn = (...args: any) => Promise; export class Web3ProviderExtras { event: EventTypes; signer: () => ethers.providers.JsonRpcSigner; provider: () => ethers.providers.Web3Provider; beforeRequest: InterceptorFn; afterRequest: InterceptorFn; transactionState: ( state: "started" | "rejected" | "waiting" | "done", ...args: any ) => void; skipEvent: boolean; constructor(extras?: Web3ProviderExtras) { if (extras?.event) { this.event = extras?.event; } if (extras?.skipEvent) { this.skipEvent = extras.skipEvent; } if (extras?.beforeRequest) { this.beforeRequest = extras.beforeRequest; } else { this.beforeRequest = (_) => Promise.resolve(_); } if (extras?.afterRequest) { this.afterRequest = extras.afterRequest; } else { this.afterRequest = (_) => Promise.resolve(_); } if (extras?.signer) { this.signer = extras.signer; } if (extras?.transactionState) { this.transactionState = extras.transactionState; } else { this.transactionState = (_) => null; } } } const waitForTransaction = async ( fnCallResult: Promise<{ wait: () => Promise }>, extras: Web3ProviderExtras ) => { const getEvent = (waitResult: EventTypes) => { if (extras?.event) { const { events } = waitResult as unknown as EventTypes as any; const foundEvent = events.find((e: any) => e.event === extras.event); if (!foundEvent) { throw NoEventException({ code: -32603, message: "Internal JSON-RPC error.", data: { code: 3, data: "", message: `Aut:${extras.event}EventMissing`, }, }); } return foundEvent.args; } return waitResult; }; // @ts-ignore if (fnCallResult?.then) { extras.transactionState("started"); const tx = await fnCallResult; extras.transactionState("waiting", tx); if (tx?.wait) { const eventResult = getEvent(await tx.wait()); extras.transactionState("done", eventResult); return extras.afterRequest(eventResult); } return extras.afterRequest(tx); } return extras.afterRequest(fnCallResult); }; const createContractProxy = ( contract: Contract, extras: Web3ProviderExtras ) => { return new Proxy(shallowCopy(contract as unknown as ContractFunctions), { get(target: any, prop: string, receiver: any) { if (!(prop in target)) { return undefined; } if (typeof target[prop] === "function") { if (extras?.skipEvent) { return new Proxy(target[prop], { apply(fn, thisArg, args) { return Reflect.apply(fn, thisArg, args); }, }); } return new Proxy(target[prop], { apply(fn, thisArg, args) { return waitForTransaction( Reflect.apply(fn, thisArg, args), extras ); }, }); } return Reflect.get(target, prop, receiver); }, }); }; export const Web3ContractProvider = ( addressOrName: string, contractInterface: ContractInterface, extras: Web3ProviderExtras ) => { extras = new Web3ProviderExtras(extras); let signer: ethers.providers.JsonRpcSigner; if (extras.signer) { signer = extras.signer(); } else { // remove in the future if (extras.provider) { const provider = extras.provider(); signer = provider.getSigner(); } else { const provider = new ethers.providers.Web3Provider(window?.ethereum); signer = provider.getSigner(); } } let contract: Contract; let proxy: any; if (cache[addressOrName]) { const data = cache[addressOrName]; contract = data.contract; proxy = data.proxy; contract.connect(signer); } else { contract = new ethers.Contract(addressOrName, contractInterface, signer); proxy = createContractProxy( contract, extras ); } return { ...proxy, contract, } as ContractFunctions & { contract: Contract }; };