diff options
Diffstat (limited to 'packages/0x.js')
-rw-r--r-- | packages/0x.js/CHANGELOG.md | 10 | ||||
-rw-r--r-- | packages/0x.js/package.json | 10 | ||||
-rw-r--r-- | packages/0x.js/src/contract.ts | 43 | ||||
-rw-r--r-- | packages/0x.js/src/contract_wrappers/ether_token_wrapper.ts | 18 | ||||
-rw-r--r-- | packages/0x.js/src/contract_wrappers/exchange_wrapper.ts | 153 | ||||
-rw-r--r-- | packages/0x.js/src/contract_wrappers/token_wrapper.ts | 41 | ||||
-rw-r--r-- | packages/0x.js/src/index.ts | 1 | ||||
-rw-r--r-- | packages/0x.js/src/order_watcher/order_state_watcher.ts | 48 | ||||
-rw-r--r-- | packages/0x.js/src/types.ts | 18 | ||||
-rw-r--r-- | packages/0x.js/test/ether_token_wrapper_test.ts | 2 | ||||
-rw-r--r-- | packages/0x.js/test/utils/constants.ts | 1 | ||||
-rw-r--r-- | packages/0x.js/test/utils/subproviders/empty_wallet_subprovider.ts (renamed from packages/0x.js/src/subproviders/empty_wallet_subprovider.ts) | 4 | ||||
-rw-r--r-- | packages/0x.js/test/utils/subproviders/fake_gas_estimate_subprovider.ts | 34 | ||||
-rw-r--r-- | packages/0x.js/test/utils/web3_factory.ts | 6 |
14 files changed, 228 insertions, 161 deletions
diff --git a/packages/0x.js/CHANGELOG.md b/packages/0x.js/CHANGELOG.md index 67b2b89b6..963b3d56c 100644 --- a/packages/0x.js/CHANGELOG.md +++ b/packages/0x.js/CHANGELOG.md @@ -1,6 +1,10 @@ # CHANGELOG -vx.x.x +v0.27.1 - _November 28, 2017_ +------------------------ + * Export `TransactionOpts` type + +v0.27.0 - _November 28, 2017_ ------------------------ * Make `ZeroExConfig` required parameter of `ZeroEx` constructor (#233) * Add a required property `networkId` to `ZeroExConfig` (#233) @@ -8,9 +12,11 @@ vx.x.x * Remove `ZeroExError.ContractNotFound` and replace it with contract-specific errors (#233) * Make `DecodedLogEvent<A>` contain `LogWithDecodedArgs<A>` under log key instead of merging it in like web3 does (#234) * Rename `removed` to `isRemoved` in `DecodedLogEvent<A>` (#234) + * Add config allowing to specify gasPrice and gasLimit for every transaction sending method (#235) + * All transaction sending methods now call `estimateGas` if no gas amount was supplied (#235) * Modify order validation methods to validate against the `latest` block, not against the `pending` block (#236) -v0.26.0 +v0.26.0 - _November 21, 2017_ ------------------------ * Add post-formatter for logs converting `blockNumber`, `logIndex`, `transactionIndex` from hexes to numbers (#231) * Remove support for Async callback types when used in Subscribe functions (#222) diff --git a/packages/0x.js/package.json b/packages/0x.js/package.json index 024cb0fe9..4b1dd478c 100644 --- a/packages/0x.js/package.json +++ b/packages/0x.js/package.json @@ -1,6 +1,6 @@ { "name": "0x.js", - "version": "0.26.1", + "version": "0.27.0", "description": "A javascript library for interacting with the 0x protocol", "keywords": [ "0x.js", @@ -44,7 +44,7 @@ "node": ">=6.0.0" }, "devDependencies": { - "@0xproject/tslint-config": "^0.1.1", + "@0xproject/tslint-config": "^0.2.0", "@types/bintrees": "^1.0.2", "@types/jsonschema": "^1.1.1", "@types/lodash": "^4.14.64", @@ -61,7 +61,7 @@ "copyfiles": "^1.2.0", "coveralls": "^3.0.0", "dirty-chai": "^2.0.1", - "ethereumjs-testrpc": "4.0.1", + "ethereumjs-testrpc": "6.0.3", "json-loader": "^0.5.4", "mocha": "^4.0.0", "npm-run-all": "^4.0.2", @@ -83,8 +83,8 @@ "webpack": "^3.1.0" }, "dependencies": { - "@0xproject/assert": "^0.0.5", - "@0xproject/json-schemas": "^0.6.8", + "@0xproject/assert": "^0.0.6", + "@0xproject/json-schemas": "^0.6.9", "bignumber.js": "~4.1.0", "bintrees": "^1.0.2", "bn.js": "4.11.8", diff --git a/packages/0x.js/src/contract.ts b/packages/0x.js/src/contract.ts index e9c49c9f1..a4ee03910 100644 --- a/packages/0x.js/src/contract.ts +++ b/packages/0x.js/src/contract.ts @@ -5,6 +5,10 @@ import * as Web3 from 'web3'; import {AbiType} from './types'; +// HACK: Gas estimates on testrpc don't take into account gas refunds. +// Our calls can trigger max 8 gas refunds for SSTORE per transaction for 15k gas each which gives 120k. +const GAS_MARGIN = 120000; + export class Contract implements Web3.ContractInstance { public address: string; public abi: Web3.ContractAbi; @@ -34,9 +38,10 @@ export class Contract implements Web3.ContractInstance { } else { const cbStyleFunction = this.contract[functionAbi.name]; const cbStyleEstimateGasFunction = this.contract[functionAbi.name].estimateGas; + const estimateGasAsync = promisify(cbStyleEstimateGasFunction, this.contract); this[functionAbi.name] = { - estimateGasAsync: promisify(cbStyleEstimateGasFunction, this.contract), - sendTransactionAsync: this.promisifyWithDefaultParams(cbStyleFunction), + estimateGasAsync, + sendTransactionAsync: this.promisifyWithDefaultParams(cbStyleFunction, estimateGasAsync), }; } }); @@ -47,28 +52,40 @@ export class Contract implements Web3.ContractInstance { this[eventAbi.name] = this.contract[eventAbi.name]; }); } - private promisifyWithDefaultParams(fn: (...args: any[]) => void): (...args: any[]) => Promise<any> { + private promisifyWithDefaultParams( + web3CbStyleFunction: (...args: any[]) => void, + estimateGasAsync: (...args: any[]) => Promise<number>, + ): (...args: any[]) => Promise<any> { const promisifiedWithDefaultParams = async (...args: any[]) => { - const promise = new Promise((resolve, reject) => { + const promise = new Promise(async (resolve, reject) => { const lastArg = args[args.length - 1]; let txData: Partial<Web3.TxData> = {}; - if (this.isTxData(lastArg)) { + if (!_.isUndefined(lastArg) && this.isTxData(lastArg)) { txData = args.pop(); } + // Gas amount sourced with the following priorities: + // 1. Optional param passed in to public method call + // 2. Global config passed in at library instantiation + // 3. Gas estimate calculation + safety margin + const removeUndefinedProperties = _.pickBy; txData = { - ...this.defaults, - ...txData, + ...removeUndefinedProperties(this.defaults), + ...removeUndefinedProperties(txData), }; - const callback = (err: Error, data: any) => { - if (_.isNull(err)) { - resolve(data); - } else { + if (_.isUndefined(txData.gas)) { + try { + const estimatedGas = await estimateGasAsync.apply(this.contract, [...args, txData]); + const gas = estimatedGas + GAS_MARGIN; + txData.gas = gas; + } catch (err) { reject(err); + return; } - }; + } + const callback = (err: Error, data: any) => _.isNull(err) ? resolve(data) : reject(err); args.push(txData); args.push(callback); - fn.apply(this.contract, args); + web3CbStyleFunction.apply(this.contract, args); }); return promise; }; diff --git a/packages/0x.js/src/contract_wrappers/ether_token_wrapper.ts b/packages/0x.js/src/contract_wrappers/ether_token_wrapper.ts index 7a3f2bc52..ede0460bd 100644 --- a/packages/0x.js/src/contract_wrappers/ether_token_wrapper.ts +++ b/packages/0x.js/src/contract_wrappers/ether_token_wrapper.ts @@ -2,7 +2,7 @@ import BigNumber from 'bignumber.js'; import * as _ from 'lodash'; import {artifacts} from '../artifacts'; -import {EtherTokenContract, ZeroExError} from '../types'; +import {EtherTokenContract, TransactionOpts, ZeroExError} from '../types'; import {assert} from '../utils/assert'; import {Web3Wrapper} from '../web3_wrapper'; @@ -27,10 +27,13 @@ export class EtherTokenWrapper extends ContractWrapper { * to the depositor address. These wrapped ETH tokens can be used in 0x trades and are redeemable for 1-to-1 * for ETH. * @param amountInWei Amount of ETH in Wei the caller wishes to deposit. - * @param depositor The hex encoded user Ethereum address that would like to make the deposit. + * @param depositor The hex encoded user Ethereum address that would like to make the deposit. + * @param txOpts Transaction parameters. * @return Transaction hash. */ - public async depositAsync(amountInWei: BigNumber, depositor: string): Promise<string> { + public async depositAsync( + amountInWei: BigNumber, depositor: string, txOpts: TransactionOpts = {}, + ): Promise<string> { assert.isValidBaseUnitAmount('amountInWei', amountInWei); await assert.isSenderAddressAsync('depositor', depositor, this._web3Wrapper); @@ -41,6 +44,8 @@ export class EtherTokenWrapper extends ContractWrapper { const txHash = await wethContract.deposit.sendTransactionAsync({ from: depositor, value: amountInWei, + gas: txOpts.gasLimit, + gasPrice: txOpts.gasPrice, }); return txHash; } @@ -49,9 +54,12 @@ export class EtherTokenWrapper extends ContractWrapper { * equivalent number of wrapped ETH tokens. * @param amountInWei Amount of ETH in Wei the caller wishes to withdraw. * @param withdrawer The hex encoded user Ethereum address that would like to make the withdrawl. + * @param txOpts Transaction parameters. * @return Transaction hash. */ - public async withdrawAsync(amountInWei: BigNumber, withdrawer: string): Promise<string> { + public async withdrawAsync( + amountInWei: BigNumber, withdrawer: string, txOpts: TransactionOpts = {}, + ): Promise<string> { assert.isValidBaseUnitAmount('amountInWei', amountInWei); await assert.isSenderAddressAsync('withdrawer', withdrawer, this._web3Wrapper); @@ -62,6 +70,8 @@ export class EtherTokenWrapper extends ContractWrapper { const wethContract = await this._getEtherTokenContractAsync(); const txHash = await wethContract.withdraw.sendTransactionAsync(amountInWei, { from: withdrawer, + gas: txOpts.gasLimit, + gasPrice: txOpts.gasPrice, }); return txHash; } diff --git a/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts b/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts index 91b41c4a4..273b348ff 100644 --- a/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts +++ b/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts @@ -169,16 +169,16 @@ export class ExchangeWrapper extends ContractWrapper { public async fillOrderAsync(signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber, shouldThrowOnInsufficientBalanceOrAllowance: boolean, takerAddress: string, - orderTransactionOpts?: OrderTransactionOpts): Promise<string> { + orderTransactionOpts: OrderTransactionOpts = {}): Promise<string> { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const exchangeInstance = await this._getExchangeContractAsync(); - const shouldValidate = _.isUndefined(orderTransactionOpts) ? - SHOULD_VALIDATE_BY_DEFAULT : - orderTransactionOpts.shouldValidate; + const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; if (shouldValidate) { const zrxTokenAddress = this.getZRXTokenAddress(); const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); @@ -188,18 +188,6 @@ export class ExchangeWrapper extends ContractWrapper { const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(signedOrder); - const gas = await exchangeInstance.fillOrder.estimateGasAsync( - orderAddresses, - orderValues, - fillTakerTokenAmount, - shouldThrowOnInsufficientBalanceOrAllowance, - signedOrder.ecSignature.v, - signedOrder.ecSignature.r, - signedOrder.ecSignature.s, - { - from: takerAddress, - }, - ); const txHash: string = await exchangeInstance.fillOrder.sendTransactionAsync( orderAddresses, orderValues, @@ -210,7 +198,8 @@ export class ExchangeWrapper extends ContractWrapper { signedOrder.ecSignature.s, { from: takerAddress, - gas, + gas: orderTransactionOpts.gasLimit, + gasPrice: orderTransactionOpts.gasPrice, }, ); return txHash; @@ -236,7 +225,7 @@ export class ExchangeWrapper extends ContractWrapper { public async fillOrdersUpToAsync(signedOrders: SignedOrder[], fillTakerTokenAmount: BigNumber, shouldThrowOnInsufficientBalanceOrAllowance: boolean, takerAddress: string, - orderTransactionOpts?: OrderTransactionOpts): Promise<string> { + orderTransactionOpts: OrderTransactionOpts = {}): Promise<string> { assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); const takerTokenAddresses = _.map(signedOrders, signedOrder => signedOrder.takerTokenAddress); assert.hasAtMostOneUniqueValue(takerTokenAddresses, @@ -248,9 +237,9 @@ export class ExchangeWrapper extends ContractWrapper { assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); - const shouldValidate = _.isUndefined(orderTransactionOpts) ? - SHOULD_VALIDATE_BY_DEFAULT : - orderTransactionOpts.shouldValidate; + const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; if (shouldValidate) { const zrxTokenAddress = this.getZRXTokenAddress(); const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); @@ -278,18 +267,6 @@ export class ExchangeWrapper extends ContractWrapper { ); const exchangeInstance = await this._getExchangeContractAsync(); - const gas = await exchangeInstance.fillOrdersUpTo.estimateGasAsync( - orderAddressesArray, - orderValuesArray, - fillTakerTokenAmount, - shouldThrowOnInsufficientBalanceOrAllowance, - vArray, - rArray, - sArray, - { - from: takerAddress, - }, - ); const txHash = await exchangeInstance.fillOrdersUpTo.sendTransactionAsync( orderAddressesArray, orderValuesArray, @@ -300,7 +277,8 @@ export class ExchangeWrapper extends ContractWrapper { sArray, { from: takerAddress, - gas, + gas: orderTransactionOpts.gasLimit, + gasPrice: orderTransactionOpts.gasPrice, }, ); return txHash; @@ -328,7 +306,7 @@ export class ExchangeWrapper extends ContractWrapper { public async batchFillOrdersAsync(orderFillRequests: OrderFillRequest[], shouldThrowOnInsufficientBalanceOrAllowance: boolean, takerAddress: string, - orderTransactionOpts?: OrderTransactionOpts): Promise<string> { + orderTransactionOpts: OrderTransactionOpts = {}): Promise<string> { assert.doesConformToSchema('orderFillRequests', orderFillRequests, schemas.orderFillRequestsSchema); const exchangeContractAddresses = _.map( orderFillRequests, @@ -338,9 +316,9 @@ export class ExchangeWrapper extends ContractWrapper { ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress); assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); - const shouldValidate = _.isUndefined(orderTransactionOpts) ? - SHOULD_VALIDATE_BY_DEFAULT : - orderTransactionOpts.shouldValidate; + const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; if (shouldValidate) { const zrxTokenAddress = this.getZRXTokenAddress(); const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); @@ -370,18 +348,6 @@ export class ExchangeWrapper extends ContractWrapper { ); const exchangeInstance = await this._getExchangeContractAsync(); - const gas = await exchangeInstance.batchFillOrders.estimateGasAsync( - orderAddressesArray, - orderValuesArray, - fillTakerTokenAmounts, - shouldThrowOnInsufficientBalanceOrAllowance, - vArray, - rArray, - sArray, - { - from: takerAddress, - }, - ); const txHash = await exchangeInstance.batchFillOrders.sendTransactionAsync( orderAddressesArray, orderValuesArray, @@ -392,7 +358,8 @@ export class ExchangeWrapper extends ContractWrapper { sArray, { from: takerAddress, - gas, + gas: orderTransactionOpts.gasLimit, + gasPrice: orderTransactionOpts.gasPrice, }, ); return txHash; @@ -411,16 +378,16 @@ export class ExchangeWrapper extends ContractWrapper { @decorators.contractCallErrorHandler public async fillOrKillOrderAsync(signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber, takerAddress: string, - orderTransactionOpts?: OrderTransactionOpts): Promise<string> { + orderTransactionOpts: OrderTransactionOpts = {}): Promise<string> { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const exchangeInstance = await this._getExchangeContractAsync(); - const shouldValidate = _.isUndefined(orderTransactionOpts) ? - SHOULD_VALIDATE_BY_DEFAULT : - orderTransactionOpts.shouldValidate; + const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; if (shouldValidate) { const zrxTokenAddress = this.getZRXTokenAddress(); const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); @@ -429,18 +396,6 @@ export class ExchangeWrapper extends ContractWrapper { } const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(signedOrder); - - const gas = await exchangeInstance.fillOrKillOrder.estimateGasAsync( - orderAddresses, - orderValues, - fillTakerTokenAmount, - signedOrder.ecSignature.v, - signedOrder.ecSignature.r, - signedOrder.ecSignature.s, - { - from: takerAddress, - }, - ); const txHash = await exchangeInstance.fillOrKillOrder.sendTransactionAsync( orderAddresses, orderValues, @@ -450,7 +405,8 @@ export class ExchangeWrapper extends ContractWrapper { signedOrder.ecSignature.s, { from: takerAddress, - gas, + gas: orderTransactionOpts.gasLimit, + gasPrice: orderTransactionOpts.gasPrice, }, ); return txHash; @@ -467,7 +423,7 @@ export class ExchangeWrapper extends ContractWrapper { @decorators.contractCallErrorHandler public async batchFillOrKillAsync(orderFillRequests: OrderFillRequest[], takerAddress: string, - orderTransactionOpts?: OrderTransactionOpts): Promise<string> { + orderTransactionOpts: OrderTransactionOpts = {}): Promise<string> { assert.doesConformToSchema('orderFillRequests', orderFillRequests, schemas.orderFillRequestsSchema); const exchangeContractAddresses = _.map( @@ -482,9 +438,9 @@ export class ExchangeWrapper extends ContractWrapper { } const exchangeInstance = await this._getExchangeContractAsync(); - const shouldValidate = _.isUndefined(orderTransactionOpts) ? - SHOULD_VALIDATE_BY_DEFAULT : - orderTransactionOpts.shouldValidate; + const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; if (shouldValidate) { const zrxTokenAddress = this.getZRXTokenAddress(); const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); @@ -509,18 +465,6 @@ export class ExchangeWrapper extends ContractWrapper { // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'( const [orderAddresses, orderValues, fillTakerTokenAmounts, vParams, rParams, sParams] = _.unzip<any>(orderAddressesValuesAndTakerTokenFillAmounts); - - const gas = await exchangeInstance.batchFillOrKillOrders.estimateGasAsync( - orderAddresses, - orderValues, - fillTakerTokenAmounts, - vParams, - rParams, - sParams, - { - from: takerAddress, - }, - ); const txHash = await exchangeInstance.batchFillOrKillOrders.sendTransactionAsync( orderAddresses, orderValues, @@ -530,7 +474,8 @@ export class ExchangeWrapper extends ContractWrapper { sParams, { from: takerAddress, - gas, + gas: orderTransactionOpts.gasLimit, + gasPrice: orderTransactionOpts.gasPrice, }, ); return txHash; @@ -546,16 +491,16 @@ export class ExchangeWrapper extends ContractWrapper { @decorators.contractCallErrorHandler public async cancelOrderAsync(order: Order|SignedOrder, cancelTakerTokenAmount: BigNumber, - orderTransactionOpts?: OrderTransactionOpts): Promise<string> { + orderTransactionOpts: OrderTransactionOpts = {}): Promise<string> { assert.doesConformToSchema('order', order, schemas.orderSchema); assert.isValidBaseUnitAmount('takerTokenCancelAmount', cancelTakerTokenAmount); await assert.isSenderAddressAsync('order.maker', order.maker, this._web3Wrapper); const exchangeInstance = await this._getExchangeContractAsync(); - const shouldValidate = _.isUndefined(orderTransactionOpts) ? - SHOULD_VALIDATE_BY_DEFAULT : - orderTransactionOpts.shouldValidate; + const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; if (shouldValidate) { const orderHash = utils.getOrderHashHex(order); const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash); @@ -564,21 +509,14 @@ export class ExchangeWrapper extends ContractWrapper { } const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(order); - const gas = await exchangeInstance.cancelOrder.estimateGasAsync( - orderAddresses, - orderValues, - cancelTakerTokenAmount, - { - from: order.maker, - }, - ); const txHash = await exchangeInstance.cancelOrder.sendTransactionAsync( orderAddresses, orderValues, cancelTakerTokenAmount, { from: order.maker, - gas, + gas: orderTransactionOpts.gasLimit, + gasPrice: orderTransactionOpts.gasPrice, }, ); return txHash; @@ -593,7 +531,7 @@ export class ExchangeWrapper extends ContractWrapper { */ @decorators.contractCallErrorHandler public async batchCancelOrdersAsync(orderCancellationRequests: OrderCancellationRequest[], - orderTransactionOpts?: OrderTransactionOpts): Promise<string> { + orderTransactionOpts: OrderTransactionOpts = {}): Promise<string> { assert.doesConformToSchema('orderCancellationRequests', orderCancellationRequests, schemas.orderCancellationRequestsSchema); const exchangeContractAddresses = _.map( @@ -606,9 +544,9 @@ export class ExchangeWrapper extends ContractWrapper { assert.hasAtMostOneUniqueValue(makers, ExchangeContractErrs.MultipleMakersInSingleCancelBatchDisallowed); const maker = makers[0]; await assert.isSenderAddressAsync('maker', maker, this._web3Wrapper); - const shouldValidate = _.isUndefined(orderTransactionOpts) ? - SHOULD_VALIDATE_BY_DEFAULT : - orderTransactionOpts.shouldValidate; + const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; if (shouldValidate) { for (const orderCancellationRequest of orderCancellationRequests) { const orderHash = utils.getOrderHashHex(orderCancellationRequest.order); @@ -633,21 +571,14 @@ export class ExchangeWrapper extends ContractWrapper { // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'( const [orderAddresses, orderValues, cancelTakerTokenAmounts] = _.unzip<any>(orderAddressesValuesAndTakerTokenCancelAmounts); - const gas = await exchangeInstance.batchCancelOrders.estimateGasAsync( - orderAddresses, - orderValues, - cancelTakerTokenAmounts, - { - from: maker, - }, - ); const txHash = await exchangeInstance.batchCancelOrders.sendTransactionAsync( orderAddresses, orderValues, cancelTakerTokenAmounts, { from: maker, - gas, + gas: orderTransactionOpts.gasLimit, + gasPrice: orderTransactionOpts.gasPrice, }, ); return txHash; diff --git a/packages/0x.js/src/contract_wrappers/token_wrapper.ts b/packages/0x.js/src/contract_wrappers/token_wrapper.ts index 5c6cfeaed..4a1dfcf8d 100644 --- a/packages/0x.js/src/contract_wrappers/token_wrapper.ts +++ b/packages/0x.js/src/contract_wrappers/token_wrapper.ts @@ -12,6 +12,7 @@ import { TokenContract, TokenContractEventArgs, TokenEvents, + TransactionOpts, ZeroExError, } from '../types'; import {AbiDecoder} from '../utils/abi_decoder'; @@ -66,24 +67,21 @@ export class TokenWrapper extends ContractWrapper { * for spenderAddress. * @param spenderAddress The hex encoded user Ethereum address who will be able to spend the set allowance. * @param amountInBaseUnits The allowance amount you would like to set. + * @param txOpts Transaction parameters. * @return Transaction hash. */ public async setAllowanceAsync(tokenAddress: string, ownerAddress: string, spenderAddress: string, - amountInBaseUnits: BigNumber): Promise<string> { + amountInBaseUnits: BigNumber, txOpts: TransactionOpts = {}): Promise<string> { await assert.isSenderAddressAsync('ownerAddress', ownerAddress, this._web3Wrapper); assert.isETHAddressHex('spenderAddress', spenderAddress); assert.isETHAddressHex('tokenAddress', tokenAddress); assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); const tokenContract = await this._getTokenContractAsync(tokenAddress); - // Hack: for some reason default estimated gas amount causes `base fee exceeds gas limit` exception - // on testrpc. Probably related to https://github.com/ethereumjs/testrpc/issues/294 - // TODO: Debug issue in testrpc and submit a PR, then remove this hack - const networkId = this._web3Wrapper.getNetworkId(); - const gas = networkId === constants.TESTRPC_NETWORK_ID ? ALLOWANCE_TO_ZERO_GAS_AMOUNT : undefined; const txHash = await tokenContract.approve.sendTransactionAsync(spenderAddress, amountInBaseUnits, { from: ownerAddress, - gas, + gas: txOpts.gasLimit, + gasPrice: txOpts.gasPrice, }); return txHash; } @@ -96,12 +94,13 @@ export class TokenWrapper extends ContractWrapper { * @param ownerAddress The hex encoded user Ethereum address who would like to set an allowance * for spenderAddress. * @param spenderAddress The hex encoded user Ethereum address who will be able to spend the set allowance. + * @param txOpts Transaction parameters. * @return Transaction hash. */ public async setUnlimitedAllowanceAsync(tokenAddress: string, ownerAddress: string, - spenderAddress: string): Promise<string> { + spenderAddress: string, txOpts: TransactionOpts = {}): Promise<string> { const txHash = await this.setAllowanceAsync( - tokenAddress, ownerAddress, spenderAddress, this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + tokenAddress, ownerAddress, spenderAddress, this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, txOpts, ); return txHash; } @@ -147,16 +146,19 @@ export class TokenWrapper extends ContractWrapper { * @param ownerAddress The hex encoded user Ethereum address who is setting an allowance * for the Proxy contract. * @param amountInBaseUnits The allowance amount specified in baseUnits. + * @param txOpts Transaction parameters. * @return Transaction hash. */ public async setProxyAllowanceAsync(tokenAddress: string, ownerAddress: string, - amountInBaseUnits: BigNumber): Promise<string> { + amountInBaseUnits: BigNumber, txOpts: TransactionOpts = {}): Promise<string> { assert.isETHAddressHex('ownerAddress', ownerAddress); assert.isETHAddressHex('tokenAddress', tokenAddress); assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); const proxyAddress = this._tokenTransferProxyWrapper.getContractAddress(); - const txHash = await this.setAllowanceAsync(tokenAddress, ownerAddress, proxyAddress, amountInBaseUnits); + const txHash = await this.setAllowanceAsync( + tokenAddress, ownerAddress, proxyAddress, amountInBaseUnits, txOpts, + ); return txHash; } /** @@ -167,11 +169,14 @@ export class TokenWrapper extends ContractWrapper { * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed. * @param ownerAddress The hex encoded user Ethereum address who is setting an allowance * for the Proxy contract. + * @param txOpts Transaction parameters. * @return Transaction hash. */ - public async setUnlimitedProxyAllowanceAsync(tokenAddress: string, ownerAddress: string): Promise<string> { + public async setUnlimitedProxyAllowanceAsync( + tokenAddress: string, ownerAddress: string, txOpts: TransactionOpts = {}, + ): Promise<string> { const txHash = await this.setProxyAllowanceAsync( - tokenAddress, ownerAddress, this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + tokenAddress, ownerAddress, this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, txOpts, ); return txHash; } @@ -181,10 +186,11 @@ export class TokenWrapper extends ContractWrapper { * @param fromAddress The hex encoded user Ethereum address that will send the funds. * @param toAddress The hex encoded user Ethereum address that will receive the funds. * @param amountInBaseUnits The amount (specified in baseUnits) of the token to transfer. + * @param txOpts Transaction parameters. * @return Transaction hash. */ public async transferAsync(tokenAddress: string, fromAddress: string, toAddress: string, - amountInBaseUnits: BigNumber): Promise<string> { + amountInBaseUnits: BigNumber, txOpts: TransactionOpts = {}): Promise<string> { assert.isETHAddressHex('tokenAddress', tokenAddress); await assert.isSenderAddressAsync('fromAddress', fromAddress, this._web3Wrapper); assert.isETHAddressHex('toAddress', toAddress); @@ -199,6 +205,8 @@ export class TokenWrapper extends ContractWrapper { const txHash = await tokenContract.transfer.sendTransactionAsync(toAddress, amountInBaseUnits, { from: fromAddress, + gas: txOpts.gasLimit, + gasPrice: txOpts.gasPrice, }); return txHash; } @@ -213,10 +221,11 @@ export class TokenWrapper extends ContractWrapper { * `fromAddress` must have set an allowance to the `senderAddress` * before this call. * @param amountInBaseUnits The amount (specified in baseUnits) of the token to transfer. + * @param txOpts Transaction parameters. * @return Transaction hash. */ public async transferFromAsync(tokenAddress: string, fromAddress: string, toAddress: string, - senderAddress: string, amountInBaseUnits: BigNumber): + senderAddress: string, amountInBaseUnits: BigNumber, txOpts: TransactionOpts = {}): Promise<string> { assert.isETHAddressHex('tokenAddress', tokenAddress); assert.isETHAddressHex('fromAddress', fromAddress); @@ -240,6 +249,8 @@ export class TokenWrapper extends ContractWrapper { fromAddress, toAddress, amountInBaseUnits, { from: senderAddress, + gas: txOpts.gasLimit, + gasPrice: txOpts.gasPrice, }, ); return txHash; diff --git a/packages/0x.js/src/index.ts b/packages/0x.js/src/index.ts index e529e2858..d4193a3e9 100644 --- a/packages/0x.js/src/index.ts +++ b/packages/0x.js/src/index.ts @@ -32,6 +32,7 @@ export { LogWithDecodedArgs, MethodOpts, OrderTransactionOpts, + TransactionOpts, FilterObject, LogEvent, DecodedLogEvent, diff --git a/packages/0x.js/src/order_watcher/order_state_watcher.ts b/packages/0x.js/src/order_watcher/order_state_watcher.ts index 8c21c1678..1ce111708 100644 --- a/packages/0x.js/src/order_watcher/order_state_watcher.ts +++ b/packages/0x.js/src/order_watcher/order_state_watcher.ts @@ -50,6 +50,8 @@ interface OrderStateByOrderHash { [orderHash: string]: OrderState; } +const DEFAULT_CLEANUP_JOB_INTERVAL_MS = 1000 * 60 * 60; // 1h + /** * This class includes all the functionality related to watching a set of orders * for potential changes in order validity/fillability. The orderWatcher notifies @@ -68,6 +70,8 @@ export class OrderStateWatcher { private _orderStateUtils: OrderStateUtils; private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore; private _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore; + private _cleanupJobInterval: number; + private _cleanupJobIntervalIdIfExists?: NodeJS.Timer; constructor( web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, token: TokenWrapper, exchange: ExchangeWrapper, config?: OrderStateWatcherConfig, @@ -92,6 +96,9 @@ export class OrderStateWatcher { this._expirationWatcher = new ExpirationWatcher( expirationMarginIfExistsMs, orderExpirationCheckingIntervalMsIfExists, ); + this._cleanupJobInterval = _.isUndefined(config) || _.isUndefined(config.cleanupJobIntervalMs) ? + DEFAULT_CLEANUP_JOB_INTERVAL_MS : + config.cleanupJobIntervalMs; } /** * Add an order to the orderStateWatcher. Before the order is added, it's @@ -139,12 +146,15 @@ export class OrderStateWatcher { this._callbackIfExists = callback; this._eventWatcher.subscribe(this._onEventWatcherCallbackAsync.bind(this)); this._expirationWatcher.subscribe(this._onOrderExpired.bind(this)); + this._cleanupJobIntervalIdIfExists = intervalUtils.setAsyncExcludingInterval( + this._cleanupAsync.bind(this), this._cleanupJobInterval, + ); } /** * Ends an orderStateWatcher subscription. */ public unsubscribe(): void { - if (_.isUndefined(this._callbackIfExists)) { + if (_.isUndefined(this._callbackIfExists) || _.isUndefined(this._cleanupJobIntervalIdIfExists)) { throw new Error(ZeroExError.SubscriptionNotFound); } this._balanceAndProxyAllowanceLazyStore.deleteAll(); @@ -152,6 +162,34 @@ export class OrderStateWatcher { delete this._callbackIfExists; this._eventWatcher.unsubscribe(); this._expirationWatcher.unsubscribe(); + intervalUtils.clearAsyncExcludingInterval(this._cleanupJobIntervalIdIfExists); + } + private async _cleanupAsync(): Promise<void> { + for (const orderHash of _.keys(this._orderByOrderHash)) { + this._cleanupOrderRelatedState(orderHash); + await this._emitRevalidateOrdersAsync([orderHash]); + } + } + private _cleanupOrderRelatedState(orderHash: string): void { + const signedOrder = this._orderByOrderHash[orderHash]; + + this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(orderHash); + this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(orderHash); + + this._balanceAndProxyAllowanceLazyStore.deleteBalance(signedOrder.makerTokenAddress, signedOrder.maker); + this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(signedOrder.makerTokenAddress, signedOrder.maker); + this._balanceAndProxyAllowanceLazyStore.deleteBalance(signedOrder.takerTokenAddress, signedOrder.taker); + this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(signedOrder.takerTokenAddress, signedOrder.taker); + + const zrxTokenAddress = this._getZRXTokenAddress(); + if (!signedOrder.makerFee.isZero()) { + this._balanceAndProxyAllowanceLazyStore.deleteBalance(zrxTokenAddress, signedOrder.maker); + this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(zrxTokenAddress, signedOrder.maker); + } + if (!signedOrder.takerFee.isZero()) { + this._balanceAndProxyAllowanceLazyStore.deleteBalance(zrxTokenAddress, signedOrder.taker); + this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(zrxTokenAddress, signedOrder.taker); + } } private _onOrderExpired(orderHash: string): void { const orderState: OrderState = { @@ -266,8 +304,7 @@ export class OrderStateWatcher { this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress] = new Set(); } this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress].add(orderHash); - const exchange = (this._orderFilledCancelledLazyStore as any).exchange as ExchangeWrapper; - const zrxTokenAddress = exchange.getZRXTokenAddress(); + const zrxTokenAddress = this._getZRXTokenAddress(); if (_.isUndefined(this._dependentOrderHashes[signedOrder.maker][zrxTokenAddress])) { this._dependentOrderHashes[signedOrder.maker][zrxTokenAddress] = new Set(); } @@ -282,4 +319,9 @@ export class OrderStateWatcher { delete this._dependentOrderHashes[makerAddress]; } } + private _getZRXTokenAddress(): string { + const exchange = (this._orderFilledCancelledLazyStore as any).exchange as ExchangeWrapper; + const zrxTokenAddress = exchange.getZRXTokenAddress(); + return zrxTokenAddress; + } } diff --git a/packages/0x.js/src/types.ts b/packages/0x.js/src/types.ts index af4f054f0..5363b02ff 100644 --- a/packages/0x.js/src/types.ts +++ b/packages/0x.js/src/types.ts @@ -332,6 +332,7 @@ export interface TxOpts { from: string; gas?: number; value?: BigNumber; + gasPrice?: BigNumber; } export interface TokenAddressBySymbol { @@ -406,11 +407,13 @@ export interface JSONRPCPayload { * eventPollingIntervalMs: How often to poll the Ethereum node for new events. Defaults: 200 * expirationMarginMs: Amount of time before order expiry that you'd like to be notified * of an orders expiration. Defaults: 0 + * cleanupJobIntervalMs: How often to run a cleanup job which revalidates all the orders. Defaults: 1h */ export interface OrderStateWatcherConfig { orderExpirationCheckingIntervalMs?: number; eventPollingIntervalMs?: number; expirationMarginMs?: number; + cleanupJobIntervalMs?: number; } /* @@ -482,11 +485,20 @@ export interface MethodOpts { } /* + * gasPrice: Gas price in Wei to use for a transaction + * gasLimit: The amount of gas to send with a transaction + */ +export interface TransactionOpts { + gasPrice?: BigNumber; + gasLimit?: number; +} + +/* * shouldValidate: Flag indicating whether the library should make attempts to validate a transaction before - * broadcasting it. For example, order has a valid signature, maker has sufficient funds, etc. + * broadcasting it. For example, order has a valid signature, maker has sufficient funds, etc. Default: true */ -export interface OrderTransactionOpts { - shouldValidate: boolean; +export interface OrderTransactionOpts extends TransactionOpts { + shouldValidate?: boolean; } export type FilterObject = Web3.FilterObject; diff --git a/packages/0x.js/test/ether_token_wrapper_test.ts b/packages/0x.js/test/ether_token_wrapper_test.ts index 5b5e4c656..d3e4439ee 100644 --- a/packages/0x.js/test/ether_token_wrapper_test.ts +++ b/packages/0x.js/test/ether_token_wrapper_test.ts @@ -18,7 +18,7 @@ const blockchainLifecycle = new BlockchainLifecycle(); // a small amount of ETH will be used to pay this gas cost. We therefore check that the difference between // the expected balance and actual balance (given the amount of ETH deposited), only deviates by the amount // required to pay gas costs. -const MAX_REASONABLE_GAS_COST_IN_WEI = 62237; +const MAX_REASONABLE_GAS_COST_IN_WEI = 62517; describe('EtherTokenWrapper', () => { let web3: Web3; diff --git a/packages/0x.js/test/utils/constants.ts b/packages/0x.js/test/utils/constants.ts index 212abf4d6..75fdf49c9 100644 --- a/packages/0x.js/test/utils/constants.ts +++ b/packages/0x.js/test/utils/constants.ts @@ -8,4 +8,5 @@ export const constants = { KOVAN_RPC_URL: 'https://kovan.infura.io', ROPSTEN_RPC_URL: 'https://ropsten.infura.io', ZRX_DECIMALS: 18, + GAS_ESTIMATE: 500000, }; diff --git a/packages/0x.js/src/subproviders/empty_wallet_subprovider.ts b/packages/0x.js/test/utils/subproviders/empty_wallet_subprovider.ts index 2993bc801..e5e279873 100644 --- a/packages/0x.js/src/subproviders/empty_wallet_subprovider.ts +++ b/packages/0x.js/test/utils/subproviders/empty_wallet_subprovider.ts @@ -1,11 +1,11 @@ -import {JSONRPCPayload} from '../types'; +import {JSONRPCPayload} from '../../../src/types'; /* * This class implements the web3-provider-engine subprovider interface and returns * that the provider has no addresses when queried. * Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js */ -export class EmptyWalletSubProvider { +export class EmptyWalletSubprovider { // This method needs to be here to satisfy the interface but linter wants it to be static. // tslint:disable-next-line:prefer-function-over-method public handleRequest(payload: JSONRPCPayload, next: () => void, end: (err: Error|null, result: any) => void) { diff --git a/packages/0x.js/test/utils/subproviders/fake_gas_estimate_subprovider.ts b/packages/0x.js/test/utils/subproviders/fake_gas_estimate_subprovider.ts new file mode 100644 index 000000000..059163f2e --- /dev/null +++ b/packages/0x.js/test/utils/subproviders/fake_gas_estimate_subprovider.ts @@ -0,0 +1,34 @@ +import {JSONRPCPayload} from '../../../src/types'; + +/* + * This class implements the web3-provider-engine subprovider interface and returns + * the constant gas estimate when queried. + * HACK: We need this so that our tests don't use testrpc gas estimation which sometimes kills the node. + * Source: https://github.com/trufflesuite/ganache-cli/issues/417 + * Source: https://github.com/trufflesuite/ganache-cli/issues/437 + * Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js + */ +export class FakeGasEstimateSubprovider { + private constantGasAmount: number; + constructor(constantGasAmount: number) { + this.constantGasAmount = constantGasAmount; + } + // This method needs to be here to satisfy the interface but linter wants it to be static. + // tslint:disable-next-line:prefer-function-over-method + public handleRequest(payload: JSONRPCPayload, next: () => void, end: (err: Error|null, result: any) => void) { + switch (payload.method) { + case 'eth_estimateGas': + end(null, this.constantGasAmount); + return; + + default: + next(); + return; + } + } + // Required to implement this method despite not needing it for this subprovider + // tslint:disable-next-line:prefer-function-over-method + public setEngine(engine: any) { + // noop + } +} diff --git a/packages/0x.js/test/utils/web3_factory.ts b/packages/0x.js/test/utils/web3_factory.ts index b4bf1acd3..da4828943 100644 --- a/packages/0x.js/test/utils/web3_factory.ts +++ b/packages/0x.js/test/utils/web3_factory.ts @@ -7,7 +7,8 @@ import * as Web3 from 'web3'; import ProviderEngine = require('web3-provider-engine'); import RpcSubprovider = require('web3-provider-engine/subproviders/rpc'); -import {EmptyWalletSubProvider} from '../../src/subproviders/empty_wallet_subprovider'; +import {EmptyWalletSubprovider} from './subproviders/empty_wallet_subprovider'; +import {FakeGasEstimateSubprovider} from './subproviders/fake_gas_estimate_subprovider'; import {constants} from './constants'; @@ -22,8 +23,9 @@ export const web3Factory = { const provider = new ProviderEngine(); const rpcUrl = `http://${constants.RPC_HOST}:${constants.RPC_PORT}`; if (!hasAddresses) { - provider.addProvider(new EmptyWalletSubProvider()); + provider.addProvider(new EmptyWalletSubprovider()); } + provider.addProvider(new FakeGasEstimateSubprovider(constants.GAS_ESTIMATE)); provider.addProvider(new RpcSubprovider({ rpcUrl, })); |