diff options
Diffstat (limited to 'packages/web3-wrapper/src/web3_wrapper.ts')
-rw-r--r-- | packages/web3-wrapper/src/web3_wrapper.ts | 244 |
1 files changed, 168 insertions, 76 deletions
diff --git a/packages/web3-wrapper/src/web3_wrapper.ts b/packages/web3-wrapper/src/web3_wrapper.ts index b79ade278..495523e44 100644 --- a/packages/web3-wrapper/src/web3_wrapper.ts +++ b/packages/web3-wrapper/src/web3_wrapper.ts @@ -1,10 +1,12 @@ +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, - ContractAbi, FilterObject, JSONRPCRequestPayload, JSONRPCResponsePayload, @@ -18,9 +20,10 @@ import { TxData, } from 'ethereum-types'; import * as _ from 'lodash'; -import * as Web3 from 'web3'; -import { Web3WrapperErrors } from './types'; +import { marshaller } from './marshaller'; +import { BlockWithoutTransactionDataRPC, BlockWithTransactionDataRPC, Web3WrapperErrors } from './types'; +import { utils } from './utils'; const BASE_TEN = 10; @@ -38,7 +41,7 @@ export enum NodeType { } /** - * A wrapper around the Web3.js 0.x library that provides a consistent, clean promise-based interface. + * An alternative to the Web3.js library that provides a consistent, clean, promise-based interface. */ export class Web3Wrapper { /** @@ -46,7 +49,7 @@ export class Web3Wrapper { */ public isZeroExWeb3Wrapper = true; public abiDecoder: AbiDecoder; - private _web3: Web3; + private _provider: Provider; private _txDefaults: Partial<TxData>; private _jsonRpcRequestId: number; /** @@ -66,6 +69,8 @@ export class Web3Wrapper { * @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; @@ -79,6 +84,8 @@ export class Web3Wrapper { * @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; @@ -93,10 +100,44 @@ export class Web3Wrapper { * @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 @@ -105,6 +146,7 @@ export class Web3Wrapper { * @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` @@ -112,8 +154,7 @@ export class Web3Wrapper { (provider as any).sendAsync = (provider as any).send; } this.abiDecoder = new AbiDecoder([]); - this._web3 = new Web3(); - this._web3.setProvider(provider); + this._provider = provider; this._txDefaults = txDefaults || {}; this._jsonRpcRequestId = 0; } @@ -129,14 +170,15 @@ export class Web3Wrapper { * @return Web3 provider instance */ public getProvider(): Provider { - return this._web3.currentProvider; + return this._provider; } /** * Update the used Web3 provider * @param provider The new Web3 provider to be set */ public setProvider(provider: Provider): void { - this._web3.setProvider(provider); + assert.isWeb3Provider('provider', provider); + this._provider = provider; } /** * Check whether an address is available through the backing provider. This can be @@ -146,6 +188,7 @@ export class Web3Wrapper { * @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); @@ -173,9 +216,13 @@ export class Web3Wrapper { * @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); + assert.isHexString('txHash', txHash); + const transactionReceipt = await this._sendRawPayloadAsync<TransactionReceipt>({ + method: 'eth_getTransactionReceipt', + params: [txHash], + }); if (!_.isNull(transactionReceipt)) { - transactionReceipt.status = this._normalizeTxReceiptStatus(transactionReceipt.status); + transactionReceipt.status = Web3Wrapper._normalizeTxReceiptStatus(transactionReceipt.status); } return transactionReceipt; } @@ -184,11 +231,19 @@ export class Web3Wrapper { * @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); + 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 - balanceInWei = new BigNumber(balanceInWei); - return balanceInWei; + return new BigNumber(balanceInWei); } /** * Check if a contract exists at a given address @@ -196,6 +251,7 @@ export class Web3Wrapper { * @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); @@ -204,10 +260,20 @@ export class Web3Wrapper { /** * 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): Promise<string> { - const code = await promisify<string>(this._web3.eth.getCode)(address); + 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; } /** @@ -217,6 +283,7 @@ export class Web3Wrapper { * @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], @@ -230,7 +297,13 @@ export class Web3Wrapper { * @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); + assert.isETHAddressHex('address', address); + 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; } /** @@ -238,8 +311,12 @@ export class Web3Wrapper { * @returns Block number */ public async getBlockNumberAsync(): Promise<number> { - const blockNumber = await promisify<number>(this._web3.eth.getBlockNumber)(); - return blockNumber; + 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 @@ -247,10 +324,18 @@ export class Web3Wrapper { * @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 blockWithoutTransactionData = await promisify<BlockWithoutTransactionData>(this._web3.eth.getBlock)( - blockParam, - shouldIncludeTransactionData, + const blockWithoutTransactionDataWithHexValues = await this._sendRawPayloadAsync< + BlockWithoutTransactionDataRPC + >({ + method, + params: [encodedBlockParam, shouldIncludeTransactionData], + }); + const blockWithoutTransactionData = marshaller.unmarshalIntoBlockWithoutTransactionData( + blockWithoutTransactionDataWithHexValues, ); return blockWithoutTransactionData; } @@ -260,12 +345,21 @@ export class Web3Wrapper { * @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 blockWithTransactionData = await promisify<BlockWithTransactionData>(this._web3.eth.getBlock)( - blockParam, - shouldIncludeTransactionData, + const blockWithTransactionDataWithHexValues = await this._sendRawPayloadAsync<BlockWithTransactionDataRPC>({ + method, + params: [encodedBlockParam, shouldIncludeTransactionData], + }); + const blockWithoutTransactionData = marshaller.unmarshalIntoBlockWithTransactionData( + blockWithTransactionDataWithHexValues, ); - return blockWithTransactionData; + return blockWithoutTransactionData; } /** * Fetch a block's timestamp @@ -273,6 +367,7 @@ export class Web3Wrapper { * @returns The block's timestamp */ public async getBlockTimestampAsync(blockParam: string | BlockParam): Promise<number> { + Web3Wrapper._assertBlockParamOrString(blockParam); const { timestamp } = await this.getBlockAsync(blockParam); return timestamp; } @@ -281,7 +376,10 @@ export class Web3Wrapper { * @returns Available user addresses */ public async getAvailableAddressesAsync(): Promise<string[]> { - const addresses = await promisify<string[]>(this._web3.eth.getAccounts)(); + const addresses = await this._sendRawPayloadAsync<string>({ + method: 'eth_accounts', + params: [], + }); const normalizedAddresses = _.map(addresses, address => address.toLowerCase()); return normalizedAddresses; } @@ -299,6 +397,7 @@ export class Web3Wrapper { * @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; } @@ -314,6 +413,7 @@ export class Web3Wrapper { * @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)) { @@ -332,11 +432,11 @@ export class Web3Wrapper { public async getLogsAsync(filter: FilterObject): Promise<LogEntry[]> { let fromBlock = filter.fromBlock; if (_.isNumber(fromBlock)) { - fromBlock = this._web3.toHex(fromBlock); + fromBlock = utils.numberToHex(fromBlock); } let toBlock = filter.toBlock; if (_.isNumber(toBlock)) { - toBlock = this._web3.toHex(toBlock); + toBlock = utils.numberToHex(toBlock); } const serializedFilter = { ...filter, @@ -344,30 +444,27 @@ export class Web3Wrapper { toBlock, }; const payload = { - jsonrpc: '2.0', method: 'eth_getLogs', params: [serializedFilter], }; const rawLogs = await this._sendRawPayloadAsync<RawLogEntry[]>(payload); - const formattedLogs = _.map(rawLogs, this._formatLog.bind(this)); + const formattedLogs = _.map(rawLogs, marshaller.unmarshalLog.bind(marshaller)); 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); + 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; } /** @@ -377,7 +474,20 @@ export class Web3Wrapper { * @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); + 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)'); } @@ -389,7 +499,13 @@ export class Web3Wrapper { * @returns Transaction hash */ public async sendTransactionAsync(txData: TxData): Promise<string> { - const txHash = await promisify<string>(this._web3.eth.sendTransaction)(txData); + 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; } /** @@ -408,6 +524,11 @@ export class Web3Wrapper { 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)) { @@ -493,7 +614,8 @@ export class Web3Wrapper { * @param blockNumber The block number to reset to. */ public async setHeadAsync(blockNumber: number): Promise<void> { - await this._sendRawPayloadAsync<void>({ method: 'debug_setHead', params: [this._web3.toHex(blockNumber)] }); + 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 @@ -510,7 +632,7 @@ export class Web3Wrapper { } } private async _sendRawPayloadAsync<A>(payload: Partial<JSONRPCRequestPayload>): Promise<A> { - const sendAsync = this._web3.currentProvider.sendAsync.bind(this._web3.currentProvider); + const sendAsync = this._provider.sendAsync.bind(this._provider); const payloadWithDefaults = { id: this._jsonRpcRequestId++, params: [], @@ -521,34 +643,4 @@ export class Web3Wrapper { 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; - } } // tslint:disable-line:max-file-line-count |