diff options
21 files changed, 450 insertions, 78 deletions
@@ -23,5 +23,6 @@ This repository contains all the 0x developer tools written in TypeScript. Our h |--------|-------|------------| | [`0x.js`](/packages/0x.js) | [![npm](https://img.shields.io/npm/v/0x.js.svg?maxAge=2592000)](https://www.npmjs.com/package/0x.js) | A Javascript library for interacting with the 0x protocol | | [`@0xproject/assert`](/packages/assert) | [![npm](https://img.shields.io/npm/v/@0xproject/assert.svg?maxAge=2592000)](https://www.npmjs.com/package/@0xproject/assert) | Standard type and schema assertions | +| [`@0xproject/connect`](/packages/connect) | [![npm](https://img.shields.io/npm/v/@0xproject/connect.svg?maxAge=2592000)](https://www.npmjs.com/package/@0xproject/connect) | A Javascript library for interacting with the standard relayer api | | [`@0xproject/json-schemas`](/packages/json-schemas) | [![npm](https://img.shields.io/npm/v/@0xproject/json-schemas.svg?maxAge=2592000)](https://www.npmjs.com/package/@0xproject/json-schemas) | 0x-related json schemas | | [`@0xproject/tslint-config`](/packages/tslint-config) | [![npm](https://img.shields.io/npm/v/@0xproject/tslint-config.svg?maxAge=2592000)](https://www.npmjs.com/package/@0xproject/tslint-config) | Custom 0x project TSLint rules | diff --git a/package.json b/package.json index 265e0459e..091ae1069 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "testrpc": "testrpc -p 8545 --networkId 50 -m \"${npm_package_config_mnemonic}\"", "lerna:run": "lerna run", - "lerna:publish": "lerna run clean; lerna run build; lerna publish" + "lerna:publish": "lerna run clean; lerna run build; lerna publish --registry=https://registry.npmjs.org/" }, "config": { "mnemonic": "concert load couple harbor equip island argue ramp clarify fence smart topic" diff --git a/packages/0x.js/CHANGELOG.md b/packages/0x.js/CHANGELOG.md index 235a6eedb..e1f80b00d 100644 --- a/packages/0x.js/CHANGELOG.md +++ b/packages/0x.js/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +vx.x.x +------------------------ + * Make `DecodedLogEvent<A>` contain `LogWithDecodedArgs<A>` under log key instead of merging it in like web3 does (#234) + * Modify order validation methods to validate against the `latest` block, not against the `pending` block (#236) + v0.26.0 ------------------------ * Add post-formatter for logs converting `blockNumber`, `logIndex`, `transactionIndex` from hexes to numbers (#231) diff --git a/packages/0x.js/package.json b/packages/0x.js/package.json index 26f9273db..afca00fe9 100644 --- a/packages/0x.js/package.json +++ b/packages/0x.js/package.json @@ -1,6 +1,6 @@ { "name": "0x.js", - "version": "0.26.0", + "version": "0.26.1", "description": "A javascript library for interacting with the 0x protocol", "keywords": [ "0x.js", diff --git a/packages/0x.js/src/contract_wrappers/contract_wrapper.ts b/packages/0x.js/src/contract_wrappers/contract_wrapper.ts index 7997b1647..c1c95c6db 100644 --- a/packages/0x.js/src/contract_wrappers/contract_wrapper.ts +++ b/packages/0x.js/src/contract_wrappers/contract_wrapper.ts @@ -95,13 +95,13 @@ export class ContractWrapper { await this._web3Wrapper.getContractInstanceFromArtifactAsync<ContractType>(artifact, addressIfExists); return contractInstance; } - private _onLogStateChanged<ArgsType extends ContractEventArgs>(removed: boolean, log: Web3.LogEntry): void { + private _onLogStateChanged<ArgsType extends ContractEventArgs>(isRemoved: boolean, log: Web3.LogEntry): void { _.forEach(this._filters, (filter: Web3.FilterObject, filterToken: string) => { if (filterUtils.matchesFilter(log, filter)) { const decodedLog = this._tryToDecodeLogOrNoop(log) as LogWithDecodedArgs<ArgsType>; const logEvent = { - ...decodedLog, - removed, + log: decodedLog, + isRemoved, }; this._filterCallbacks[filterToken](null, logEvent); } @@ -117,13 +117,13 @@ export class ContractWrapper { this._blockAndLogStreamInterval = intervalUtils.setAsyncExcludingInterval( this._reconcileBlockAsync.bind(this), constants.DEFAULT_BLOCK_POLLING_INTERVAL, ); - let removed = false; + let isRemoved = false; this._onLogAddedSubscriptionToken = this._blockAndLogStreamer.subscribeToOnLogAdded( - this._onLogStateChanged.bind(this, removed), + this._onLogStateChanged.bind(this, isRemoved), ); - removed = true; + isRemoved = true; this._onLogRemovedSubscriptionToken = this._blockAndLogStreamer.subscribeToOnLogRemoved( - this._onLogStateChanged.bind(this, removed), + this._onLogStateChanged.bind(this, isRemoved), ); } private _stopBlockAndLogStream(): void { diff --git a/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts b/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts index 3e631b73e..7c33dc6ec 100644 --- a/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts +++ b/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts @@ -29,6 +29,7 @@ import { EventCallback, ExchangeContractEventArgs, DecodedLogArgs, + BlockParamLiteral, } from '../types'; import {assert} from '../utils/assert'; import {utils} from '../utils/utils'; @@ -178,7 +179,7 @@ export class ExchangeWrapper extends ContractWrapper { orderTransactionOpts.shouldValidate; if (shouldValidate) { const zrxTokenAddress = await this.getZRXTokenAddressAsync(); - const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync( exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress); } @@ -250,7 +251,7 @@ export class ExchangeWrapper extends ContractWrapper { orderTransactionOpts.shouldValidate; if (shouldValidate) { const zrxTokenAddress = await this.getZRXTokenAddressAsync(); - const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); for (const signedOrder of signedOrders) { await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync( exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress); @@ -340,7 +341,7 @@ export class ExchangeWrapper extends ContractWrapper { orderTransactionOpts.shouldValidate; if (shouldValidate) { const zrxTokenAddress = await this.getZRXTokenAddressAsync(); - const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); for (const orderFillRequest of orderFillRequests) { await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync( exchangeTradeEmulator, orderFillRequest.signedOrder, orderFillRequest.takerTokenFillAmount, @@ -420,7 +421,7 @@ export class ExchangeWrapper extends ContractWrapper { orderTransactionOpts.shouldValidate; if (shouldValidate) { const zrxTokenAddress = await this.getZRXTokenAddressAsync(); - const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync( exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress); } @@ -484,7 +485,7 @@ export class ExchangeWrapper extends ContractWrapper { orderTransactionOpts.shouldValidate; if (shouldValidate) { const zrxTokenAddress = await this.getZRXTokenAddressAsync(); - const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); for (const orderFillRequest of orderFillRequests) { await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync( exchangeTradeEmulator, orderFillRequest.signedOrder, orderFillRequest.takerTokenFillAmount, @@ -721,7 +722,7 @@ export class ExchangeWrapper extends ContractWrapper { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); const zrxTokenAddress = await this.getZRXTokenAddressAsync(); const expectedFillTakerTokenAmount = !_.isUndefined(opts) ? opts.expectedFillTakerTokenAmount : undefined; - const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); await this._orderValidationUtils.validateOrderFillableOrThrowAsync( exchangeTradeEmulator, signedOrder, zrxTokenAddress, expectedFillTakerTokenAmount, ); @@ -741,7 +742,7 @@ export class ExchangeWrapper extends ContractWrapper { assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const zrxTokenAddress = await this.getZRXTokenAddressAsync(); - const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync( exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress); } @@ -775,7 +776,7 @@ export class ExchangeWrapper extends ContractWrapper { assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const zrxTokenAddress = await this.getZRXTokenAddressAsync(); - const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync( exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress); } 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 fd7496699..d02e31160 100644 --- a/packages/0x.js/src/order_watcher/order_state_watcher.ts +++ b/packages/0x.js/src/order_watcher/order_state_watcher.ts @@ -44,6 +44,10 @@ interface OrderByOrderHash { [orderHash: string]: SignedOrder; } +interface OrderStateByOrderHash { + [orderHash: string]: OrderState; +} + /** * This class includes all the functionality related to watching a set of orders * for potential changes in order validity/fillability. The orderWatcher notifies @@ -51,6 +55,7 @@ interface OrderByOrderHash { * the order should be deemed invalid. */ export class OrderStateWatcher { + private _orderStateByOrderHashCache: OrderStateByOrderHash = {}; private _orderByOrderHash: OrderByOrderHash = {}; private _dependentOrderHashes: DependentOrderHashes = {}; private _callbackIfExists?: OnOrderStateChangeCallback; @@ -69,7 +74,9 @@ export class OrderStateWatcher { this._web3Wrapper = web3Wrapper; const pollingIntervalIfExistsMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs; this._eventWatcher = new EventWatcher(web3Wrapper, pollingIntervalIfExistsMs); - this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore(token); + this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore( + token, BlockParamLiteral.Pending, + ); this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore(exchange); this._orderStateUtils = new OrderStateUtils( this._balanceAndProxyAllowanceLazyStore, this._orderFilledCancelledLazyStore, @@ -109,6 +116,7 @@ export class OrderStateWatcher { return; // noop } delete this._orderByOrderHash[orderHash]; + delete this._orderStateByOrderHashCache[orderHash]; const exchange = (this._orderFilledCancelledLazyStore as any).exchange as ExchangeWrapper; const zrxTokenAddress = await exchange.getZRXTokenAddressAsync(); this.removeFromDependentOrderHashes(signedOrder.maker, zrxTokenAddress, orderHash); @@ -239,6 +247,12 @@ export class OrderStateWatcher { if (_.isUndefined(this._callbackIfExists)) { break; // Unsubscribe was called } + if (_.isEqual(orderState, this._orderStateByOrderHashCache[orderHash])) { + // Actual order state didn't change + continue; + } else { + this._orderStateByOrderHashCache[orderHash] = orderState; + } this._callbackIfExists(orderState); } } 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/stores/balance_proxy_allowance_lazy_store.ts b/packages/0x.js/src/stores/balance_proxy_allowance_lazy_store.ts index c83e61606..7c94031c3 100644 --- a/packages/0x.js/src/stores/balance_proxy_allowance_lazy_store.ts +++ b/packages/0x.js/src/stores/balance_proxy_allowance_lazy_store.ts @@ -9,6 +9,7 @@ import {BlockParamLiteral} from '../types'; */ export class BalanceAndProxyAllowanceLazyStore { private token: TokenWrapper; + private defaultBlock: BlockParamLiteral; private balance: { [tokenAddress: string]: { [userAddress: string]: BigNumber, @@ -19,15 +20,16 @@ export class BalanceAndProxyAllowanceLazyStore { [userAddress: string]: BigNumber, }, }; - constructor(token: TokenWrapper) { + constructor(token: TokenWrapper, defaultBlock: BlockParamLiteral) { this.token = token; + this.defaultBlock = defaultBlock; this.balance = {}; this.proxyAllowance = {}; } public async getBalanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> { if (_.isUndefined(this.balance[tokenAddress]) || _.isUndefined(this.balance[tokenAddress][userAddress])) { const methodOpts = { - defaultBlock: BlockParamLiteral.Pending, + defaultBlock: this.defaultBlock, }; const balance = await this.token.getBalanceAsync(tokenAddress, userAddress, methodOpts); this.setBalance(tokenAddress, userAddress, balance); @@ -53,7 +55,7 @@ export class BalanceAndProxyAllowanceLazyStore { if (_.isUndefined(this.proxyAllowance[tokenAddress]) || _.isUndefined(this.proxyAllowance[tokenAddress][userAddress])) { const methodOpts = { - defaultBlock: BlockParamLiteral.Pending, + defaultBlock: this.defaultBlock, }; const proxyAllowance = await this.token.getProxyAllowanceAsync(tokenAddress, userAddress, methodOpts); this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance); diff --git a/packages/0x.js/src/types.ts b/packages/0x.js/src/types.ts index c3aabfd86..143e2b6b2 100644 --- a/packages/0x.js/src/types.ts +++ b/packages/0x.js/src/types.ts @@ -40,7 +40,10 @@ export type OrderValues = [BigNumber, BigNumber, BigNumber, BigNumber, BigNumber, BigNumber]; export type LogEvent = Web3.LogEntryEvent; -export type DecodedLogEvent<ArgsType> = Web3.DecodedLogEntryEvent<ArgsType>; +export interface DecodedLogEvent<ArgsType> { + isRemoved: boolean; + log: LogWithDecodedArgs<ArgsType>; +} export type EventCallback<ArgsType> = (err: null|Error, log?: DecodedLogEvent<ArgsType>) => void; export type EventWatcherCallback = (log: LogEvent) => void; @@ -348,9 +351,11 @@ export interface IndexedFilterValues { [index: string]: ContractEventArg; } +// Earliest is omitted by design. It is simply an alias for the `0` constant and +// is thus not very helpful. Moreover, this type is used in places that only accept +// `latest` or `pending`. export enum BlockParamLiteral { Latest = 'latest', - Earliest = 'earliest', Pending = 'pending', } diff --git a/packages/0x.js/src/utils/exchange_transfer_simulator.ts b/packages/0x.js/src/utils/exchange_transfer_simulator.ts index 308ef06db..eeb6081cb 100644 --- a/packages/0x.js/src/utils/exchange_transfer_simulator.ts +++ b/packages/0x.js/src/utils/exchange_transfer_simulator.ts @@ -35,8 +35,8 @@ const ERR_MSG_MAPPING = { export class ExchangeTransferSimulator { private store: BalanceAndProxyAllowanceLazyStore; private UNLIMITED_ALLOWANCE_IN_BASE_UNITS: BigNumber; - constructor(token: TokenWrapper) { - this.store = new BalanceAndProxyAllowanceLazyStore(token); + constructor(token: TokenWrapper, defaultBlock: BlockParamLiteral) { + this.store = new BalanceAndProxyAllowanceLazyStore(token, defaultBlock); this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS = token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; } /** 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/test/exchange_transfer_simulator_test.ts b/packages/0x.js/test/exchange_transfer_simulator_test.ts index 99cb7fb4f..43a6404a1 100644 --- a/packages/0x.js/test/exchange_transfer_simulator_test.ts +++ b/packages/0x.js/test/exchange_transfer_simulator_test.ts @@ -3,7 +3,7 @@ import BigNumber from 'bignumber.js'; import {chaiSetup} from './utils/chai_setup'; import {web3Factory} from './utils/web3_factory'; import {ZeroEx, ExchangeContractErrs, Token} from '../src'; -import {TradeSide, TransferType} from '../src/types'; +import {TradeSide, TransferType, BlockParamLiteral} from '../src/types'; import {BlockchainLifecycle} from './utils/blockchain_lifecycle'; import {ExchangeTransferSimulator} from '../src/utils/exchange_transfer_simulator'; @@ -37,7 +37,7 @@ describe('ExchangeTransferSimulator', () => { }); describe('#transferFromAsync', () => { beforeEach(() => { - exchangeTransferSimulator = new ExchangeTransferSimulator(zeroEx.token); + exchangeTransferSimulator = new ExchangeTransferSimulator(zeroEx.token, BlockParamLiteral.Latest); }); it('throws if the user doesn\'t have enough allowance', async () => { return expect(exchangeTransferSimulator.transferFromAsync( diff --git a/packages/0x.js/test/exchange_wrapper_test.ts b/packages/0x.js/test/exchange_wrapper_test.ts index add89a3b2..13ac4c1d3 100644 --- a/packages/0x.js/test/exchange_wrapper_test.ts +++ b/packages/0x.js/test/exchange_wrapper_test.ts @@ -649,7 +649,7 @@ describe('ExchangeWrapper', () => { (async () => { const callback = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => { - expect(logEvent.event).to.be.equal(ExchangeEvents.LogFill); + expect(logEvent.log.event).to.be.equal(ExchangeEvents.LogFill); done(); }; await zeroEx.exchange.subscribeAsync( @@ -665,7 +665,7 @@ describe('ExchangeWrapper', () => { (async () => { const callback = (err: Error, logEvent: DecodedLogEvent<LogCancelContractEventArgs>) => { - expect(logEvent.event).to.be.equal(ExchangeEvents.LogCancel); + expect(logEvent.log.event).to.be.equal(ExchangeEvents.LogCancel); done(); }; await zeroEx.exchange.subscribeAsync( @@ -688,7 +688,7 @@ describe('ExchangeWrapper', () => { await zeroEx.setProviderAsync(newProvider); const callback = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => { - expect(logEvent.event).to.be.equal(ExchangeEvents.LogFill); + expect(logEvent.log.event).to.be.equal(ExchangeEvents.LogFill); done(); }; await zeroEx.exchange.subscribeAsync( @@ -754,7 +754,7 @@ describe('ExchangeWrapper', () => { const fillableAmount = new BigNumber(5); const shouldThrowOnInsufficientBalanceOrAllowance = true; const subscriptionOpts: SubscriptionOpts = { - fromBlock: BlockParamLiteral.Earliest, + fromBlock: 0, toBlock: BlockParamLiteral.Latest, }; let txHash: string; diff --git a/packages/0x.js/test/order_state_watcher_test.ts b/packages/0x.js/test/order_state_watcher_test.ts index 00b290252..e635071b8 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); @@ -178,16 +180,12 @@ describe('OrderStateWatcher', () => { const orderHash = ZeroEx.getOrderHashHex(signedOrder); await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); - let eventCount = 0; const callback = reportCallbackErrors(done)((orderState: OrderState) => { - eventCount++; expect(orderState.isValid).to.be.false(); const invalidOrderState = orderState as OrderStateInvalid; expect(invalidOrderState.orderHash).to.be.equal(orderHash); expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderRemainingFillAmountZero); - if (eventCount === 2) { - done(); - } + done(); }); zeroEx.orderStateWatcher.subscribe(callback); @@ -210,9 +208,7 @@ describe('OrderStateWatcher', () => { const orderHash = ZeroEx.getOrderHashHex(signedOrder); await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); - let eventCount = 0; const callback = reportCallbackErrors(done)((orderState: OrderState) => { - eventCount++; expect(orderState.isValid).to.be.true(); const validOrderState = orderState as OrderStateValid; expect(validOrderState.orderHash).to.be.equal(orderHash); @@ -224,9 +220,7 @@ describe('OrderStateWatcher', () => { expect(orderRelevantState.remainingFillableTakerTokenAmount).to.be.bignumber.equal( remainingFillable); expect(orderRelevantState.makerBalance).to.be.bignumber.equal(remainingMakerBalance); - if (eventCount === 2) { - done(); - } + done(); }); zeroEx.orderStateWatcher.subscribe(callback); const shouldThrowOnInsufficientBalanceOrAllowance = true; @@ -254,31 +248,27 @@ 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; const callback = reportCallbackErrors(done)((orderState: OrderState) => { - eventCount++; expect(orderState.isValid).to.be.true(); const validOrderState = orderState as OrderStateValid; 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)); - if (eventCount === 2) { - done(); - } + ZeroEx.toBaseUnitAmount(new BigNumber(8), decimals)); + done(); }); zeroEx.orderStateWatcher.subscribe(callback); const shouldThrowOnInsufficientBalanceOrAllowance = true; @@ -295,7 +285,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 +309,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 +328,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/order_validation_test.ts b/packages/0x.js/test/order_validation_test.ts index 4f18742d3..d689ef073 100644 --- a/packages/0x.js/test/order_validation_test.ts +++ b/packages/0x.js/test/order_validation_test.ts @@ -5,7 +5,7 @@ import * as Sinon from 'sinon'; import {chaiSetup} from './utils/chai_setup'; import {web3Factory} from './utils/web3_factory'; import {ZeroEx, SignedOrder, Token, ExchangeContractErrs, ZeroExError} from '../src'; -import {TradeSide, TransferType} from '../src/types'; +import {TradeSide, TransferType, BlockParamLiteral} from '../src/types'; import {TokenUtils} from './utils/token_utils'; import {BlockchainLifecycle} from './utils/blockchain_lifecycle'; import {FillScenarios} from './utils/fill_scenarios'; @@ -215,7 +215,7 @@ describe('OrderValidation', () => { return Sinon.match((value: BigNumber) => value.eq(expected)); }; beforeEach('create exchangeTransferSimulator', async () => { - exchangeTransferSimulator = new ExchangeTransferSimulator(zeroEx.token); + exchangeTransferSimulator = new ExchangeTransferSimulator(zeroEx.token, BlockParamLiteral.Latest); transferFromAsync = Sinon.spy(); exchangeTransferSimulator.transferFromAsync = transferFromAsync as any; }); 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 1a7cb9e40..b3c8edcc4 100644 --- a/packages/0x.js/test/token_wrapper_test.ts +++ b/packages/0x.js/test/token_wrapper_test.ts @@ -361,10 +361,11 @@ 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(logEvent.isRemoved).to.be.false(); + expect(logEvent.log.logIndex).to.be.equal(0); + expect(logEvent.log.transactionIndex).to.be.equal(0); + expect(logEvent.log.blockNumber).to.be.a('number'); + const args = logEvent.log.args; expect(args._from).to.be.equal(coinbase); expect(args._to).to.be.equal(addressWithoutFunds); expect(args._value).to.be.bignumber.equal(transferAmount); @@ -379,7 +380,8 @@ describe('TokenWrapper', () => { (async () => { const callback = (err: Error, logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => { expect(logEvent).to.not.be.undefined(); - const args = logEvent.args; + expect(logEvent.isRemoved).to.be.false(); + const args = logEvent.log.args; expect(args._owner).to.be.equal(coinbase); expect(args._spender).to.be.equal(addressWithoutFunds); expect(args._value).to.be.bignumber.equal(allowanceAmount); @@ -426,7 +428,7 @@ describe('TokenWrapper', () => { let tokenAddress: string; let tokenTransferProxyAddress: string; const subscriptionOpts: SubscriptionOpts = { - fromBlock: BlockParamLiteral.Earliest, + fromBlock: 0, toBlock: BlockParamLiteral.Latest, }; let txHash: string; 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/connect/CHANGELOG.md b/packages/connect/CHANGELOG.md index a6e30ed4a..ec6727a12 100644 --- a/packages/connect/CHANGELOG.md +++ b/packages/connect/CHANGELOG.md @@ -1,5 +1,5 @@ # CHANGELOG -v0.0.2 - _November 22, 2017_ +v0.1.0 - _November 22, 2017_ ------------------------ * Provide a HttpClient class for interacting with standard relayer api compliant HTTP urls diff --git a/packages/connect/package.json b/packages/connect/package.json index 180a79eea..9131eef14 100644 --- a/packages/connect/package.json +++ b/packages/connect/package.json @@ -1,6 +1,6 @@ { "name": "@0xproject/connect", - "version": "0.0.1", + "version": "0.1.0", "description": "A javascript library for interacting with the standard relayer api", "keywords": [ "connect", @@ -36,7 +36,7 @@ }, "homepage": "https://github.com/0xProject/0x.js/packages/connect/README.md", "dependencies": { - "0x.js": "^0.26.0", + "0x.js": "^0.26.1", "@0xproject/assert": "^0.0.5", "@0xproject/json-schemas": "^0.6.8", "bignumber.js": "~4.1.0", |