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 { BytesLike, isHexString } from "@ethersproject/bytes";
import { Network } from "@ethersproject/networks";
import { Description, defineReadOnly } from "@ethersproject/properties";
import { Deferrable, Description, defineReadOnly } from "@ethersproject/properties";
import { Transaction } from "@ethersproject/transactions";
import { OnceBlockable } from "@ethersproject/web";
@ -16,16 +16,16 @@ const logger = new Logger(version);
export type TransactionRequest = {
to?: string | Promise<string>,
from?: string | Promise<string>,
nonce?: BigNumberish | Promise<BigNumberish>,
to?: string,
from?: string,
nonce?: BigNumberish,
gasLimit?: BigNumberish | Promise<BigNumberish>,
gasPrice?: BigNumberish | Promise<BigNumberish>,
gasLimit?: BigNumberish,
gasPrice?: BigNumberish,
data?: BytesLike | Promise<BytesLike>,
value?: BigNumberish | Promise<BigNumberish>,
chainId?: number | Promise<number>,
data?: BytesLike,
value?: BigNumberish,
chainId?: number
}
export interface TransactionResponse extends Transaction {
@ -221,8 +221,8 @@ export abstract class Provider implements OnceBlockable {
// Execution
abstract sendTransaction(signedTransaction: string | Promise<string>): Promise<TransactionResponse>;
abstract call(transaction: TransactionRequest, blockTag?: BlockTag | Promise<BlockTag>): Promise<string>;
abstract estimateGas(transaction: TransactionRequest): Promise<BigNumber>;
abstract call(transaction: Deferrable<TransactionRequest>, blockTag?: BlockTag | Promise<BlockTag>): Promise<string>;
abstract estimateGas(transaction: Deferrable<TransactionRequest>): Promise<BigNumber>;
// Queries
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 { BigNumber } from "@ethersproject/bignumber";
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 { version } from "./_version";
@ -46,7 +46,7 @@ export abstract class Signer {
// The EXACT transaction MUST be signed, and NO additional properties to be added.
// - This MAY throw if signing transactions is not supports, but if
// 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.
// 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
async estimateGas(transaction: TransactionRequest): Promise<BigNumber> {
async estimateGas(transaction: Deferrable<TransactionRequest>): Promise<BigNumber> {
this._checkProvider("estimateGas");
const tx = await resolveProperties(this.checkTransaction(transaction));
return await this.provider.estimateGas(tx);
}
// 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");
const tx = await resolveProperties(this.checkTransaction(transaction));
return await this.provider.call(tx, blockTag);
}
// 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");
return this.populateTransaction(transaction).then((tx) => {
return this.signTransaction(tx).then((signedTx) => {
@ -128,7 +128,7 @@ export abstract class Signer {
// - call
// - estimateGas
// - populateTransaction (and therefor sendTransaction)
checkTransaction(transaction: TransactionRequest): TransactionRequest {
checkTransaction(transaction: Deferrable<TransactionRequest>): Deferrable<TransactionRequest> {
for (const key in transaction) {
if (allowedTransactionKeys.indexOf(key) === -1) {
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.
// By default called from: (overriding these prevents it)
// - 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.gasPrice == null) { tx.gasPrice = this.getGasPrice(); }
@ -232,7 +232,7 @@ export class VoidSigner extends Signer {
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");
}

View File

@ -3,11 +3,12 @@
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 { Signer, VoidSigner } from "@ethersproject/abstract-signer";
import { getContractAddress } from "@ethersproject/address";
import { getAddress, getContractAddress } from "@ethersproject/address";
import { BigNumber, BigNumberish } from "@ethersproject/bignumber";
import { BytesLike, concat, hexlify, isBytes, isHexString } from "@ethersproject/bytes";
import { defineReadOnly, deepCopy, getStatic, resolveProperties, shallowCopy } from "@ethersproject/properties";
import { UnsignedTransaction } from "@ethersproject/transactions";
//import { AddressZero } from "@ethersproject/constants";
import { Deferrable, defineReadOnly, deepCopy, getStatic, resolveProperties, shallowCopy } from "@ethersproject/properties";
// @TOOD remove dependences transactions
import { Logger } from "@ethersproject/logger";
import { version } from "./_version";
@ -18,7 +19,7 @@ export interface Overrides {
gasLimit?: BigNumberish | Promise<BigNumberish>;
gasPrice?: BigNumberish | Promise<BigNumberish>;
nonce?: BigNumberish | Promise<BigNumberish>;
}
};
export interface PayableOverrides extends Overrides {
value?: BigNumberish | Promise<BigNumberish>;
@ -26,9 +27,26 @@ export interface PayableOverrides extends Overrides {
export interface CallOverrides extends PayableOverrides {
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 = {
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
}
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
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)) {
return Promise.all(paramType.map((paramType, index) => {
return resolveAddresses(
signerOrProvider,
resolver,
((Array.isArray(value)) ? value[index]: value[paramType.name]),
paramType
);
@ -94,25 +128,62 @@ function resolveAddresses(signerOrProvider: Signer | Provider, value: any, param
}
if (paramType.type === "address") {
return signerOrProvider.resolveName(value);
return resolveName(resolver, value);
}
if (paramType.type === "tuple") {
return resolveAddresses(signerOrProvider, value, paramType.components);
return resolveAddresses(resolver, value, paramType.components);
}
if (paramType.baseType === "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);
}
async function _populateTransaction(contract: Contract, fragment: FunctionFragment, args: Array<any>, overrides?: Overrides): Promise<UnsignedTransaction> {
overrides = shallowCopy(overrides);
async function populateTransaction(contract: Contract, fragment: FunctionFragment, args: Array<any>): Promise<PopulatedTransaction> {
// 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({
args: resolveAddresses(contract.signer || contract.provider, args, fragment.inputs),
address: contract.resolvedAddress,
@ -120,28 +191,43 @@ async function _populateTransaction(contract: Contract, fragment: FunctionFragme
});
// The ABI coded transaction
const tx: UnsignedTransaction = {
const tx: PopulatedTransaction = {
data: contract.interface.encodeFunctionData(fragment, resolved.args),
to: resolved.address
};
// Resolved Overrides
const ro = resolved.overrides;
// Populate simple overrides
if (ro.nonce != null) { tx.nonce = BigNumber.from(ro.nonce).toNumber(); }
if (ro.gasLimit != null) { tx.gasLimit = BigNumber.from(ro.gasLimit); }
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) {
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
delete overrides.nonce;
delete overrides.gasLimit;
delete overrides.gasPrice;
// @TODO: Maybe move all tx property validation to the Signer and Provider?
delete overrides.from;
delete overrides.value;
// Make sure there are no stray overrides, which may indicate a
// typo or using an unsupported key.
@ -149,122 +235,64 @@ async function _populateTransaction(contract: Contract, fragment: FunctionFragme
if (leftovers.length) {
logger.throwError(`cannot override ${ leftovers.map((l) => JSON.stringify(l)).join(",") }`, Logger.errors.UNSUPPORTED_OPERATION, {
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;
}
function buildPopulate(contract: Contract, fragment: FunctionFragment): ContractFunction<UnsignedTransaction> {
const populate = (fragment.constant) ? populateCallTransaction: populateTransaction;
return async function(...args: Array<any>): Promise<UnsignedTransaction> {
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 buildPopulate(contract: Contract, fragment: FunctionFragment): ContractFunction<PopulatedTransaction> {
return async function(...args: Array<any>): Promise<PopulatedTransaction> {
return populateTransaction(contract, fragment, args);
};
}
function buildEstimate(contract: Contract, fragment: FunctionFragment): ContractFunction<BigNumber> {
const signerOrProvider = (contract.signer || contract.provider);
const populate = (fragment.constant) ? populateCallTransaction: populateTransaction;
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) {
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);
};
}
function buildCall(contract: Contract, fragment: FunctionFragment, collapseSimple: boolean): ContractFunction {
const signerOrProvider = (contract.signer || contract.provider);
const populate = (fragment.constant) ? populateCallTransaction: populateTransaction;
return async function(...args: Array<any>): Promise<any> {
let overrides: CallOverrides = null;
// Extract the "blockTag" override if present
let blockTag = undefined;
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) {
blockTag = await 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);
const value = await signerOrProvider.call(tx, blockTag);
// If the contract was just deployed, wait until it is mined
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 {
let result = contract.interface.decodeFunctionResult(fragment, value);
let value = contract.interface.decodeFunctionResult(fragment, result);
if (collapseSimple && fragment.outputs.length === 1) {
result = result[0];
value = value[0];
}
return result;
return value;
} catch (error) {
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> {
return async function(...args: Array<any>): Promise<TransactionResponse> {
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
let overrides: CallOverrides = null;
if (args.length === fragment.inputs.length + 1 && typeof(args[args.length - 1]) === "object") {
overrides = shallowCopy(args.pop());
if (overrides.blockTag != null) {
logger.throwArgumentError(`cannot override "blockTag" in transaction`, "overrides", overrides);
}
// If the contract was just deployed, wait until it is minded
if (contract.deployTransaction != null) {
await contract._deployed();
}
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);
@ -539,7 +564,7 @@ export class Contract {
readonly callStatic: { [ name: string ]: ContractFunction };
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 };
@ -561,14 +586,17 @@ export class Contract {
// Wrapped functions to call emit and allow deregistration from the provider
_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);
// @TODO: Maybe still check the addressOrName looks like a valid address or name?
//address = getAddress(address);
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, "signer", signerOrProvider);
} else if (Provider.isProvider(signerOrProvider)) {
@ -623,10 +651,12 @@ export class Contract {
}));
} else {
try {
defineReadOnly(this, "resolvedAddress", Promise.resolve((<any>(this.interface.constructor)).getAddress(addressOrName)));
defineReadOnly(this, "resolvedAddress", Promise.resolve(getAddress(addressOrName)));
} catch (error) {
// 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)" })
}
const tx: TransactionRequest = shallowCopy(overrides || {});
const tx: Deferrable<TransactionRequest> = shallowCopy(overrides || {});
["from", "to"].forEach(function(key) {
if ((<any>tx)[key] == null) { return; }
@ -1068,8 +1098,8 @@ export class ContractFactory {
}
// @TODO: Future; rename to populteTransaction?
getDeployTransaction(...args: Array<any>): UnsignedTransaction {
let tx: UnsignedTransaction = { };
getDeployTransaction(...args: Array<any>): TransactionRequest {
let tx: TransactionRequest = { };
// 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") {

View File

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

View File

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

View File

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

View File

@ -61,11 +61,11 @@ export class NonceManager extends ethers.Signer {
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);
}
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) {
transaction = ethers.utils.shallowCopy(transaction);
transaction.nonce = this.getTransactionCount("pending");

View File

@ -22,15 +22,16 @@ export function getStatic<T>(ctor: any, key: string): T {
return null;
}
export type Resolvable<T> = {
[P in keyof T]: T[P] | Promise<T[P]>;
export type Deferrable<T> = {
[ K in keyof T ]: T[K] | Promise<T[K]>;
}
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 value = object[<keyof Resolvable<T>>key];
const value = object[<keyof Deferrable<T>>key];
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 { namehash } from "@ethersproject/hash";
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 { toUtf8String } from "@ethersproject/strings";
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 tx: any = { };
@ -723,8 +723,7 @@ export class BaseProvider extends Provider {
return this.formatter.filter(await resolveProperties(result));
}
async call(transaction: TransactionRequest | Promise<TransactionRequest>, blockTag?: BlockTag | Promise<BlockTag>): Promise<string> {
async call(transaction: Deferrable<TransactionRequest>, blockTag?: BlockTag | Promise<BlockTag>): Promise<string> {
await this.ready;
const params = await resolveProperties({
transaction: this._getTransactionRequest(transaction),
@ -733,7 +732,7 @@ export class BaseProvider extends Provider {
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;
const params = await resolveProperties({
transaction: this._getTransactionRequest(transaction)

View File

@ -7,7 +7,7 @@ import { Signer } from "@ethersproject/abstract-signer";
import { BigNumber } from "@ethersproject/bignumber";
import { Bytes, hexlify, hexValue } from "@ethersproject/bytes";
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 { 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);
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, {
operation: "signTransaction"
});
}
sendTransaction(transaction: TransactionRequest): Promise<TransactionResponse> {
sendTransaction(transaction: Deferrable<TransactionRequest>): Promise<TransactionResponse> {
return this.sendUncheckedTransaction(transaction).then((hash) => {
return poll(() => {
return this.provider.getTransaction(hash).then((tx: TransactionResponse) => {
@ -188,7 +188,7 @@ export class JsonRpcSigner extends Signer {
}
class UncheckedJsonRpcSigner extends JsonRpcSigner {
sendTransaction(transaction: TransactionRequest): Promise<TransactionResponse> {
sendTransaction(transaction: Deferrable<TransactionRequest>): Promise<TransactionResponse> {
return this.sendUncheckedTransaction(transaction).then((hash) => {
return <TransactionResponse>{
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);
});
});
*/