Initial support for fallback and receive methods on contracts.

This commit is contained in:
Richard Moore 2023-01-30 22:28:18 -05:00
parent 67d2af809e
commit c3f8e8ac07
5 changed files with 387 additions and 27 deletions

@ -3,7 +3,9 @@ import assert from "assert";
import { getProvider, setupProviders } from "./create-provider.js"; import { getProvider, setupProviders } from "./create-provider.js";
import { Contract, EventLog, Typed, Wallet } from "../index.js"; import {
Contract, EventLog, isError, Typed, Wallet
} from "../index.js";
import type { ContractEventPayload, ContractEventName, Log } from "../index.js"; import type { ContractEventPayload, ContractEventName, Log } from "../index.js";
setupProviders(); setupProviders();
@ -22,7 +24,7 @@ describe("Test Contract", function() {
"function testErrorString(bool pass, string calldata message) pure returns (uint256)", "function testErrorString(bool pass, string calldata message) pure returns (uint256)",
"function testPanic(uint256 code) returns (uint256)", "function testPanic(uint256 code) returns (uint256)",
"function testEvent(uint256 valueUint256, address valueAddress, string valueString, bytes valueBytes) public", "function testEvent(uint256 valueUint256, address valueAddress, string valueString, bytes valueBytes) public",
"function testCallAdd(uint256 a, uint256 b) pure returns (uint256 result)" "function testCallAdd(uint256 a, uint256 b) pure returns (uint256 result)",
]; ];
it("tests contract calls", async function() { it("tests contract calls", async function() {
@ -319,3 +321,146 @@ describe("Test Contract Interface", function() {
}); });
}); });
*/ */
type TestContractFallbackResult = {
data: string;
} | {
error: string;
};
type TestContractFallback = {
name: string;
address: string;
abi: Array<string>;
sendNone: TestContractFallbackResult;
sendData: TestContractFallbackResult;
sendValue: TestContractFallbackResult;
sendDataAndValue: TestContractFallbackResult;
};
describe("Test Contract Fallback", function() {
const tests: Array<TestContractFallback> = [
{
name: "none",
address: "0x0ccdace3d8353fed9b87a2d63c40452923ccdae5",
abi: [ ],
sendNone: { error: "no fallback" },
sendData: { error: "no fallback" },
sendValue: { error: "no fallback" },
sendDataAndValue: { error: "no fallback" },
},
{
name: "non-payable fallback",
address: "0x3f10193f79a639b11ec9d2ab42a25a4a905a8870",
abi: [
"fallback()"
],
sendNone: { data: "0x" },
sendData: { data: "0x1234" },
sendValue: { error: "overrides.value" },
sendDataAndValue: { error: "overrides.value" },
},
{
name: "payable fallback",
address: "0xe2de6b97c5eb9fee8a47ca6c0fa642331e0b6330",
abi: [
"fallback() payable"
],
sendNone: { data: "0x" },
sendData: { data: "0x1234" },
sendValue: { data: "0x" },
sendDataAndValue: { data: "0x1234" },
},
{
name: "receive-only",
address: "0xf8f2afbbe37f6a4520e4db7f99495655aa31229b",
abi: [
"receive()"
],
sendNone: { data: "0x" },
sendData: { error: "overrides.data" },
sendValue: { data: "0x" },
sendDataAndValue: { error: "overrides.data" },
},
{
name: "receive and payable fallback",
address: "0x7d97ca5d9dea1cd0364f1d493252006a3c4e18a0",
abi: [
"fallback() payable",
"receive()"
],
sendNone: { data: "0x" },
sendData: { data: "0x1234" },
sendValue: { data: "0x" },
sendDataAndValue: { data: "0x1234" },
},
{
name: "receive and non-payable fallback",
address: "0x5b59d934f0d22b15e73b5d6b9ae83486b70df67e",
abi: [
"fallback() payable",
"receive()"
],
sendNone: { data: "0x" },
sendData: { data: "0x" },
sendValue: { data: "0x" },
sendDataAndValue: { error: "overrides.value" },
},
];
const provider = getProvider("InfuraProvider", "goerli");
const testGroups: Array<{ group: "sendNone" | "sendData" | "sendValue" | "sendDataAndValue", tx: any }> = [
{
group: "sendNone",
tx: { }
},
{
group: "sendData",
tx: { data: "0x1234" }
},
{
group: "sendValue",
tx: { value: 123 }
},
{
group: "sendDataAndValue",
tx: { data: "0x1234", value: 123 }
},
];
for (const { group, tx } of testGroups) {
for (const test of tests) {
const { name, address, abi } = test;
const send = test[group];
const contract = new Contract(address, abi, provider);
it(`test contract fallback checks: ${ group } - ${ name }`, async function() {
const func = async function() {
if (abi.length === 0) {
throw new Error("no fallback");
}
assert.ok(contract.fallback);
return await contract.fallback.populateTransaction(tx)
};
if ("data" in send) {
await func();
//const result = await func();
//@TODO: Test for the correct populated tx
//console.log(result);
assert.ok(true);
} else {
assert.rejects(func, function(error: any) {
if (error.message === send.error) { return true; }
if (isError(error, "INVALID_ARGUMENT")) {
return error.argument === send.error;
}
console.log("EE", error);
return true;
});
}
});
}
}
});

