diff options
Diffstat (limited to 'packages/contract-wrappers')
8 files changed, 1371 insertions, 8 deletions
diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index 33d9bc4c0..e34b50c1a 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -1,5 +1,13 @@ [ { + "version": "0.0.6", + "changes": [ + { + "note": "Update blockstream to v5.0 and propogate up caught errors to active subscriptions" + } + ] + }, + { "timestamp": 1529397769, "version": "0.0.5", "changes": [ diff --git a/packages/contract-wrappers/package.json b/packages/contract-wrappers/package.json index d6c4fc285..075cd2fe1 100644 --- a/packages/contract-wrappers/package.json +++ b/packages/contract-wrappers/package.json @@ -71,7 +71,7 @@ "source-map-support": "^0.5.0", "tslint": "5.8.0", "typescript": "2.7.1", - "web3-provider-engine": "^14.0.4" + "web3-provider-engine": "14.0.6" }, "dependencies": { "@0xproject/assert": "^0.3.0", @@ -83,8 +83,8 @@ "@0xproject/fill-scenarios": "^1.0.0", "@0xproject/json-schemas": "^1.0.0", "@0xproject/types": "^1.0.0", - "ethereum-types": "^0.0.1", - "ethereumjs-blockstream": "^2.0.6", + "ethereum-types": "^0.0.2", + "ethereumjs-blockstream": "5.0.0", "ethereumjs-util": "^5.1.1", "ethers": "3.0.22", "js-sha3": "^0.7.0", diff --git a/packages/contract-wrappers/src/contract_wrappers/contract_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/contract_wrapper.ts index a88745485..7895a42ee 100644 --- a/packages/contract-wrappers/src/contract_wrappers/contract_wrapper.ts +++ b/packages/contract-wrappers/src/contract_wrappers/contract_wrapper.ts @@ -2,7 +2,7 @@ import { ContractArtifact } from '@0xproject/sol-compiler'; import { AbiDecoder, intervalUtils } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import { BlockParamLiteral, ContractAbi, FilterObject, LogEntry, LogWithDecodedArgs, RawLog } from 'ethereum-types'; -import { Block, BlockAndLogStreamer } from 'ethereumjs-blockstream'; +import { Block, BlockAndLogStreamer, Log } from 'ethereumjs-blockstream'; import * as _ from 'lodash'; import { @@ -33,7 +33,7 @@ export abstract class ContractWrapper { public abstract abi: ContractAbi; protected _web3Wrapper: Web3Wrapper; protected _networkId: number; - private _blockAndLogStreamerIfExists?: BlockAndLogStreamer; + private _blockAndLogStreamerIfExists: BlockAndLogStreamer<Block, Log> | undefined; private _blockAndLogStreamIntervalIfExists?: NodeJS.Timer; private _filters: { [filterToken: string]: FilterObject }; private _filterCallbacks: { @@ -155,6 +155,7 @@ export abstract class ContractWrapper { this._blockAndLogStreamerIfExists = new BlockAndLogStreamer( this._web3Wrapper.getBlockAsync.bind(this._web3Wrapper), this._web3Wrapper.getLogsAsync.bind(this._web3Wrapper), + this._onBlockAndLogStreamerError.bind(this), ); const catchAllLogFilter = {}; this._blockAndLogStreamerIfExists.addLogFilter(catchAllLogFilter); @@ -172,6 +173,14 @@ export abstract class ContractWrapper { this._onLogStateChanged.bind(this, isRemoved), ); } + private _onBlockAndLogStreamerError(err: Error): void { + // Propogate all Blockstream subscriber errors to all + // top-level subscriptions + const filterCallbacks = _.values(this._filterCallbacks); + _.each(filterCallbacks, filterCallback => { + filterCallback(err); + }); + } private _onReconcileBlockError(err: Error): void { const filterTokens = _.keys(this._filterCallbacks); _.each(filterTokens, filterToken => { diff --git a/packages/contract-wrappers/src/utils/exchange_transfer_simulator.ts b/packages/contract-wrappers/src/utils/exchange_transfer_simulator.ts new file mode 100644 index 000000000..279f2a796 --- /dev/null +++ b/packages/contract-wrappers/src/utils/exchange_transfer_simulator.ts @@ -0,0 +1,111 @@ +import { ExchangeContractErrs } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; + +import { AbstractBalanceAndProxyAllowanceLazyStore } from '../abstract/abstract_balance_and_proxy_allowance_lazy_store'; +import { TradeSide, TransferType } from '../types'; +import { constants } from '../utils/constants'; + +enum FailureReason { + Balance = 'balance', + ProxyAllowance = 'proxyAllowance', +} + +const ERR_MSG_MAPPING = { + [FailureReason.Balance]: { + [TradeSide.Maker]: { + [TransferType.Trade]: ExchangeContractErrs.InsufficientMakerBalance, + [TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeBalance, + }, + [TradeSide.Taker]: { + [TransferType.Trade]: ExchangeContractErrs.InsufficientTakerBalance, + [TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeBalance, + }, + }, + [FailureReason.ProxyAllowance]: { + [TradeSide.Maker]: { + [TransferType.Trade]: ExchangeContractErrs.InsufficientMakerAllowance, + [TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeAllowance, + }, + [TradeSide.Taker]: { + [TransferType.Trade]: ExchangeContractErrs.InsufficientTakerAllowance, + [TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeAllowance, + }, + }, +}; + +export class ExchangeTransferSimulator { + private _store: AbstractBalanceAndProxyAllowanceLazyStore; + private static _throwValidationError( + failureReason: FailureReason, + tradeSide: TradeSide, + transferType: TransferType, + ): never { + const errMsg = ERR_MSG_MAPPING[failureReason][tradeSide][transferType]; + throw new Error(errMsg); + } + constructor(store: AbstractBalanceAndProxyAllowanceLazyStore) { + this._store = store; + } + /** + * Simulates transferFrom call performed by a proxy + * @param tokenAddress Address of the token to be transferred + * @param from Owner of the transferred tokens + * @param to Recipient of the transferred tokens + * @param amountInBaseUnits The amount of tokens being transferred + * @param tradeSide Is Maker/Taker transferring + * @param transferType Is it a fee payment or a value transfer + */ + public async transferFromAsync( + tokenAddress: string, + from: string, + to: string, + amountInBaseUnits: BigNumber, + tradeSide: TradeSide, + transferType: TransferType, + ): Promise<void> { + // HACK: When simulating an open order (e.g taker is NULL_ADDRESS), we don't want to adjust balances/ + // allowances for the taker. We do however, want to increase the balance of the maker since the maker + // might be relying on those funds to fill subsequent orders or pay the order's fees. + if (from === constants.NULL_ADDRESS && tradeSide === TradeSide.Taker) { + await this._increaseBalanceAsync(tokenAddress, to, amountInBaseUnits); + return; + } + const balance = await this._store.getBalanceAsync(tokenAddress, from); + const proxyAllowance = await this._store.getProxyAllowanceAsync(tokenAddress, from); + if (proxyAllowance.lessThan(amountInBaseUnits)) { + ExchangeTransferSimulator._throwValidationError(FailureReason.ProxyAllowance, tradeSide, transferType); + } + if (balance.lessThan(amountInBaseUnits)) { + ExchangeTransferSimulator._throwValidationError(FailureReason.Balance, tradeSide, transferType); + } + await this._decreaseProxyAllowanceAsync(tokenAddress, from, amountInBaseUnits); + await this._decreaseBalanceAsync(tokenAddress, from, amountInBaseUnits); + await this._increaseBalanceAsync(tokenAddress, to, amountInBaseUnits); + } + private async _decreaseProxyAllowanceAsync( + tokenAddress: string, + userAddress: string, + amountInBaseUnits: BigNumber, + ): Promise<void> { + const proxyAllowance = await this._store.getProxyAllowanceAsync(tokenAddress, userAddress); + if (!proxyAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) { + this._store.setProxyAllowance(tokenAddress, userAddress, proxyAllowance.minus(amountInBaseUnits)); + } + } + private async _increaseBalanceAsync( + tokenAddress: string, + userAddress: string, + amountInBaseUnits: BigNumber, + ): Promise<void> { + const balance = await this._store.getBalanceAsync(tokenAddress, userAddress); + this._store.setBalance(tokenAddress, userAddress, balance.plus(amountInBaseUnits)); + } + private async _decreaseBalanceAsync( + tokenAddress: string, + userAddress: string, + amountInBaseUnits: BigNumber, + ): Promise<void> { + const balance = await this._store.getBalanceAsync(tokenAddress, userAddress); + this._store.setBalance(tokenAddress, userAddress, balance.minus(amountInBaseUnits)); + } +} diff --git a/packages/contract-wrappers/test/erc20_wrapper_test.ts b/packages/contract-wrappers/test/erc20_wrapper_test.ts index dd15a9b82..ae47a11ad 100644 --- a/packages/contract-wrappers/test/erc20_wrapper_test.ts +++ b/packages/contract-wrappers/test/erc20_wrapper_test.ts @@ -528,7 +528,11 @@ describe('ERC20Wrapper', () => { it('Outstanding subscriptions are cancelled when contractWrappers.setProvider called', (done: DoneCallback) => { (async () => { const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( +<<<<<<< HEAD:packages/contract-wrappers/test/erc20_wrapper_test.ts (logEvent: DecodedLogEvent<ERC20TokenApprovalEventArgs>) => { +======= + (_logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => { +>>>>>>> v2-prototype:packages/contract-wrappers/test/token_wrapper_test.ts done(new Error('Expected this subscription to have been cancelled')); }, ); @@ -557,7 +561,11 @@ describe('ERC20Wrapper', () => { it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => { (async () => { const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( +<<<<<<< HEAD:packages/contract-wrappers/test/erc20_wrapper_test.ts (logEvent: DecodedLogEvent<ERC20TokenApprovalEventArgs>) => { +======= + (_logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => { +>>>>>>> v2-prototype:packages/contract-wrappers/test/token_wrapper_test.ts done(new Error('Expected this subscription to have been cancelled')); }, ); diff --git a/packages/contract-wrappers/test/ether_token_wrapper_test.ts b/packages/contract-wrappers/test/ether_token_wrapper_test.ts index 373d4e935..0a860884a 100644 --- a/packages/contract-wrappers/test/ether_token_wrapper_test.ts +++ b/packages/contract-wrappers/test/ether_token_wrapper_test.ts @@ -281,7 +281,7 @@ describe('EtherTokenWrapper', () => { it('should cancel outstanding subscriptions when ZeroEx.setProvider is called', (done: DoneCallback) => { (async () => { const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( - (logEvent: DecodedLogEvent<WETH9ApprovalEventArgs>) => { + (_logEvent: DecodedLogEvent<WETH9ApprovalEventArgs>) => { done(new Error('Expected this subscription to have been cancelled')); }, ); @@ -311,7 +311,7 @@ describe('EtherTokenWrapper', () => { it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => { (async () => { const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( - (logEvent: DecodedLogEvent<WETH9ApprovalEventArgs>) => { + (_logEvent: DecodedLogEvent<WETH9ApprovalEventArgs>) => { done(new Error('Expected this subscription to have been cancelled')); }, ); diff --git a/packages/contract-wrappers/test/exchange_wrapper_test.ts b/packages/contract-wrappers/test/exchange_wrapper_test.ts new file mode 100644 index 000000000..c945b4c70 --- /dev/null +++ b/packages/contract-wrappers/test/exchange_wrapper_test.ts @@ -0,0 +1,1227 @@ +import { BlockchainLifecycle, callbackErrorReporter } from '@0xproject/dev-utils'; +import { FillScenarios } from '@0xproject/fill-scenarios'; +import { getOrderHashHex } from '@0xproject/order-utils'; +import { BlockParamLiteral, DoneCallback, OrderState } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; +import 'mocha'; + +import { + BlockRange, + ContractWrappers, + DecodedLogEvent, + ExchangeContractErrs, + ExchangeEvents, + LogCancelContractEventArgs, + LogFillContractEventArgs, + OrderCancellationRequest, + OrderFillRequest, + SignedOrder, + Token, +} from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { constants } from './utils/constants'; +import { TokenUtils } from './utils/token_utils'; +import { provider, web3Wrapper } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +const NON_EXISTENT_ORDER_HASH = '0x79370342234e7acd6bbeac335bd3bb1d368383294b64b8160a00f4060e4d3777'; + +describe('ExchangeWrapper', () => { + let contractWrappers: ContractWrappers; + let tokenUtils: TokenUtils; + let tokens: Token[]; + let userAddresses: string[]; + let zrxTokenAddress: string; + let fillScenarios: FillScenarios; + let exchangeContractAddress: string; + const config = { + networkId: constants.TESTRPC_NETWORK_ID, + }; + before(async () => { + contractWrappers = new ContractWrappers(provider, config); + exchangeContractAddress = contractWrappers.exchange.getContractAddress(); + userAddresses = await web3Wrapper.getAvailableAddressesAsync(); + tokens = await contractWrappers.tokenRegistry.getTokensAsync(); + tokenUtils = new TokenUtils(tokens); + zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address; + fillScenarios = new FillScenarios(provider, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress); + await fillScenarios.initTokenBalancesAsync(); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('fillOrKill order(s)', () => { + let makerTokenAddress: string; + let takerTokenAddress: string; + let coinbase: string; + let makerAddress: string; + let takerAddress: string; + let feeRecipient: string; + const takerTokenFillAmount = new BigNumber(5); + before(async () => { + [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses; + tokens = await contractWrappers.tokenRegistry.getTokensAsync(); + const [makerToken, takerToken] = tokenUtils.getDummyTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + }); + describe('#batchFillOrKillAsync', () => { + it('successfully batch fillOrKill', async () => { + const fillableAmount = new BigNumber(5); + const partialFillTakerAmount = new BigNumber(2); + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + const anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + const orderFillRequests = [ + { + signedOrder, + takerTokenFillAmount: partialFillTakerAmount, + }, + { + signedOrder: anotherSignedOrder, + takerTokenFillAmount: partialFillTakerAmount, + }, + ]; + await contractWrappers.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress); + }); + describe('order transaction options', () => { + let signedOrder: SignedOrder; + let orderFillRequests: OrderFillRequest[]; + const fillableAmount = new BigNumber(5); + beforeEach(async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + orderFillRequests = [ + { + signedOrder, + takerTokenFillAmount: new BigNumber(0), + }, + ]; + }); + it('should validate when orderTransactionOptions are not present', async () => { + return expect( + contractWrappers.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect( + contractWrappers.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress, { + shouldValidate: true, + }), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect( + contractWrappers.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress, { + shouldValidate: false, + }), + ).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + }); + }); + describe('#fillOrKillOrderAsync', () => { + let signedOrder: SignedOrder; + const fillableAmount = new BigNumber(5); + beforeEach(async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + }); + describe('successful fills', () => { + it('should fill a valid order', async () => { + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress), + ).to.be.bignumber.equal(fillableAmount); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress), + ).to.be.bignumber.equal(0); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress), + ).to.be.bignumber.equal(0); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, takerAddress), + ).to.be.bignumber.equal(fillableAmount); + await contractWrappers.exchange.fillOrKillOrderAsync( + signedOrder, + takerTokenFillAmount, + takerAddress, + ); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress), + ).to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount)); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress), + ).to.be.bignumber.equal(takerTokenFillAmount); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress), + ).to.be.bignumber.equal(takerTokenFillAmount); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, takerAddress), + ).to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount)); + }); + it('should partially fill a valid order', async () => { + const partialFillAmount = new BigNumber(3); + await contractWrappers.exchange.fillOrKillOrderAsync(signedOrder, partialFillAmount, takerAddress); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress), + ).to.be.bignumber.equal(fillableAmount.minus(partialFillAmount)); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress), + ).to.be.bignumber.equal(partialFillAmount); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress), + ).to.be.bignumber.equal(partialFillAmount); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, takerAddress), + ).to.be.bignumber.equal(fillableAmount.minus(partialFillAmount)); + }); + }); + describe('order transaction options', () => { + const emptyFillableAmount = new BigNumber(0); + it('should validate when orderTransactionOptions are not present', async () => { + return expect( + contractWrappers.exchange.fillOrKillOrderAsync(signedOrder, emptyFillableAmount, takerAddress), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect( + contractWrappers.exchange.fillOrKillOrderAsync(signedOrder, emptyFillableAmount, takerAddress, { + shouldValidate: true, + }), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect( + contractWrappers.exchange.fillOrKillOrderAsync(signedOrder, emptyFillableAmount, takerAddress, { + shouldValidate: false, + }), + ).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + }); + }); + }); + describe('fill order(s)', () => { + let makerTokenAddress: string; + let takerTokenAddress: string; + let coinbase: string; + let makerAddress: string; + let takerAddress: string; + let feeRecipient: string; + const fillableAmount = new BigNumber(5); + const takerTokenFillAmount = new BigNumber(5); + const shouldThrowOnInsufficientBalanceOrAllowance = true; + before(async () => { + [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses; + tokens = await contractWrappers.tokenRegistry.getTokensAsync(); + const [makerToken, takerToken] = tokenUtils.getDummyTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + }); + describe('#fillOrderAsync', () => { + describe('successful fills', () => { + it('should fill a valid order', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress), + ).to.be.bignumber.equal(fillableAmount); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress), + ).to.be.bignumber.equal(0); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress), + ).to.be.bignumber.equal(0); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, takerAddress), + ).to.be.bignumber.equal(fillableAmount); + const txHash = await contractWrappers.exchange.fillOrderAsync( + signedOrder, + takerTokenFillAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress), + ).to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount)); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress), + ).to.be.bignumber.equal(takerTokenFillAmount); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress), + ).to.be.bignumber.equal(takerTokenFillAmount); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, takerAddress), + ).to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount)); + }); + it('should partially fill the valid order', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + const partialFillAmount = new BigNumber(3); + const txHash = await contractWrappers.exchange.fillOrderAsync( + signedOrder, + partialFillAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress), + ).to.be.bignumber.equal(fillableAmount.minus(partialFillAmount)); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress), + ).to.be.bignumber.equal(partialFillAmount); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress), + ).to.be.bignumber.equal(partialFillAmount); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, takerAddress), + ).to.be.bignumber.equal(fillableAmount.minus(partialFillAmount)); + }); + it('should fill the valid orders with fees', async () => { + const makerFee = new BigNumber(1); + const takerFee = new BigNumber(2); + const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( + makerTokenAddress, + takerTokenAddress, + makerFee, + takerFee, + makerAddress, + takerAddress, + fillableAmount, + feeRecipient, + ); + const txHash = await contractWrappers.exchange.fillOrderAsync( + signedOrder, + takerTokenFillAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + expect( + await contractWrappers.token.getBalanceAsync(zrxTokenAddress, feeRecipient), + ).to.be.bignumber.equal(makerFee.plus(takerFee)); + }); + }); + describe('order transaction options', () => { + let signedOrder: SignedOrder; + const emptyFillTakerAmount = new BigNumber(0); + beforeEach(async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + }); + it('should validate when orderTransactionOptions are not present', async () => { + return expect( + contractWrappers.exchange.fillOrderAsync( + signedOrder, + emptyFillTakerAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect( + contractWrappers.exchange.fillOrderAsync( + signedOrder, + emptyFillTakerAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + { + shouldValidate: true, + }, + ), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect( + contractWrappers.exchange.fillOrderAsync( + signedOrder, + emptyFillTakerAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + { + shouldValidate: false, + }, + ), + ).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + }); + describe('negative fill amount', async () => { + let signedOrder: SignedOrder; + const negativeFillTakerAmount = new BigNumber(-100); + beforeEach(async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + }); + it('should not allow the exchange wrapper to fill if amount is negative', async () => { + return expect( + contractWrappers.exchange.fillOrderAsync( + signedOrder, + negativeFillTakerAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ), + ).to.be.rejected(); + }); + }); + }); + describe('#batchFillOrdersAsync', () => { + let signedOrder: SignedOrder; + let signedOrderHashHex: string; + let anotherSignedOrder: SignedOrder; + let anotherOrderHashHex: string; + let orderFillBatch: OrderFillRequest[]; + beforeEach(async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + signedOrderHashHex = getOrderHashHex(signedOrder); + anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + anotherOrderHashHex = getOrderHashHex(anotherSignedOrder); + }); + describe('successful batch fills', () => { + beforeEach(() => { + orderFillBatch = [ + { + signedOrder, + takerTokenFillAmount, + }, + { + signedOrder: anotherSignedOrder, + takerTokenFillAmount, + }, + ]; + }); + it('should throw if a batch is empty', async () => { + return expect( + contractWrappers.exchange.batchFillOrdersAsync( + [], + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem); + }); + it('should successfully fill multiple orders', async () => { + const txHash = await contractWrappers.exchange.batchFillOrdersAsync( + orderFillBatch, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const filledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync(signedOrderHashHex); + const anotherFilledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync( + anotherOrderHashHex, + ); + expect(filledAmount).to.be.bignumber.equal(takerTokenFillAmount); + expect(anotherFilledAmount).to.be.bignumber.equal(takerTokenFillAmount); + }); + }); + describe('order transaction options', () => { + beforeEach(async () => { + const emptyFillTakerAmount = new BigNumber(0); + orderFillBatch = [ + { + signedOrder, + takerTokenFillAmount: emptyFillTakerAmount, + }, + { + signedOrder: anotherSignedOrder, + takerTokenFillAmount: emptyFillTakerAmount, + }, + ]; + }); + it('should validate when orderTransactionOptions are not present', async () => { + return expect( + contractWrappers.exchange.batchFillOrdersAsync( + orderFillBatch, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect( + contractWrappers.exchange.batchFillOrdersAsync( + orderFillBatch, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + { + shouldValidate: true, + }, + ), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect( + contractWrappers.exchange.batchFillOrdersAsync( + orderFillBatch, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + { + shouldValidate: false, + }, + ), + ).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + }); + describe('negative batch fill amount', async () => { + beforeEach(async () => { + const negativeFillTakerAmount = new BigNumber(-100); + orderFillBatch = [ + { + signedOrder, + takerTokenFillAmount, + }, + { + signedOrder: anotherSignedOrder, + takerTokenFillAmount: negativeFillTakerAmount, + }, + ]; + }); + it('should not allow the exchange wrapper to batch fill if any amount is negative', async () => { + return expect( + contractWrappers.exchange.batchFillOrdersAsync( + orderFillBatch, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ), + ).to.be.rejected(); + }); + }); + }); + describe('#fillOrdersUpTo', () => { + let signedOrder: SignedOrder; + let signedOrderHashHex: string; + let anotherSignedOrder: SignedOrder; + let anotherOrderHashHex: string; + let signedOrders: SignedOrder[]; + const fillUpToAmount = fillableAmount.plus(fillableAmount).minus(1); + beforeEach(async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + signedOrderHashHex = getOrderHashHex(signedOrder); + anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + anotherOrderHashHex = getOrderHashHex(anotherSignedOrder); + signedOrders = [signedOrder, anotherSignedOrder]; + }); + describe('successful batch fills', () => { + it('should throw if a batch is empty', async () => { + return expect( + contractWrappers.exchange.fillOrdersUpToAsync( + [], + fillUpToAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem); + }); + it('should successfully fill up to specified amount when all orders are fully funded', async () => { + const txHash = await contractWrappers.exchange.fillOrdersUpToAsync( + signedOrders, + fillUpToAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const filledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync(signedOrderHashHex); + const anotherFilledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync( + anotherOrderHashHex, + ); + expect(filledAmount).to.be.bignumber.equal(fillableAmount); + const remainingFillAmount = fillableAmount.minus(1); + expect(anotherFilledAmount).to.be.bignumber.equal(remainingFillAmount); + }); + it('should successfully fill up to specified amount and leave the rest of the orders untouched', async () => { + const txHash = await contractWrappers.exchange.fillOrdersUpToAsync( + signedOrders, + fillableAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const filledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync(signedOrderHashHex); + const zeroAmount = await contractWrappers.exchange.getFilledTakerAmountAsync(anotherOrderHashHex); + expect(filledAmount).to.be.bignumber.equal(fillableAmount); + expect(zeroAmount).to.be.bignumber.equal(0); + }); + it('should successfully fill up to specified amount even if filling all orders would fail', async () => { + const missingBalance = new BigNumber(1); // User will still have enough balance to fill up to 9, + // but won't have 10 to fully fill all orders in a batch. + await contractWrappers.token.transferAsync( + makerTokenAddress, + makerAddress, + coinbase, + missingBalance, + ); + const txHash = await contractWrappers.exchange.fillOrdersUpToAsync( + signedOrders, + fillUpToAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const filledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync(signedOrderHashHex); + const anotherFilledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync( + anotherOrderHashHex, + ); + expect(filledAmount).to.be.bignumber.equal(fillableAmount); + const remainingFillAmount = fillableAmount.minus(1); + expect(anotherFilledAmount).to.be.bignumber.equal(remainingFillAmount); + }); + }); + describe('failed batch fills', () => { + it("should fail validation if user doesn't have enough balance without fill up to", async () => { + const missingBalance = new BigNumber(2); // User will only have enough balance to fill up to 8 + await contractWrappers.token.transferAsync( + makerTokenAddress, + makerAddress, + coinbase, + missingBalance, + ); + return expect( + contractWrappers.exchange.fillOrdersUpToAsync( + signedOrders, + fillUpToAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerBalance); + }); + }); + describe('order transaction options', () => { + const emptyFillUpToAmount = new BigNumber(0); + it('should validate when orderTransactionOptions are not present', async () => { + return expect( + contractWrappers.exchange.fillOrdersUpToAsync( + signedOrders, + emptyFillUpToAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect( + contractWrappers.exchange.fillOrdersUpToAsync( + signedOrders, + emptyFillUpToAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + { + shouldValidate: true, + }, + ), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect( + contractWrappers.exchange.fillOrdersUpToAsync( + signedOrders, + emptyFillUpToAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + { + shouldValidate: false, + }, + ), + ).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + }); + }); + }); + describe('cancel order(s)', () => { + let makerTokenAddress: string; + let takerTokenAddress: string; + let coinbase: string; + let makerAddress: string; + let takerAddress: string; + const fillableAmount = new BigNumber(5); + let signedOrder: SignedOrder; + let orderHashHex: string; + const cancelAmount = new BigNumber(3); + beforeEach(async () => { + [coinbase, makerAddress, takerAddress] = userAddresses; + const [makerToken, takerToken] = tokenUtils.getDummyTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + orderHashHex = getOrderHashHex(signedOrder); + }); + describe('#cancelOrderAsync', () => { + describe('successful cancels', () => { + it('should cancel an order', async () => { + const txHash = await contractWrappers.exchange.cancelOrderAsync(signedOrder, cancelAmount); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const cancelledAmount = await contractWrappers.exchange.getCancelledTakerAmountAsync(orderHashHex); + expect(cancelledAmount).to.be.bignumber.equal(cancelAmount); + }); + }); + describe('order transaction options', () => { + const emptyCancelTakerTokenAmount = new BigNumber(0); + it('should validate when orderTransactionOptions are not present', async () => { + return expect( + contractWrappers.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount), + ).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect( + contractWrappers.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount, { + shouldValidate: true, + }), + ).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect( + contractWrappers.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount, { + shouldValidate: false, + }), + ).to.not.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + }); + }); + describe('#batchCancelOrdersAsync', () => { + let anotherSignedOrder: SignedOrder; + let anotherOrderHashHex: string; + let cancelBatch: OrderCancellationRequest[]; + beforeEach(async () => { + anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + anotherOrderHashHex = getOrderHashHex(anotherSignedOrder); + cancelBatch = [ + { + order: signedOrder, + takerTokenCancelAmount: cancelAmount, + }, + { + order: anotherSignedOrder, + takerTokenCancelAmount: cancelAmount, + }, + ]; + }); + describe('failed batch cancels', () => { + it('should throw when orders have different makers', async () => { + const signedOrderWithDifferentMaker = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + takerAddress, + takerAddress, + fillableAmount, + ); + return expect( + contractWrappers.exchange.batchCancelOrdersAsync([ + cancelBatch[0], + { + order: signedOrderWithDifferentMaker, + takerTokenCancelAmount: cancelAmount, + }, + ]), + ).to.be.rejectedWith(ExchangeContractErrs.MultipleMakersInSingleCancelBatchDisallowed); + }); + }); + describe('successful batch cancels', () => { + it('should cancel a batch of orders', async () => { + await contractWrappers.exchange.batchCancelOrdersAsync(cancelBatch); + const cancelledAmount = await contractWrappers.exchange.getCancelledTakerAmountAsync(orderHashHex); + const anotherCancelledAmount = await contractWrappers.exchange.getCancelledTakerAmountAsync( + anotherOrderHashHex, + ); + expect(cancelledAmount).to.be.bignumber.equal(cancelAmount); + expect(anotherCancelledAmount).to.be.bignumber.equal(cancelAmount); + }); + }); + describe('order transaction options', () => { + beforeEach(async () => { + const emptyTakerTokenCancelAmount = new BigNumber(0); + cancelBatch = [ + { + order: signedOrder, + takerTokenCancelAmount: emptyTakerTokenCancelAmount, + }, + { + order: anotherSignedOrder, + takerTokenCancelAmount: emptyTakerTokenCancelAmount, + }, + ]; + }); + it('should validate when orderTransactionOptions are not present', async () => { + return expect(contractWrappers.exchange.batchCancelOrdersAsync(cancelBatch)).to.be.rejectedWith( + ExchangeContractErrs.OrderCancelAmountZero, + ); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect( + contractWrappers.exchange.batchCancelOrdersAsync(cancelBatch, { + shouldValidate: true, + }), + ).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect( + contractWrappers.exchange.batchCancelOrdersAsync(cancelBatch, { + shouldValidate: false, + }), + ).to.not.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + }); + }); + }); + describe('tests that require partially filled order', () => { + let makerTokenAddress: string; + let takerTokenAddress: string; + let takerAddress: string; + let fillableAmount: BigNumber; + let partialFillAmount: BigNumber; + let signedOrder: SignedOrder; + let orderHash: string; + before(() => { + takerAddress = userAddresses[1]; + tokenUtils = new TokenUtils(tokens); + const [makerToken, takerToken] = tokenUtils.getDummyTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + }); + beforeEach(async () => { + fillableAmount = new BigNumber(5); + partialFillAmount = new BigNumber(2); + signedOrder = await fillScenarios.createPartiallyFilledSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + takerAddress, + fillableAmount, + partialFillAmount, + ); + orderHash = getOrderHashHex(signedOrder); + }); + describe('#getUnavailableTakerAmountAsync', () => { + it('should throw if passed an invalid orderHash', async () => { + const invalidOrderHashHex = '0x123'; + return expect( + contractWrappers.exchange.getUnavailableTakerAmountAsync(invalidOrderHashHex), + ).to.be.rejected(); + }); + it('should return zero if passed a valid but non-existent orderHash', async () => { + const unavailableValueT = await contractWrappers.exchange.getUnavailableTakerAmountAsync( + NON_EXISTENT_ORDER_HASH, + ); + expect(unavailableValueT).to.be.bignumber.equal(0); + }); + it('should return the unavailableValueT for a valid and partially filled orderHash', async () => { + const unavailableValueT = await contractWrappers.exchange.getUnavailableTakerAmountAsync(orderHash); + expect(unavailableValueT).to.be.bignumber.equal(partialFillAmount); + }); + }); + describe('#getFilledTakerAmountAsync', () => { + it('should throw if passed an invalid orderHash', async () => { + const invalidOrderHashHex = '0x123'; + return expect( + contractWrappers.exchange.getFilledTakerAmountAsync(invalidOrderHashHex), + ).to.be.rejected(); + }); + it('should return zero if passed a valid but non-existent orderHash', async () => { + const filledValueT = await contractWrappers.exchange.getFilledTakerAmountAsync(NON_EXISTENT_ORDER_HASH); + expect(filledValueT).to.be.bignumber.equal(0); + }); + it('should return the filledValueT for a valid and partially filled orderHash', async () => { + const filledValueT = await contractWrappers.exchange.getFilledTakerAmountAsync(orderHash); + expect(filledValueT).to.be.bignumber.equal(partialFillAmount); + }); + }); + describe('#getCancelledTakerAmountAsync', () => { + it('should throw if passed an invalid orderHash', async () => { + const invalidOrderHashHex = '0x123'; + return expect( + contractWrappers.exchange.getCancelledTakerAmountAsync(invalidOrderHashHex), + ).to.be.rejected(); + }); + it('should return zero if passed a valid but non-existent orderHash', async () => { + const cancelledValueT = await contractWrappers.exchange.getCancelledTakerAmountAsync( + NON_EXISTENT_ORDER_HASH, + ); + expect(cancelledValueT).to.be.bignumber.equal(0); + }); + it('should return the cancelledValueT for a valid and partially filled orderHash', async () => { + const cancelledValueT = await contractWrappers.exchange.getCancelledTakerAmountAsync(orderHash); + expect(cancelledValueT).to.be.bignumber.equal(0); + }); + it('should return the cancelledValueT for a valid and cancelled orderHash', async () => { + const cancelAmount = fillableAmount.minus(partialFillAmount); + await contractWrappers.exchange.cancelOrderAsync(signedOrder, cancelAmount); + const cancelledValueT = await contractWrappers.exchange.getCancelledTakerAmountAsync(orderHash); + expect(cancelledValueT).to.be.bignumber.equal(cancelAmount); + }); + }); + }); + describe('#subscribe', () => { + const indexFilterValues = {}; + const shouldThrowOnInsufficientBalanceOrAllowance = true; + let makerTokenAddress: string; + let takerTokenAddress: string; + let coinbase: string; + let takerAddress: string; + let makerAddress: string; + let fillableAmount: BigNumber; + let signedOrder: SignedOrder; + const takerTokenFillAmountInBaseUnits = new BigNumber(1); + const cancelTakerAmountInBaseUnits = new BigNumber(1); + before(() => { + [coinbase, makerAddress, takerAddress] = userAddresses; + const [makerToken, takerToken] = tokenUtils.getDummyTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + }); + beforeEach(async () => { + fillableAmount = new BigNumber(5); + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + }); + afterEach(async () => { + contractWrappers.exchange.unsubscribeAll(); + }); + // Hack: Mocha does not allow a test to be both async and have a `done` callback + // Since we need to await the receipt of the event in the `subscribe` callback, + // we do need both. A hack is to make the top-level a sync fn w/ a done callback and then + // wrap the rest of the test in an async block + // Source: https://github.com/mochajs/mocha/issues/2407 + it('Should receive the LogFill event when an order is filled', (done: DoneCallback) => { + (async () => { + const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<LogFillContractEventArgs>) => { + expect(logEvent.log.event).to.be.equal(ExchangeEvents.LogFill); + }, + ); + contractWrappers.exchange.subscribe(ExchangeEvents.LogFill, indexFilterValues, callback); + await contractWrappers.exchange.fillOrderAsync( + signedOrder, + takerTokenFillAmountInBaseUnits, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + })().catch(done); + }); + it('Should receive the LogCancel event when an order is cancelled', (done: DoneCallback) => { + (async () => { + const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<LogCancelContractEventArgs>) => { + expect(logEvent.log.event).to.be.equal(ExchangeEvents.LogCancel); + }, + ); + contractWrappers.exchange.subscribe(ExchangeEvents.LogCancel, indexFilterValues, callback); + await contractWrappers.exchange.cancelOrderAsync(signedOrder, cancelTakerAmountInBaseUnits); + })().catch(done); + }); + it('Outstanding subscriptions are cancelled when contractWrappers.setProvider called', (done: DoneCallback) => { + (async () => { + const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( + (_logEvent: DecodedLogEvent<LogFillContractEventArgs>) => { + done(new Error('Expected this subscription to have been cancelled')); + }, + ); + contractWrappers.exchange.subscribe(ExchangeEvents.LogFill, indexFilterValues, callbackNeverToBeCalled); + + contractWrappers.setProvider(provider, constants.TESTRPC_NETWORK_ID); + + const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<LogFillContractEventArgs>) => { + expect(logEvent.log.event).to.be.equal(ExchangeEvents.LogFill); + }, + ); + contractWrappers.exchange.subscribe(ExchangeEvents.LogFill, indexFilterValues, callback); + await contractWrappers.exchange.fillOrderAsync( + signedOrder, + takerTokenFillAmountInBaseUnits, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + })().catch(done); + }); + it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => { + (async () => { + const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( + (_logEvent: DecodedLogEvent<LogFillContractEventArgs>) => { + done(new Error('Expected this subscription to have been cancelled')); + }, + ); + const subscriptionToken = contractWrappers.exchange.subscribe( + ExchangeEvents.LogFill, + indexFilterValues, + callbackNeverToBeCalled, + ); + contractWrappers.exchange.unsubscribe(subscriptionToken); + await contractWrappers.exchange.fillOrderAsync( + signedOrder, + takerTokenFillAmountInBaseUnits, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + done(); + })().catch(done); + }); + }); + describe('#getOrderHashHexUsingContractCallAsync', () => { + let makerTokenAddress: string; + let takerTokenAddress: string; + let makerAddress: string; + let takerAddress: string; + const fillableAmount = new BigNumber(5); + before(async () => { + [, makerAddress, takerAddress] = userAddresses; + const [makerToken, takerToken] = tokenUtils.getDummyTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + }); + it("get's the same hash as the local function", async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + const orderHash = getOrderHashHex(signedOrder); + const orderHashFromContract = await (contractWrappers.exchange as any)._getOrderHashHexUsingContractCallAsync( + signedOrder, + ); + expect(orderHash).to.equal(orderHashFromContract); + }); + }); + describe('#getZRXTokenAddressAsync', () => { + it('gets the same token as is in token registry', () => { + const zrxAddress = contractWrappers.exchange.getZRXTokenAddress(); + const zrxToken = tokenUtils.getProtocolTokenOrThrow(); + expect(zrxAddress).to.equal(zrxToken.address); + }); + }); + describe('#getLogsAsync', () => { + let makerTokenAddress: string; + let takerTokenAddress: string; + let makerAddress: string; + let takerAddress: string; + const fillableAmount = new BigNumber(5); + const shouldThrowOnInsufficientBalanceOrAllowance = true; + const blockRange: BlockRange = { + fromBlock: 0, + toBlock: BlockParamLiteral.Latest, + }; + let txHash: string; + before(async () => { + [, makerAddress, takerAddress] = userAddresses; + const [makerToken, takerToken] = tokenUtils.getDummyTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + }); + it('should get logs with decoded args emitted by LogFill', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + txHash = await contractWrappers.exchange.fillOrderAsync( + signedOrder, + fillableAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const eventName = ExchangeEvents.LogFill; + const indexFilterValues = {}; + const logs = await contractWrappers.exchange.getLogsAsync(eventName, blockRange, indexFilterValues); + expect(logs).to.have.length(1); + expect(logs[0].event).to.be.equal(eventName); + }); + it('should only get the logs with the correct event name', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + txHash = await contractWrappers.exchange.fillOrderAsync( + signedOrder, + fillableAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const differentEventName = ExchangeEvents.LogCancel; + const indexFilterValues = {}; + const logs = await contractWrappers.exchange.getLogsAsync( + differentEventName, + blockRange, + indexFilterValues, + ); + expect(logs).to.have.length(0); + }); + it('should only get the logs with the correct indexed fields', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + txHash = await contractWrappers.exchange.fillOrderAsync( + signedOrder, + fillableAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + + const differentMakerAddress = userAddresses[2]; + const anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + differentMakerAddress, + takerAddress, + fillableAmount, + ); + txHash = await contractWrappers.exchange.fillOrderAsync( + anotherSignedOrder, + fillableAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + + const eventName = ExchangeEvents.LogFill; + const indexFilterValues = { + maker: differentMakerAddress, + }; + const logs = await contractWrappers.exchange.getLogsAsync<LogFillContractEventArgs>( + eventName, + blockRange, + indexFilterValues, + ); + expect(logs).to.have.length(1); + const args = logs[0].args; + expect(args.maker).to.be.equal(differentMakerAddress); + }); + }); + describe('#getOrderStateAsync', () => { + let maker: string; + let taker: string; + let makerToken: Token; + let takerToken: Token; + let signedOrder: SignedOrder; + let orderState: OrderState; + const fillableAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(5), constants.ZRX_DECIMALS); + before(async () => { + [, maker, taker] = userAddresses; + tokens = await contractWrappers.tokenRegistry.getTokensAsync(); + [makerToken, takerToken] = tokenUtils.getDummyTokens(); + }); + it('should report orderStateValid when order is fillable', async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerToken.address, + takerToken.address, + maker, + taker, + fillableAmount, + ); + orderState = await contractWrappers.exchange.getOrderStateAsync(signedOrder); + expect(orderState.isValid).to.be.true(); + }); + it('should report orderStateInvalid when maker allowance set to 0', async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerToken.address, + takerToken.address, + maker, + taker, + fillableAmount, + ); + await contractWrappers.token.setProxyAllowanceAsync(makerToken.address, maker, new BigNumber(0)); + orderState = await contractWrappers.exchange.getOrderStateAsync(signedOrder); + expect(orderState.isValid).to.be.false(); + }); + }); +}); // tslint:disable:max-file-line-count diff --git a/packages/contract-wrappers/test/subscription_test.ts b/packages/contract-wrappers/test/subscription_test.ts index 9b7b856bd..df80c4743 100644 --- a/packages/contract-wrappers/test/subscription_test.ts +++ b/packages/contract-wrappers/test/subscription_test.ts @@ -89,7 +89,7 @@ describe('SubscriptionTest', () => { }); it('Should allow unsubscribeAll to be called successfully after an error', (done: DoneCallback) => { (async () => { - const callback = (err: Error | null, logEvent?: DecodedLogEvent<ERC20TokenApprovalEventArgs>) => _.noop; + const callback = (err: Error | null, _logEvent?: DecodedLogEvent<ERC20TokenApprovalEventArgs>) => _.noop; contractWrappers.erc20Token.subscribe( tokenAddress, ERC20TokenEvents.Approval, |