Major Contract refactor for overrides (#819, #845, #847, #860).

This commit is contained in:
Richard Moore 2020-06-01 04:46:37 -04:00
parent 7f5035bb05
commit 42dee67187
No known key found for this signature in database
GPG Key ID: 665176BE8E9DC651
11 changed files with 335 additions and 151 deletions

View File

@ -3,7 +3,7 @@
import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; import { BigNumber, BigNumberish } from "@ethersproject/bignumber";
import { BytesLike, isHexString } from "@ethersproject/bytes"; import { BytesLike, isHexString } from "@ethersproject/bytes";
import { Network } from "@ethersproject/networks"; import { Network } from "@ethersproject/networks";
import { Description, defineReadOnly } from "@ethersproject/properties"; import { Deferrable, Description, defineReadOnly } from "@ethersproject/properties";
import { Transaction } from "@ethersproject/transactions"; import { Transaction } from "@ethersproject/transactions";
import { OnceBlockable } from "@ethersproject/web"; import { OnceBlockable } from "@ethersproject/web";
@ -16,16 +16,16 @@ const logger = new Logger(version);
export type TransactionRequest = { export type TransactionRequest = {
to?: string | Promise<string>, to?: string,
from?: string | Promise<string>, from?: string,
nonce?: BigNumberish | Promise<BigNumberish>, nonce?: BigNumberish,
gasLimit?: BigNumberish | Promise<BigNumberish>, gasLimit?: BigNumberish,
gasPrice?: BigNumberish | Promise<BigNumberish>, gasPrice?: BigNumberish,
data?: BytesLike | Promise<BytesLike>, data?: BytesLike,
value?: BigNumberish | Promise<BigNumberish>, value?: BigNumberish,
chainId?: number | Promise<number>, chainId?: number
} }
export interface TransactionResponse extends Transaction { export interface TransactionResponse extends Transaction {
@ -221,8 +221,8 @@ export abstract class Provider implements OnceBlockable {
// Execution // Execution
abstract sendTransaction(signedTransaction: string | Promise<string>): Promise<TransactionResponse>; abstract sendTransaction(signedTransaction: string | Promise<string>): Promise<TransactionResponse>;
abstract call(transaction: TransactionRequest, blockTag?: BlockTag | Promise<BlockTag>): Promise<string>; abstract call(transaction: Deferrable<TransactionRequest>, blockTag?: BlockTag | Promise<BlockTag>): Promise<string>;
abstract estimateGas(transaction: TransactionRequest): Promise<BigNumber>; abstract estimateGas(transaction: Deferrable<TransactionRequest>): Promise<BigNumber>;
// Queries // Queries
abstract getBlock(blockHashOrBlockTag: BlockTag | string | Promise<BlockTag | string>): Promise<Block>; abstract getBlock(blockHashOrBlockTag: BlockTag | string | Promise<BlockTag | string>): Promise<Block>;

View File

@ -3,7 +3,7 @@
import { BlockTag, Provider, TransactionRequest, TransactionResponse } from "@ethersproject/abstract-provider"; import { BlockTag, Provider, TransactionRequest, TransactionResponse } from "@ethersproject/abstract-provider";
import { BigNumber } from "@ethersproject/bignumber"; import { BigNumber } from "@ethersproject/bignumber";
import { Bytes } from "@ethersproject/bytes"; import { Bytes } from "@ethersproject/bytes";
import { defineReadOnly, resolveProperties, shallowCopy } from "@ethersproject/properties"; import { Deferrable, defineReadOnly, resolveProperties, shallowCopy } from "@ethersproject/properties";
import { Logger } from "@ethersproject/logger"; import { Logger } from "@ethersproject/logger";
import { version } from "./_version"; import { version } from "./_version";
@ -46,7 +46,7 @@ export abstract class Signer {
// The EXACT transaction MUST be signed, and NO additional properties to be added. // The EXACT transaction MUST be signed, and NO additional properties to be added.
// - This MAY throw if signing transactions is not supports, but if // - This MAY throw if signing transactions is not supports, but if
// it does, sentTransaction MUST be overridden. // it does, sentTransaction MUST be overridden.
abstract signTransaction(transaction: TransactionRequest): Promise<string>; abstract signTransaction(transaction: Deferrable<TransactionRequest>): Promise<string>;
// Returns a new instance of the Signer, connected to provider. // Returns a new instance of the Signer, connected to provider.
// This MAY throw if changing providers is not supported. // This MAY throw if changing providers is not supported.
@ -77,21 +77,21 @@ export abstract class Signer {
} }
// Populates "from" if unspecified, and estimates the gas for the transation // Populates "from" if unspecified, and estimates the gas for the transation
async estimateGas(transaction: TransactionRequest): Promise<BigNumber> { async estimateGas(transaction: Deferrable<TransactionRequest>): Promise<BigNumber> {
this._checkProvider("estimateGas"); this._checkProvider("estimateGas");
const tx = await resolveProperties(this.checkTransaction(transaction)); const tx = await resolveProperties(this.checkTransaction(transaction));
return await this.provider.estimateGas(tx); return await this.provider.estimateGas(tx);
} }
// Populates "from" if unspecified, and calls with the transation // Populates "from" if unspecified, and calls with the transation
async call(transaction: TransactionRequest, blockTag?: BlockTag): Promise<string> { async call(transaction: Deferrable<TransactionRequest>, blockTag?: BlockTag): Promise<string> {
this._checkProvider("call"); this._checkProvider("call");
const tx = await resolveProperties(this.checkTransaction(transaction)); const tx = await resolveProperties(this.checkTransaction(transaction));
return await this.provider.call(tx, blockTag); return await this.provider.call(tx, blockTag);
} }
// Populates all fields in a transaction, signs it and sends it to the network // Populates all fields in a transaction, signs it and sends it to the network
sendTransaction(transaction: TransactionRequest): Promise<TransactionResponse> { sendTransaction(transaction: Deferrable<TransactionRequest>): Promise<TransactionResponse> {
this._checkProvider("sendTransaction"); this._checkProvider("sendTransaction");
return this.populateTransaction(transaction).then((tx) => { return this.populateTransaction(transaction).then((tx) => {
return this.signTransaction(tx).then((signedTx) => { return this.signTransaction(tx).then((signedTx) => {
@ -128,7 +128,7 @@ export abstract class Signer {
// - call // - call
// - estimateGas // - estimateGas
// - populateTransaction (and therefor sendTransaction) // - populateTransaction (and therefor sendTransaction)
checkTransaction(transaction: TransactionRequest): TransactionRequest { checkTransaction(transaction: Deferrable<TransactionRequest>): Deferrable<TransactionRequest> {
for (const key in transaction) { for (const key in transaction) {
if (allowedTransactionKeys.indexOf(key) === -1) { if (allowedTransactionKeys.indexOf(key) === -1) {
logger.throwArgumentError("invalid transaction key: " + key, "transaction", transaction); logger.throwArgumentError("invalid transaction key: " + key, "transaction", transaction);
@ -159,9 +159,9 @@ export abstract class Signer {
// this Signer. Should be used by sendTransaction but NOT by signTransaction. // this Signer. Should be used by sendTransaction but NOT by signTransaction.
// By default called from: (overriding these prevents it) // By default called from: (overriding these prevents it)
// - sendTransaction // - sendTransaction
async populateTransaction(transaction: TransactionRequest): Promise<TransactionRequest> { async populateTransaction(transaction: Deferrable<TransactionRequest>): Promise<TransactionRequest> {
const tx: TransactionRequest = await resolveProperties(this.checkTransaction(transaction)) const tx: Deferrable<TransactionRequest> = await resolveProperties(this.checkTransaction(transaction))
if (tx.to != null) { tx.to = Promise.resolve(tx.to).then((to) => this.resolveName(to)); } if (tx.to != null) { tx.to = Promise.resolve(tx.to).then((to) => this.resolveName(to)); }
if (tx.gasPrice == null) { tx.gasPrice = this.getGasPrice(); } if (tx.gasPrice == null) { tx.gasPrice = this.getGasPrice(); }
@ -232,7 +232,7 @@ export class VoidSigner extends Signer {
return this._fail("VoidSigner cannot sign messages", "signMessage"); return this._fail("VoidSigner cannot sign messages", "signMessage");
} }
signTransaction(transaction: TransactionRequest): Promise<string> { signTransaction(transaction: Deferrable<TransactionRequest>): Promise<string> {
return this._fail("VoidSigner cannot sign transactions", "signTransaction"); return this._fail("VoidSigner cannot sign transactions", "signTransaction");
} }

View File

@ -3,11 +3,12 @@
import { checkResultErrors, EventFragment, Fragment, FunctionFragment, Indexed, Interface, JsonFragment, LogDescription, ParamType, Result } from "@ethersproject/abi"; import { checkResultErrors, EventFragment, Fragment, FunctionFragment, Indexed, Interface, JsonFragment, LogDescription, ParamType, Result } from "@ethersproject/abi";
import { Block, BlockTag, Filter, FilterByBlockHash, Listener, Log, Provider, TransactionReceipt, TransactionRequest, TransactionResponse } from "@ethersproject/abstract-provider"; import { Block, BlockTag, Filter, FilterByBlockHash, Listener, Log, Provider, TransactionReceipt, TransactionRequest, TransactionResponse } from "@ethersproject/abstract-provider";
import { Signer, VoidSigner } from "@ethersproject/abstract-signer"; import { Signer, VoidSigner } from "@ethersproject/abstract-signer";
import { getContractAddress } from "@ethersproject/address"; import { getAddress, getContractAddress } from "@ethersproject/address";
import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; import { BigNumber, BigNumberish } from "@ethersproject/bignumber";
import { BytesLike, concat, hexlify, isBytes, isHexString } from "@ethersproject/bytes"; import { BytesLike, concat, hexlify, isBytes, isHexString } from "@ethersproject/bytes";
import { defineReadOnly, deepCopy, getStatic, resolveProperties, shallowCopy } from "@ethersproject/properties"; //import { AddressZero } from "@ethersproject/constants";
import { UnsignedTransaction } from "@ethersproject/transactions"; import { Deferrable, defineReadOnly, deepCopy, getStatic, resolveProperties, shallowCopy } from "@ethersproject/properties";
// @TOOD remove dependences transactions
import { Logger } from "@ethersproject/logger"; import { Logger } from "@ethersproject/logger";
import { version } from "./_version"; import { version } from "./_version";
@ -18,7 +19,7 @@ export interface Overrides {
gasLimit?: BigNumberish | Promise<BigNumberish>; gasLimit?: BigNumberish | Promise<BigNumberish>;
gasPrice?: BigNumberish | Promise<BigNumberish>; gasPrice?: BigNumberish | Promise<BigNumberish>;
nonce?: BigNumberish | Promise<BigNumberish>; nonce?: BigNumberish | Promise<BigNumberish>;
} };
export interface PayableOverrides extends Overrides { export interface PayableOverrides extends Overrides {
value?: BigNumberish | Promise<BigNumberish>; value?: BigNumberish | Promise<BigNumberish>;
@ -26,9 +27,26 @@ export interface PayableOverrides extends Overrides {
export interface CallOverrides extends PayableOverrides { export interface CallOverrides extends PayableOverrides {
blockTag?: BlockTag | Promise<BlockTag>; blockTag?: BlockTag | Promise<BlockTag>;
from?: string | Promise<string> from?: string | Promise<string>;
} }
// @TODO: Better hierarchy with: (in v6)
// - abstract-provider:TransactionRequest
// - transactions:Transaction
// - transaction:UnsignedTransaction
export interface PopulatedTransaction {
to?: string;
from?: string;
nonce?: number;
gasLimit?: BigNumber;
gasPrice?: BigNumber;
data?: string;
value?: BigNumber;
chainId?: number;
};
export type EventFilter = { export type EventFilter = {
address?: string; address?: string;
@ -80,13 +98,29 @@ const allowedTransactionKeys: { [ key: string ]: boolean } = {
chainId: true, data: true, from: true, gasLimit: true, gasPrice:true, nonce: true, to: true, value: true chainId: true, data: true, from: true, gasLimit: true, gasPrice:true, nonce: true, to: true, value: true
} }
async function resolveName(resolver: Signer | Provider, nameOrPromise: string | Promise<string>): Promise<string> {
const name = await nameOrPromise;
// If it is already an address, just use it (after adding checksum)
try {
return getAddress(name);
} catch (error) { }
if (!resolver) {
logger.throwError("a provider or signer is needed to resolve ENS names", Logger.errors.UNSUPPORTED_OPERATION, {
operation: "resolveName"
});
}
return await resolver.resolveName(name);
}
// Recursively replaces ENS names with promises to resolve the name and resolves all properties // Recursively replaces ENS names with promises to resolve the name and resolves all properties
function resolveAddresses(signerOrProvider: Signer | Provider, value: any, paramType: ParamType | Array<ParamType>): Promise<any> { function resolveAddresses(resolver: Signer | Provider, value: any, paramType: ParamType | Array<ParamType>): Promise<any> {
if (Array.isArray(paramType)) { if (Array.isArray(paramType)) {
return Promise.all(paramType.map((paramType, index) => { return Promise.all(paramType.map((paramType, index) => {
return resolveAddresses( return resolveAddresses(
signerOrProvider, resolver,
((Array.isArray(value)) ? value[index]: value[paramType.name]), ((Array.isArray(value)) ? value[index]: value[paramType.name]),
paramType paramType
); );
@ -94,25 +128,62 @@ function resolveAddresses(signerOrProvider: Signer | Provider, value: any, param
} }
if (paramType.type === "address") { if (paramType.type === "address") {
return signerOrProvider.resolveName(value); return resolveName(resolver, value);
} }
if (paramType.type === "tuple") { if (paramType.type === "tuple") {
return resolveAddresses(signerOrProvider, value, paramType.components); return resolveAddresses(resolver, value, paramType.components);
} }
if (paramType.baseType === "array") { if (paramType.baseType === "array") {
if (!Array.isArray(value)) { throw new Error("invalid value for array"); } if (!Array.isArray(value)) { throw new Error("invalid value for array"); }
return Promise.all(value.map((v) => resolveAddresses(signerOrProvider, v, paramType.arrayChildren))); return Promise.all(value.map((v) => resolveAddresses(resolver, v, paramType.arrayChildren)));
} }
return Promise.resolve(value); return Promise.resolve(value);
} }
async function _populateTransaction(contract: Contract, fragment: FunctionFragment, args: Array<any>, overrides?: Overrides): Promise<UnsignedTransaction> { async function populateTransaction(contract: Contract, fragment: FunctionFragment, args: Array<any>): Promise<PopulatedTransaction> {
overrides = shallowCopy(overrides);
// Wait for all dependency addresses to be resolved (prefer the signer over the provider) // If an extra argument is given, it is overrides
let overrides: CallOverrides = { };
if (args.length === fragment.inputs.length + 1 && typeof(args[args.length - 1]) === "object") {
overrides = shallowCopy(args.pop());
}
// Make sure the parameter count matches
logger.checkArgumentCount(args.length, fragment.inputs.length, "passed to contract");
// Populate "from" override (allow promises)
if (contract.signer) {
if (overrides.from) {
// Contracts with a Signer are from the Signer's frame-of-reference;
// but we allow overriding "from" if it matches the signer
overrides.from = resolveProperties({
override: resolveName(contract.signer, overrides.from),
signer: contract.signer.getAddress()
}).then(async (check) => {
if (getAddress(check.signer) !== check.override) {
logger.throwError("Contract with a Signer cannot override from", Logger.errors.UNSUPPORTED_OPERATION, {
operation: "overrides.from"
});
}
return check.override;
});
} else {
overrides.from = contract.signer.getAddress();
}
} else if (overrides.from) {
overrides.from = resolveName(contract.provider, overrides.from);
//} else {
// Contracts without a signer can override "from", and if
// unspecified the zero address is used
//overrides.from = AddressZero;
}
// Wait for all dependencies to be resolved (prefer the signer over the provider)
const resolved = await resolveProperties({ const resolved = await resolveProperties({
args: resolveAddresses(contract.signer || contract.provider, args, fragment.inputs), args: resolveAddresses(contract.signer || contract.provider, args, fragment.inputs),
address: contract.resolvedAddress, address: contract.resolvedAddress,
@ -120,28 +191,43 @@ async function _populateTransaction(contract: Contract, fragment: FunctionFragme
}); });
// The ABI coded transaction // The ABI coded transaction
const tx: UnsignedTransaction = { const tx: PopulatedTransaction = {
data: contract.interface.encodeFunctionData(fragment, resolved.args), data: contract.interface.encodeFunctionData(fragment, resolved.args),
to: resolved.address to: resolved.address
}; };
// Resolved Overrides // Resolved Overrides
const ro = resolved.overrides; const ro = resolved.overrides;
// Populate simple overrides
if (ro.nonce != null) { tx.nonce = BigNumber.from(ro.nonce).toNumber(); } if (ro.nonce != null) { tx.nonce = BigNumber.from(ro.nonce).toNumber(); }
if (ro.gasLimit != null) { tx.gasLimit = BigNumber.from(ro.gasLimit); } if (ro.gasLimit != null) { tx.gasLimit = BigNumber.from(ro.gasLimit); }
if (ro.gasPrice != null) { tx.gasPrice = BigNumber.from(ro.gasPrice); } if (ro.gasPrice != null) { tx.gasPrice = BigNumber.from(ro.gasPrice); }
if (ro.from != null) { tx.from = ro.from; }
// If there was no gasLimit override, but the ABI specifies one use it // If there was no "gasLimit" override, but the ABI specifies a default, use it
if (tx.gasLimit == null && fragment.gas != null) { if (tx.gasLimit == null && fragment.gas != null) {
tx.gasLimit = BigNumber.from(fragment.gas).add(21000); tx.gasLimit = BigNumber.from(fragment.gas).add(21000);
} }
// Populate "value" override
if (ro.value) {
const roValue = BigNumber.from(ro.value);
if (!roValue.isZero() && !fragment.payable) {
logger.throwError("non-payable method cannot override value", Logger.errors.UNSUPPORTED_OPERATION, {
operation: "overrides.value",
value: overrides.value
});
}
tx.value = roValue;
}
// Remvoe the overrides // Remvoe the overrides
delete overrides.nonce; delete overrides.nonce;
delete overrides.gasLimit; delete overrides.gasLimit;
delete overrides.gasPrice; delete overrides.gasPrice;
delete overrides.from;
// @TODO: Maybe move all tx property validation to the Signer and Provider? delete overrides.value;
// Make sure there are no stray overrides, which may indicate a // Make sure there are no stray overrides, which may indicate a
// typo or using an unsupported key. // typo or using an unsupported key.
@ -149,122 +235,64 @@ async function _populateTransaction(contract: Contract, fragment: FunctionFragme
if (leftovers.length) { if (leftovers.length) {
logger.throwError(`cannot override ${ leftovers.map((l) => JSON.stringify(l)).join(",") }`, Logger.errors.UNSUPPORTED_OPERATION, { logger.throwError(`cannot override ${ leftovers.map((l) => JSON.stringify(l)).join(",") }`, Logger.errors.UNSUPPORTED_OPERATION, {
operation: "overrides", operation: "overrides",
keys: leftovers overrides: leftovers
}); });
} }
return tx;
}
async function populateTransaction(contract: Contract, fragment: FunctionFragment, args: Array<any>, overrides?: PayableOverrides): Promise<UnsignedTransaction> {
overrides = shallowCopy(overrides);
// If the contract was just deployed, wait until it is minded
if (contract.deployTransaction != null) {
await contract._deployed();
}
// Resolved Overrides (keep value for errors)
const ro = await resolveProperties(overrides);
const value = overrides.value;
delete overrides.value;
const tx = await _populateTransaction(contract, fragment, args, overrides);
if (ro.value) {
const roValue = BigNumber.from(ro.value);
if (!roValue.isZero() && !fragment.payable) {
logger.throwError("non-payable method cannot override value", Logger.errors.UNSUPPORTED_OPERATION, {
operation: "overrides.value",
value: value
});
}
tx.value = roValue;
}
return tx;
}
async function populateCallTransaction(contract: Contract, fragment: FunctionFragment, args: Array<any>, overrides?: CallOverrides): Promise<UnsignedTransaction> {
overrides = shallowCopy(overrides);
// If the contract was just deployed, wait until it is minded
if (contract.deployTransaction != null) {
let blockTag = undefined;
if (overrides.blockTag) { blockTag = await overrides.blockTag; }
await contract._deployed(blockTag);
}
// Resolved Overrides
delete overrides.blockTag;
const ro = await resolveProperties(overrides);
delete overrides.from;
const tx = await populateTransaction(contract, fragment, args, overrides);
if (ro.from) { (<any>tx).from = this.interface.constructor.getAddress(ro.from); }
return tx; return tx;
} }
function buildPopulate(contract: Contract, fragment: FunctionFragment): ContractFunction<UnsignedTransaction> { function buildPopulate(contract: Contract, fragment: FunctionFragment): ContractFunction<PopulatedTransaction> {
const populate = (fragment.constant) ? populateCallTransaction: populateTransaction; return async function(...args: Array<any>): Promise<PopulatedTransaction> {
return async function(...args: Array<any>): Promise<UnsignedTransaction> { return populateTransaction(contract, fragment, args);
let overrides: CallOverrides = null;
if (args.length === fragment.inputs.length + 1 && typeof(args[args.length - 1]) === "object") {
overrides = args.pop();
}
logger.checkArgumentCount(args.length, fragment.inputs.length, "passed to contract");
return populate(contract, fragment, args, overrides);
}; };
} }
function buildEstimate(contract: Contract, fragment: FunctionFragment): ContractFunction<BigNumber> { function buildEstimate(contract: Contract, fragment: FunctionFragment): ContractFunction<BigNumber> {
const signerOrProvider = (contract.signer || contract.provider); const signerOrProvider = (contract.signer || contract.provider);
const populate = (fragment.constant) ? populateCallTransaction: populateTransaction;
return async function(...args: Array<any>): Promise<BigNumber> { return async function(...args: Array<any>): Promise<BigNumber> {
let overrides: CallOverrides = null;
if (args.length === fragment.inputs.length + 1 && typeof(args[args.length - 1]) === "object") {
overrides = args.pop();
}
logger.checkArgumentCount(args.length, fragment.inputs.length, "passed to contract");
if (!signerOrProvider) { if (!signerOrProvider) {
logger.throwError("estimate require a provider or signer", Logger.errors.UNSUPPORTED_OPERATION, { operation: "estimateGas" }) logger.throwError("estimate require a provider or signer", Logger.errors.UNSUPPORTED_OPERATION, {
operation: "estimateGas"
})
} }
const tx = await populate(contract, fragment, args, overrides); const tx = await populateTransaction(contract, fragment, args);
return await signerOrProvider.estimateGas(tx); return await signerOrProvider.estimateGas(tx);
}; };
} }
function buildCall(contract: Contract, fragment: FunctionFragment, collapseSimple: boolean): ContractFunction { function buildCall(contract: Contract, fragment: FunctionFragment, collapseSimple: boolean): ContractFunction {
const signerOrProvider = (contract.signer || contract.provider); const signerOrProvider = (contract.signer || contract.provider);
const populate = (fragment.constant) ? populateCallTransaction: populateTransaction;
return async function(...args: Array<any>): Promise<any> { return async function(...args: Array<any>): Promise<any> {
let overrides: CallOverrides = null; // Extract the "blockTag" override if present
let blockTag = undefined; let blockTag = undefined;
if (args.length === fragment.inputs.length + 1 && typeof(args[args.length - 1]) === "object") { if (args.length === fragment.inputs.length + 1 && typeof(args[args.length - 1]) === "object") {
overrides = shallowCopy(args.pop()); const overrides = shallowCopy(args.pop());
if (overrides.blockTag) { if (overrides.blockTag) {
blockTag = await overrides.blockTag; blockTag = await overrides.blockTag;
delete overrides.blockTag; delete overrides.blockTag;
} }
args.push(overrides);
} }
logger.checkArgumentCount(args.length, fragment.inputs.length, "passed to contract");
const tx = await populate(contract, fragment, args, overrides); // If the contract was just deployed, wait until it is mined
const value = await signerOrProvider.call(tx, blockTag); if (contract.deployTransaction != null) {
await contract._deployed(blockTag);
}
// Call a node and get the result
const tx = await populateTransaction(contract, fragment, args);
const result = await signerOrProvider.call(tx, blockTag);
try { try {
let result = contract.interface.decodeFunctionResult(fragment, value); let value = contract.interface.decodeFunctionResult(fragment, result);
if (collapseSimple && fragment.outputs.length === 1) { if (collapseSimple && fragment.outputs.length === 1) {
result = result[0]; value = value[0];
} }
return result; return value;
} catch (error) { } catch (error) {
if (error.code === Logger.errors.CALL_EXCEPTION) { if (error.code === Logger.errors.CALL_EXCEPTION) {
@ -280,20 +308,17 @@ function buildCall(contract: Contract, fragment: FunctionFragment, collapseSimpl
function buildSend(contract: Contract, fragment: FunctionFragment): ContractFunction<TransactionResponse> { function buildSend(contract: Contract, fragment: FunctionFragment): ContractFunction<TransactionResponse> {
return async function(...args: Array<any>): Promise<TransactionResponse> { return async function(...args: Array<any>): Promise<TransactionResponse> {
if (!contract.signer) { if (!contract.signer) {
logger.throwError("sending a transaction requires a signer", Logger.errors.UNSUPPORTED_OPERATION, { operation: "sendTransaction" }) logger.throwError("sending a transaction requires a signer", Logger.errors.UNSUPPORTED_OPERATION, {
operation: "sendTransaction"
})
} }
// We allow CallOverrides, since the Signer can accept from // If the contract was just deployed, wait until it is minded
let overrides: CallOverrides = null; if (contract.deployTransaction != null) {
if (args.length === fragment.inputs.length + 1 && typeof(args[args.length - 1]) === "object") { await contract._deployed();
overrides = shallowCopy(args.pop());
if (overrides.blockTag != null) {
logger.throwArgumentError(`cannot override "blockTag" in transaction`, "overrides", overrides);
} }
}
logger.checkArgumentCount(args.length, fragment.inputs.length, "passed to contract");
const txRequest = await populateCallTransaction(contract, fragment, args, overrides); const txRequest = await populateTransaction(contract, fragment, args);
const tx = await contract.signer.sendTransaction(txRequest); const tx = await contract.signer.sendTransaction(txRequest);
@ -539,7 +564,7 @@ export class Contract {
readonly callStatic: { [ name: string ]: ContractFunction }; readonly callStatic: { [ name: string ]: ContractFunction };
readonly estimateGas: { [ name: string ]: ContractFunction<BigNumber> }; readonly estimateGas: { [ name: string ]: ContractFunction<BigNumber> };
readonly populateTransaction: { [ name: string ]: ContractFunction<UnsignedTransaction> }; readonly populateTransaction: { [ name: string ]: ContractFunction<PopulatedTransaction> };
readonly filters: { [ name: string ]: (...args: Array<any>) => EventFilter }; readonly filters: { [ name: string ]: (...args: Array<any>) => EventFilter };
@ -561,14 +586,17 @@ export class Contract {
// Wrapped functions to call emit and allow deregistration from the provider // Wrapped functions to call emit and allow deregistration from the provider
_wrappedEmits: { [ eventTag: string ]: (...args: Array<any>) => void }; _wrappedEmits: { [ eventTag: string ]: (...args: Array<any>) => void };
constructor(addressOrName: string, contractInterface: ContractInterface, signerOrProvider: Signer | Provider) { constructor(addressOrName: string, contractInterface: ContractInterface, signerOrProvider?: Signer | Provider) {
logger.checkNew(new.target, Contract); logger.checkNew(new.target, Contract);
// @TODO: Maybe still check the addressOrName looks like a valid address or name? // @TODO: Maybe still check the addressOrName looks like a valid address or name?
//address = getAddress(address); //address = getAddress(address);
defineReadOnly(this, "interface", getStatic<InterfaceFunc>(new.target, "getInterface")(contractInterface)); defineReadOnly(this, "interface", getStatic<InterfaceFunc>(new.target, "getInterface")(contractInterface));
if (Signer.isSigner(signerOrProvider)) { if (signerOrProvider == null) {
defineReadOnly(this, "provider", null);
defineReadOnly(this, "signer", null);
} else if (Signer.isSigner(signerOrProvider)) {
defineReadOnly(this, "provider", signerOrProvider.provider || null); defineReadOnly(this, "provider", signerOrProvider.provider || null);
defineReadOnly(this, "signer", signerOrProvider); defineReadOnly(this, "signer", signerOrProvider);
} else if (Provider.isProvider(signerOrProvider)) { } else if (Provider.isProvider(signerOrProvider)) {
@ -623,10 +651,12 @@ export class Contract {
})); }));
} else { } else {
try { try {
defineReadOnly(this, "resolvedAddress", Promise.resolve((<any>(this.interface.constructor)).getAddress(addressOrName))); defineReadOnly(this, "resolvedAddress", Promise.resolve(getAddress(addressOrName)));
} catch (error) { } catch (error) {
// Without a provider, we cannot use ENS names // Without a provider, we cannot use ENS names
logger.throwArgumentError("provider is required to use non-address contract address", "addressOrName", addressOrName); logger.throwError("provider is required to use ENS name as contract address", Logger.errors.UNSUPPORTED_OPERATION, {
operation: "new Contract"
});
} }
} }
@ -761,7 +791,7 @@ export class Contract {
logger.throwError("sending a transactions require a signer", Logger.errors.UNSUPPORTED_OPERATION, { operation: "sendTransaction(fallback)" }) logger.throwError("sending a transactions require a signer", Logger.errors.UNSUPPORTED_OPERATION, { operation: "sendTransaction(fallback)" })
} }
const tx: TransactionRequest = shallowCopy(overrides || {}); const tx: Deferrable<TransactionRequest> = shallowCopy(overrides || {});
["from", "to"].forEach(function(key) { ["from", "to"].forEach(function(key) {
if ((<any>tx)[key] == null) { return; } if ((<any>tx)[key] == null) { return; }
@ -1068,8 +1098,8 @@ export class ContractFactory {
} }
// @TODO: Future; rename to populteTransaction? // @TODO: Future; rename to populteTransaction?
getDeployTransaction(...args: Array<any>): UnsignedTransaction { getDeployTransaction(...args: Array<any>): TransactionRequest {
let tx: UnsignedTransaction = { }; let tx: TransactionRequest = { };
// If we have 1 additional argument, we allow transaction overrides // If we have 1 additional argument, we allow transaction overrides
if (args.length === this.interface.deploy.inputs.length + 1 && typeof(args[args.length - 1]) === "object") { if (args.length === this.interface.deploy.inputs.length + 1 && typeof(args[args.length - 1]) === "object") {

View File

@ -49,6 +49,8 @@ import {
PayableOverrides, PayableOverrides,
CallOverrides, CallOverrides,
PopulatedTransaction,
ContractInterface ContractInterface
} from "@ethersproject/contracts"; } from "@ethersproject/contracts";
@ -100,6 +102,8 @@ export {
PayableOverrides, PayableOverrides,
CallOverrides, CallOverrides,
PopulatedTransaction,
ContractInterface, ContractInterface,
BigNumberish, BigNumberish,

View File

@ -58,6 +58,8 @@ export {
PayableOverrides, PayableOverrides,
CallOverrides, CallOverrides,
PopulatedTransaction,
ContractInterface, ContractInterface,
BigNumberish, BigNumberish,

View File

@ -35,6 +35,7 @@ import { CoerceFunc } from "@ethersproject/abi";
import { Bytes, BytesLike, Hexable } from "@ethersproject/bytes" import { Bytes, BytesLike, Hexable } from "@ethersproject/bytes"
import { Mnemonic } from "@ethersproject/hdnode"; import { Mnemonic } from "@ethersproject/hdnode";
import { EncryptOptions, ProgressCallback } from "@ethersproject/json-wallets"; import { EncryptOptions, ProgressCallback } from "@ethersproject/json-wallets";
import { Deferrable } from "@ethersproject/properties";
import { Utf8ErrorFunc } from "@ethersproject/strings"; import { Utf8ErrorFunc } from "@ethersproject/strings";
import { ConnectionInfo, FetchJsonResponse, OnceBlockable, OncePollable, PollOptions } from "@ethersproject/web"; import { ConnectionInfo, FetchJsonResponse, OnceBlockable, OncePollable, PollOptions } from "@ethersproject/web";
@ -183,6 +184,8 @@ export {
Mnemonic, Mnemonic,
Deferrable,
Utf8ErrorFunc, Utf8ErrorFunc,
ConnectionInfo, ConnectionInfo,

View File

@ -61,11 +61,11 @@ export class NonceManager extends ethers.Signer {
return this.signer.signMessage(message);; return this.signer.signMessage(message);;
} }
signTransaction(transaction: ethers.providers.TransactionRequest): Promise<string> { signTransaction(transaction: ethers.utils.Deferrable<ethers.providers.TransactionRequest>): Promise<string> {
return this.signer.signTransaction(transaction); return this.signer.signTransaction(transaction);
} }
sendTransaction(transaction: ethers.providers.TransactionRequest): Promise<ethers.providers.TransactionResponse> { sendTransaction(transaction: ethers.utils.Deferrable<ethers.providers.TransactionRequest>): Promise<ethers.providers.TransactionResponse> {
if (transaction.nonce == null) { if (transaction.nonce == null) {
transaction = ethers.utils.shallowCopy(transaction); transaction = ethers.utils.shallowCopy(transaction);
transaction.nonce = this.getTransactionCount("pending"); transaction.nonce = this.getTransactionCount("pending");

View File

@ -22,15 +22,16 @@ export function getStatic<T>(ctor: any, key: string): T {
return null; return null;
} }
export type Resolvable<T> = { export type Deferrable<T> = {
[P in keyof T]: T[P] | Promise<T[P]>; [ K in keyof T ]: T[K] | Promise<T[K]>;
} }
type Result = { key: string, value: any}; type Result = { key: string, value: any};
export async function resolveProperties<T>(object: Readonly<Resolvable<T>>): Promise<T> { export async function resolveProperties<T>(object: Readonly<Deferrable<T>>): Promise<T> {
const promises: Array<Promise<Result>> = Object.keys(object).map((key) => { const promises: Array<Promise<Result>> = Object.keys(object).map((key) => {
const value = object[<keyof Resolvable<T>>key]; const value = object[<keyof Deferrable<T>>key];
return Promise.resolve(value).then((v) => ({ key: key, value: v })); return Promise.resolve(value).then((v) => ({ key: key, value: v }));
}); });

View File

@ -8,7 +8,7 @@ import { BigNumber, BigNumberish } from "@ethersproject/bignumber";
import { arrayify, hexDataLength, hexlify, hexValue, isHexString } from "@ethersproject/bytes"; import { arrayify, hexDataLength, hexlify, hexValue, isHexString } from "@ethersproject/bytes";
import { namehash } from "@ethersproject/hash"; import { namehash } from "@ethersproject/hash";
import { getNetwork, Network, Networkish } from "@ethersproject/networks"; import { getNetwork, Network, Networkish } from "@ethersproject/networks";
import { defineReadOnly, getStatic, resolveProperties } from "@ethersproject/properties"; import { Deferrable, defineReadOnly, getStatic, resolveProperties } from "@ethersproject/properties";
import { Transaction } from "@ethersproject/transactions"; import { Transaction } from "@ethersproject/transactions";
import { toUtf8String } from "@ethersproject/strings"; import { toUtf8String } from "@ethersproject/strings";
import { poll } from "@ethersproject/web"; import { poll } from "@ethersproject/web";
@ -678,7 +678,7 @@ export class BaseProvider extends Provider {
} }
} }
async _getTransactionRequest(transaction: TransactionRequest | Promise<TransactionRequest>): Promise<Transaction> { async _getTransactionRequest(transaction: Deferrable<TransactionRequest>): Promise<Transaction> {
const values: any = await transaction; const values: any = await transaction;
const tx: any = { }; const tx: any = { };
@ -723,8 +723,7 @@ export class BaseProvider extends Provider {
return this.formatter.filter(await resolveProperties(result)); return this.formatter.filter(await resolveProperties(result));
} }
async call(transaction: Deferrable<TransactionRequest>, blockTag?: BlockTag | Promise<BlockTag>): Promise<string> {
async call(transaction: TransactionRequest | Promise<TransactionRequest>, blockTag?: BlockTag | Promise<BlockTag>): Promise<string> {
await this.ready; await this.ready;
const params = await resolveProperties({ const params = await resolveProperties({
transaction: this._getTransactionRequest(transaction), transaction: this._getTransactionRequest(transaction),
@ -733,7 +732,7 @@ export class BaseProvider extends Provider {
return hexlify(await this.perform("call", params)); return hexlify(await this.perform("call", params));
} }
async estimateGas(transaction: TransactionRequest | Promise<TransactionRequest>): Promise<BigNumber> { async estimateGas(transaction: Deferrable<TransactionRequest>): Promise<BigNumber> {
await this.ready; await this.ready;
const params = await resolveProperties({ const params = await resolveProperties({
transaction: this._getTransactionRequest(transaction) transaction: this._getTransactionRequest(transaction)

View File

@ -7,7 +7,7 @@ import { Signer } from "@ethersproject/abstract-signer";
import { BigNumber } from "@ethersproject/bignumber"; import { BigNumber } from "@ethersproject/bignumber";
import { Bytes, hexlify, hexValue } from "@ethersproject/bytes"; import { Bytes, hexlify, hexValue } from "@ethersproject/bytes";
import { Network, Networkish } from "@ethersproject/networks"; import { Network, Networkish } from "@ethersproject/networks";
import { checkProperties, deepCopy, defineReadOnly, getStatic, resolveProperties, shallowCopy } from "@ethersproject/properties"; import { checkProperties, deepCopy, Deferrable, defineReadOnly, getStatic, resolveProperties, shallowCopy } from "@ethersproject/properties";
import { toUtf8Bytes } from "@ethersproject/strings"; import { toUtf8Bytes } from "@ethersproject/strings";
import { ConnectionInfo, fetchJson, poll } from "@ethersproject/web"; import { ConnectionInfo, fetchJson, poll } from "@ethersproject/web";
@ -99,7 +99,7 @@ export class JsonRpcSigner extends Signer {
}); });
} }
sendUncheckedTransaction(transaction: TransactionRequest): Promise<string> { sendUncheckedTransaction(transaction: Deferrable<TransactionRequest>): Promise<string> {
transaction = shallowCopy(transaction); transaction = shallowCopy(transaction);
let fromAddress = this.getAddress().then((address) => { let fromAddress = this.getAddress().then((address) => {
@ -149,13 +149,13 @@ export class JsonRpcSigner extends Signer {
}); });
} }
signTransaction(transaction: TransactionRequest): Promise<string> { signTransaction(transaction: Deferrable<TransactionRequest>): Promise<string> {
return logger.throwError("signing transactions is unsupported", Logger.errors.UNSUPPORTED_OPERATION, { return logger.throwError("signing transactions is unsupported", Logger.errors.UNSUPPORTED_OPERATION, {
operation: "signTransaction" operation: "signTransaction"
}); });
} }
sendTransaction(transaction: TransactionRequest): Promise<TransactionResponse> { sendTransaction(transaction: Deferrable<TransactionRequest>): Promise<TransactionResponse> {
return this.sendUncheckedTransaction(transaction).then((hash) => { return this.sendUncheckedTransaction(transaction).then((hash) => {
return poll(() => { return poll(() => {
return this.provider.getTransaction(hash).then((tx: TransactionResponse) => { return this.provider.getTransaction(hash).then((tx: TransactionResponse) => {
@ -188,7 +188,7 @@ export class JsonRpcSigner extends Signer {
} }
class UncheckedJsonRpcSigner extends JsonRpcSigner { class UncheckedJsonRpcSigner extends JsonRpcSigner {
sendTransaction(transaction: TransactionRequest): Promise<TransactionResponse> { sendTransaction(transaction: Deferrable<TransactionRequest>): Promise<TransactionResponse> {
return this.sendUncheckedTransaction(transaction).then((hash) => { return this.sendUncheckedTransaction(transaction).then((hash) => {
return <TransactionResponse>{ return <TransactionResponse>{
hash: hash, hash: hash,

View File

@ -152,3 +152,148 @@ describe('Test Contract Objects', function() {
}); });
}); });
}); });
// @TODO: Exapnd this
describe("Test Contract Transaction Population", function() {
const abi = [
"function transfer(address to, uint amount)",
"function unstake() nonpayable",
"function mint() payable",
"function balanceOf(address owner) view returns (uint)"
];
const testAddress = "0xdeadbeef00deadbeef01deadbeef02deadbeef03"
const testAddressCheck = "0xDEAdbeeF00deAdbeEF01DeAdBEEF02DeADBEEF03";
const fireflyAddress = "0x8ba1f109551bD432803012645Ac136ddd64DBA72";
const contract = new ethers.Contract(testAddress, abi);
const contractConnected = contract.connect(ethers.getDefaultProvider());
it("standard populatation", async function() {
const tx = await contract.populateTransaction.balanceOf(testAddress);
//console.log(tx);
assert.equal(Object.keys(tx).length, 2, "correct number of keys");
assert.equal(tx.data, "0x70a08231000000000000000000000000deadbeef00deadbeef01deadbeef02deadbeef03", "data matches");
assert.equal(tx.to, testAddressCheck, "to address matches");
});
it("allows 'from' overrides", async function() {
const tx = await contract.populateTransaction.balanceOf(testAddress, {
from: testAddress
});
//console.log(tx);
assert.equal(Object.keys(tx).length, 3, "correct number of keys");
assert.equal(tx.data, "0x70a08231000000000000000000000000deadbeef00deadbeef01deadbeef02deadbeef03", "data matches");
assert.equal(tx.to, testAddressCheck, "to address matches");
assert.equal((<any>tx).from, testAddressCheck, "from address matches");
});
it("allows ENS 'from' overrides", async function() {
this.timeout(20000);
const tx = await contractConnected.populateTransaction.balanceOf(testAddress, {
from: "ricmoo.firefly.eth"
});
//console.log(tx);
assert.equal(Object.keys(tx).length, 3, "correct number of keys");
assert.equal(tx.data, "0x70a08231000000000000000000000000deadbeef00deadbeef01deadbeef02deadbeef03", "data matches");
assert.equal(tx.to, testAddressCheck, "to address matches");
assert.equal((<any>tx).from, fireflyAddress, "from address matches");
});
it("allows send overrides", async function() {
const tx = await contract.populateTransaction.mint({
gasLimit: 150000,
gasPrice: 1900000000,
nonce: 5,
value: 1234,
from: testAddress
});
//console.log(tx);
assert.equal(Object.keys(tx).length, 7, "correct number of keys");
assert.equal(tx.data, "0x1249c58b", "data matches");
assert.equal(tx.to, testAddressCheck, "to address matches");
assert.equal(tx.nonce, 5, "nonce address matches");
assert.ok(tx.gasLimit.eq(150000), "gasLimit matches");
assert.ok(tx.gasPrice.eq(1900000000), "gasPrice matches");
assert.ok(tx.value.eq(1234), "value matches");
assert.equal(tx.from, testAddressCheck, "from address matches");
});
it("allows zero 'value' to non-payable", async function() {
const tx = await contract.populateTransaction.unstake({
from: testAddress,
value: 0
});
//console.log(tx);
assert.equal(Object.keys(tx).length, 3, "correct number of keys");
assert.equal(tx.data, "0x2def6620", "data matches");
assert.equal(tx.to, testAddressCheck, "to address matches");
assert.equal(tx.from, testAddressCheck, "from address matches");
});
// @TODO: Add test cases to check for fault cases
// - cannot send non-zero value to non-payable
// - using the wrong from for a Signer-connected contract
it("forbids non-zero 'value' to non-payable", async function() {
try {
const tx = await contract.populateTransaction.unstake({
value: 1
});
console.log("Tx", tx);
assert.ok(false, "throws on non-zero value to non-payable");
} catch(error) {
assert.ok(error.operation === "overrides.value");
}
});
it("allows overriding same 'from' with a Signer", async function() {
const contractSigner = contract.connect(testAddress);
const tx = await contractSigner.populateTransaction.unstake({
from: testAddress
});
//console.log(tx);
assert.equal(Object.keys(tx).length, 3, "correct number of keys");
assert.equal(tx.data, "0x2def6620", "data matches");
assert.equal(tx.to, testAddressCheck, "to address matches");
assert.equal(tx.from, testAddressCheck, "from address matches");
});
it("forbids overriding 'from' with a Signer", async function() {
const contractSigner = contract.connect(testAddress);
try {
const tx = await contractSigner.populateTransaction.unstake({
from: fireflyAddress
});
console.log("Tx", tx);
assert.ok(false, "throws on non-zero value to non-payable");
} catch(error) {
assert.ok(error.operation === "overrides.from");
}
});
});
/*
// Test Contract interaction inside Grid-deployed Geth
describe("Test Contract Life-Cycle", function() {
this.timeout(10000);
let blockNumber: number = null;
before(async function() {
const provider = ethers.getDefaultProvider();
blockNumber = await provider.getBlockNumber();
//console.log(blockNumber);
this.skip();
});
it("says hi", function() {
console.log("hi", blockNumber);
});
});
*/