diff --git a/src.ts/contract/contract.ts b/src.ts/contract/contract.ts index 627e61ad8e..9465700b70 100644 --- a/src.ts/contract/contract.ts +++ b/src.ts/contract/contract.ts @@ -22,15 +22,16 @@ import type { } from "../providers/index.js"; import type { + BaseContractMethod, ContractEventName, ContractInterface, ContractMethodArgs, - BaseContractMethod, ContractMethod, ContractEventArgs, ContractEvent, ContractTransaction, - DeferredTopicFilter + DeferredTopicFilter, + WrappedFallback } from "./types.js"; const BN_0 = BigInt(0); @@ -110,10 +111,6 @@ class PreparedTopicFilter implements DeferredTopicFilter { // TransactionResponse otherwise) //export interface ContractMethod = Array, R = any, D extends R | ContractTransactionResponse = ContractTransactionResponse> { -function _WrappedMethodBase(): new () => Function & BaseContractMethod { - return Function as any; -} - function getRunner(value: any, feature: keyof ContractRunner): null | T { if (value == null) { return null; } if (typeof(value[feature]) === "function") { return value; } @@ -165,8 +162,82 @@ export async function resolveArgs(_runner: null | ContractRunner, inputs: Readon })); } +function buildWrappedFallback(contract: BaseContract): WrappedFallback { + + const populateTransaction = async function(overrides?: Omit): Promise { + // If an overrides was passed in, copy it and normalize the values + + const tx: ContractTransaction = (await copyOverrides<"data">(overrides, [ "data" ])); + tx.to = await contract.getAddress(); + + const iface = contract.interface; + + // Only allow payable contracts to set non-zero value + const payable = iface.receive || (iface.fallback && iface.fallback.payable); + assertArgument(payable || (tx.value || BN_0) === BN_0, + "cannot send value to non-payable contract", "overrides.value", tx.value); + + // Only allow fallback contracts to set non-empty data + assertArgument(iface.fallback || (tx.data || "0x") === "0x", + "cannot send data to receive-only contract", "overrides.data", tx.data); + + return tx; + } + + const staticCall = async function(overrides?: Omit): Promise { + const runner = getRunner(contract.runner, "call"); + assert(canCall(runner), "contract runner does not support calling", + "UNSUPPORTED_OPERATION", { operation: "call" }); + + const tx = await populateTransaction(overrides); + + try { + return await runner.call(tx); + } catch (error: any) { + if (isCallException(error) && error.data) { + throw contract.interface.makeError(error.data, tx); + } + throw error; + } + } + + const send = async function(overrides?: Omit): Promise { + const runner = contract.runner; + assert(canSend(runner), "contract runner does not support sending transactions", + "UNSUPPORTED_OPERATION", { operation: "sendTransaction" }); + + const tx = await runner.sendTransaction(await populateTransaction(overrides)); + const provider = getProvider(contract.runner); + // @TODO: the provider can be null; make a custom dummy provider that will throw a + // meaningful error + return new ContractTransactionResponse(contract.interface, provider, tx); + } + + const estimateGas = async function(overrides?: Omit): Promise { + const runner = getRunner(contract.runner, "estimateGas"); + assert(canEstimate(runner), "contract runner does not support gas estimation", + "UNSUPPORTED_OPERATION", { operation: "estimateGas" }); + + return await runner.estimateGas(await populateTransaction(overrides)); + } + + const method = async (overrides?: Omit) => { + return await send(overrides); + }; + + defineProperties(method, { + _contract: contract, + + estimateGas, + populateTransaction, + send, staticCall + }); + + return method; +} + +/* class WrappedFallback { - readonly _contract!: BaseContract; constructor (contract: BaseContract) { defineProperties(this, { _contract: contract }); @@ -238,53 +309,20 @@ class WrappedFallback { return await runner.estimateGas(await this.populateTransaction(overrides)); } } +*/ -class WrappedMethod = Array, R = any, D extends R | ContractTransactionResponse = ContractTransactionResponse> - extends _WrappedMethodBase() implements BaseContractMethod { - - readonly name: string = ""; // Investigate! - readonly _contract!: BaseContract; - readonly _key!: string; - - constructor (contract: BaseContract, key: string) { - super(); +function buildWrappedMethod = Array, R = any, D extends R | ContractTransactionResponse = ContractTransactionResponse>(contract: BaseContract, key: string): BaseContractMethod { - defineProperties(this, { - name: contract.interface.getFunctionName(key), - _contract: contract, _key: key - }); - - const proxy = new Proxy(this, { - // Perform the default operation for this fragment type - apply: async (target, thisArg, args: ContractMethodArgs) => { - const fragment = target.getFragment(...args); - if (fragment.constant) { return await target.staticCall(...args); } - return await target.send(...args); - }, - }); - - return proxy; - } - - // Only works on non-ambiguous keys (refined fragment is always non-ambiguous) - get fragment(): FunctionFragment { - const fragment = this._contract.interface.getFunction(this._key); - assert(fragment, "no matching fragment", "UNSUPPORTED_OPERATION", { - operation: "fragment" - }); - return fragment; - } - - getFragment(...args: ContractMethodArgs): FunctionFragment { - const fragment = this._contract.interface.getFunction(this._key, args); + const getFragment = function(...args: ContractMethodArgs): FunctionFragment { + const fragment = contract.interface.getFunction(key, args); assert(fragment, "no matching fragment", "UNSUPPORTED_OPERATION", { operation: "fragment" }); return fragment; } - async populateTransaction(...args: ContractMethodArgs): Promise { - const fragment = this.getFragment(...args); + const populateTransaction = async function(...args: ContractMethodArgs): Promise { + const fragment = getFragment(...args); // If an overrides was passed in, copy it and normalize the values let overrides: Omit = { }; @@ -296,91 +334,98 @@ class WrappedMethod = Array, R = any, D extends R | Co throw new Error("internal error: fragment inputs doesn't match arguments; should not happen"); } - const resolvedArgs = await resolveArgs(this._contract.runner, fragment.inputs, args); + const resolvedArgs = await resolveArgs(contract.runner, fragment.inputs, args); return Object.assign({ }, overrides, await resolveProperties({ - to: this._contract.getAddress(), - data: this._contract.interface.encodeFunctionData(fragment, resolvedArgs) + to: contract.getAddress(), + data: contract.interface.encodeFunctionData(fragment, resolvedArgs) })); } - async staticCall(...args: ContractMethodArgs): Promise { - const result = await this.staticCallResult(...args); + const staticCall = async function(...args: ContractMethodArgs): Promise { + const result = await staticCallResult(...args); if (result.length === 1) { return result[0]; } return result; } - async send(...args: ContractMethodArgs): Promise { - const runner = this._contract.runner; + const send = async function(...args: ContractMethodArgs): Promise { + const runner = contract.runner; assert(canSend(runner), "contract runner does not support sending transactions", "UNSUPPORTED_OPERATION", { operation: "sendTransaction" }); - const tx = await runner.sendTransaction(await this.populateTransaction(...args)); - const provider = getProvider(this._contract.runner); + const tx = await runner.sendTransaction(await populateTransaction(...args)); + const provider = getProvider(contract.runner); // @TODO: the provider can be null; make a custom dummy provider that will throw a // meaningful error - return new ContractTransactionResponse(this._contract.interface, provider, tx); + return new ContractTransactionResponse(contract.interface, provider, tx); } - async estimateGas(...args: ContractMethodArgs): Promise { - const runner = getRunner(this._contract.runner, "estimateGas"); + const estimateGas = async function(...args: ContractMethodArgs): Promise { + const runner = getRunner(contract.runner, "estimateGas"); assert(canEstimate(runner), "contract runner does not support gas estimation", "UNSUPPORTED_OPERATION", { operation: "estimateGas" }); - return await runner.estimateGas(await this.populateTransaction(...args)); + return await runner.estimateGas(await populateTransaction(...args)); } - async staticCallResult(...args: ContractMethodArgs): Promise { - const runner = getRunner(this._contract.runner, "call"); + const staticCallResult = async function(...args: ContractMethodArgs): Promise { + const runner = getRunner(contract.runner, "call"); assert(canCall(runner), "contract runner does not support calling", "UNSUPPORTED_OPERATION", { operation: "call" }); - const tx = await this.populateTransaction(...args); + const tx = await populateTransaction(...args); let result = "0x"; try { result = await runner.call(tx); } catch (error: any) { if (isCallException(error) && error.data) { - throw this._contract.interface.makeError(error.data, tx); + throw contract.interface.makeError(error.data, tx); } throw error; } - const fragment = this.getFragment(...args); - return this._contract.interface.decodeFunctionResult(fragment, result); - } -} + const fragment = getFragment(...args); + return contract.interface.decodeFunctionResult(fragment, result); + }; -function _WrappedEventBase(): new () => Function & ContractEvent { - return Function as any; -} + const method = async (...args: ContractMethodArgs) => { + const fragment = getFragment(...args); + if (fragment.constant) { return await staticCall(...args); } + return await send(...args); + }; -class WrappedEvent = Array> extends _WrappedEventBase() implements ContractEvent { - readonly name: string = ""; // @TODO: investigate + defineProperties(method, { + name: contract.interface.getFunctionName(key), + _contract: contract, _key: key, - readonly _contract!: BaseContract; - readonly _key!: string; + getFragment, - constructor (contract: BaseContract, key: string) { - super(); + estimateGas, + populateTransaction, + send, staticCall, staticCallResult, + }); - defineProperties(this, { - name: contract.interface.getEventName(key), - _contract: contract, _key: key - }); + // Only works on non-ambiguous keys (refined fragment is always non-ambiguous) + Object.defineProperty(method, "fragment", { + configurable: false, + enumerable: false, + get: () => { + const fragment = contract.interface.getFunction(key); + assert(fragment, "no matching fragment", "UNSUPPORTED_OPERATION", { + operation: "fragment" + }); + return fragment; + } + }); - return new Proxy(this, { - // Perform the default operation for this fragment type - apply: (target, thisArg, args: ContractEventArgs) => { - return new PreparedTopicFilter(contract, target.getFragment(...args), args); - }, - }); - } + return >method; +} - // Only works on non-ambiguous keys - get fragment(): EventFragment { - const fragment = this._contract.interface.getEvent(this._key); +function buildWrappedEvent = Array>(contract: BaseContract, key: string): ContractEvent { + + const getFragment = function(...args: ContractEventArgs): EventFragment { + const fragment = contract.interface.getEvent(key, args); assert(fragment, "no matching fragment", "UNSUPPORTED_OPERATION", { operation: "fragment" @@ -389,16 +434,34 @@ class WrappedEvent = Array> extends _WrappedEventBase( return fragment; } - getFragment(...args: ContractEventArgs): EventFragment { - const fragment = this._contract.interface.getEvent(this._key, args); + const method = async function(...args: ContractMethodArgs): Promise { + return new PreparedTopicFilter(contract, getFragment(...args), args); + }; - assert(fragment, "no matching fragment", "UNSUPPORTED_OPERATION", { - operation: "fragment" - }); + defineProperties(method, { + name: contract.interface.getEventName(key), + _contract: contract, _key: key, - return fragment; - } -}; + getFragment + }); + + // Only works on non-ambiguous keys (refined fragment is always non-ambiguous) + Object.defineProperty(method, "fragment", { + configurable: false, + enumerable: false, + get: () => { + const fragment = contract.interface.getEvent(key); + + assert(fragment, "no matching fragment", "UNSUPPORTED_OPERATION", { + operation: "fragment" + }); + + return fragment; + } + }); + + return >method; +} type Sub = { tag: string; @@ -690,7 +753,7 @@ export class BaseContract implements Addressable, EventEmitterable(this, { filters }); defineProperties(this, { - fallback: ((iface.receive || iface.fallback) ? (new WrappedFallback(this)): null) + fallback: ((iface.receive || iface.fallback) ? (buildWrappedFallback(this)): null) }); // Return a Proxy that will respond to functions @@ -764,12 +827,13 @@ export class BaseContract implements Addressable, EventEmitterable(key: string | FunctionFragment): T { if (typeof(key) !== "string") { key = key.format(); } - return (new WrappedMethod(this, key)); + const func = buildWrappedMethod(this, key); + return func; } getEvent(key: string | EventFragment): ContractEvent { if (typeof(key) !== "string") { key = key.format(); } - return (new WrappedEvent(this, key)); + return buildWrappedEvent(this, key); } async queryTransaction(hash: string): Promise> { diff --git a/src.ts/contract/index.ts b/src.ts/contract/index.ts index 6d2ebb010f..aec2e15ea5 100644 --- a/src.ts/contract/index.ts +++ b/src.ts/contract/index.ts @@ -23,5 +23,6 @@ export type { ContractEvent, ContractEventArgs, ContractEventName, ContractDeployTransaction, ContractInterface, ContractMethod, ContractMethodArgs, ContractTransaction, - DeferredTopicFilter, Overrides + DeferredTopicFilter, Overrides, + WrappedFallback } from "./types.js"; diff --git a/src.ts/contract/types.ts b/src.ts/contract/types.ts index 80054507b9..30ad89629a 100644 --- a/src.ts/contract/types.ts +++ b/src.ts/contract/types.ts @@ -84,3 +84,12 @@ export interface ContractEvent = Array> { fragment: EventFragment; getFragment(...args: ContractEventArgs): EventFragment; }; + +export interface WrappedFallback { + (overrides?: Omit): Promise; + + populateTransaction(overrides?: Omit): Promise; + staticCall(overrides?: Omit): Promise; + send(overrides?: Omit): Promise; + estimateGas(overrides?: Omit): Promise; +}