From 4d9029bb0e3b215efdf165704c80d3bacef0e85a Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Wed, 28 Mar 2018 11:05:36 +0200 Subject: Add metacoin example project --- packages/web3-wrapper/src/web3_wrapper.ts | 375 ++++++++++++++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 packages/web3-wrapper/src/web3_wrapper.ts (limited to 'packages/web3-wrapper/src/web3_wrapper.ts') diff --git a/packages/web3-wrapper/src/web3_wrapper.ts b/packages/web3-wrapper/src/web3_wrapper.ts new file mode 100644 index 000000000..d75f39ed5 --- /dev/null +++ b/packages/web3-wrapper/src/web3_wrapper.ts @@ -0,0 +1,375 @@ +import { + BlockParam, + BlockWithoutTransactionData, + CallData, + ContractAbi, + FilterObject, + JSONRPCRequestPayload, + JSONRPCResponsePayload, + LogEntry, + RawLogEntry, + TransactionReceipt, + TransactionReceiptWithDecodedLogs, + TxData, +} from '@0xproject/types'; +import { AbiDecoder, BigNumber, intervalUtils, promisify } from '@0xproject/utils'; +import * as _ from 'lodash'; +import * as Web3 from 'web3'; + +import { Web3WrapperErrors } from './types'; + +/** + * 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; + public abiDecoder: AbiDecoder; + private _web3: Web3; + private _defaults: Partial; + 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) { + 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._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 { + 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 { + 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 { + const nodeVersion = await promisify(this._web3.version.getNode)(); + return nodeVersion; + } + /** + * Fetches the networkId of the backing Ethereum node + * @returns The network id + */ + public async getNetworkIdAsync(): Promise { + const networkIdStr = await promisify(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 { + const transactionReceipt = await promisify(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 { + let balanceInWei = await promisify(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 { + const code = await promisify(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 { + const signData = await promisify(this._web3.eth.sign)(address, message); + return signData; + } + /** + * Fetches the latest block number + * @returns Block number + */ + public async getBlockNumberAsync(): Promise { + const blockNumber = await promisify(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 { + const block = await promisify(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 { + 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 { + const addresses = await promisify(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 { + const snapshotId = Number(await this._sendRawPayloadAsync({ 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 { + const didRevert = await this._sendRawPayloadAsync({ method: 'evm_revert', params: [snapshotId] }); + return didRevert; + } + /** + * Mine a block on a TestRPC/Ganache local node + */ + public async mineBlockAsync(): Promise { + await this._sendRawPayloadAsync({ 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 { + await this._sendRawPayloadAsync({ 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 { + 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(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 { + 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): Promise { + const gas = await promisify(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 { + const rawCallResult = await promisify(this._web3.eth.call)(callData, defaultBlock); + return rawCallResult; + } + /** + * Send a transaction + * @param txData Transaction data + * @returns Transaction hash + */ + public async sendTransactionAsync(txData: TxData): Promise { + const txHash = await promisify(this._web3.eth.sendTransaction)(txData); + return txHash; + } + public async awaitTransactionMinedAsync( + txHash: string, + pollingIntervalMs = 1000, + timeoutMs?: number, + ): Promise { + let timeoutExceeded = false; + if (timeoutMs) { + setTimeout(() => (timeoutExceeded = true), timeoutMs); + } + + const txReceiptPromise = new Promise( + (resolve: (receipt: TransactionReceiptWithDecodedLogs) => void, reject) => { + const intervalId = intervalUtils.setAsyncExcludingInterval( + async () => { + if (timeoutExceeded) { + intervalUtils.clearAsyncExcludingInterval(intervalId); + return reject(Web3WrapperErrors.TransactionMiningTimeout); + } + + const 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; + } + private async _sendRawPayloadAsync(payload: Partial): Promise { + const sendAsync = this._web3.currentProvider.sendAsync.bind(this._web3.currentProvider); + const response = await promisify(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; + } +} -- cgit v1.2.3