import { tokenUtils } from '@0x/contract-wrappers/lib/test/utils/token_utils'; import { BlockchainLifecycle, callbackErrorReporter } from '@0x/dev-utils'; import { FillScenarios } from '@0x/fill-scenarios'; import { assetDataUtils, orderHashUtils } from '@0x/order-utils'; import { DoneCallback } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as chai from 'chai'; import * as _ from 'lodash'; import 'mocha'; import * as Sinon from 'sinon'; import { ExpirationWatcher } from '../src/order_watcher/expiration_watcher'; import { utils } from '../src/utils/utils'; import { chaiSetup } from './utils/chai_setup'; import { migrateOnceAsync } from './utils/migrate'; import { provider, web3Wrapper } from './utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); const MILISECONDS_IN_SECOND = 1000; describe('ExpirationWatcher', () => { let userAddresses: string[]; let fillScenarios: FillScenarios; let makerAssetData: string; let takerAssetData: 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 () => { const contractAddresses = await migrateOnceAsync(); await blockchainLifecycle.startAsync(); userAddresses = await web3Wrapper.getAvailableAddressesAsync(); fillScenarios = new FillScenarios( provider, userAddresses, contractAddresses.zrxToken, contractAddresses.exchange, contractAddresses.erc20Proxy, contractAddresses.erc721Proxy, ); [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses; const [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses(); [makerAssetData, takerAssetData] = [ assetDataUtils.encodeERC20AssetData(makerTokenAddress), assetDataUtils.encodeERC20AssetData(takerTokenAddress), ]; }); after(async () => { await blockchainLifecycle.revertAsync(); }); 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( makerAssetData, takerAssetData, makerAddress, takerAddress, fillableAmount, expirationUnixTimestampSec, ); const orderHash = orderHashUtils.getOrderHashHex(signedOrder); expirationWatcher.addOrder(orderHash, signedOrder.expirationTimeSeconds.times(MILISECONDS_IN_SECOND)); const callbackAsync = callbackErrorReporter.reportNoErrorCallbackErrors(done)((hash: string) => { expect(hash).to.be.equal(orderHash); expect(utils.getCurrentUnixTimestampSec()).to.be.bignumber.gte(expirationUnixTimestampSec); }); expirationWatcher.subscribe(callbackAsync); timer.tick(orderLifetimeSec * MILISECONDS_IN_SECOND); })().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( makerAssetData, takerAssetData, makerAddress, takerAddress, fillableAmount, expirationUnixTimestampSec, ); const orderHash = orderHashUtils.getOrderHashHex(signedOrder); expirationWatcher.addOrder(orderHash, signedOrder.expirationTimeSeconds.times(MILISECONDS_IN_SECOND)); const callbackAsync = callbackErrorReporter.reportNoErrorCallbackErrors(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 * MILISECONDS_IN_SECOND); 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( makerAssetData, takerAssetData, makerAddress, takerAddress, fillableAmount, order1ExpirationUnixTimestampSec, ); const signedOrder2 = await fillScenarios.createFillableSignedOrderAsync( makerAssetData, takerAssetData, makerAddress, takerAddress, fillableAmount, order2ExpirationUnixTimestampSec, ); const orderHash1 = orderHashUtils.getOrderHashHex(signedOrder1); const orderHash2 = orderHashUtils.getOrderHashHex(signedOrder2); expirationWatcher.addOrder(orderHash2, signedOrder2.expirationTimeSeconds.times(MILISECONDS_IN_SECOND)); expirationWatcher.addOrder(orderHash1, signedOrder1.expirationTimeSeconds.times(MILISECONDS_IN_SECOND)); const expirationOrder = [orderHash1, orderHash2]; const expectToBeCalledOnce = false; const callbackAsync = callbackErrorReporter.reportNoErrorCallbackErrors(done, expectToBeCalledOnce)( (hash: string) => { const orderHash = expirationOrder.shift(); expect(hash).to.be.equal(orderHash); if (_.isEmpty(expirationOrder)) { done(); } }, ); expirationWatcher.subscribe(callbackAsync); timer.tick(order2Lifetime * MILISECONDS_IN_SECOND); })().catch(done); }); it('emits events in correct order when expirations are equal', (done: DoneCallback) => { (async () => { const order1Lifetime = 60; const order2Lifetime = 60; const order1ExpirationUnixTimestampSec = currentUnixTimestampSec.plus(order1Lifetime); const order2ExpirationUnixTimestampSec = currentUnixTimestampSec.plus(order2Lifetime); const signedOrder1 = await fillScenarios.createFillableSignedOrderAsync( makerAssetData, takerAssetData, makerAddress, takerAddress, fillableAmount, order1ExpirationUnixTimestampSec, ); const signedOrder2 = await fillScenarios.createFillableSignedOrderAsync( makerAssetData, takerAssetData, makerAddress, takerAddress, fillableAmount, order2ExpirationUnixTimestampSec, ); const orderHash1 = orderHashUtils.getOrderHashHex(signedOrder1); const orderHash2 = orderHashUtils.getOrderHashHex(signedOrder2); expirationWatcher.addOrder(orderHash1, signedOrder1.expirationTimeSeconds.times(MILISECONDS_IN_SECOND)); expirationWatcher.addOrder(orderHash2, signedOrder2.expirationTimeSeconds.times(MILISECONDS_IN_SECOND)); const expirationOrder = orderHash1 < orderHash2 ? [orderHash1, orderHash2] : [orderHash2, orderHash1]; const expectToBeCalledOnce = false; const callbackAsync = callbackErrorReporter.reportNoErrorCallbackErrors(done, expectToBeCalledOnce)( (hash: string) => { const orderHash = expirationOrder.shift(); expect(hash).to.be.equal(orderHash); if (_.isEmpty(expirationOrder)) { done(); } }, ); expirationWatcher.subscribe(callbackAsync); timer.tick(order2Lifetime * MILISECONDS_IN_SECOND); })().catch(done); }); });