UNPKG

vue-blocklink

Version:

Vue support for the Blockchain Link browser extension

465 lines (464 loc) 19.7 kB
import { assert } from '../0xassert'; import { schemas } from '../validations'; import { AbiDecoder, addressUtils, intervalUtils, promisify, providerUtils } from '../utils'; import { B } from '../utils/configured_bignumber'; import { BlockParamLiteral, } from '../types'; import * as _ from 'lodash'; import { marshaller } from './marshaller'; import { NodeType, Web3WrapperErrors, } from './types'; import { utils } from './utils'; const BASE_TEN = 10; const uniqueVersionIds = { geth: 'Geth', ganache: 'EthereumJS TestRPC', }; export class Web3Wrapper { constructor(supportedProvider, callAndTxnDefaults = {}) { this.isZeroExWeb3Wrapper = true; this.abiDecoder = new AbiDecoder([]); this._supportedProvider = supportedProvider; this._provider = providerUtils.standardizeOrThrow(supportedProvider); this._callAndTxnDefaults = callAndTxnDefaults; this._jsonRpcRequestId = 1; } static isAddress(address) { return addressUtils.isAddress(address); } static toUnitAmount(amount, decimals) { assert.isValidBaseUnitAmount('amount', amount); assert.isNumber('decimals', decimals); const aUnit = new B.BigNumber(BASE_TEN).pow(decimals); const unit = amount.div(aUnit); return unit; } static toBaseUnitAmount(amount, decimals) { assert.isNumber('decimals', decimals); const unit = new B.BigNumber(BASE_TEN).pow(decimals); const baseUnitAmount = unit.times(amount); const hasDecimals = baseUnitAmount.decimalPlaces() !== 0; if (hasDecimals) { throw new Error(`Invalid unit amount: ${amount.toString(BASE_TEN)} - Too many decimal places`); } return baseUnitAmount; } static toWei(ethAmount) { assert.isBigNumber('ethAmount', ethAmount); const ETH_DECIMALS = 18; const balanceWei = Web3Wrapper.toBaseUnitAmount(ethAmount, ETH_DECIMALS); return balanceWei; } static _assertBlockParam(blockParam) { if (_.isNumber(blockParam)) { return; } else if (_.isString(blockParam)) { assert.doesBelongToStringEnum('blockParam', blockParam, BlockParamLiteral); } } static _assertBlockParamOrString(blockParam) { try { Web3Wrapper._assertBlockParam(blockParam); } catch (err) { try { assert.isHexString('blockParam', blockParam); return; } catch (err) { throw new Error(`Expected blockParam to be of type "string | BlockParam", encountered ${blockParam}`); } } } static _normalizeTxReceiptStatus(status) { if (_.isString(status)) { return utils.convertHexToNumber(status); } else if (status === undefined) { return null; } else { return status; } } getContractDefaults() { return this._callAndTxnDefaults; } getProvider() { return this._supportedProvider; } setProvider(supportedProvider) { const provider = providerUtils.standardizeOrThrow(supportedProvider); this._provider = provider; } async isSenderAddressAvailableAsync(senderAddress) { assert.isETHAddressHex('senderAddress', senderAddress); const addresses = await this.getAvailableAddressesAsync(); const normalizedAddress = senderAddress.toLowerCase(); return _.includes(addresses, normalizedAddress); } async getNodeVersionAsync() { const nodeVersion = await this.sendRawPayloadAsync({ method: 'web3_clientVersion' }); return nodeVersion; } async getNetworkIdAsync() { const networkIdStr = await this.sendRawPayloadAsync({ method: 'net_version' }); const networkId = _.parseInt(networkIdStr); return networkId; } async getChainIdAsync() { const chainIdStr = await this.sendRawPayloadAsync({ method: 'eth_chainId' }); const chainId = _.parseInt(chainIdStr); return chainId; } async getTransactionReceiptIfExistsAsync(txHash) { assert.isHexString('txHash', txHash); const transactionReceiptRpc = await this.sendRawPayloadAsync({ method: 'eth_getTransactionReceipt', params: [txHash], }); if (transactionReceiptRpc !== null && transactionReceiptRpc.blockNumber !== null) { transactionReceiptRpc.status = Web3Wrapper._normalizeTxReceiptStatus(transactionReceiptRpc.status); const transactionReceipt = marshaller.unmarshalTransactionReceipt(transactionReceiptRpc); return transactionReceipt; } else { return undefined; } } async getTransactionByHashAsync(txHash) { assert.isHexString('txHash', txHash); const transactionRpc = await this.sendRawPayloadAsync({ method: 'eth_getTransactionByHash', params: [txHash], }); const transaction = marshaller.unmarshalTransaction(transactionRpc); return transaction; } async getBalanceInWeiAsync(owner, defaultBlock) { assert.isETHAddressHex('owner', owner); if (defaultBlock !== undefined) { Web3Wrapper._assertBlockParam(defaultBlock); } const marshalledDefaultBlock = marshaller.marshalBlockParam(defaultBlock); const encodedOwner = marshaller.marshalAddress(owner); const balanceInWei = await this.sendRawPayloadAsync({ method: 'eth_getBalance', params: [encodedOwner, marshalledDefaultBlock], }); return new B.BigNumber(balanceInWei); } async doesContractExistAtAddressAsync(address) { assert.isETHAddressHex('address', address); const code = await this.getContractCodeAsync(address); const isCodeEmpty = /^0x0{0,40}$/i.test(code); return !isCodeEmpty; } async getContractCodeAsync(address, defaultBlock) { assert.isETHAddressHex('address', address); if (defaultBlock !== undefined) { Web3Wrapper._assertBlockParam(defaultBlock); } const marshalledDefaultBlock = marshaller.marshalBlockParam(defaultBlock); const encodedAddress = marshaller.marshalAddress(address); const code = await this.sendRawPayloadAsync({ method: 'eth_getCode', params: [encodedAddress, marshalledDefaultBlock], }); return code; } async getTransactionTraceAsync(txHash, traceParams) { assert.isHexString('txHash', txHash); const trace = await this.sendRawPayloadAsync({ method: 'debug_traceTransaction', params: [txHash, traceParams], }); return trace; } async signMessageAsync(address, message) { assert.isETHAddressHex('address', address); assert.isString('message', message); const signData = await this.sendRawPayloadAsync({ method: 'eth_sign', params: [address, message], }); return signData; } async signTypedDataAsync(address, typedData) { assert.isETHAddressHex('address', address); assert.doesConformToSchema('typedData', typedData, schemas.eip712TypedDataSchema); const methodsToTry = ['eth_signTypedData_v4', 'eth_signTypedData_v3', 'eth_signTypedData']; let lastErr; for (const method of methodsToTry) { try { return await this.sendRawPayloadAsync({ method, params: [address, method === 'eth_signTypedData' ? typedData : JSON.stringify(typedData)], }); } catch (err) { lastErr = err; if (!/(not handled|does not exist|not supported)/.test(err.message)) { throw err; } } } throw lastErr; } async getBlockNumberAsync() { const blockNumberHex = await this.sendRawPayloadAsync({ method: 'eth_blockNumber', params: [], }); const blockNumber = utils.convertHexToNumberOrNull(blockNumberHex); return blockNumber; } async getAccountNonceAsync(address, defaultBlock) { assert.isETHAddressHex('address', address); if (defaultBlock !== undefined) { Web3Wrapper._assertBlockParam(defaultBlock); } const marshalledDefaultBlock = marshaller.marshalBlockParam(defaultBlock); const encodedAddress = marshaller.marshalAddress(address); const nonceHex = await this.sendRawPayloadAsync({ method: 'eth_getTransactionCount', params: [encodedAddress, marshalledDefaultBlock], }); assert.isHexString('nonce', nonceHex); return parseInt(nonceHex.substr(2), 16); } async getBlockIfExistsAsync(blockParam) { Web3Wrapper._assertBlockParamOrString(blockParam); const encodedBlockParam = marshaller.marshalBlockParam(blockParam); const method = utils.isHexStrict(blockParam) ? 'eth_getBlockByHash' : 'eth_getBlockByNumber'; const shouldIncludeTransactionData = false; const blockWithoutTransactionDataWithHexValuesOrNull = await this.sendRawPayloadAsync({ method, params: [encodedBlockParam, shouldIncludeTransactionData], }); let blockWithoutTransactionDataIfExists; if (blockWithoutTransactionDataWithHexValuesOrNull !== null) { blockWithoutTransactionDataIfExists = marshaller.unmarshalIntoBlockWithoutTransactionData(blockWithoutTransactionDataWithHexValuesOrNull); } return blockWithoutTransactionDataIfExists; } async getBlockWithTransactionDataAsync(blockParam) { 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({ method, params: [encodedBlockParam, shouldIncludeTransactionData], }); const blockWithoutTransactionData = marshaller.unmarshalIntoBlockWithTransactionData(blockWithTransactionDataWithHexValues); return blockWithoutTransactionData; } async getBlockTimestampAsync(blockParam) { Web3Wrapper._assertBlockParamOrString(blockParam); const blockIfExists = await this.getBlockIfExistsAsync(blockParam); if (blockIfExists === undefined) { throw new Error(`Failed to fetch block with blockParam: ${JSON.stringify(blockParam)}`); } return blockIfExists.timestamp; } async getAvailableAddressesAsync() { const addresses = await this.sendRawPayloadAsync({ method: 'eth_accounts', params: [], }); const normalizedAddresses = _.map(addresses, address => address.toLowerCase()); return normalizedAddresses; } async takeSnapshotAsync() { const snapshotId = Number(await this.sendRawPayloadAsync({ method: 'evm_snapshot', params: [] })); return snapshotId; } async revertSnapshotAsync(snapshotId) { assert.isNumber('snapshotId', snapshotId); const didRevert = await this.sendRawPayloadAsync({ method: 'evm_revert', params: [snapshotId] }); return didRevert; } async mineBlockAsync() { await this.sendRawPayloadAsync({ method: 'evm_mine', params: [] }); } async increaseTimeAsync(timeDelta) { assert.isNumber('timeDelta', timeDelta); const version = await this.getNodeVersionAsync(); if (_.includes(version, uniqueVersionIds.geth)) { return this.sendRawPayloadAsync({ method: 'debug_increaseTime', params: [timeDelta] }); } else if (_.includes(version, uniqueVersionIds.ganache)) { return this.sendRawPayloadAsync({ method: 'evm_increaseTime', params: [timeDelta] }); } else { throw new Error(`Unknown client version: ${version}`); } } async getLogsAsync(filter) { if (filter.blockHash !== undefined && (filter.fromBlock !== undefined || filter.toBlock !== undefined)) { throw new Error(`Cannot specify 'blockHash' as well as 'fromBlock'/'toBlock' in the filter supplied to 'getLogsAsync'`); } 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(payload); const formattedLogs = _.map(rawLogs, marshaller.unmarshalLog.bind(marshaller)); return formattedLogs; } async estimateGasAsync(txData) { assert.doesConformToSchema('txData', txData, schemas.txDataSchema); const txDataHex = marshaller.marshalTxData(txData); const gasHex = await this.sendRawPayloadAsync({ method: 'eth_estimateGas', params: [txDataHex] }); const gas = utils.convertHexToNumber(gasHex); return gas; } async createAccessListAsync(callData, defaultBlock) { assert.doesConformToSchema('callData', callData, schemas.callDataSchema, [ schemas.addressSchema, schemas.numberSchema, schemas.jsNumber, ]); const rawResult = await this.sendRawPayloadAsync({ method: 'eth_createAccessList', params: [marshaller.marshalCallData(callData), marshaller.marshalBlockParam(defaultBlock)], }); if (rawResult.error) { throw new Error(rawResult.error); } return { accessList: rawResult.accessList.reduce((o, v) => { o[v.address] = o[v.address] || []; o[v.address].push(...(v.storageKeys || [])); return o; }, {}), gasUsed: parseInt(rawResult.gasUsed.slice(2), 16), }; } async callAsync(callData, defaultBlock) { assert.doesConformToSchema('callData', callData, schemas.callDataSchema); if (defaultBlock !== undefined) { Web3Wrapper._assertBlockParam(defaultBlock); } const marshalledDefaultBlock = marshaller.marshalBlockParam(defaultBlock); const callDataHex = marshaller.marshalCallData(callData); const overrides = marshaller.marshalCallOverrides(callData.overrides || {}); const rawCallResult = await this.sendRawPayloadAsync({ method: 'eth_call', params: [callDataHex, marshalledDefaultBlock, ...(Object.keys(overrides).length === 0 ? [] : [overrides])], }); return rawCallResult; } async sendTransactionAsync(txData) { assert.doesConformToSchema('txData', txData, schemas.txDataSchema); const txDataHex = marshaller.marshalTxData(txData); const txHash = await this.sendRawPayloadAsync({ method: 'eth_sendTransaction', params: [txDataHex] }); return txHash; } async awaitTransactionMinedAsync(txHash, pollingIntervalMs = 1000, timeoutMs) { assert.isHexString('txHash', txHash); assert.isNumber('pollingIntervalMs', pollingIntervalMs); if (timeoutMs !== undefined) { assert.isNumber('timeoutMs', timeoutMs); } let transactionReceipt = await this.getTransactionReceiptIfExistsAsync(txHash); if (transactionReceipt !== undefined) { const logsWithDecodedArgs = _.map(transactionReceipt.logs, this.abiDecoder.tryToDecodeLogOrNoop.bind(this.abiDecoder)); const transactionReceiptWithDecodedLogArgs = { ...transactionReceipt, logs: logsWithDecodedArgs, }; return transactionReceiptWithDecodedLogArgs; } let wasTimeoutExceeded = false; if (timeoutMs) { setTimeout(() => (wasTimeoutExceeded = true), timeoutMs); } const txReceiptPromise = new Promise((resolve, reject) => { const intervalId = intervalUtils.setAsyncExcludingInterval(async () => { if (wasTimeoutExceeded) { intervalUtils.clearAsyncExcludingInterval(intervalId); return reject(Web3WrapperErrors.TransactionMiningTimeout); } transactionReceipt = await this.getTransactionReceiptIfExistsAsync(txHash); if (transactionReceipt !== undefined) { intervalUtils.clearAsyncExcludingInterval(intervalId); const logsWithDecodedArgs = _.map(transactionReceipt.logs, this.abiDecoder.tryToDecodeLogOrNoop.bind(this.abiDecoder)); const transactionReceiptWithDecodedLogArgs = { ...transactionReceipt, logs: logsWithDecodedArgs, }; resolve(transactionReceiptWithDecodedLogArgs); } }, pollingIntervalMs, (err) => { intervalUtils.clearAsyncExcludingInterval(intervalId); reject(err); }); }); const txReceipt = await txReceiptPromise; return txReceipt; } async awaitTransactionSuccessAsync(txHash, pollingIntervalMs = 1000, timeoutMs) { const receipt = await this.awaitTransactionMinedAsync(txHash, pollingIntervalMs, timeoutMs); if (receipt.status !== 1) { throw new Error(`Transaction failed: ${txHash}`); } return receipt; } async setHeadAsync(blockNumber) { assert.isNumber('blockNumber', blockNumber); await this.sendRawPayloadAsync({ method: 'debug_setHead', params: [utils.numberToHex(blockNumber)] }); } async sendRawPayloadAsync(payload) { if (!payload.method) { throw new Error(`Must supply method in JSONRPCRequestPayload, tried: [${payload}]`); } const payloadWithDefaults = { id: this._jsonRpcRequestId++, params: [], jsonrpc: '2.0', ...payload, }; const sendAsync = promisify(this._provider.sendAsync.bind(this._provider)); const response = await sendAsync(payloadWithDefaults); if (!response) { throw new Error(`No response`); } const errorMessage = response.error ? response.error.message || response.error : undefined; if (errorMessage) { throw new Error(errorMessage); } if (response.result === undefined) { throw new Error(`JSON RPC response has no result`); } return response.result; } async getNodeTypeAsync() { 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}`); } } }