@ -117,7 +117,7 @@ function setify(items: Array<string>): ReadonlySet<string> {
const _kwVisib = "constant external internal payable private public pure view"; const _kwVisib = "constant external internal payable private public pure view";
const KwVisib = setify(_kwVisib.split(" ")); const KwVisib = setify(_kwVisib.split(" "));
const _kwTypes = "constructor error event function struct"; const _kwTypes = "constructor error event fallback function receive struct";
const KwTypes = setify(_kwTypes.split(" ")); const KwTypes = setify(_kwTypes.split(" "));
const _kwModifiers = "calldata memory storage payable indexed"; const _kwModifiers = "calldata memory storage payable indexed";
@ -420,7 +420,7 @@ function consumeKeywords(tokens: TokenString, allowed?: ReadonlySet<string>): Re
} }
// ...all visibility keywords, returning the coalesced mutability // ...all visibility keywords, returning the coalesced mutability
function consumeMutability(tokens: TokenString): string { function consumeMutability(tokens: TokenString): "payable" | "nonpayable" | "view" | "pure" {
let modifiers = consumeKeywords(tokens, KwVisib); let modifiers = consumeKeywords(tokens, KwVisib);
// Detect conflicting modifiers // Detect conflicting modifiers
@ -506,6 +506,7 @@ const ParamTypeInternal = "_ParamTypeInternal";
const ErrorFragmentInternal = "_ErrorInternal"; const ErrorFragmentInternal = "_ErrorInternal";
const EventFragmentInternal = "_EventInternal"; const EventFragmentInternal = "_EventInternal";
const ConstructorFragmentInternal = "_ConstructorInternal"; const ConstructorFragmentInternal = "_ConstructorInternal";
const FallbackFragmentInternal = "_FallbackInternal";
const FunctionFragmentInternal = "_FunctionInternal"; const FunctionFragmentInternal = "_FunctionInternal";
const StructFragmentInternal = "_StructInternal"; const StructFragmentInternal = "_StructInternal";
@ -885,7 +886,7 @@ export class ParamType {
/** /**
* The type of a [[Fragment]]. * The type of a [[Fragment]].
*/ */
export type FragmentType = "constructor" | "error" | "event" | "function" | "struct"; export type FragmentType = "constructor" | "error" | "event" | "fallback" | "function" | "struct";
/** /**
* An abstract class to represent An individual fragment from a parse ABI. * An abstract class to represent An individual fragment from a parse ABI.
@ -921,39 +922,50 @@ export abstract class Fragment {
*/ */
static from(obj: any): Fragment { static from(obj: any): Fragment {
if (typeof(obj) === "string") { if (typeof(obj) === "string") {
// Try parsing JSON...
try { try {
Fragment.from(JSON.parse(obj)); Fragment.from(JSON.parse(obj));
} catch (e) { } } catch (e) { }
// ...otherwise, use the human-readable lexer
return Fragment.from(lex(obj)); return Fragment.from(lex(obj));
} }
if (obj instanceof TokenString) { if (obj instanceof TokenString) {
const type = obj.popKeyword(KwTypes); // Human-readable ABI (already lexed)
const type = obj.peekKeyword(KwTypes);
switch (type) { switch (type) {
case "constructor": return ConstructorFragment.from(obj); case "constructor": return ConstructorFragment.from(obj);
case "error": return ErrorFragment.from(obj); case "error": return ErrorFragment.from(obj);
case "event": return EventFragment.from(obj); case "event": return EventFragment.from(obj);
case "fallback": case "receive":
return FallbackFragment.from(obj);
case "function": return FunctionFragment.from(obj); case "function": return FunctionFragment.from(obj);
case "struct": return StructFragment.from(obj); case "struct": return StructFragment.from(obj);
} }
throw new Error(`unsupported type: ${ type }`); } else if (typeof(obj) === "object") {
} // JSON ABI
if (typeof(obj) === "object") {
switch (obj.type) { switch (obj.type) {
case "constructor": return ConstructorFragment.from(obj); case "constructor": return ConstructorFragment.from(obj);
case "error": return ErrorFragment.from(obj); case "error": return ErrorFragment.from(obj);
case "event": return EventFragment.from(obj); case "event": return EventFragment.from(obj);
case "fallback": case "receive":
return FallbackFragment.from(obj);
case "function": return FunctionFragment.from(obj); case "function": return FunctionFragment.from(obj);
case "struct": return StructFragment.from(obj); case "struct": return StructFragment.from(obj);
} }
throw new Error(`not implemented yet: ${ obj.type }`);
assert(false, `unsupported type: ${ obj.type }`, "UNSUPPORTED_OPERATION", {
operation: "Fragment.from"
});
} }
throw new Error(`unsupported type: ${ obj }`); assertArgument(false, "unsupported frgament object", "obj", obj);
} }
/** /**
@ -1202,6 +1214,101 @@ export class ConstructorFragment extends Fragment {
} }
} }
/**
* A Fragment which represents a method.
*/
export class FallbackFragment extends Fragment {
/**
* If the function can be sent value during invocation.
*/
readonly payable!: boolean;
constructor(guard: any, inputs: ReadonlyArray<ParamType>, payable: boolean) {
super(guard, "fallback", inputs);
Object.defineProperty(this, internal, { value: FallbackFragmentInternal });
defineProperties<FallbackFragment>(this, { payable });
}
format(format?: FormatType): string {
const type = ((this.inputs.length === 0) ? "receive": "fallback");
if (format === "json") {
const stateMutability = (this.payable ? "payable": "nonpayable");
return JSON.stringify({ type, stateMutability });
}
return `${ type }()${ this.payable ? " payable": "" }`;
}
static from(obj: any): FallbackFragment {
if (FallbackFragment.isFragment(obj)) { return obj; }
if (typeof(obj) === "string") {
return FallbackFragment.from(lex(obj));
} else if (obj instanceof TokenString) {
const errorObj = obj.toString();
const topIsValid = obj.peekKeyword(setify([ "fallback", "receive" ]));
assertArgument(topIsValid, "type must be fallback or receive", "obj", errorObj);
const type = obj.popKeyword(setify([ "fallback", "receive" ]));
// receive()
if (type === "receive") {
const inputs = consumeParams(obj);
assertArgument(inputs.length === 0, `receive cannot have arguments`, "obj.inputs", inputs);
consumeKeywords(obj, setify([ "payable" ]));
consumeEoi(obj);
return new FallbackFragment(_guard, [ ], true);
}
// fallback() [payable]
// fallback(bytes) [payable] returns (bytes)
let inputs = consumeParams(obj);
if (inputs.length) {
assertArgument(inputs.length === 1 && inputs[0].type === "bytes",
"invalid fallback inputs", "obj.inputs",
inputs.map((i) => i.format("minimal")).join(", "));
} else {
inputs = [ ParamType.from("bytes") ];
}
const mutability = consumeMutability(obj);
assertArgument(mutability === "nonpayable" || mutability === "payable", "fallback cannot be constants", "obj.stateMutability", mutability);
if (consumeKeywords(obj, setify([ "returns" ])).has("returns")) {
const outputs = consumeParams(obj);
assertArgument(outputs.length === 1 && outputs[0].type === "bytes",
"invalid fallback outputs", "obj.outputs",
outputs.map((i) => i.format("minimal")).join(", "));
}
consumeEoi(obj);
return new FallbackFragment(_guard, inputs, mutability === "payable");
}
if (obj.type === "receive") {
return new FallbackFragment(_guard, [ ], true);
}
if (obj.type === "fallback") {
const inputs = [ ParamType.from("bytes") ];
const payable = (obj.stateMutability === "payable");
return new FallbackFragment(_guard, inputs, payable);
}
assertArgument(false, "invalid fallback description", "obj", obj);
}
static isFragment(value: any): value is FallbackFragment {
return (value && value[internal] === FallbackFragmentInternal);
}
}
/** /**
* A Fragment which represents a method. * A Fragment which represents a method.
*/ */
@ -1220,10 +1327,10 @@ export class FunctionFragment extends NamedFragment {
* The state mutability (e.g. ``payable``, ``nonpayable``, ``view`` * The state mutability (e.g. ``payable``, ``nonpayable``, ``view``
* or ``pure``) * or ``pure``)
*/ */
readonly stateMutability!: string; readonly stateMutability!: "payable" | "nonpayable" | "view" | "pure";
/** /**
* If the function can be send a value during invocation. * If the function can be sent value during invocation.
*/ */
readonly payable!: boolean; readonly payable!: boolean;
@ -1235,7 +1342,7 @@ export class FunctionFragment extends NamedFragment {
/** /**
* @private * @private
*/ */
constructor(guard: any, name: string, stateMutability: string, inputs: ReadonlyArray<ParamType>, outputs: ReadonlyArray<ParamType>, gas: null | bigint) { constructor(guard: any, name: string, stateMutability: "payable" | "nonpayable" | "view" | "pure", inputs: ReadonlyArray<ParamType>, outputs: ReadonlyArray<ParamType>, gas: null | bigint) {
super(guard, "function", name, inputs); super(guard, "function", name, inputs);
Object.defineProperty(this, internal, { value: FunctionFragmentInternal }); Object.defineProperty(this, internal, { value: FunctionFragmentInternal });
outputs = Object.freeze(outputs.slice()); outputs = Object.freeze(outputs.slice());

@ -12,8 +12,8 @@ export { AbiCoder } from "./abi-coder.js";
export { decodeBytes32String, encodeBytes32String } from "./bytes32.js"; export { decodeBytes32String, encodeBytes32String } from "./bytes32.js";
export { export {
ConstructorFragment, ErrorFragment, EventFragment, Fragment, ConstructorFragment, ErrorFragment, EventFragment, FallbackFragment,
FunctionFragment, NamedFragment, ParamType, StructFragment, Fragment, FunctionFragment, NamedFragment, ParamType, StructFragment,
} from "./fragments.js"; } from "./fragments.js";
export { export {

@ -14,7 +14,10 @@ import {
import { AbiCoder } from "./abi-coder.js"; import { AbiCoder } from "./abi-coder.js";
import { checkResultErrors, Result } from "./coders/abstract-coder.js"; import { checkResultErrors, Result } from "./coders/abstract-coder.js";
import { ConstructorFragment, ErrorFragment, EventFragment, Fragment, FunctionFragment, ParamType } from "./fragments.js"; import {
ConstructorFragment, ErrorFragment, EventFragment, FallbackFragment,
Fragment, FunctionFragment, ParamType
} from "./fragments.js";
import { Typed } from "./typed.js"; import { Typed } from "./typed.js";
import type { BigNumberish, BytesLike, CallExceptionError, CallExceptionTransaction } from "../utils/index.js"; import type { BigNumberish, BytesLike, CallExceptionError, CallExceptionTransaction } from "../utils/index.js";
@ -175,6 +178,16 @@ export class Interface {
*/ */
readonly deploy!: ConstructorFragment; readonly deploy!: ConstructorFragment;
/**
* The Fallback method, if any.
*/
readonly fallback!: null | FallbackFragment;
/**
* If receiving ether is supported.
*/
readonly receive!: boolean;
#errors: Map<string, ErrorFragment>; #errors: Map<string, ErrorFragment>;
#events: Map<string, EventFragment>; #events: Map<string, EventFragment>;
#functions: Map<string, FunctionFragment>; #functions: Map<string, FunctionFragment>;
@ -212,10 +225,13 @@ export class Interface {
fragments: Object.freeze(frags) fragments: Object.freeze(frags)
}); });
let fallback: null | FallbackFragment = null;
let receive = false;
this.#abiCoder = this.getAbiCoder(); this.#abiCoder = this.getAbiCoder();
// Add all fragments by their signature // Add all fragments by their signature
this.fragments.forEach((fragment) => { this.fragments.forEach((fragment, index) => {
let bucket: Map<string, Fragment>; let bucket: Map<string, Fragment>;
switch (fragment.type) { switch (fragment.type) {
case "constructor": case "constructor":
@ -227,6 +243,17 @@ export class Interface {
defineProperties<Interface>(this, { deploy: <ConstructorFragment>fragment }); defineProperties<Interface>(this, { deploy: <ConstructorFragment>fragment });
return; return;
case "fallback":
if (fragment.inputs.length === 0) {
receive = true;
} else {
assertArgument(!fallback || (<FallbackFragment>fragment).payable !== fallback.payable,
"conflicting fallback fragments", `fragments[${ index }]`, fragment);
fallback = <FallbackFragment>fragment;
receive = fallback.payable;
}
return;
case "function": case "function":
//checkNames(fragment, "input", fragment.inputs); //checkNames(fragment, "input", fragment.inputs);
//checkNames(fragment, "output", (<FunctionFragment>fragment).outputs); //checkNames(fragment, "output", (<FunctionFragment>fragment).outputs);
@ -259,6 +286,8 @@ export class Interface {
deploy: ConstructorFragment.from("constructor()") deploy: ConstructorFragment.from("constructor()")
}); });
} }
defineProperties<Interface>(this, { fallback, receive });
} }
/** /**

@ -33,6 +33,8 @@ import type {
DeferredTopicFilter DeferredTopicFilter
} from "./types.js"; } from "./types.js";
const BN_0 = BigInt(0);
interface ContractRunnerCaller extends ContractRunner { interface ContractRunnerCaller extends ContractRunner {
call: (tx: TransactionRequest) => Promise<string>; call: (tx: TransactionRequest) => Promise<string>;
} }
@ -129,25 +131,22 @@ function getProvider(value: null | ContractRunner): null | Provider {
/** /**
* @_ignore: * @_ignore:
*/ */
export async function copyOverrides(arg: any): Promise<Omit<ContractTransaction, "data" | "to">> { export async function copyOverrides<O extends string = "data" | "to">(arg: any, allowed?: Array<string>): Promise<Omit<ContractTransaction, O>> {
// Create a shallow copy (we'll deep-ify anything needed during normalizing) // Create a shallow copy (we'll deep-ify anything needed during normalizing)
const overrides = copyRequest(Typed.dereference(arg, "overrides")); const overrides = copyRequest(Typed.dereference(arg, "overrides"));
// Some sanity checking; these are what these methods adds assertArgument(overrides.to == null || (allowed || [ ]).indexOf("to") >= 0,
//if ((<any>overrides).to) { "cannot override to", "overrides.to", overrides.to);
if (overrides.to) { assertArgument(overrides.data == null || (allowed || [ ]).indexOf("data") >= 0,
assertArgument(false, "cannot override to", "overrides.to", overrides.to); "cannot override data", "overrides.data", overrides.data);
} else if (overrides.data) {
assertArgument(false, "cannot override data", "overrides.data", overrides.data);
}
// Resolve any from // Resolve any from
if (overrides.from) { if (overrides.from) {
overrides.from = await resolveAddress(overrides.from); overrides.from = await resolveAddress(overrides.from);
} }
return <Omit<ContractTransaction, "data" | "to">>overrides; return <Omit<ContractTransaction, O>>overrides;
} }
/** /**
@ -166,6 +165,80 @@ export async function resolveArgs(_runner: null | ContractRunner, inputs: Readon
})); }));
} }
class WrappedFallback {
readonly _contract!: BaseContract;
constructor (contract: BaseContract) {
defineProperties<WrappedFallback>(this, { _contract: contract });
const proxy = new Proxy(this, {
// Perform send when called
apply: async (target, thisArg, args: Array<any>) => {
return await target.send(...args);
},
});
return proxy;
}
async populateTransaction(overrides?: Omit<TransactionRequest, "to">): Promise<ContractTransaction> {
// If an overrides was passed in, copy it and normalize the values
const tx: ContractTransaction = <any>(await copyOverrides<"data">(overrides, [ "data" ]));
tx.to = await this._contract.getAddress();
const iface = this._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;
}
async staticCall(overrides?: Omit<TransactionRequest, "to">): Promise<string> {
const runner = getRunner(this._contract.runner, "call");
assert(canCall(runner), "contract runner does not support calling",
"UNSUPPORTED_OPERATION", { operation: "call" });
const tx = await this.populateTransaction(overrides);
try {
return await runner.call(tx);
} catch (error: any) {
if (isCallException(error) && error.data) {
throw this._contract.interface.makeError(error.data, tx);
}
throw error;
}
}
async send(overrides?: Omit<TransactionRequest, "to">): Promise<ContractTransactionResponse> {
const runner = this._contract.runner;
assert(canSend(runner), "contract runner does not support sending transactions",
"UNSUPPORTED_OPERATION", { operation: "sendTransaction" });
const tx = await runner.sendTransaction(await this.populateTransaction(overrides));
const provider = getProvider(this._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>provider, tx);
}
async estimateGas(overrides?: Omit<TransactionRequest, "to">): Promise<bigint> {
const runner = getRunner(this._contract.runner, "estimateGas");
assert(canEstimate(runner), "contract runner does not support gas estimation",
"UNSUPPORTED_OPERATION", { operation: "estimateGas" });
return await runner.estimateGas(await this.populateTransaction(overrides));
}
}
class WrappedMethod<A extends Array<any> = Array<any>, R = any, D extends R | ContractTransactionResponse = ContractTransactionResponse> class WrappedMethod<A extends Array<any> = Array<any>, R = any, D extends R | ContractTransactionResponse = ContractTransactionResponse>
extends _WrappedMethodBase() implements BaseContractMethod<A, R, D> { extends _WrappedMethodBase() implements BaseContractMethod<A, R, D> {
@ -545,6 +618,8 @@ export class BaseContract implements Addressable, EventEmitterable<ContractEvent
readonly [internal]: any; readonly [internal]: any;
readonly fallback!: null | WrappedFallback;
constructor(target: string | Addressable, abi: Interface | InterfaceAbi, runner?: null | ContractRunner, _deployTx?: null | TransactionResponse) { constructor(target: string | Addressable, abi: Interface | InterfaceAbi, runner?: null | ContractRunner, _deployTx?: null | TransactionResponse) {
if (runner == null) { runner = null; } if (runner == null) { runner = null; }
const iface = Interface.from(abi); const iface = Interface.from(abi);
@ -614,6 +689,10 @@ export class BaseContract implements Addressable, EventEmitterable<ContractEvent
}); });
defineProperties<BaseContract>(this, { filters }); defineProperties<BaseContract>(this, { filters });
defineProperties<BaseContract>(this, {
fallback: ((iface.receive || iface.fallback) ? (new WrappedFallback(this)): null)
});
// Return a Proxy that will respond to functions // Return a Proxy that will respond to functions
return new Proxy(this, { return new Proxy(this, {
get: (target, _prop, receiver) => { get: (target, _prop, receiver) => {