diff options
Diffstat (limited to 'packages/0x.js')
-rw-r--r-- | packages/0x.js/CHANGELOG.md | 13 | ||||
-rw-r--r-- | packages/0x.js/package.json | 12 | ||||
-rw-r--r-- | packages/0x.js/src/order_watcher/event_watcher.ts | 10 | ||||
-rw-r--r-- | packages/0x.js/src/order_watcher/expiration_watcher.ts | 76 | ||||
-rw-r--r-- | packages/0x.js/src/order_watcher/order_state_watcher.ts | 37 | ||||
-rw-r--r-- | packages/0x.js/src/order_watcher/remaining_fillable_calculator.ts | 86 | ||||
-rw-r--r-- | packages/0x.js/src/types.ts | 7 | ||||
-rw-r--r-- | packages/0x.js/src/utils/order_state_utils.ts | 18 | ||||
-rw-r--r-- | packages/0x.js/src/utils/order_validation_utils.ts | 4 | ||||
-rw-r--r-- | packages/0x.js/src/utils/utils.ts | 7 | ||||
-rw-r--r-- | packages/0x.js/src/web3_wrapper.ts | 38 | ||||
-rw-r--r-- | packages/0x.js/test/expiration_watcher_test.ts | 138 | ||||
-rw-r--r-- | packages/0x.js/test/order_state_watcher_test.ts | 115 | ||||
-rw-r--r-- | packages/0x.js/test/remaining_fillable_calculator_test.ts | 176 | ||||
-rw-r--r-- | packages/0x.js/test/token_wrapper_test.ts | 3 | ||||
-rw-r--r-- | packages/0x.js/test/utils/constants.ts | 1 | ||||
-rw-r--r-- | packages/0x.js/test/utils/report_callback_errors.ts | 6 |
17 files changed, 694 insertions, 53 deletions
diff --git a/packages/0x.js/CHANGELOG.md b/packages/0x.js/CHANGELOG.md index 6245308c3..235a6eedb 100644 --- a/packages/0x.js/CHANGELOG.md +++ b/packages/0x.js/CHANGELOG.md @@ -1,19 +1,20 @@ # CHANGELOG -vx.x.x +v0.26.0 ------------------------ - * Remove support for Async callback types when used in Subscribe functions + * 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) * In OrderWatcher subscribe to ZRX Token Transfer and Approval events when maker token is different (#225) v0.25.1 - _November 13, 2017_ ------------------------ - * Standardise on Cancelled over Canceled - * Add missing `DecodedLogEvent` type to exported types - * Normalized the transactionReceipt status to be `null|0|1`, 1 meaning transaction execution successful, 0 unsuccessful and `null` if it is a pre-byzantinium transaction. + * Standardise on Cancelled over Canceled (#217) + * Add missing `DecodedLogEvent` type to exported types (#205) + * Normalized the transactionReceipt status to be `null|0|1`, 1 meaning transaction execution successful, 0 unsuccessful and `null` if it is a pre-byzantinium transaction. (#200) v0.23.0 - _November 12, 2017_ ------------------------ - * Fixed unhandled promise rejection error in subscribe methods (#209) + * Fixed unhandled promise rejection error in subscribe methods (#209) * Subscribe callbacks now receive an error object as their first argument v0.22.6 - _November 10, 2017_ diff --git a/packages/0x.js/package.json b/packages/0x.js/package.json index 7aa360954..afca00fe9 100644 --- a/packages/0x.js/package.json +++ b/packages/0x.js/package.json @@ -1,6 +1,6 @@ { "name": "0x.js", - "version": "0.25.1", + "version": "0.26.1", "description": "A javascript library for interacting with the 0x protocol", "keywords": [ "0x.js", @@ -17,7 +17,7 @@ "docs:json": "typedoc --excludePrivate --excludeExternals --target ES5 --json $JSON_FILE_PATH $PROJECT_DIR", "upload_docs_json": "aws s3 cp docs/index.json $S3_URL --profile 0xproject --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type aplication/json", "lint": "tslint src/**/*.ts test/**/*.ts", - "test:circleci": "run-s test:coverage report_test_coverage; if [ $CIRCLE_BRANCH = \"development\" ]; then yarn test:umd; fi", + "test:circleci": "run-s test:coverage report_test_coverage && if [ $CIRCLE_BRANCH = \"development\" ]; then yarn test:umd; fi", "test": "run-s clean test:commonjs", "test:umd": "./scripts/test_umd.sh", "test:coverage": "nyc npm run test --all", @@ -44,7 +44,8 @@ "node": ">=6.0.0" }, "devDependencies": { - "@0xproject/tslint-config": "^0.1.0", + "@0xproject/tslint-config": "^0.1.1", + "@types/bintrees": "^1.0.2", "@types/jsonschema": "^1.1.1", "@types/lodash": "^4.14.64", "@types/mocha": "^2.2.41", @@ -82,9 +83,10 @@ "webpack": "^3.1.0" }, "dependencies": { - "@0xproject/assert": "^0.0.4", - "@0xproject/json-schemas": "^0.6.7", + "@0xproject/assert": "^0.0.5", + "@0xproject/json-schemas": "^0.6.8", "bignumber.js": "~4.1.0", + "bintrees": "^1.0.2", "bn.js": "4.11.8", "compare-versions": "^3.0.1", "es6-promisify": "^5.0.0", diff --git a/packages/0x.js/src/order_watcher/event_watcher.ts b/packages/0x.js/src/order_watcher/event_watcher.ts index c39431f6d..ecbab0cd5 100644 --- a/packages/0x.js/src/order_watcher/event_watcher.ts +++ b/packages/0x.js/src/order_watcher/event_watcher.ts @@ -12,7 +12,7 @@ import {intervalUtils} from '../utils/interval_utils'; import {assert} from '../utils/assert'; import {utils} from '../utils/utils'; -const DEFAULT_EVENT_POLLING_INTERVAL = 200; +const DEFAULT_EVENT_POLLING_INTERVAL_MS = 200; enum LogEventState { Removed, @@ -28,11 +28,11 @@ export class EventWatcher { private _pollingIntervalMs: number; private _intervalIdIfExists?: NodeJS.Timer; private _lastEvents: Web3.LogEntry[] = []; - constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number) { + constructor(web3Wrapper: Web3Wrapper, pollingIntervalIfExistsMs: undefined|number) { this._web3Wrapper = web3Wrapper; - this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ? - DEFAULT_EVENT_POLLING_INTERVAL : - pollingIntervalMs; + this._pollingIntervalMs = _.isUndefined(pollingIntervalIfExistsMs) ? + DEFAULT_EVENT_POLLING_INTERVAL_MS : + pollingIntervalIfExistsMs; } public subscribe(callback: EventWatcherCallback): void { assert.isFunction('callback', callback); diff --git a/packages/0x.js/src/order_watcher/expiration_watcher.ts b/packages/0x.js/src/order_watcher/expiration_watcher.ts new file mode 100644 index 000000000..717edaad7 --- /dev/null +++ b/packages/0x.js/src/order_watcher/expiration_watcher.ts @@ -0,0 +1,76 @@ +import * as _ from 'lodash'; +import {BigNumber} from 'bignumber.js'; +import {RBTree} from 'bintrees'; +import {utils} from '../utils/utils'; +import {intervalUtils} from '../utils/interval_utils'; +import {SignedOrder, ZeroExError} from '../types'; +import {ZeroEx} from '../0x'; + +const DEFAULT_EXPIRATION_MARGIN_MS = 0; +const DEFAULT_ORDER_EXPIRATION_CHECKING_INTERVAL_MS = 50; + +/** + * This class includes the functionality to detect expired orders. + * It stores them in a min heap by expiration time and checks for expired ones every `orderExpirationCheckingIntervalMs` + */ +export class ExpirationWatcher { + private orderHashByExpirationRBTree: RBTree<string>; + private expiration: {[orderHash: string]: BigNumber} = {}; + private orderExpirationCheckingIntervalMs: number; + private expirationMarginMs: number; + private orderExpirationCheckingIntervalIdIfExists?: NodeJS.Timer; + constructor(expirationMarginIfExistsMs?: number, + orderExpirationCheckingIntervalIfExistsMs?: number) { + this.expirationMarginMs = expirationMarginIfExistsMs || + DEFAULT_EXPIRATION_MARGIN_MS; + this.orderExpirationCheckingIntervalMs = expirationMarginIfExistsMs || + DEFAULT_ORDER_EXPIRATION_CHECKING_INTERVAL_MS; + const scoreFunction = (orderHash: string) => this.expiration[orderHash].toNumber(); + const comparator = (lhs: string, rhs: string) => scoreFunction(lhs) - scoreFunction(rhs); + this.orderHashByExpirationRBTree = new RBTree(comparator); + } + public subscribe(callbackAsync: (orderHash: string) => Promise<void>): void { + if (!_.isUndefined(this.orderExpirationCheckingIntervalIdIfExists)) { + throw new Error(ZeroExError.SubscriptionAlreadyPresent); + } + this.orderExpirationCheckingIntervalIdIfExists = intervalUtils.setAsyncExcludingInterval( + this.pruneExpiredOrdersAsync.bind(this, callbackAsync), this.orderExpirationCheckingIntervalMs, + ); + } + public unsubscribe(): void { + if (_.isUndefined(this.orderExpirationCheckingIntervalIdIfExists)) { + throw new Error(ZeroExError.SubscriptionNotFound); + } + intervalUtils.clearAsyncExcludingInterval(this.orderExpirationCheckingIntervalIdIfExists); + delete this.orderExpirationCheckingIntervalIdIfExists; + } + public addOrder(orderHash: string, expirationUnixTimestampMs: BigNumber): void { + this.expiration[orderHash] = expirationUnixTimestampMs; + this.orderHashByExpirationRBTree.insert(orderHash); + } + public removeOrder(orderHash: string): void { + this.orderHashByExpirationRBTree.remove(orderHash); + delete this.expiration[orderHash]; + } + private async pruneExpiredOrdersAsync(callbackAsync: (orderHash: string) => Promise<void>): Promise<void> { + const currentUnixTimestampMs = utils.getCurrentUnixTimestampMs(); + while (true) { + const hasTrakedOrders = this.orderHashByExpirationRBTree.size === 0; + if (hasTrakedOrders) { + break; + } + const nextOrderHashToExpire = this.orderHashByExpirationRBTree.min(); + const hasNoExpiredOrders = this.expiration[nextOrderHashToExpire].greaterThan( + currentUnixTimestampMs.plus(this.expirationMarginMs), + ); + const isSubscriptionActive = _.isUndefined(this.orderExpirationCheckingIntervalIdIfExists); + if (hasNoExpiredOrders || isSubscriptionActive) { + break; + } + const orderHash = this.orderHashByExpirationRBTree.min(); + this.orderHashByExpirationRBTree.remove(orderHash); + delete this.expiration[orderHash]; + await callbackAsync(orderHash); + } + } +} 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 648345c48..44a41669d 100644 --- a/packages/0x.js/src/order_watcher/order_state_watcher.ts +++ b/packages/0x.js/src/order_watcher/order_state_watcher.ts @@ -6,6 +6,7 @@ import {assert} from '../utils/assert'; import {utils} from '../utils/utils'; import {artifacts} from '../artifacts'; import {AbiDecoder} from '../utils/abi_decoder'; +import {intervalUtils} from '../utils/interval_utils'; import {OrderStateUtils} from '../utils/order_state_utils'; import { LogEvent, @@ -24,14 +25,14 @@ import { ExchangeEvents, TokenEvents, ZeroExError, + ExchangeContractErrs, } from '../types'; import {Web3Wrapper} from '../web3_wrapper'; import {TokenWrapper} from '../contract_wrappers/token_wrapper'; import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper'; import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store'; import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store'; - -const DEFAULT_NUM_CONFIRMATIONS = 0; +import {ExpirationWatcher} from './expiration_watcher'; interface DependentOrderHashes { [makerAddress: string]: { @@ -61,6 +62,7 @@ export class OrderStateWatcher { private _eventWatcher: EventWatcher; private _web3Wrapper: Web3Wrapper; private _abiDecoder: AbiDecoder; + private _expirationWatcher: ExpirationWatcher; private _orderStateUtils: OrderStateUtils; private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore; private _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore; @@ -70,13 +72,22 @@ export class OrderStateWatcher { ) { this._abiDecoder = abiDecoder; this._web3Wrapper = web3Wrapper; - const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs; - this._eventWatcher = new EventWatcher(web3Wrapper, eventPollingIntervalMs); + const pollingIntervalIfExistsMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs; + this._eventWatcher = new EventWatcher(web3Wrapper, pollingIntervalIfExistsMs); this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore(token); this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore(exchange); this._orderStateUtils = new OrderStateUtils( this._balanceAndProxyAllowanceLazyStore, this._orderFilledCancelledLazyStore, ); + const orderExpirationCheckingIntervalMsIfExists = _.isUndefined(config) ? + undefined : + config.orderExpirationCheckingIntervalMs; + const expirationMarginIfExistsMs = _.isUndefined(config) ? + undefined : + config.expirationMarginMs; + this._expirationWatcher = new ExpirationWatcher( + expirationMarginIfExistsMs, orderExpirationCheckingIntervalMsIfExists, + ); } /** * Add an order to the orderStateWatcher. Before the order is added, it's @@ -89,6 +100,8 @@ export class OrderStateWatcher { assert.isValidSignature(orderHash, signedOrder.ecSignature, signedOrder.maker); this._orderByOrderHash[orderHash] = signedOrder; await this.addToDependentOrderHashesAsync(signedOrder, orderHash); + const expirationUnixTimestampMs = signedOrder.expirationUnixTimestampSec.times(1000); + this._expirationWatcher.addOrder(orderHash, expirationUnixTimestampMs); } /** * Removes an order from the orderStateWatcher @@ -106,6 +119,7 @@ export class OrderStateWatcher { const zrxTokenAddress = await exchange.getZRXTokenAddressAsync(); this.removeFromDependentOrderHashes(signedOrder.maker, zrxTokenAddress, orderHash); this.removeFromDependentOrderHashes(signedOrder.maker, signedOrder.makerTokenAddress, orderHash); + this._expirationWatcher.removeOrder(orderHash); } /** * Starts an orderStateWatcher subscription. The callback will be called every time a watched order's @@ -120,6 +134,7 @@ export class OrderStateWatcher { } this._callbackIfExists = callback; this._eventWatcher.subscribe(this._onEventWatcherCallbackAsync.bind(this)); + this._expirationWatcher.subscribe(this._onOrderExpiredAsync.bind(this)); } /** * Ends an orderStateWatcher subscription. @@ -132,6 +147,20 @@ export class OrderStateWatcher { this._orderFilledCancelledLazyStore.deleteAll(); delete this._callbackIfExists; this._eventWatcher.unsubscribe(); + this._expirationWatcher.unsubscribe(); + } + private async _onOrderExpiredAsync(orderHash: string): Promise<void> { + const orderState: OrderState = { + isValid: false, + orderHash, + error: ExchangeContractErrs.OrderFillExpired, + }; + if (!_.isUndefined(this._orderByOrderHash[orderHash])) { + await this.removeOrderAsync(orderHash); + if (!_.isUndefined(this._callbackIfExists)) { + this._callbackIfExists(orderState); + } + } } private async _onEventWatcherCallbackAsync(log: LogEvent): Promise<void> { const maybeDecodedLog = this._abiDecoder.tryToDecodeLogOrNoop(log); diff --git a/packages/0x.js/src/order_watcher/remaining_fillable_calculator.ts b/packages/0x.js/src/order_watcher/remaining_fillable_calculator.ts new file mode 100644 index 000000000..c77d4428c --- /dev/null +++ b/packages/0x.js/src/order_watcher/remaining_fillable_calculator.ts @@ -0,0 +1,86 @@ +import {SignedOrder} from '../types'; +import {BigNumber} from 'bignumber.js'; + +export class RemainingFillableCalculator { + private signedOrder: SignedOrder; + private isMakerTokenZRX: boolean; + // Transferrable Amount is the minimum of Approval and Balance + private transferrableMakerTokenAmount: BigNumber; + private transferrableMakerFeeTokenAmount: BigNumber; + private remainingMakerTokenAmount: BigNumber; + private remainingMakerFeeAmount: BigNumber; + constructor(signedOrder: SignedOrder, + isMakerTokenZRX: boolean, + transferrableMakerTokenAmount: BigNumber, + transferrableMakerFeeTokenAmount: BigNumber, + remainingMakerTokenAmount: BigNumber) { + this.signedOrder = signedOrder; + this.isMakerTokenZRX = isMakerTokenZRX; + this.transferrableMakerTokenAmount = transferrableMakerTokenAmount; + this.transferrableMakerFeeTokenAmount = transferrableMakerFeeTokenAmount; + this.remainingMakerTokenAmount = remainingMakerTokenAmount; + this.remainingMakerFeeAmount = remainingMakerTokenAmount.times(signedOrder.makerFee) + .dividedToIntegerBy(signedOrder.makerTokenAmount); + } + public computeRemainingMakerFillable(): BigNumber { + if (this.hasSufficientFundsForFeeAndTransferAmount()) { + return this.remainingMakerTokenAmount; + } + if (this.signedOrder.makerFee.isZero()) { + return BigNumber.min(this.remainingMakerTokenAmount, this.transferrableMakerTokenAmount); + } + return this.calculatePartiallyFillableMakerTokenAmount(); + } + public computeRemainingTakerFillable(): BigNumber { + return this.computeRemainingMakerFillable().times(this.signedOrder.takerTokenAmount) + .dividedToIntegerBy(this.signedOrder.makerTokenAmount); + } + private hasSufficientFundsForFeeAndTransferAmount(): boolean { + if (this.isMakerTokenZRX) { + const totalZRXTransferAmountRequired = this.remainingMakerTokenAmount.plus(this.remainingMakerFeeAmount); + const hasSufficientFunds = this.transferrableMakerTokenAmount.greaterThanOrEqualTo( + totalZRXTransferAmountRequired); + return hasSufficientFunds; + } else { + const hasSufficientFundsForTransferAmount = this.transferrableMakerTokenAmount.greaterThanOrEqualTo( + this.remainingMakerTokenAmount); + const hasSufficientFundsForFeeAmount = this.transferrableMakerFeeTokenAmount.greaterThanOrEqualTo( + this.remainingMakerFeeAmount); + const hasSufficientFunds = hasSufficientFundsForTransferAmount && hasSufficientFundsForFeeAmount; + return hasSufficientFunds; + } + } + private calculatePartiallyFillableMakerTokenAmount(): BigNumber { + // Given an order for 200 wei for 2 ZRXwei fee, find 100 wei for 1 ZRXwei. Order ratio is then 100:1 + const orderToFeeRatio = this.signedOrder.makerTokenAmount.dividedBy(this.signedOrder.makerFee); + // The number of times the maker can fill the order, if each fill only required the transfer of a single + // baseUnit of fee tokens. + // Given 2 ZRXwei, the maximum amount of times Maker can fill this order, in terms of fees, is 2 + const fillableTimesInFeeTokenBaseUnits = BigNumber.min(this.transferrableMakerFeeTokenAmount, + this.remainingMakerFeeAmount); + // The number of times the Maker can fill the order, given the Maker Token Balance + // Assuming a balance of 150 wei, and an orderToFeeRatio of 100:1, maker can fill this order 1 time. + let fillableTimesInMakerTokenUnits = this.transferrableMakerTokenAmount.dividedBy(orderToFeeRatio); + if (this.isMakerTokenZRX) { + // If ZRX is the maker token, the Fee and the Maker amount need to be removed from the same pool; + // 200 ZRXwei for 2ZRXwei fee can only be filled once (need 202 ZRXwei) + const totalZRXTokenPooled = this.transferrableMakerTokenAmount; + // The purchasing power here is less as the tokens are taken from the same Pool + // For every one number of fills, we have to take an extra ZRX out of the pool + fillableTimesInMakerTokenUnits = totalZRXTokenPooled.dividedBy( + orderToFeeRatio.plus(new BigNumber(1))); + + } + // When Ratio is not fully divisible there can be remainders which cannot be represented, so they are floored. + // This can result in a RoundingError being thrown by the Exchange Contract. + const partiallyFillableMakerTokenAmount = fillableTimesInMakerTokenUnits + .times(this.signedOrder.makerTokenAmount) + .dividedToIntegerBy(this.signedOrder.makerFee); + const partiallyFillableFeeTokenAmount = fillableTimesInFeeTokenBaseUnits + .times(this.signedOrder.makerTokenAmount) + .dividedToIntegerBy(this.signedOrder.makerFee); + const partiallyFillableAmount = BigNumber.min(partiallyFillableMakerTokenAmount, + partiallyFillableFeeTokenAmount); + return partiallyFillableAmount; + } +} diff --git a/packages/0x.js/src/types.ts b/packages/0x.js/src/types.ts index 39e5fa9f2..c3aabfd86 100644 --- a/packages/0x.js/src/types.ts +++ b/packages/0x.js/src/types.ts @@ -392,10 +392,15 @@ export interface JSONRPCPayload { } /* - * eventPollingIntervalMs: How often to poll the Ethereum node for new events + * orderExpirationCheckingIntervalMs: How often to check for expired orders. Default: 50 + * 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 */ export interface OrderStateWatcherConfig { + orderExpirationCheckingIntervalMs?: number; eventPollingIntervalMs?: number; + expirationMarginMs?: number; } /* diff --git a/packages/0x.js/src/utils/order_state_utils.ts b/packages/0x.js/src/utils/order_state_utils.ts index 123584f90..1d8f02a18 100644 --- a/packages/0x.js/src/utils/order_state_utils.ts +++ b/packages/0x.js/src/utils/order_state_utils.ts @@ -17,6 +17,7 @@ import {utils} from '../utils/utils'; import {constants} from '../utils/constants'; import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store'; import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store'; +import {RemainingFillableCalculator} from '../order_watcher/remaining_fillable_calculator'; const ACCEPTABLE_RELATIVE_ROUNDING_ERROR = 0.0001; @@ -78,12 +79,17 @@ export class OrderStateUtils { const remainingTakerTokenAmount = totalTakerTokenAmount.minus(unavailableTakerTokenAmount); const remainingMakerTokenAmount = remainingTakerTokenAmount.times(totalMakerTokenAmount) .dividedToIntegerBy(totalTakerTokenAmount); - const fillableMakerTokenAmount = BigNumber.min([makerProxyAllowance, makerBalance]); - const remainingFillableMakerTokenAmount = BigNumber.min(fillableMakerTokenAmount, remainingMakerTokenAmount); - const remainingFillableTakerTokenAmount = remainingFillableMakerTokenAmount - .times(totalTakerTokenAmount) - .dividedToIntegerBy(totalMakerTokenAmount); - // TODO: Handle edge case where maker token is ZRX with fee + const transferrableMakerTokenAmount = BigNumber.min([makerProxyAllowance, makerBalance]); + const transferrableFeeTokenAmount = BigNumber.min([makerFeeProxyAllowance, makerFeeBalance]); + + const isMakerTokenZRX = signedOrder.makerTokenAddress === zrxTokenAddress; + const remainingFillableCalculator = new RemainingFillableCalculator(signedOrder, + isMakerTokenZRX, + transferrableMakerTokenAmount, + transferrableFeeTokenAmount, + remainingMakerTokenAmount); + const remainingFillableMakerTokenAmount = remainingFillableCalculator.computeRemainingMakerFillable(); + const remainingFillableTakerTokenAmount = remainingFillableCalculator.computeRemainingTakerFillable(); const orderRelevantState = { makerBalance, makerProxyAllowance, diff --git a/packages/0x.js/src/utils/order_validation_utils.ts b/packages/0x.js/src/utils/order_validation_utils.ts index f03703c4e..ed723e3d4 100644 --- a/packages/0x.js/src/utils/order_validation_utils.ts +++ b/packages/0x.js/src/utils/order_validation_utils.ts @@ -102,7 +102,7 @@ export class OrderValidationUtils { if (order.takerTokenAmount.eq(unavailableTakerTokenAmount)) { throw new Error(ExchangeContractErrs.OrderAlreadyCancelledOrFilled); } - const currentUnixTimestampSec = utils.getCurrentUnixTimestamp(); + const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec(); if (order.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) { throw new Error(ExchangeContractErrs.OrderCancelExpired); } @@ -150,7 +150,7 @@ export class OrderValidationUtils { } } private validateOrderNotExpiredOrThrow(expirationUnixTimestampSec: BigNumber) { - const currentUnixTimestampSec = utils.getCurrentUnixTimestamp(); + const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec(); if (expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) { throw new Error(ExchangeContractErrs.OrderFillExpired); } diff --git a/packages/0x.js/src/utils/utils.ts b/packages/0x.js/src/utils/utils.ts index 280f3e979..5370c3b4b 100644 --- a/packages/0x.js/src/utils/utils.ts +++ b/packages/0x.js/src/utils/utils.ts @@ -49,7 +49,10 @@ export const utils = { const hashHex = ethUtil.bufferToHex(hashBuff); return hashHex; }, - getCurrentUnixTimestamp(): BigNumber { - return new BigNumber(Date.now() / 1000); + getCurrentUnixTimestampSec(): BigNumber { + return new BigNumber(Date.now() / 1000).round(); + }, + getCurrentUnixTimestampMs(): BigNumber { + return new BigNumber(Date.now()); }, }; diff --git a/packages/0x.js/src/web3_wrapper.ts b/packages/0x.js/src/web3_wrapper.ts index c937f9288..7bd8ea093 100644 --- a/packages/0x.js/src/web3_wrapper.ts +++ b/packages/0x.js/src/web3_wrapper.ts @@ -5,6 +5,17 @@ import promisify = require('es6-promisify'); import {ZeroExError, Artifact, TransactionReceipt} from './types'; import {Contract} from './contract'; +interface RawLogEntry { + logIndex: string|null; + transactionIndex: string|null; + transactionHash: string; + blockHash: string|null; + blockNumber: string|null; + address: string; + data: string; + topics: string[]; +} + export class Web3Wrapper { private web3: Web3; private defaults: Partial<Web3.TxData>; @@ -39,7 +50,9 @@ export class Web3Wrapper { } public async getTransactionReceiptAsync(txHash: string): Promise<TransactionReceipt> { const transactionReceipt = await promisify(this.web3.eth.getTransactionReceipt)(txHash); - transactionReceipt.status = this.normalizeTxReceiptStatus(transactionReceipt.status); + if (!_.isNull(transactionReceipt)) { + transactionReceipt.status = this.normalizeTxReceiptStatus(transactionReceipt.status); + } return transactionReceipt; } public getCurrentProvider(): Web3.Provider { @@ -137,8 +150,9 @@ export class Web3Wrapper { method: 'eth_getLogs', params: [serializedFilter], }; - const logs = await this.sendRawPayloadAsync(payload); - return logs; + const rawLogs = await this.sendRawPayloadAsync<RawLogEntry[]>(payload); + const formattedLogs = _.map(rawLogs, this.formatLog.bind(this)); + return formattedLogs; } private getContractInstance<A extends Web3.ContractInstance>(abi: Web3.ContractAbi, address: string): A { const web3ContractInstance = this.web3.eth.contract(abi).at(address); @@ -149,7 +163,7 @@ export class Web3Wrapper { const networkId = await promisify(this.web3.version.getNetwork)(); return networkId; } - private async sendRawPayloadAsync(payload: Web3.JSONRPCRequestPayload): Promise<any> { + private async sendRawPayloadAsync<A>(payload: Web3.JSONRPCRequestPayload): Promise<A> { const sendAsync = this.web3.currentProvider.sendAsync.bind(this.web3.currentProvider); const response = await promisify(sendAsync)(payload); const result = response.result; @@ -169,4 +183,20 @@ export class Web3Wrapper { return status; } } + private formatLog(rawLog: RawLogEntry): Web3.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; + } } diff --git a/packages/0x.js/test/expiration_watcher_test.ts b/packages/0x.js/test/expiration_watcher_test.ts new file mode 100644 index 000000000..0f2470070 --- /dev/null +++ b/packages/0x.js/test/expiration_watcher_test.ts @@ -0,0 +1,138 @@ +import 'mocha'; +import * as chai from 'chai'; +import * as _ from 'lodash'; +import * as Sinon from 'sinon'; +import * as Web3 from 'web3'; +import BigNumber from 'bignumber.js'; +import {chaiSetup} from './utils/chai_setup'; +import {web3Factory} from './utils/web3_factory'; +import {utils} from '../src/utils/utils'; +import {Web3Wrapper} from '../src/web3_wrapper'; +import {TokenUtils} from './utils/token_utils'; +import {ExpirationWatcher} from '../src/order_watcher/expiration_watcher'; +import {Token, DoneCallback} from '../src/types'; +import {ZeroEx} from '../src/0x'; +import {BlockchainLifecycle} from './utils/blockchain_lifecycle'; +import {FillScenarios} from './utils/fill_scenarios'; +import {reportCallbackErrors} from './utils/report_callback_errors'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(); + +describe('ExpirationWatcher', () => { + let web3: Web3; + let zeroEx: ZeroEx; + let tokenUtils: TokenUtils; + let tokens: Token[]; + let userAddresses: string[]; + let zrxTokenAddress: string; + let fillScenarios: FillScenarios; + let exchangeContractAddress: string; + let makerTokenAddress: string; + let takerTokenAddress: string; + let coinbase: string; + let makerAddress: string; + let takerAddress: string; + let feeRecipient: string; + const fillableAmount = new BigNumber(5); + let currentUnixTimestampSec: BigNumber; + let timer: Sinon.SinonFakeTimers; + let expirationWatcher: ExpirationWatcher; + before(async () => { + web3 = web3Factory.create(); + zeroEx = new ZeroEx(web3.currentProvider); + exchangeContractAddress = await zeroEx.exchange.getContractAddressAsync(); + userAddresses = await zeroEx.getAvailableAddressesAsync(); + tokens = await zeroEx.tokenRegistry.getTokensAsync(); + tokenUtils = new TokenUtils(tokens); + zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address; + fillScenarios = new FillScenarios(zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress); + [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses; + tokens = await zeroEx.tokenRegistry.getTokensAsync(); + const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + const sinonTimerConfig = {shouldAdvanceTime: true} as any; + // This constructor has incorrect types + timer = Sinon.useFakeTimers(sinonTimerConfig); + currentUnixTimestampSec = utils.getCurrentUnixTimestampSec(); + expirationWatcher = new ExpirationWatcher(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + timer.restore(); + expirationWatcher.unsubscribe(); + }); + it('correctly emits events when order expires', (done: DoneCallback) => { + (async () => { + const orderLifetimeSec = 60; + const expirationUnixTimestampSec = currentUnixTimestampSec.plus(orderLifetimeSec); + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + expirationUnixTimestampSec, + ); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + expirationWatcher.addOrder(orderHash, signedOrder.expirationUnixTimestampSec.times(1000)); + const callbackAsync = reportCallbackErrors(done)(async (hash: string) => { + expect(hash).to.be.equal(orderHash); + expect(utils.getCurrentUnixTimestampSec()).to.be.bignumber.gte(expirationUnixTimestampSec); + done(); + }); + expirationWatcher.subscribe(callbackAsync); + timer.tick(orderLifetimeSec * 1000); + })().catch(done); + }); + it('doesn\'t emit events before order expires', (done: DoneCallback) => { + (async () => { + const orderLifetimeSec = 60; + const expirationUnixTimestampSec = currentUnixTimestampSec.plus(orderLifetimeSec); + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + expirationUnixTimestampSec, + ); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + expirationWatcher.addOrder(orderHash, signedOrder.expirationUnixTimestampSec.times(1000)); + const callbackAsync = reportCallbackErrors(done)(async (hash: string) => { + done(new Error('Emitted expiration went before the order actually expired')); + }); + expirationWatcher.subscribe(callbackAsync); + const notEnoughTime = orderLifetimeSec - 1; + timer.tick(notEnoughTime * 1000); + done(); + })().catch(done); + }); + it('emits events in correct order', (done: DoneCallback) => { + (async () => { + const order1Lifetime = 60; + const order2Lifetime = 120; + const order1ExpirationUnixTimestampSec = currentUnixTimestampSec.plus(order1Lifetime); + const order2ExpirationUnixTimestampSec = currentUnixTimestampSec.plus(order2Lifetime); + const signedOrder1 = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + order1ExpirationUnixTimestampSec, + ); + const signedOrder2 = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + order2ExpirationUnixTimestampSec, + ); + const orderHash1 = ZeroEx.getOrderHashHex(signedOrder1); + const orderHash2 = ZeroEx.getOrderHashHex(signedOrder2); + expirationWatcher.addOrder(orderHash2, signedOrder2.expirationUnixTimestampSec.times(1000)); + expirationWatcher.addOrder(orderHash1, signedOrder1.expirationUnixTimestampSec.times(1000)); + const expirationOrder = [orderHash1, orderHash2]; + const callbackAsync = reportCallbackErrors(done)(async (hash: string) => { + const orderHash = expirationOrder.shift(); + expect(hash).to.be.equal(orderHash); + if (_.isEmpty(expirationOrder)) { + done(); + } + }); + expirationWatcher.subscribe(callbackAsync); + timer.tick(order2Lifetime * 1000); + })().catch(done); + }); +}); diff --git a/packages/0x.js/test/order_state_watcher_test.ts b/packages/0x.js/test/order_state_watcher_test.ts index 00b290252..834099ef6 100644 --- a/packages/0x.js/test/order_state_watcher_test.ts +++ b/packages/0x.js/test/order_state_watcher_test.ts @@ -3,10 +3,10 @@ import * as chai from 'chai'; import * as _ from 'lodash'; import * as Web3 from 'web3'; import BigNumber from 'bignumber.js'; -import { chaiSetup } from './utils/chai_setup'; -import { web3Factory } from './utils/web3_factory'; -import { Web3Wrapper } from '../src/web3_wrapper'; -import { OrderStateWatcher } from '../src/order_watcher/order_state_watcher'; +import {chaiSetup} from './utils/chai_setup'; +import {web3Factory} from './utils/web3_factory'; +import {Web3Wrapper} from '../src/web3_wrapper'; +import {OrderStateWatcher} from '../src/order_watcher/order_state_watcher'; import { Token, ZeroEx, @@ -20,11 +20,12 @@ import { OrderStateInvalid, ExchangeContractErrs, } from '../src'; -import { TokenUtils } from './utils/token_utils'; -import { FillScenarios } from './utils/fill_scenarios'; -import { DoneCallback } from '../src/types'; +import {TokenUtils} from './utils/token_utils'; +import {FillScenarios} from './utils/fill_scenarios'; +import {DoneCallback} from '../src/types'; import {BlockchainLifecycle} from './utils/blockchain_lifecycle'; import {reportCallbackErrors} from './utils/report_callback_errors'; +import {constants as constants} from './utils/constants'; const TIMEOUT_MS = 150; @@ -47,7 +48,8 @@ describe('OrderStateWatcher', () => { let taker: string; let web3Wrapper: Web3Wrapper; let signedOrder: SignedOrder; - const fillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(5), 18); + const decimals = constants.ZRX_DECIMALS; + const fillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(5), decimals); before(async () => { web3 = web3Factory.create(); zeroEx = new ZeroEx(web3.currentProvider); @@ -254,15 +256,15 @@ describe('OrderStateWatcher', () => { describe('remainingFillable(M|T)akerTokenAmount', () => { it('should calculate correct remaining fillable', (done: DoneCallback) => { (async () => { - const takerFillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(10), 18); - const makerFillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(20), 18); + const takerFillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(10), decimals); + const makerFillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(20), decimals); signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync( makerToken.address, takerToken.address, maker, taker, makerFillableAmount, takerFillableAmount, ); const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker); - const fillAmountInBaseUnits = ZeroEx.toBaseUnitAmount(new BigNumber(2), 18); + const fillAmountInBaseUnits = ZeroEx.toBaseUnitAmount(new BigNumber(2), decimals); const orderHash = ZeroEx.getOrderHashHex(signedOrder); await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); let eventCount = 0; @@ -273,9 +275,9 @@ describe('OrderStateWatcher', () => { expect(validOrderState.orderHash).to.be.equal(orderHash); const orderRelevantState = validOrderState.orderRelevantState; expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal( - ZeroEx.toBaseUnitAmount(new BigNumber(16), 18)); + ZeroEx.toBaseUnitAmount(new BigNumber(16), decimals)); expect(orderRelevantState.remainingFillableTakerTokenAmount).to.be.bignumber.equal( - ZeroEx.toBaseUnitAmount(new BigNumber(8), 18)); + ZeroEx.toBaseUnitAmount(new BigNumber(8), decimals)); if (eventCount === 2) { done(); } @@ -295,7 +297,7 @@ describe('OrderStateWatcher', () => { const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); - const changedMakerApprovalAmount = ZeroEx.toBaseUnitAmount(new BigNumber(3), 18); + const changedMakerApprovalAmount = ZeroEx.toBaseUnitAmount(new BigNumber(3), decimals); await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); const callback = reportCallbackErrors(done)((orderState: OrderState) => { @@ -319,11 +321,12 @@ describe('OrderStateWatcher', () => { const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); - const remainingAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), 18); + const remainingAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), decimals); const transferAmount = makerBalance.sub(remainingAmount); await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); const callback = reportCallbackErrors(done)((orderState: OrderState) => { + expect(orderState.isValid).to.be.true(); const validOrderState = orderState as OrderStateValid; const orderRelevantState = validOrderState.orderRelevantState; expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal( @@ -337,6 +340,88 @@ describe('OrderStateWatcher', () => { makerToken.address, maker, ZeroEx.NULL_ADDRESS, transferAmount); })().catch(done); }); + it('should equal remaining amount when partially cancelled and order has fees', (done: DoneCallback) => { + (async () => { + const takerFee = ZeroEx.toBaseUnitAmount(new BigNumber(0), decimals); + const makerFee = ZeroEx.toBaseUnitAmount(new BigNumber(5), decimals); + const feeRecipient = taker; + signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( + makerToken.address, takerToken.address, makerFee, takerFee, maker, + taker, fillableAmount, feeRecipient); + + const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); + + const remainingTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(4), decimals); + const transferTokenAmount = makerFee.sub(remainingTokenAmount); + await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); + + const callback = reportCallbackErrors(done)((orderState: OrderState) => { + expect(orderState.isValid).to.be.true(); + const validOrderState = orderState as OrderStateValid; + const orderRelevantState = validOrderState.orderRelevantState; + expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal( + remainingTokenAmount); + done(); + }); + zeroEx.orderStateWatcher.subscribe(callback); + await zeroEx.exchange.cancelOrderAsync(signedOrder, transferTokenAmount); + })().catch(done); + }); + it('should equal ratio amount when fee balance is lowered', (done: DoneCallback) => { + (async () => { + const takerFee = ZeroEx.toBaseUnitAmount(new BigNumber(0), decimals); + const makerFee = ZeroEx.toBaseUnitAmount(new BigNumber(5), decimals); + const feeRecipient = taker; + signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( + makerToken.address, takerToken.address, makerFee, takerFee, maker, + taker, fillableAmount, feeRecipient); + + const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); + + const remainingFeeAmount = ZeroEx.toBaseUnitAmount(new BigNumber(3), decimals); + const transferFeeAmount = makerFee.sub(remainingFeeAmount); + + const remainingTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(4), decimals); + const transferTokenAmount = makerFee.sub(remainingTokenAmount); + await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); + + const callback = reportCallbackErrors(done)((orderState: OrderState) => { + const validOrderState = orderState as OrderStateValid; + const orderRelevantState = validOrderState.orderRelevantState; + expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal( + remainingFeeAmount); + done(); + }); + zeroEx.orderStateWatcher.subscribe(callback); + await zeroEx.token.setProxyAllowanceAsync(zrxTokenAddress, maker, remainingFeeAmount); + await zeroEx.token.transferAsync( + makerToken.address, maker, ZeroEx.NULL_ADDRESS, transferTokenAmount); + })().catch(done); + }); + it('should calculate full amount when all available and non-divisible', (done: DoneCallback) => { + (async () => { + const takerFee = ZeroEx.toBaseUnitAmount(new BigNumber(0), decimals); + const makerFee = ZeroEx.toBaseUnitAmount(new BigNumber(2), decimals); + const feeRecipient = taker; + signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( + makerToken.address, takerToken.address, makerFee, takerFee, maker, + taker, fillableAmount, feeRecipient); + + const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); + await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); + + const callback = reportCallbackErrors(done)((orderState: OrderState) => { + const validOrderState = orderState as OrderStateValid; + const orderRelevantState = validOrderState.orderRelevantState; + expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal( + fillableAmount); + done(); + }); + zeroEx.orderStateWatcher.subscribe(callback); + await zeroEx.token.setProxyAllowanceAsync( + makerToken.address, maker, ZeroEx.toBaseUnitAmount(new BigNumber(100), decimals)); + })().catch(done); + }); }); it('should emit orderStateInvalid when watched order cancelled', (done: DoneCallback) => { (async () => { diff --git a/packages/0x.js/test/remaining_fillable_calculator_test.ts b/packages/0x.js/test/remaining_fillable_calculator_test.ts new file mode 100644 index 000000000..65b65efd8 --- /dev/null +++ b/packages/0x.js/test/remaining_fillable_calculator_test.ts @@ -0,0 +1,176 @@ +import 'mocha'; +import * as chai from 'chai'; +import BigNumber from 'bignumber.js'; +import { chaiSetup } from './utils/chai_setup'; +import { RemainingFillableCalculator } from '../src/order_watcher/remaining_fillable_calculator'; +import { SignedOrder, ECSignature } from '../src/types'; +import { TokenUtils } from './utils/token_utils'; +import { ZeroEx } from '../src/0x'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('RemainingFillableCalculator', () => { + let calculator: RemainingFillableCalculator; + let signedOrder: SignedOrder; + let transferrableMakerTokenAmount: BigNumber; + let transferrableMakerFeeTokenAmount: BigNumber; + let remainingMakerTokenAmount: BigNumber; + let makerAmount: BigNumber; + let takerAmount: BigNumber; + let makerFeeAmount: BigNumber; + let isMakerTokenZRX: boolean; + const makerToken: string = '0x1'; + const takerToken: string = '0x2'; + const decimals: number = 4; + const zero: BigNumber = new BigNumber(0); + const zeroAddress = '0x0'; + const signature: ECSignature = { v: 27, r: '', s: ''}; + beforeEach(async () => { + [makerAmount, takerAmount, makerFeeAmount] = [ZeroEx.toBaseUnitAmount(new BigNumber(50), decimals), + ZeroEx.toBaseUnitAmount(new BigNumber(5), decimals), + ZeroEx.toBaseUnitAmount(new BigNumber(1), decimals)]; + [transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount] = [ + ZeroEx.toBaseUnitAmount(new BigNumber(50), decimals), + ZeroEx.toBaseUnitAmount(new BigNumber(5), decimals)]; + }); + function buildSignedOrder(): SignedOrder { + return { ecSignature: signature, + exchangeContractAddress: zeroAddress, + feeRecipient: zeroAddress, + maker: zeroAddress, + taker: zeroAddress, + makerFee: makerFeeAmount, + takerFee: zero, + makerTokenAmount: makerAmount, + takerTokenAmount: takerAmount, + makerTokenAddress: makerToken, + takerTokenAddress: takerToken, + salt: zero, + expirationUnixTimestampSec: zero }; + } + describe('Maker token is NOT ZRX', () => { + before(async () => { + isMakerTokenZRX = false; + }); + it('calculates the correct amount when unfilled and funds available', () => { + signedOrder = buildSignedOrder(); + remainingMakerTokenAmount = signedOrder.makerTokenAmount; + calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX, + transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, remainingMakerTokenAmount); + expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(remainingMakerTokenAmount); + }); + it('calculates the correct amount when partially filled and funds available', () => { + signedOrder = buildSignedOrder(); + remainingMakerTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), decimals); + calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX, + transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, remainingMakerTokenAmount); + expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(remainingMakerTokenAmount); + }); + it('calculates the amount to be 0 when all fee funds are transferred', () => { + signedOrder = buildSignedOrder(); + transferrableMakerFeeTokenAmount = zero; + remainingMakerTokenAmount = signedOrder.makerTokenAmount; + calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX, + transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, remainingMakerTokenAmount); + expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(zero); + }); + it('calculates the correct amount when balance is less than remaining fillable', () => { + signedOrder = buildSignedOrder(); + const partiallyFilledAmount = ZeroEx.toBaseUnitAmount(new BigNumber(2), decimals); + remainingMakerTokenAmount = signedOrder.makerTokenAmount.minus(partiallyFilledAmount); + transferrableMakerTokenAmount = remainingMakerTokenAmount.minus(partiallyFilledAmount); + calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX, + transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, remainingMakerTokenAmount); + expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(transferrableMakerTokenAmount); + }); + describe('Order to Fee Ratio is < 1', () => { + beforeEach(async () => { + [makerAmount, takerAmount, makerFeeAmount] = [ZeroEx.toBaseUnitAmount(new BigNumber(3), decimals), + ZeroEx.toBaseUnitAmount(new BigNumber(6), decimals), + ZeroEx.toBaseUnitAmount(new BigNumber(6), decimals)]; + }); + it('calculates the correct amount when funds unavailable', () => { + signedOrder = buildSignedOrder(); + remainingMakerTokenAmount = signedOrder.makerTokenAmount; + const transferredAmount = ZeroEx.toBaseUnitAmount(new BigNumber(2), decimals); + transferrableMakerTokenAmount = remainingMakerTokenAmount.minus(transferredAmount); + calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX, + transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, + remainingMakerTokenAmount); + expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(transferrableMakerTokenAmount); + }); + }); + describe('Ratio is not evenly divisble', () => { + beforeEach(async () => { + [makerAmount, takerAmount, makerFeeAmount] = [ZeroEx.toBaseUnitAmount(new BigNumber(3), decimals), + ZeroEx.toBaseUnitAmount(new BigNumber(7), decimals), + ZeroEx.toBaseUnitAmount(new BigNumber(7), decimals)]; + }); + it('calculates the correct amount when funds unavailable', () => { + signedOrder = buildSignedOrder(); + remainingMakerTokenAmount = signedOrder.makerTokenAmount; + const transferredAmount = ZeroEx.toBaseUnitAmount(new BigNumber(2), decimals); + transferrableMakerTokenAmount = remainingMakerTokenAmount.minus(transferredAmount); + calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX, + transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, + remainingMakerTokenAmount); + const calculatedFillableAmount = calculator.computeRemainingMakerFillable(); + expect(calculatedFillableAmount.lessThanOrEqualTo(transferrableMakerTokenAmount)).to.be.true(); + expect(calculatedFillableAmount).to.be.bignumber.greaterThan(new BigNumber(0)); + const orderToFeeRatio = signedOrder.makerTokenAmount.dividedBy(signedOrder.makerFee); + const calculatedFeeAmount = calculatedFillableAmount.dividedBy(orderToFeeRatio); + expect(calculatedFeeAmount).to.be.bignumber.lessThan(transferrableMakerFeeTokenAmount); + }); + }); + }); + describe('Maker Token is ZRX', () => { + before(async () => { + isMakerTokenZRX = true; + }); + it('calculates the correct amount when unfilled and funds available', () => { + signedOrder = buildSignedOrder(); + transferrableMakerTokenAmount = makerAmount.plus(makerFeeAmount); + transferrableMakerFeeTokenAmount = transferrableMakerTokenAmount; + remainingMakerTokenAmount = signedOrder.makerTokenAmount; + calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX, + transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, remainingMakerTokenAmount); + expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(remainingMakerTokenAmount); + }); + it('calculates the correct amount when partially filled and funds available', () => { + signedOrder = buildSignedOrder(); + remainingMakerTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), decimals); + calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX, + transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, remainingMakerTokenAmount); + expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(remainingMakerTokenAmount); + }); + it('calculates the amount to be 0 when all fee funds are transferred', () => { + signedOrder = buildSignedOrder(); + transferrableMakerTokenAmount = zero; + transferrableMakerFeeTokenAmount = zero; + remainingMakerTokenAmount = signedOrder.makerTokenAmount; + calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX, + transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, remainingMakerTokenAmount); + expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(zero); + }); + it('calculates the correct amount when balance is less than remaining fillable', () => { + signedOrder = buildSignedOrder(); + const partiallyFilledAmount = ZeroEx.toBaseUnitAmount(new BigNumber(2), decimals); + remainingMakerTokenAmount = signedOrder.makerTokenAmount.minus(partiallyFilledAmount); + transferrableMakerTokenAmount = remainingMakerTokenAmount.minus(partiallyFilledAmount); + transferrableMakerFeeTokenAmount = transferrableMakerTokenAmount; + + const orderToFeeRatio = signedOrder.makerTokenAmount.dividedToIntegerBy(signedOrder.makerFee); + const expectedFillableAmount = new BigNumber(450980); + calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX, + transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, remainingMakerTokenAmount); + const calculatedFillableAmount = calculator.computeRemainingMakerFillable(); + const numberOfFillsInRatio = calculatedFillableAmount.dividedToIntegerBy(orderToFeeRatio); + const calculatedFillableAmountPlusFees = calculatedFillableAmount.plus(numberOfFillsInRatio); + expect(calculatedFillableAmountPlusFees).to.be.bignumber.lessThan(transferrableMakerTokenAmount); + expect(calculatedFillableAmountPlusFees).to.be.bignumber.lessThan(remainingMakerTokenAmount); + expect(calculatedFillableAmount).to.be.bignumber.equal(expectedFillableAmount); + expect(numberOfFillsInRatio.decimalPlaces()).to.be.equal(0); + }); + }); +}); diff --git a/packages/0x.js/test/token_wrapper_test.ts b/packages/0x.js/test/token_wrapper_test.ts index b30762e8c..1a7cb9e40 100644 --- a/packages/0x.js/test/token_wrapper_test.ts +++ b/packages/0x.js/test/token_wrapper_test.ts @@ -361,6 +361,9 @@ describe('TokenWrapper', () => { (async () => { const callback = (err: Error, logEvent: DecodedLogEvent<TransferContractEventArgs>) => { expect(logEvent).to.not.be.undefined(); + expect(logEvent.logIndex).to.be.equal(0); + expect(logEvent.transactionIndex).to.be.equal(0); + expect(logEvent.blockNumber).to.be.a('number'); const args = logEvent.args; expect(args._from).to.be.equal(coinbase); expect(args._to).to.be.equal(addressWithoutFunds); diff --git a/packages/0x.js/test/utils/constants.ts b/packages/0x.js/test/utils/constants.ts index c7d3aebca..5992c226e 100644 --- a/packages/0x.js/test/utils/constants.ts +++ b/packages/0x.js/test/utils/constants.ts @@ -5,4 +5,5 @@ export const constants = { TESTRPC_NETWORK_ID: 50, KOVAN_RPC_URL: 'https://kovan.infura.io', ROPSTEN_RPC_URL: 'https://ropsten.infura.io', + ZRX_DECIMALS: 18, }; diff --git a/packages/0x.js/test/utils/report_callback_errors.ts b/packages/0x.js/test/utils/report_callback_errors.ts index d471b2af2..4f9517704 100644 --- a/packages/0x.js/test/utils/report_callback_errors.ts +++ b/packages/0x.js/test/utils/report_callback_errors.ts @@ -1,10 +1,10 @@ import { DoneCallback } from '../../src/types'; export const reportCallbackErrors = (done: DoneCallback) => { - return (f: (...args: any[]) => void) => { - const wrapped = (...args: any[]) => { + return (fAsync: (...args: any[]) => void|Promise<void>) => { + const wrapped = async (...args: any[]) => { try { - f(...args); + await fAsync(...args); } catch (err) { done(err); } |