aboutsummaryrefslogtreecommitdiffstats
path: root/packages/web3-wrapper/src
diff options
context:
space:
mode:
authorAlex Browne <stephenalexbrowne@gmail.com>2018-08-14 09:42:09 +0800
committerAlex Browne <stephenalexbrowne@gmail.com>2018-08-14 09:42:09 +0800
commit88766a02c7e6688e72d5c4c69ce68028b322f154 (patch)
treefa06552a80249e7998691b64df6b3b2827f9f947 /packages/web3-wrapper/src
parent8162394797342cef268cc8072fc860326974e269 (diff)
parentfadd292ecf367e42154856509d0ea0c20b23f2f1 (diff)
downloaddexon-0x-contracts-88766a02c7e6688e72d5c4c69ce68028b322f154.tar
dexon-0x-contracts-88766a02c7e6688e72d5c4c69ce68028b322f154.tar.gz
dexon-0x-contracts-88766a02c7e6688e72d5c4c69ce68028b322f154.tar.bz2
dexon-0x-contracts-88766a02c7e6688e72d5c4c69ce68028b322f154.tar.lz
dexon-0x-contracts-88766a02c7e6688e72d5c4c69ce68028b322f154.tar.xz
dexon-0x-contracts-88766a02c7e6688e72d5c4c69ce68028b322f154.tar.zst
dexon-0x-contracts-88766a02c7e6688e72d5c4c69ce68028b322f154.zip
Merge branch 'development'
Diffstat (limited to 'packages/web3-wrapper/src')
-rw-r--r--packages/web3-wrapper/src/index.ts339
-rw-r--r--packages/web3-wrapper/src/marshaller.ts210
-rw-r--r--packages/web3-wrapper/src/types.ts65
-rw-r--r--packages/web3-wrapper/src/utils.ts58
-rw-r--r--packages/web3-wrapper/src/web3_wrapper.ts653
5 files changed, 999 insertions, 326 deletions
diff --git a/packages/web3-wrapper/src/index.ts b/packages/web3-wrapper/src/index.ts
index 87c69b269..5d3d135e4 100644
--- a/packages/web3-wrapper/src/index.ts
+++ b/packages/web3-wrapper/src/index.ts
@@ -1,326 +1,13 @@
-import {
- BlockParam,
- BlockWithoutTransactionData,
- CallData,
- ContractAbi,
- FilterObject,
- JSONRPCRequestPayload,
- JSONRPCResponsePayload,
- LogEntry,
- RawLogEntry,
- TransactionReceipt,
- TxData,
-} from '@0xproject/types';
-import { BigNumber, promisify } from '@0xproject/utils';
-import * as _ from 'lodash';
-import * as Web3 from 'web3';
-
-/**
- * A wrapper around the Web3.js 0.x library that provides a consistent, clean promise-based interface.
- */
-export class Web3Wrapper {
- /**
- * Flag to check if this instance is of type Web3Wrapper
- */
- public isZeroExWeb3Wrapper = true;
- private _web3: Web3;
- private _defaults: Partial<TxData>;
- private _jsonRpcRequestId: number;
- /**
- * Instantiates a new Web3Wrapper.
- * @param provider The Web3 provider instance you would like the Web3Wrapper to use for interacting with
- * the backing Ethereum node.
- * @param defaults Override TxData defaults sent with RPC requests to the backing Ethereum node.
- * @return An instance of the Web3Wrapper class.
- */
- constructor(provider: Web3.Provider, defaults?: Partial<TxData>) {
- if (_.isUndefined((provider as any).sendAsync)) {
- // Web3@1.0 provider doesn't support synchronous http requests,
- // so it only has an async `send` method, instead of a `send` and `sendAsync` in web3@0.x.x`
- // We re-assign the send method so that Web3@1.0 providers work with @0xproject/web3-wrapper
- (provider as any).sendAsync = (provider as any).send;
- }
- this._web3 = new Web3();
- this._web3.setProvider(provider);
- this._defaults = defaults || {};
- this._jsonRpcRequestId = 0;
- }
- /**
- * Get the contract defaults set to the Web3Wrapper instance
- * @return TxData defaults (e.g gas, gasPrice, nonce, etc...)
- */
- public getContractDefaults(): Partial<TxData> {
- return this._defaults;
- }
- /**
- * Retrieve the Web3 provider
- * @return Web3 provider instance
- */
- public getProvider(): Web3.Provider {
- return this._web3.currentProvider;
- }
- /**
- * Update the used Web3 provider
- * @param provider The new Web3 provider to be set
- */
- public setProvider(provider: Web3.Provider) {
- this._web3.setProvider(provider);
- }
- /**
- * Check if an address is a valid Ethereum address
- * @param address Address to check
- * @returns Whether the address is a valid Ethereum address
- */
- public isAddress(address: string): boolean {
- return this._web3.isAddress(address);
- }
- /**
- * Check whether an address is available through the backing provider. This can be
- * useful if you want to know whether a user can sign messages or transactions from
- * a given Ethereum address.
- * @param senderAddress Address to check availability for
- * @returns Whether the address is available through the provider.
- */
- public async isSenderAddressAvailableAsync(senderAddress: string): Promise<boolean> {
- const addresses = await this.getAvailableAddressesAsync();
- const normalizedAddress = senderAddress.toLowerCase();
- return _.includes(addresses, normalizedAddress);
- }
- /**
- * Fetch the backing Ethereum node's version string (e.g `MetaMask/v4.2.0`)
- * @returns Ethereum node's version string
- */
- public async getNodeVersionAsync(): Promise<string> {
- const nodeVersion = await promisify<string>(this._web3.version.getNode)();
- return nodeVersion;
- }
- /**
- * Fetches the networkId of the backing Ethereum node
- * @returns The network id
- */
- public async getNetworkIdAsync(): Promise<number> {
- const networkIdStr = await promisify<string>(this._web3.version.getNetwork)();
- const networkId = _.parseInt(networkIdStr);
- return networkId;
- }
- /**
- * Retrieves the transaction receipt for a given transaction hash
- * @param txHash Transaction hash
- * @returns The transaction receipt, including it's status (0: failed, 1: succeeded or undefined: not found)
- */
- public async getTransactionReceiptAsync(txHash: string): Promise<TransactionReceipt> {
- const transactionReceipt = await promisify<TransactionReceipt>(this._web3.eth.getTransactionReceipt)(txHash);
- if (!_.isNull(transactionReceipt)) {
- transactionReceipt.status = this._normalizeTxReceiptStatus(transactionReceipt.status);
- }
- return transactionReceipt;
- }
- /**
- * Convert an Ether amount from ETH to Wei
- * @param ethAmount Amount of Ether to convert to wei
- * @returns Amount in wei
- */
- public toWei(ethAmount: BigNumber): BigNumber {
- const balanceWei = this._web3.toWei(ethAmount, 'ether');
- return balanceWei;
- }
- /**
- * Retrieves an accounts Ether balance in wei
- * @param owner Account whose balance you wish to check
- * @returns Balance in wei
- */
- public async getBalanceInWeiAsync(owner: string): Promise<BigNumber> {
- let balanceInWei = await promisify<BigNumber>(this._web3.eth.getBalance)(owner);
- // Rewrap in a new BigNumber
- balanceInWei = new BigNumber(balanceInWei);
- return balanceInWei;
- }
- /**
- * Check if a contract exists at a given address
- * @param address Address to which to check
- * @returns Whether or not contract code was found at the supplied address
- */
- public async doesContractExistAtAddressAsync(address: string): Promise<boolean> {
- const code = await promisify<string>(this._web3.eth.getCode)(address);
- // Regex matches 0x0, 0x00, 0x in order to accommodate poorly implemented clients
- const codeIsEmpty = /^0x0{0,40}$/i.test(code);
- return !codeIsEmpty;
- }
- /**
- * Sign a message with a specific address's private key (`eth_sign`)
- * @param address Address of signer
- * @param message Message to sign
- * @returns Signature string (might be VRS or RSV depending on the Signer)
- */
- public async signMessageAsync(address: string, message: string): Promise<string> {
- const signData = await promisify<string>(this._web3.eth.sign)(address, message);
- return signData;
- }
- /**
- * Fetches the latest block number
- * @returns Block number
- */
- public async getBlockNumberAsync(): Promise<number> {
- const blockNumber = await promisify<number>(this._web3.eth.getBlockNumber)();
- return blockNumber;
- }
- /**
- * Fetch a specific Ethereum block
- * @param blockParam The block you wish to fetch (blockHash, blockNumber or blockLiteral)
- * @returns The requested block without transaction data
- */
- public async getBlockAsync(blockParam: string | BlockParam): Promise<BlockWithoutTransactionData> {
- const block = await promisify<BlockWithoutTransactionData>(this._web3.eth.getBlock)(blockParam);
- return block;
- }
- /**
- * Fetch a block's timestamp
- * @param blockParam The block you wish to fetch (blockHash, blockNumber or blockLiteral)
- * @returns The block's timestamp
- */
- public async getBlockTimestampAsync(blockParam: string | BlockParam): Promise<number> {
- const { timestamp } = await this.getBlockAsync(blockParam);
- return timestamp;
- }
- /**
- * Retrieve the user addresses available through the backing provider
- * @returns Available user addresses
- */
- public async getAvailableAddressesAsync(): Promise<string[]> {
- const addresses = await promisify<string[]>(this._web3.eth.getAccounts)();
- const normalizedAddresses = _.map(addresses, address => address.toLowerCase());
- return normalizedAddresses;
- }
- /**
- * Take a snapshot of the blockchain state on a TestRPC/Ganache local node
- * @returns The snapshot id. This can be used to revert to this snapshot
- */
- public async takeSnapshotAsync(): Promise<number> {
- const snapshotId = Number(await this._sendRawPayloadAsync<string>({ method: 'evm_snapshot', params: [] }));
- return snapshotId;
- }
- /**
- * Revert the blockchain state to a previous snapshot state on TestRPC/Ganache local node
- * @param snapshotId snapshot id to revert to
- * @returns Whether the revert was successful
- */
- public async revertSnapshotAsync(snapshotId: number): Promise<boolean> {
- const didRevert = await this._sendRawPayloadAsync<boolean>({ method: 'evm_revert', params: [snapshotId] });
- return didRevert;
- }
- /**
- * Mine a block on a TestRPC/Ganache local node
- */
- public async mineBlockAsync(): Promise<void> {
- await this._sendRawPayloadAsync<string>({ method: 'evm_mine', params: [] });
- }
- /**
- * Increase the next blocks timestamp on TestRPC/Ganache local node
- * @param timeDelta Amount of time to add in seconds
- */
- public async increaseTimeAsync(timeDelta: number): Promise<void> {
- await this._sendRawPayloadAsync<string>({ method: 'evm_increaseTime', params: [timeDelta] });
- }
- /**
- * Retrieve smart contract logs for a given filter
- * @param filter Parameters by which to filter which logs to retrieve
- * @returns The corresponding log entries
- */
- public async getLogsAsync(filter: FilterObject): Promise<LogEntry[]> {
- let fromBlock = filter.fromBlock;
- if (_.isNumber(fromBlock)) {
- fromBlock = this._web3.toHex(fromBlock);
- }
- let toBlock = filter.toBlock;
- if (_.isNumber(toBlock)) {
- toBlock = this._web3.toHex(toBlock);
- }
- const serializedFilter = {
- ...filter,
- fromBlock,
- toBlock,
- };
- const payload = {
- jsonrpc: '2.0',
- id: this._jsonRpcRequestId++,
- method: 'eth_getLogs',
- params: [serializedFilter],
- };
- const rawLogs = await this._sendRawPayloadAsync<RawLogEntry[]>(payload);
- const formattedLogs = _.map(rawLogs, this._formatLog.bind(this));
- return formattedLogs;
- }
- /**
- * Get a Web3 contract factory instance for a given ABI
- * @param abi Smart contract ABI
- * @returns Web3 contract factory which can create Web3 Contract instances from the supplied ABI
- */
- public getContractFromAbi(abi: ContractAbi): Web3.Contract<any> {
- const web3Contract = this._web3.eth.contract(abi);
- return web3Contract;
- }
- /**
- * Calculate the estimated gas cost for a given transaction
- * @param txData Transaction data
- * @returns Estimated gas cost
- */
- public async estimateGasAsync(txData: Partial<TxData>): Promise<number> {
- const gas = await promisify<number>(this._web3.eth.estimateGas)(txData);
- return gas;
- }
- /**
- * Call a smart contract method at a given block height
- * @param callData Call data
- * @param defaultBlock Block height at which to make the call. Defaults to `latest`
- * @returns The raw call result
- */
- public async callAsync(callData: CallData, defaultBlock?: BlockParam): Promise<string> {
- const rawCallResult = await promisify<string>(this._web3.eth.call)(callData, defaultBlock);
- return rawCallResult;
- }
- /**
- * Send a transaction
- * @param txData Transaction data
- * @returns Transaction hash
- */
- public async sendTransactionAsync(txData: TxData): Promise<string> {
- const txHash = await promisify<string>(this._web3.eth.sendTransaction)(txData);
- return txHash;
- }
- private async _sendRawPayloadAsync<A>(payload: Partial<JSONRPCRequestPayload>): Promise<A> {
- const sendAsync = this._web3.currentProvider.sendAsync.bind(this._web3.currentProvider);
- const response = await promisify<JSONRPCResponsePayload>(sendAsync)(payload);
- const result = response.result;
- return result;
- }
- private _normalizeTxReceiptStatus(status: undefined | null | string | 0 | 1): null | 0 | 1 {
- // Transaction status might have four values
- // undefined - Testrpc and other old clients
- // null - New clients on old transactions
- // number - Parity
- // hex - Geth
- if (_.isString(status)) {
- return this._web3.toDecimal(status) as 0 | 1;
- } else if (_.isUndefined(status)) {
- return null;
- } else {
- return status;
- }
- }
- private _formatLog(rawLog: RawLogEntry): LogEntry {
- const formattedLog = {
- ...rawLog,
- logIndex: this._hexToDecimal(rawLog.logIndex),
- blockNumber: this._hexToDecimal(rawLog.blockNumber),
- transactionIndex: this._hexToDecimal(rawLog.transactionIndex),
- };
- return formattedLog;
- }
- private _hexToDecimal(hex: string | null): number | null {
- if (_.isNull(hex)) {
- return null;
- }
- const decimal = this._web3.toDecimal(hex);
- return decimal;
- }
-}
+export { Web3Wrapper } from './web3_wrapper';
+export { marshaller } from './marshaller';
+export {
+ Web3WrapperErrors,
+ NodeType,
+ CallDataRPC,
+ CallTxDataBaseRPC,
+ AbstractBlockRPC,
+ BlockWithoutTransactionDataRPC,
+ BlockWithTransactionDataRPC,
+ TransactionRPC,
+ TxDataRPC,
+} from './types';
diff --git a/packages/web3-wrapper/src/marshaller.ts b/packages/web3-wrapper/src/marshaller.ts
new file mode 100644
index 000000000..572a322d6
--- /dev/null
+++ b/packages/web3-wrapper/src/marshaller.ts
@@ -0,0 +1,210 @@
+import { addressUtils } from '@0xproject/utils';
+import {
+ BlockParam,
+ BlockParamLiteral,
+ BlockWithoutTransactionData,
+ BlockWithTransactionData,
+ CallData,
+ CallTxDataBase,
+ LogEntry,
+ RawLogEntry,
+ Transaction,
+ TxData,
+} from 'ethereum-types';
+import ethUtil = require('ethereumjs-util');
+import * as _ from 'lodash';
+
+import { utils } from './utils';
+
+import {
+ BlockWithoutTransactionDataRPC,
+ BlockWithTransactionDataRPC,
+ CallDataRPC,
+ CallTxDataBaseRPC,
+ TransactionRPC,
+ TxDataRPC,
+} from './types';
+
+/**
+ * Utils to convert ethereum structures from user-space format to RPC format. (marshall/unmarshall)
+ */
+export const marshaller = {
+ /**
+ * Unmarshall block without transaction data
+ * @param blockWithHexValues block to unmarshall
+ * @return unmarshalled block without transaction data
+ */
+ unmarshalIntoBlockWithoutTransactionData(
+ blockWithHexValues: BlockWithoutTransactionDataRPC,
+ ): BlockWithoutTransactionData {
+ const block = {
+ ...blockWithHexValues,
+ gasLimit: utils.convertHexToNumber(blockWithHexValues.gasLimit),
+ gasUsed: utils.convertHexToNumber(blockWithHexValues.gasUsed),
+ size: utils.convertHexToNumber(blockWithHexValues.size),
+ timestamp: utils.convertHexToNumber(blockWithHexValues.timestamp),
+ number: _.isNull(blockWithHexValues.number) ? null : utils.convertHexToNumber(blockWithHexValues.number),
+ difficulty: utils.convertAmountToBigNumber(blockWithHexValues.difficulty),
+ totalDifficulty: utils.convertAmountToBigNumber(blockWithHexValues.totalDifficulty),
+ };
+ return block;
+ },
+ /**
+ * Unmarshall block with transaction data
+ * @param blockWithHexValues block to unmarshall
+ * @return unmarshalled block with transaction data
+ */
+ unmarshalIntoBlockWithTransactionData(blockWithHexValues: BlockWithTransactionDataRPC): BlockWithTransactionData {
+ const block = {
+ ...blockWithHexValues,
+ gasLimit: utils.convertHexToNumber(blockWithHexValues.gasLimit),
+ gasUsed: utils.convertHexToNumber(blockWithHexValues.gasUsed),
+ size: utils.convertHexToNumber(blockWithHexValues.size),
+ timestamp: utils.convertHexToNumber(blockWithHexValues.timestamp),
+ number: _.isNull(blockWithHexValues.number) ? null : utils.convertHexToNumber(blockWithHexValues.number),
+ difficulty: utils.convertAmountToBigNumber(blockWithHexValues.difficulty),
+ totalDifficulty: utils.convertAmountToBigNumber(blockWithHexValues.totalDifficulty),
+ transactions: [] as Transaction[],
+ };
+ block.transactions = _.map(blockWithHexValues.transactions, (tx: TransactionRPC) => {
+ const transaction = marshaller.unmarshalTransaction(tx);
+ return transaction;
+ });
+ return block;
+ },
+ /**
+ * Unmarshall transaction
+ * @param txRpc transaction to unmarshall
+ * @return unmarshalled transaction
+ */
+ unmarshalTransaction(txRpc: TransactionRPC): Transaction {
+ const tx = {
+ ...txRpc,
+ blockNumber: !_.isNull(txRpc.blockNumber) ? utils.convertHexToNumber(txRpc.blockNumber) : null,
+ transactionIndex: !_.isNull(txRpc.transactionIndex)
+ ? utils.convertHexToNumber(txRpc.transactionIndex)
+ : null,
+ nonce: utils.convertHexToNumber(txRpc.nonce),
+ gas: utils.convertHexToNumber(txRpc.gas),
+ gasPrice: utils.convertAmountToBigNumber(txRpc.gasPrice),
+ value: utils.convertAmountToBigNumber(txRpc.value),
+ };
+ return tx;
+ },
+ /**
+ * Unmarshall transaction data
+ * @param txDataRpc transaction data to unmarshall
+ * @return unmarshalled transaction data
+ */
+ unmarshalTxData(txDataRpc: TxDataRPC): TxData {
+ if (_.isUndefined(txDataRpc.from)) {
+ throw new Error(`txData must include valid 'from' value.`);
+ }
+ const txData = {
+ ...txDataRpc,
+ value: !_.isUndefined(txDataRpc.value) ? utils.convertHexToNumber(txDataRpc.value) : undefined,
+ gas: !_.isUndefined(txDataRpc.gas) ? utils.convertHexToNumber(txDataRpc.gas) : undefined,
+ gasPrice: !_.isUndefined(txDataRpc.gasPrice) ? utils.convertHexToNumber(txDataRpc.gasPrice) : undefined,
+ nonce: !_.isUndefined(txDataRpc.nonce) ? utils.convertHexToNumber(txDataRpc.nonce) : undefined,
+ };
+ return txData;
+ },
+ /**
+ * Marshall transaction data
+ * @param txData transaction data to marshall
+ * @return marshalled transaction data
+ */
+ marshalTxData(txData: Partial<TxData>): Partial<TxDataRPC> {
+ if (_.isUndefined(txData.from)) {
+ throw new Error(`txData must include valid 'from' value.`);
+ }
+ const callTxDataBase = {
+ ...txData,
+ };
+ delete callTxDataBase.from;
+ const callTxDataBaseRPC = marshaller._marshalCallTxDataBase(callTxDataBase);
+ const txDataRPC = {
+ ...callTxDataBaseRPC,
+ from: marshaller.marshalAddress(txData.from),
+ };
+ const prunableIfUndefined = ['gasPrice', 'gas', 'value', 'nonce'];
+ _.each(txDataRPC, (value: any, key: string) => {
+ if (_.isUndefined(value) && _.includes(prunableIfUndefined, key)) {
+ delete (txDataRPC as any)[key];
+ }
+ });
+ return txDataRPC;
+ },
+ /**
+ * Marshall call data
+ * @param callData call data to marshall
+ * @return marshalled call data
+ */
+ marshalCallData(callData: Partial<CallData>): Partial<CallDataRPC> {
+ const callTxDataBase = {
+ ...callData,
+ };
+ delete callTxDataBase.from;
+ const callTxDataBaseRPC = marshaller._marshalCallTxDataBase(callTxDataBase);
+ const callDataRPC = {
+ ...callTxDataBaseRPC,
+ from: _.isUndefined(callData.from) ? undefined : marshaller.marshalAddress(callData.from),
+ };
+ return callDataRPC;
+ },
+ /**
+ * Marshall address
+ * @param address address to marshall
+ * @return marshalled address
+ */
+ marshalAddress(address: string): string {
+ if (addressUtils.isAddress(address)) {
+ return ethUtil.addHexPrefix(address);
+ }
+ throw new Error(`Invalid address encountered: ${address}`);
+ },
+ /**
+ * Marshall block param
+ * @param blockParam block param to marshall
+ * @return marshalled block param
+ */
+ marshalBlockParam(blockParam: BlockParam | string | number | undefined): string | undefined {
+ if (_.isUndefined(blockParam)) {
+ return BlockParamLiteral.Latest;
+ }
+ const encodedBlockParam = _.isNumber(blockParam) ? utils.numberToHex(blockParam) : blockParam;
+ return encodedBlockParam;
+ },
+ /**
+ * Unmarshall log
+ * @param rawLog log to unmarshall
+ * @return unmarshalled log
+ */
+ unmarshalLog(rawLog: RawLogEntry): LogEntry {
+ const formattedLog = {
+ ...rawLog,
+ logIndex: utils.convertHexToNumberOrNull(rawLog.logIndex),
+ blockNumber: utils.convertHexToNumberOrNull(rawLog.blockNumber),
+ transactionIndex: utils.convertHexToNumberOrNull(rawLog.transactionIndex),
+ };
+ return formattedLog;
+ },
+ _marshalCallTxDataBase(callTxDataBase: Partial<CallTxDataBase>): Partial<CallTxDataBaseRPC> {
+ const callTxDataBaseRPC = {
+ ...callTxDataBase,
+ to: _.isUndefined(callTxDataBase.to) ? undefined : marshaller.marshalAddress(callTxDataBase.to),
+ gasPrice: _.isUndefined(callTxDataBase.gasPrice)
+ ? undefined
+ : utils.encodeAmountAsHexString(callTxDataBase.gasPrice),
+ gas: _.isUndefined(callTxDataBase.gas) ? undefined : utils.encodeAmountAsHexString(callTxDataBase.gas),
+ value: _.isUndefined(callTxDataBase.value)
+ ? undefined
+ : utils.encodeAmountAsHexString(callTxDataBase.value),
+ nonce: _.isUndefined(callTxDataBase.nonce)
+ ? undefined
+ : utils.encodeAmountAsHexString(callTxDataBase.nonce),
+ };
+
+ return callTxDataBaseRPC;
+ },
+};
diff --git a/packages/web3-wrapper/src/types.ts b/packages/web3-wrapper/src/types.ts
new file mode 100644
index 000000000..e81039186
--- /dev/null
+++ b/packages/web3-wrapper/src/types.ts
@@ -0,0 +1,65 @@
+export enum Web3WrapperErrors {
+ TransactionMiningTimeout = 'TRANSACTION_MINING_TIMEOUT',
+}
+
+export interface AbstractBlockRPC {
+ number: string | null;
+ hash: string | null;
+ parentHash: string;
+ nonce: string | null;
+ sha3Uncles: string;
+ logsBloom: string | null;
+ transactionsRoot: string;
+ stateRoot: string;
+ miner: string;
+ difficulty: string;
+ totalDifficulty: string;
+ extraData: string;
+ size: string;
+ gasLimit: string;
+ gasUsed: string;
+ timestamp: string;
+ uncles: string[];
+}
+export interface BlockWithoutTransactionDataRPC extends AbstractBlockRPC {
+ transactions: string[];
+}
+export interface BlockWithTransactionDataRPC extends AbstractBlockRPC {
+ transactions: TransactionRPC[];
+}
+export interface TransactionRPC {
+ hash: string;
+ nonce: string;
+ blockHash: string | null;
+ blockNumber: string | null;
+ transactionIndex: string | null;
+ from: string;
+ to: string | null;
+ value: string;
+ gasPrice: string;
+ gas: string;
+ input: string;
+}
+
+export interface CallTxDataBaseRPC {
+ to?: string;
+ value?: string;
+ gas?: string;
+ gasPrice?: string;
+ data?: string;
+ nonce?: string;
+}
+
+export interface TxDataRPC extends CallTxDataBaseRPC {
+ from: string;
+}
+
+export interface CallDataRPC extends CallTxDataBaseRPC {
+ from?: string;
+}
+
+// NodeType represents the type of the backing Ethereum node.
+export enum NodeType {
+ Geth = 'GETH',
+ Ganache = 'GANACHE',
+}
diff --git a/packages/web3-wrapper/src/utils.ts b/packages/web3-wrapper/src/utils.ts
new file mode 100644
index 000000000..01605dc9a
--- /dev/null
+++ b/packages/web3-wrapper/src/utils.ts
@@ -0,0 +1,58 @@
+import { BigNumber } from '@0xproject/utils';
+import * as _ from 'lodash';
+
+export const utils = {
+ isBigNumber(value: any): boolean {
+ const isBigNumber = _.isObject(value) && value.isBigNumber;
+ return isBigNumber;
+ },
+ convertHexToNumber(value: string): number {
+ const valueBigNumber = new BigNumber(value);
+ const valueNumber = valueBigNumber.toNumber();
+ return valueNumber;
+ },
+ convertHexToNumberOrNull(hex: string | null): number | null {
+ if (_.isNull(hex)) {
+ return null;
+ }
+ const decimal = utils.convertHexToNumber(hex);
+ return decimal;
+ },
+ convertAmountToBigNumber(value: string | number | BigNumber): BigNumber {
+ const num = value || 0;
+ const isBigNumber = utils.isBigNumber(num);
+ if (isBigNumber) {
+ return num as BigNumber;
+ }
+
+ if (_.isString(num) && (num.indexOf('0x') === 0 || num.indexOf('-0x') === 0)) {
+ return new BigNumber(num.replace('0x', ''), 16);
+ }
+
+ const baseTen = 10;
+ return new BigNumber((num as number).toString(baseTen), baseTen);
+ },
+ encodeAmountAsHexString(value: string | number | BigNumber): string {
+ const valueBigNumber = utils.convertAmountToBigNumber(value);
+ const hexBase = 16;
+ const valueHex = valueBigNumber.toString(hexBase);
+
+ return valueBigNumber.lessThan(0) ? '-0x' + valueHex.substr(1) : '0x' + valueHex;
+ },
+ numberToHex(value: number): string {
+ if (!isFinite(value) && !utils.isHexStrict(value)) {
+ throw new Error(`Given input ${value} is not a number.`);
+ }
+
+ const valueBigNumber = new BigNumber(value);
+ const hexBase = 16;
+ const result = valueBigNumber.toString(hexBase);
+
+ return valueBigNumber.lt(0) ? '-0x' + result.substr(1) : '0x' + result;
+ },
+ isHexStrict(hex: string | number): boolean {
+ return (
+ (_.isString(hex) || _.isNumber(hex)) && /^(-)?0x[0-9a-f]*$/i.test(_.isNumber(hex) ? hex.toString() : hex)
+ );
+ },
+};
diff --git a/packages/web3-wrapper/src/web3_wrapper.ts b/packages/web3-wrapper/src/web3_wrapper.ts
new file mode 100644
index 000000000..dd35e2094
--- /dev/null
+++ b/packages/web3-wrapper/src/web3_wrapper.ts
@@ -0,0 +1,653 @@
+import { assert } from '@0xproject/assert';
+import { schemas } from '@0xproject/json-schemas';
+import { AbiDecoder, addressUtils, BigNumber, intervalUtils, promisify } from '@0xproject/utils';
+import {
+ BlockParam,
+ BlockParamLiteral,
+ BlockWithoutTransactionData,
+ BlockWithTransactionData,
+ CallData,
+ FilterObject,
+ JSONRPCRequestPayload,
+ JSONRPCResponsePayload,
+ LogEntry,
+ Provider,
+ RawLogEntry,
+ TraceParams,
+ Transaction,
+ TransactionReceipt,
+ TransactionReceiptWithDecodedLogs,
+ TransactionTrace,
+ TxData,
+} from 'ethereum-types';
+import * as _ from 'lodash';
+
+import { marshaller } from './marshaller';
+import { BlockWithoutTransactionDataRPC, BlockWithTransactionDataRPC, NodeType, Web3WrapperErrors } from './types';
+import { utils } from './utils';
+
+const BASE_TEN = 10;
+
+// These are unique identifiers contained in the response of the
+// web3_clientVersion call.
+const uniqueVersionIds = {
+ geth: 'Geth',
+ ganache: 'EthereumJS TestRPC',
+};
+
+/**
+ * An alternative to the Web3.js library that provides a consistent, clean, promise-based interface.
+ */
+export class Web3Wrapper {
+ /**
+ * Flag to check if this instance is of type Web3Wrapper
+ */
+ public isZeroExWeb3Wrapper = true;
+ public abiDecoder: AbiDecoder;
+ private _provider: Provider;
+ private readonly _txDefaults: Partial<TxData>;
+ private _jsonRpcRequestId: number;
+ /**
+ * Check if an address is a valid Ethereum address
+ * @param address Address to check
+ * @returns Whether the address is a valid Ethereum address
+ */
+ public static isAddress(address: string): boolean {
+ return addressUtils.isAddress(address);
+ }
+ /**
+ * A unit amount is defined as the amount of a token above the specified decimal places (integer part).
+ * E.g: If a currency has 18 decimal places, 1e18 or one quintillion of the currency is equivalent
+ * to 1 unit.
+ * @param amount The amount in baseUnits that you would like converted to units.
+ * @param decimals The number of decimal places the unit amount has.
+ * @return The amount in units.
+ */
+ public static toUnitAmount(amount: BigNumber, decimals: number): BigNumber {
+ assert.isValidBaseUnitAmount('amount', amount);
+ assert.isNumber('decimals', decimals);
+ const aUnit = new BigNumber(BASE_TEN).pow(decimals);
+ const unit = amount.div(aUnit);
+ return unit;
+ }
+ /**
+ * A baseUnit is defined as the smallest denomination of a token. An amount expressed in baseUnits
+ * is the amount expressed in the smallest denomination.
+ * E.g: 1 unit of a token with 18 decimal places is expressed in baseUnits as 1000000000000000000
+ * @param amount The amount of units that you would like converted to baseUnits.
+ * @param decimals The number of decimal places the unit amount has.
+ * @return The amount in baseUnits.
+ */
+ public static toBaseUnitAmount(amount: BigNumber, decimals: number): BigNumber {
+ assert.isBigNumber('amount', amount);
+ assert.isNumber('decimals', decimals);
+ const unit = new BigNumber(BASE_TEN).pow(decimals);
+ const baseUnitAmount = amount.times(unit);
+ const hasDecimals = baseUnitAmount.decimalPlaces() !== 0;
+ if (hasDecimals) {
+ throw new Error(`Invalid unit amount: ${amount.toString()} - Too many decimal places`);
+ }
+ return baseUnitAmount;
+ }
+ /**
+ * Convert an Ether amount from ETH to Wei
+ * @param ethAmount Amount of Ether to convert to wei
+ * @returns Amount in wei
+ */
+ public static toWei(ethAmount: BigNumber): BigNumber {
+ assert.isBigNumber('ethAmount', ethAmount);
+ const ETH_DECIMALS = 18;
+ const balanceWei = Web3Wrapper.toBaseUnitAmount(ethAmount, ETH_DECIMALS);
+ return balanceWei;
+ }
+ private static _assertBlockParam(blockParam: string | BlockParam): void {
+ if (_.isNumber(blockParam)) {
+ return;
+ } else if (_.isString(blockParam)) {
+ assert.doesBelongToStringEnum('blockParam', blockParam, BlockParamLiteral);
+ }
+ }
+ private static _assertBlockParamOrString(blockParam: string | BlockParam): void {
+ try {
+ Web3Wrapper._assertBlockParam(blockParam);
+ } catch (err) {
+ try {
+ assert.isHexString('blockParam', blockParam as string);
+ return;
+ } catch (err) {
+ throw new Error(`Expected blockParam to be of type "string | BlockParam", encountered ${blockParam}`);
+ }
+ }
+ }
+ private static _normalizeTxReceiptStatus(status: undefined | null | string | 0 | 1): null | 0 | 1 {
+ // Transaction status might have four values
+ // undefined - Testrpc and other old clients
+ // null - New clients on old transactions
+ // number - Parity
+ // hex - Geth
+ if (_.isString(status)) {
+ return utils.convertHexToNumber(status) as 0 | 1;
+ } else if (_.isUndefined(status)) {
+ return null;
+ } else {
+ return status;
+ }
+ }
+ /**
+ * Instantiates a new Web3Wrapper.
+ * @param provider The Web3 provider instance you would like the Web3Wrapper to use for interacting with
+ * the backing Ethereum node.
+ * @param txDefaults Override TxData defaults sent with RPC requests to the backing Ethereum node.
+ * @return An instance of the Web3Wrapper class.
+ */
+ constructor(provider: Provider, txDefaults?: Partial<TxData>) {
+ assert.isWeb3Provider('provider', provider);
+ if (_.isUndefined((provider as any).sendAsync)) {
+ // Web3@1.0 provider doesn't support synchronous http requests,
+ // so it only has an async `send` method, instead of a `send` and `sendAsync` in web3@0.x.x`
+ // We re-assign the send method so that Web3@1.0 providers work with @0xproject/web3-wrapper
+ (provider as any).sendAsync = (provider as any).send;
+ }
+ this.abiDecoder = new AbiDecoder([]);
+ this._provider = provider;
+ this._txDefaults = txDefaults || {};
+ this._jsonRpcRequestId = 0;
+ }
+ /**
+ * Get the contract defaults set to the Web3Wrapper instance
+ * @return TxData defaults (e.g gas, gasPrice, nonce, etc...)
+ */
+ public getContractDefaults(): Partial<TxData> {
+ return this._txDefaults;
+ }
+ /**
+ * Retrieve the Web3 provider
+ * @return Web3 provider instance
+ */
+ public getProvider(): Provider {
+ return this._provider;
+ }
+ /**
+ * Update the used Web3 provider
+ * @param provider The new Web3 provider to be set
+ */
+ public setProvider(provider: Provider): void {
+ assert.isWeb3Provider('provider', provider);
+ this._provider = provider;
+ }
+ /**
+ * Check whether an address is available through the backing provider. This can be
+ * useful if you want to know whether a user can sign messages or transactions from
+ * a given Ethereum address.
+ * @param senderAddress Address to check availability for
+ * @returns Whether the address is available through the provider.
+ */
+ public async isSenderAddressAvailableAsync(senderAddress: string): Promise<boolean> {
+ assert.isETHAddressHex('senderAddress', senderAddress);
+ const addresses = await this.getAvailableAddressesAsync();
+ const normalizedAddress = senderAddress.toLowerCase();
+ return _.includes(addresses, normalizedAddress);
+ }
+ /**
+ * Fetch the backing Ethereum node's version string (e.g `MetaMask/v4.2.0`)
+ * @returns Ethereum node's version string
+ */
+ public async getNodeVersionAsync(): Promise<string> {
+ const nodeVersion = await this._sendRawPayloadAsync<string>({ method: 'web3_clientVersion' });
+ return nodeVersion;
+ }
+ /**
+ * Fetches the networkId of the backing Ethereum node
+ * @returns The network id
+ */
+ public async getNetworkIdAsync(): Promise<number> {
+ const networkIdStr = await this._sendRawPayloadAsync<string>({ method: 'net_version' });
+ const networkId = _.parseInt(networkIdStr);
+ return networkId;
+ }
+ /**
+ * Retrieves the transaction receipt for a given transaction hash
+ * @param txHash Transaction hash
+ * @returns The transaction receipt, including it's status (0: failed, 1: succeeded or undefined: not found)
+ */
+ public async getTransactionReceiptAsync(txHash: string): Promise<TransactionReceipt> {
+ assert.isHexString('txHash', txHash);
+ const transactionReceipt = await this._sendRawPayloadAsync<TransactionReceipt>({
+ method: 'eth_getTransactionReceipt',
+ params: [txHash],
+ });
+ if (!_.isNull(transactionReceipt)) {
+ transactionReceipt.status = Web3Wrapper._normalizeTxReceiptStatus(transactionReceipt.status);
+ }
+ return transactionReceipt;
+ }
+ /**
+ * Retrieves the transaction data for a given transaction
+ * @param txHash Transaction hash
+ * @returns The raw transaction data
+ */
+ public async getTransactionByHashAsync(txHash: string): Promise<Transaction> {
+ assert.isHexString('txHash', txHash);
+ const transaction = await this._sendRawPayloadAsync<Transaction>({
+ method: 'eth_getTransactionByHash',
+ params: [txHash],
+ });
+ return transaction;
+ }
+ /**
+ * Retrieves an accounts Ether balance in wei
+ * @param owner Account whose balance you wish to check
+ * @returns Balance in wei
+ */
+ public async getBalanceInWeiAsync(owner: string, defaultBlock?: BlockParam): Promise<BigNumber> {
+ assert.isETHAddressHex('owner', owner);
+ if (!_.isUndefined(defaultBlock)) {
+ Web3Wrapper._assertBlockParam(defaultBlock);
+ }
+ const marshalledDefaultBlock = marshaller.marshalBlockParam(defaultBlock);
+ const encodedOwner = marshaller.marshalAddress(owner);
+ const balanceInWei = await this._sendRawPayloadAsync<string>({
+ method: 'eth_getBalance',
+ params: [encodedOwner, marshalledDefaultBlock],
+ });
+ // Rewrap in a new BigNumber
+ return new BigNumber(balanceInWei);
+ }
+ /**
+ * Check if a contract exists at a given address
+ * @param address Address to which to check
+ * @returns Whether or not contract code was found at the supplied address
+ */
+ public async doesContractExistAtAddressAsync(address: string): Promise<boolean> {
+ assert.isETHAddressHex('address', address);
+ const code = await this.getContractCodeAsync(address);
+ // Regex matches 0x0, 0x00, 0x in order to accommodate poorly implemented clients
+ const isCodeEmpty = /^0x0{0,40}$/i.test(code);
+ return !isCodeEmpty;
+ }
+ /**
+ * Gets the contract code by address
+ * @param address Address of the contract
+ * @param defaultBlock Block height at which to make the call. Defaults to `latest`
+ * @return Code of the contract
+ */
+ public async getContractCodeAsync(address: string, defaultBlock?: BlockParam): Promise<string> {
+ assert.isETHAddressHex('address', address);
+ if (!_.isUndefined(defaultBlock)) {
+ Web3Wrapper._assertBlockParam(defaultBlock);
+ }
+ const marshalledDefaultBlock = marshaller.marshalBlockParam(defaultBlock);
+ const encodedAddress = marshaller.marshalAddress(address);
+ const code = await this._sendRawPayloadAsync<string>({
+ method: 'eth_getCode',
+ params: [encodedAddress, marshalledDefaultBlock],
+ });
+ return code;
+ }
+ /**
+ * Gets the debug trace of a transaction
+ * @param txHash Hash of the transactuon to get a trace for
+ * @param traceParams Config object allowing you to specify if you need memory/storage/stack traces.
+ * @return Transaction trace
+ */
+ public async getTransactionTraceAsync(txHash: string, traceParams: TraceParams): Promise<TransactionTrace> {
+ assert.isHexString('txHash', txHash);
+ const trace = await this._sendRawPayloadAsync<TransactionTrace>({
+ method: 'debug_traceTransaction',
+ params: [txHash, traceParams],
+ });
+ return trace;
+ }
+ /**
+ * Sign a message with a specific address's private key (`eth_sign`)
+ * @param address Address of signer
+ * @param message Message to sign
+ * @returns Signature string (might be VRS or RSV depending on the Signer)
+ */
+ public async signMessageAsync(address: string, message: string): Promise<string> {
+ assert.isETHAddressHex('address', address);
+ assert.isString('message', message); // TODO: Should this be stricter? Hex string?
+ const signData = await this._sendRawPayloadAsync<string>({
+ method: 'eth_sign',
+ params: [address, message],
+ });
+ return signData;
+ }
+ /**
+ * Fetches the latest block number
+ * @returns Block number
+ */
+ public async getBlockNumberAsync(): Promise<number> {
+ const blockNumberHex = await this._sendRawPayloadAsync<string>({
+ method: 'eth_blockNumber',
+ params: [],
+ });
+ const blockNumber = utils.convertHexToNumberOrNull(blockNumberHex);
+ return blockNumber as number;
+ }
+ /**
+ * Fetch a specific Ethereum block without transaction data
+ * @param blockParam The block you wish to fetch (blockHash, blockNumber or blockLiteral)
+ * @returns The requested block without transaction data
+ */
+ public async getBlockAsync(blockParam: string | BlockParam): Promise<BlockWithoutTransactionData> {
+ Web3Wrapper._assertBlockParamOrString(blockParam);
+ const encodedBlockParam = marshaller.marshalBlockParam(blockParam);
+ const method = utils.isHexStrict(blockParam) ? 'eth_getBlockByHash' : 'eth_getBlockByNumber';
+ const shouldIncludeTransactionData = false;
+ const blockWithoutTransactionDataWithHexValues = await this._sendRawPayloadAsync<
+ BlockWithoutTransactionDataRPC
+ >({
+ method,
+ params: [encodedBlockParam, shouldIncludeTransactionData],
+ });
+ const blockWithoutTransactionData = marshaller.unmarshalIntoBlockWithoutTransactionData(
+ blockWithoutTransactionDataWithHexValues,
+ );
+ return blockWithoutTransactionData;
+ }
+ /**
+ * Fetch a specific Ethereum block with transaction data
+ * @param blockParam The block you wish to fetch (blockHash, blockNumber or blockLiteral)
+ * @returns The requested block with transaction data
+ */
+ public async getBlockWithTransactionDataAsync(blockParam: string | BlockParam): Promise<BlockWithTransactionData> {
+ Web3Wrapper._assertBlockParamOrString(blockParam);
+ let encodedBlockParam = blockParam;
+ if (_.isNumber(blockParam)) {
+ encodedBlockParam = utils.numberToHex(blockParam);
+ }
+ const method = utils.isHexStrict(blockParam) ? 'eth_getBlockByHash' : 'eth_getBlockByNumber';
+ const shouldIncludeTransactionData = true;
+ const blockWithTransactionDataWithHexValues = await this._sendRawPayloadAsync<BlockWithTransactionDataRPC>({
+ method,
+ params: [encodedBlockParam, shouldIncludeTransactionData],
+ });
+ const blockWithoutTransactionData = marshaller.unmarshalIntoBlockWithTransactionData(
+ blockWithTransactionDataWithHexValues,
+ );
+ return blockWithoutTransactionData;
+ }
+ /**
+ * Fetch a block's timestamp
+ * @param blockParam The block you wish to fetch (blockHash, blockNumber or blockLiteral)
+ * @returns The block's timestamp
+ */
+ public async getBlockTimestampAsync(blockParam: string | BlockParam): Promise<number> {
+ Web3Wrapper._assertBlockParamOrString(blockParam);
+ const { timestamp } = await this.getBlockAsync(blockParam);
+ return timestamp;
+ }
+ /**
+ * Retrieve the user addresses available through the backing provider
+ * @returns Available user addresses
+ */
+ public async getAvailableAddressesAsync(): Promise<string[]> {
+ const addresses = await this._sendRawPayloadAsync<string>({
+ method: 'eth_accounts',
+ params: [],
+ });
+ const normalizedAddresses = _.map(addresses, address => address.toLowerCase());
+ return normalizedAddresses;
+ }
+ /**
+ * Take a snapshot of the blockchain state on a TestRPC/Ganache local node
+ * @returns The snapshot id. This can be used to revert to this snapshot
+ */
+ public async takeSnapshotAsync(): Promise<number> {
+ const snapshotId = Number(await this._sendRawPayloadAsync<string>({ method: 'evm_snapshot', params: [] }));
+ return snapshotId;
+ }
+ /**
+ * Revert the blockchain state to a previous snapshot state on TestRPC/Ganache local node
+ * @param snapshotId snapshot id to revert to
+ * @returns Whether the revert was successful
+ */
+ public async revertSnapshotAsync(snapshotId: number): Promise<boolean> {
+ assert.isNumber('snapshotId', snapshotId);
+ const didRevert = await this._sendRawPayloadAsync<boolean>({ method: 'evm_revert', params: [snapshotId] });
+ return didRevert;
+ }
+ /**
+ * Mine a block on a TestRPC/Ganache local node
+ */
+ public async mineBlockAsync(): Promise<void> {
+ await this._sendRawPayloadAsync<string>({ method: 'evm_mine', params: [] });
+ }
+ /**
+ * Increase the next blocks timestamp on TestRPC/Ganache or Geth local node.
+ * Will throw if provider is neither TestRPC/Ganache or Geth.
+ * @param timeDelta Amount of time to add in seconds
+ */
+ public async increaseTimeAsync(timeDelta: number): Promise<number> {
+ assert.isNumber('timeDelta', timeDelta);
+ // Detect Geth vs. Ganache and use appropriate endpoint.
+ const version = await this.getNodeVersionAsync();
+ if (_.includes(version, uniqueVersionIds.geth)) {
+ return this._sendRawPayloadAsync<number>({ method: 'debug_increaseTime', params: [timeDelta] });
+ } else if (_.includes(version, uniqueVersionIds.ganache)) {
+ return this._sendRawPayloadAsync<number>({ method: 'evm_increaseTime', params: [timeDelta] });
+ } else {
+ throw new Error(`Unknown client version: ${version}`);
+ }
+ }
+ /**
+ * Retrieve smart contract logs for a given filter
+ * @param filter Parameters by which to filter which logs to retrieve
+ * @returns The corresponding log entries
+ */
+ public async getLogsAsync(filter: FilterObject): Promise<LogEntry[]> {
+ let fromBlock = filter.fromBlock;
+ if (_.isNumber(fromBlock)) {
+ fromBlock = utils.numberToHex(fromBlock);
+ }
+ let toBlock = filter.toBlock;
+ if (_.isNumber(toBlock)) {
+ toBlock = utils.numberToHex(toBlock);
+ }
+ const serializedFilter = {
+ ...filter,
+ fromBlock,
+ toBlock,
+ };
+ const payload = {
+ method: 'eth_getLogs',
+ params: [serializedFilter],
+ };
+ const rawLogs = await this._sendRawPayloadAsync<RawLogEntry[]>(payload);
+ const formattedLogs = _.map(rawLogs, marshaller.unmarshalLog.bind(marshaller));
+ return formattedLogs;
+ }
+ /**
+ * Calculate the estimated gas cost for a given transaction
+ * @param txData Transaction data
+ * @returns Estimated gas cost
+ */
+ public async estimateGasAsync(txData: Partial<TxData>): Promise<number> {
+ assert.doesConformToSchema('txData', txData, schemas.txDataSchema, [
+ schemas.addressSchema,
+ schemas.numberSchema,
+ schemas.jsNumber,
+ ]);
+ const txDataHex = marshaller.marshalTxData(txData);
+ const gasHex = await this._sendRawPayloadAsync<string>({ method: 'eth_estimateGas', params: [txDataHex] });
+ const gas = utils.convertHexToNumber(gasHex);
+ return gas;
+ }
+ /**
+ * Call a smart contract method at a given block height
+ * @param callData Call data
+ * @param defaultBlock Block height at which to make the call. Defaults to `latest`
+ * @returns The raw call result
+ */
+ public async callAsync(callData: CallData, defaultBlock?: BlockParam): Promise<string> {
+ assert.doesConformToSchema('callData', callData, schemas.callDataSchema, [
+ schemas.addressSchema,
+ schemas.numberSchema,
+ schemas.jsNumber,
+ ]);
+ if (!_.isUndefined(defaultBlock)) {
+ Web3Wrapper._assertBlockParam(defaultBlock);
+ }
+ const marshalledDefaultBlock = marshaller.marshalBlockParam(defaultBlock);
+ const callDataHex = marshaller.marshalCallData(callData);
+ const rawCallResult = await this._sendRawPayloadAsync<string>({
+ method: 'eth_call',
+ params: [callDataHex, marshalledDefaultBlock],
+ });
+ if (rawCallResult === '0x') {
+ throw new Error('Contract call failed (returned null)');
+ }
+ return rawCallResult;
+ }
+ /**
+ * Send a transaction
+ * @param txData Transaction data
+ * @returns Transaction hash
+ */
+ public async sendTransactionAsync(txData: TxData): Promise<string> {
+ assert.doesConformToSchema('txData', txData, schemas.txDataSchema, [
+ schemas.addressSchema,
+ schemas.numberSchema,
+ schemas.jsNumber,
+ ]);
+ const txDataHex = marshaller.marshalTxData(txData);
+ const txHash = await this._sendRawPayloadAsync<string>({ method: 'eth_sendTransaction', params: [txDataHex] });
+ return txHash;
+ }
+ /**
+ * Waits for a transaction to be mined and returns the transaction receipt.
+ * Note that just because a transaction was mined does not mean it was
+ * successful. You need to check the status code of the transaction receipt
+ * to find out if it was successful, or use the helper method
+ * awaitTransactionSuccessAsync.
+ * @param txHash Transaction hash
+ * @param pollingIntervalMs How often (in ms) should we check if the transaction is mined.
+ * @param timeoutMs How long (in ms) to poll for transaction mined until aborting.
+ * @return Transaction receipt with decoded log args.
+ */
+ public async awaitTransactionMinedAsync(
+ txHash: string,
+ pollingIntervalMs: number = 1000,
+ timeoutMs?: number,
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ assert.isHexString('txHash', txHash);
+ assert.isNumber('pollingIntervalMs', pollingIntervalMs);
+ if (!_.isUndefined(timeoutMs)) {
+ assert.isNumber('timeoutMs', timeoutMs);
+ }
+ // Immediately check if the transaction has already been mined.
+ let transactionReceipt = await this.getTransactionReceiptAsync(txHash);
+ if (!_.isNull(transactionReceipt)) {
+ const logsWithDecodedArgs = _.map(
+ transactionReceipt.logs,
+ this.abiDecoder.tryToDecodeLogOrNoop.bind(this.abiDecoder),
+ );
+ const transactionReceiptWithDecodedLogArgs: TransactionReceiptWithDecodedLogs = {
+ ...transactionReceipt,
+ logs: logsWithDecodedArgs,
+ };
+ return transactionReceiptWithDecodedLogArgs;
+ }
+
+ // Otherwise, check again every pollingIntervalMs.
+ let wasTimeoutExceeded = false;
+ if (timeoutMs) {
+ setTimeout(() => (wasTimeoutExceeded = true), timeoutMs);
+ }
+
+ const txReceiptPromise = new Promise(
+ (resolve: (receipt: TransactionReceiptWithDecodedLogs) => void, reject) => {
+ const intervalId = intervalUtils.setAsyncExcludingInterval(
+ async () => {
+ if (wasTimeoutExceeded) {
+ intervalUtils.clearAsyncExcludingInterval(intervalId);
+ return reject(Web3WrapperErrors.TransactionMiningTimeout);
+ }
+
+ transactionReceipt = await this.getTransactionReceiptAsync(txHash);
+ if (!_.isNull(transactionReceipt)) {
+ intervalUtils.clearAsyncExcludingInterval(intervalId);
+ const logsWithDecodedArgs = _.map(
+ transactionReceipt.logs,
+ this.abiDecoder.tryToDecodeLogOrNoop.bind(this.abiDecoder),
+ );
+ const transactionReceiptWithDecodedLogArgs: TransactionReceiptWithDecodedLogs = {
+ ...transactionReceipt,
+ logs: logsWithDecodedArgs,
+ };
+ resolve(transactionReceiptWithDecodedLogArgs);
+ }
+ },
+ pollingIntervalMs,
+ (err: Error) => {
+ intervalUtils.clearAsyncExcludingInterval(intervalId);
+ reject(err);
+ },
+ );
+ },
+ );
+ const txReceipt = await txReceiptPromise;
+ return txReceipt;
+ }
+ /**
+ * Waits for a transaction to be mined and returns the transaction receipt.
+ * Unlike awaitTransactionMinedAsync, it will throw if the receipt has a
+ * status that is not equal to 1. A status of 0 or null indicates that the
+ * transaction was mined, but failed. See:
+ * https://github.com/ethereum/wiki/wiki/JavaScript-API#web3ethgettransactionreceipt
+ * @param txHash Transaction hash
+ * @param pollingIntervalMs How often (in ms) should we check if the transaction is mined.
+ * @param timeoutMs How long (in ms) to poll for transaction mined until aborting.
+ * @return Transaction receipt with decoded log args.
+ */
+ public async awaitTransactionSuccessAsync(
+ txHash: string,
+ pollingIntervalMs: number = 1000,
+ timeoutMs?: number,
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const receipt = await this.awaitTransactionMinedAsync(txHash, pollingIntervalMs, timeoutMs);
+ if (receipt.status !== 1) {
+ throw new Error(`Transaction failed: ${txHash}`);
+ }
+ return receipt;
+ }
+ /**
+ * Calls the 'debug_setHead' JSON RPC method, which sets the current head of
+ * the local chain by block number. Note, this is a destructive action and
+ * may severely damage your chain. Use with extreme caution. As of now, this
+ * is only supported by Geth. It sill throw if the 'debug_setHead' method is
+ * not supported.
+ * @param blockNumber The block number to reset to.
+ */
+ public async setHeadAsync(blockNumber: number): Promise<void> {
+ assert.isNumber('blockNumber', blockNumber);
+ await this._sendRawPayloadAsync<void>({ method: 'debug_setHead', params: [utils.numberToHex(blockNumber)] });
+ }
+ /**
+ * Returns either NodeType.Geth or NodeType.Ganache depending on the type of
+ * the backing Ethereum node. Throws for any other type of node.
+ */
+ public async getNodeTypeAsync(): Promise<NodeType> {
+ const version = await this.getNodeVersionAsync();
+ if (_.includes(version, uniqueVersionIds.geth)) {
+ return NodeType.Geth;
+ } else if (_.includes(version, uniqueVersionIds.ganache)) {
+ return NodeType.Ganache;
+ } else {
+ throw new Error(`Unknown client version: ${version}`);
+ }
+ }
+ private async _sendRawPayloadAsync<A>(payload: Partial<JSONRPCRequestPayload>): Promise<A> {
+ const sendAsync = this._provider.sendAsync.bind(this._provider);
+ const payloadWithDefaults = {
+ id: this._jsonRpcRequestId++,
+ params: [],
+ jsonrpc: '2.0',
+ ...payload,
+ };
+ const response = await promisify<JSONRPCResponsePayload>(sendAsync)(payloadWithDefaults);
+ const result = response.result;
+ return result;
+ }
+} // tslint:disable-line:max-file-line-count