diff options
author | Alex Browne <stephenalexbrowne@gmail.com> | 2018-08-14 09:42:09 +0800 |
---|---|---|
committer | Alex Browne <stephenalexbrowne@gmail.com> | 2018-08-14 09:42:09 +0800 |
commit | 88766a02c7e6688e72d5c4c69ce68028b322f154 (patch) | |
tree | fa06552a80249e7998691b64df6b3b2827f9f947 /packages/web3-wrapper/src | |
parent | 8162394797342cef268cc8072fc860326974e269 (diff) | |
parent | fadd292ecf367e42154856509d0ea0c20b23f2f1 (diff) | |
download | dexon-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.ts | 339 | ||||
-rw-r--r-- | packages/web3-wrapper/src/marshaller.ts | 210 | ||||
-rw-r--r-- | packages/web3-wrapper/src/types.ts | 65 | ||||
-rw-r--r-- | packages/web3-wrapper/src/utils.ts | 58 | ||||
-rw-r--r-- | packages/web3-wrapper/src/web3_wrapper.ts | 653 |
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 |