diff options
-rw-r--r-- | CHANGELOG.md | 10 | ||||
-rw-r--r-- | src/contract_wrappers/exchange_wrapper.ts | 151 | ||||
-rw-r--r-- | src/types.ts | 1 | ||||
-rw-r--r-- | src/utils/order_validation_utils.ts | 69 | ||||
-rw-r--r-- | test/exchange_wrapper_test.ts | 83 | ||||
-rw-r--r-- | test/order_validation_test.ts | 161 |
6 files changed, 291 insertions, 184 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ca451d85..b5fef398c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,14 @@ # CHANGELOG -v0.9.4 - _TBD_ +v0.10.0 - _TBD_ ------------------------ - * Add clear error message when checksummed address is passed to a public method (#124) + * Added `zeroEx.exchange.validateFillOrderThrowIfInvalidAsync` (#128) + * Added `zeroEx.exchange.validateFillOrKillOrderThrowIfInvalidAsync` (#128) + * Added `zeroEx.exchange.validateCancelOrderThrowIfInvalidAsync` (#128) + * Added `zeroEx.exchange.isRoundingErrorAsync` (#128) + * Added `zeroEx.proxy.getContractAddressAsync` (#130) + * Added clear error message when checksummed address is passed to a public method (#124) * Fixes the description of `shouldThrowOnInsufficientBalanceOrAllowance` in docs (#127) - * Add `zeroEx.proxy.getContractAddressAsync` to public interface (#130) v0.9.3 - _Aug 22, 2017_ ------------------------ diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index 7994ee72f..66cf06dee 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -84,7 +84,7 @@ export class ExchangeWrapper extends ContractWrapper { constructor(web3Wrapper: Web3Wrapper, tokenWrapper: TokenWrapper) { super(web3Wrapper); this._tokenWrapper = tokenWrapper; - this._orderValidationUtils = new OrderValidationUtils(tokenWrapper); + this._orderValidationUtils = new OrderValidationUtils(tokenWrapper, this); this._exchangeLogEventEmitters = []; } /** @@ -161,7 +161,7 @@ export class ExchangeWrapper extends ContractWrapper { await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const exchangeInstance = await this._getExchangeContractAsync(); - await this._validateFillOrderAndThrowIfInvalidAsync(signedOrder, fillTakerTokenAmount, takerAddress); + await this.validateFillOrderThrowIfInvalidAsync(signedOrder, fillTakerTokenAmount, takerAddress); const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(signedOrder); @@ -226,7 +226,7 @@ export class ExchangeWrapper extends ContractWrapper { assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); for (const signedOrder of signedOrders) { - await this._validateFillOrderAndThrowIfInvalidAsync( + await this.validateFillOrderThrowIfInvalidAsync( signedOrder, fillTakerTokenAmount, takerAddress); } if (_.isEmpty(signedOrders)) { @@ -311,7 +311,7 @@ export class ExchangeWrapper extends ContractWrapper { assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); for (const orderFillRequest of orderFillRequests) { - await this._validateFillOrderAndThrowIfInvalidAsync( + await this.validateFillOrderThrowIfInvalidAsync( orderFillRequest.signedOrder, orderFillRequest.takerTokenFillAmount, takerAddress); } if (_.isEmpty(orderFillRequests)) { @@ -377,9 +377,8 @@ export class ExchangeWrapper extends ContractWrapper { await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const exchangeInstance = await this._getExchangeContractAsync(); - await this._validateFillOrderAndThrowIfInvalidAsync(signedOrder, fillTakerTokenAmount, takerAddress); - await this._validateFillOrKillOrderAndThrowIfInvalidAsync(signedOrder, fillTakerTokenAmount); + await this.validateFillOrKillOrderThrowIfInvalidAsync(signedOrder, fillTakerTokenAmount, takerAddress); const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(signedOrder); @@ -431,7 +430,8 @@ export class ExchangeWrapper extends ContractWrapper { } const exchangeInstance = await this._getExchangeContractAsync(); for (const request of orderFillOrKillRequests) { - await this._validateFillOrKillOrderAndThrowIfInvalidAsync(request.signedOrder, request.fillTakerAmount); + await this.validateFillOrKillOrderThrowIfInvalidAsync( + request.signedOrder, request.fillTakerAmount, takerAddress); } const orderAddressesValuesAndTakerTokenFillAmounts = _.map(orderFillOrKillRequests, request => { @@ -488,7 +488,7 @@ export class ExchangeWrapper extends ContractWrapper { await assert.isSenderAddressAsync('order.maker', order.maker, this._web3Wrapper); const exchangeInstance = await this._getExchangeContractAsync(); - await this._validateCancelOrderAndThrowIfInvalidAsync(order, cancelTakerTokenAmount); + await this.validateCancelOrderThrowIfInvalidAsync(order, cancelTakerTokenAmount); const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(order); const gas = await exchangeInstance.cancelOrder.estimateGas( @@ -534,7 +534,7 @@ export class ExchangeWrapper extends ContractWrapper { const maker = makers[0]; await assert.isSenderAddressAsync('maker', maker, this._web3Wrapper); for (const cancellationRequest of orderCancellationRequests) { - await this._validateCancelOrderAndThrowIfInvalidAsync( + await this.validateCancelOrderThrowIfInvalidAsync( cancellationRequest.order, cancellationRequest.takerTokenCancelAmount, ); } @@ -626,6 +626,78 @@ export class ExchangeWrapper extends ContractWrapper { const exchangeAddress = exchangeInstance.address; return exchangeAddress; } + /** + * Checks if order fill will succeed and throws an error otherwise. + * @param signedOrder An object that conforms to the SignedOrder interface. The + * signedOrder you wish to fill. + * @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill. + * @param takerAddress The user Ethereum address who would like to fill this order. + * Must be available via the supplied Web3.Provider passed to 0x.js. + */ + public async validateFillOrderThrowIfInvalidAsync(signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber.BigNumber, + takerAddress: string): Promise<void> { + assert.doesConformToSchema('signedOrder', signedOrder, signedOrderSchema); + assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + const zrxTokenAddress = await this._getZRXTokenAddressAsync(); + await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync( + signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress); + } + /** + * Checks if cancelling a given order will succeed and throws an informative error if it won't. + * @param order An object that conforms to the Order or SignedOrder interface. + * The order you would like to cancel. + * @param cancelTakerTokenAmount The amount (specified in taker tokens) that you would like to cancel. + */ + public async validateCancelOrderThrowIfInvalidAsync( + order: Order, cancelTakerTokenAmount: BigNumber.BigNumber): Promise<void> { + assert.doesConformToSchema('order', order, orderSchema); + assert.isBigNumber('cancelTakerTokenAmount', cancelTakerTokenAmount); + const orderHash = utils.getOrderHashHex(order); + const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash); + await this._orderValidationUtils.validateCancelOrderThrowIfInvalidAsync( + order, cancelTakerTokenAmount, unavailableTakerTokenAmount); + } + /** + * Checks if calling fillOrKill on a given order will succeed and throws an informative error if it won't. + * @param signedOrder An object that conforms to the SignedOrder interface. The + * signedOrder you wish to fill. + * @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill. + * @param takerAddress The user Ethereum address who would like to fill this order. + * Must be available via the supplied Web3.Provider passed to 0x.js. + */ + public async validateFillOrKillOrderThrowIfInvalidAsync(signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber.BigNumber, + takerAddress: string): Promise<void> { + assert.doesConformToSchema('signedOrder', signedOrder, signedOrderSchema); + assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + const zrxTokenAddress = await this._getZRXTokenAddressAsync(); + await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync( + signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress); + } + /** + * Checks if rounding error will be > 0.1% when computing makerTokenAmount by doing: + * `(fillTakerTokenAmount * makerTokenAmount) / takerTokenAmount`. + * 0x Protocol does not accept any trades that result in large rounding errors. This means that tokens with few or + * no decimals can only be filled in quantities and ratios that avoid large rounding errors. + * @param fillTakerTokenAmount The amount of the order (in taker tokens baseUnits) that you wish to fill. + * @param takerTokenAmount The order size on the taker side + * @param makerTokenAmount The order size on the maker side + */ + public async isRoundingErrorAsync(fillTakerTokenAmount: BigNumber.BigNumber, + takerTokenAmount: BigNumber.BigNumber, + makerTokenAmount: BigNumber.BigNumber): Promise<boolean> { + assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount); + assert.isBigNumber('takerTokenAmount', takerTokenAmount); + assert.isBigNumber('makerTokenAmount', makerTokenAmount); + const exchangeInstance = await this._getExchangeContractAsync(); + const isRoundingError = await exchangeInstance.isRoundingError.call( + fillTakerTokenAmount, takerTokenAmount, makerTokenAmount, + ); + return isRoundingError; + } private async _invalidateContractInstancesAsync(): Promise<void> { await this.stopWatchingAllEventsAsync(); delete this._exchangeContractIfExists; @@ -653,56 +725,6 @@ export class ExchangeWrapper extends ContractWrapper { const orderHashHex = await exchangeInstance.getOrderHash.call(orderAddresses, orderValues); return orderHashHex; } - private async _validateFillOrderAndThrowIfInvalidAsync(signedOrder: SignedOrder, - fillTakerAmount: BigNumber.BigNumber, - senderAddress: string): Promise<void> { - if (fillTakerAmount.eq(0)) { - throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); - } - if (signedOrder.taker !== constants.NULL_ADDRESS && signedOrder.taker !== senderAddress) { - throw new Error(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker); - } - const currentUnixTimestampSec = utils.getCurrentUnixTimestamp(); - if (signedOrder.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) { - throw new Error(ExchangeContractErrs.OrderFillExpired); - } - const zrxTokenAddress = await this._getZRXTokenAddressAsync(signedOrder.exchangeContractAddress); - await this._orderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( - signedOrder, fillTakerAmount, senderAddress, zrxTokenAddress, - ); - - const wouldRoundingErrorOccur = await this._isRoundingErrorAsync( - fillTakerAmount, signedOrder.takerTokenAmount, signedOrder.makerTokenAmount, - ); - if (wouldRoundingErrorOccur) { - throw new Error(ExchangeContractErrs.OrderFillRoundingError); - } - } - private async _validateCancelOrderAndThrowIfInvalidAsync( - order: Order, takerTokenCancelAmount: BigNumber.BigNumber): Promise<void> { - if (takerTokenCancelAmount.eq(0)) { - throw new Error(ExchangeContractErrs.OrderCancelAmountZero); - } - const orderHash = utils.getOrderHashHex(order); - const unavailableAmount = await this.getUnavailableTakerAmountAsync(orderHash); - if (order.takerTokenAmount.minus(unavailableAmount).eq(0)) { - throw new Error(ExchangeContractErrs.OrderAlreadyCancelledOrFilled); - } - const currentUnixTimestampSec = utils.getCurrentUnixTimestamp(); - if (order.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) { - throw new Error(ExchangeContractErrs.OrderCancelExpired); - } - } - private async _validateFillOrKillOrderAndThrowIfInvalidAsync(signedOrder: SignedOrder, - fillTakerAmount: BigNumber.BigNumber) { - // Check that fillValue available >= fillTakerAmount - const orderHashHex = utils.getOrderHashHex(signedOrder); - const unavailableTakerAmount = await this.getUnavailableTakerAmountAsync(orderHashHex); - const remainingTakerAmount = signedOrder.takerTokenAmount.minus(unavailableTakerAmount); - if (remainingTakerAmount < fillTakerAmount) { - throw new Error(ExchangeContractErrs.InsufficientRemainingFillAmount); - } - } private _throwErrorLogsAsErrors(logs: ContractEvent[]): void { const errEvent = _.find(logs, {event: 'LogError'}); if (!_.isUndefined(errEvent)) { @@ -711,15 +733,6 @@ export class ExchangeWrapper extends ContractWrapper { throw new Error(errMessage); } } - private async _isRoundingErrorAsync(numerator: BigNumber.BigNumber, - demoninator: BigNumber.BigNumber, - makerTokenAmount: BigNumber.BigNumber): Promise<boolean> { - const exchangeInstance = await this._getExchangeContractAsync(); - const isRoundingError = await exchangeInstance.isRoundingError.call( - numerator, demoninator, makerTokenAmount, - ); - return isRoundingError; - } private async _getExchangeContractAsync(): Promise<ExchangeContract> { if (!_.isUndefined(this._exchangeContractIfExists)) { return this._exchangeContractIfExists; @@ -728,7 +741,7 @@ export class ExchangeWrapper extends ContractWrapper { this._exchangeContractIfExists = contractInstance as ExchangeContract; return this._exchangeContractIfExists; } - private async _getZRXTokenAddressAsync(exchangeContractAddress: string): Promise<string> { + private async _getZRXTokenAddressAsync(): Promise<string> { const exchangeInstance = await this._getExchangeContractAsync(); const ZRXtokenAddress = await exchangeInstance.ZRX_TOKEN_CONTRACT.call(); return ZRXtokenAddress; diff --git a/src/types.ts b/src/types.ts index 0de87ca71..5ba85a7d5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -180,6 +180,7 @@ export enum ExchangeContractErrs { OrderCancelExpired = 'ORDER_CANCEL_EXPIRED', OrderCancelAmountZero = 'ORDER_CANCEL_AMOUNT_ZERO', OrderAlreadyCancelledOrFilled = 'ORDER_ALREADY_CANCELLED_OR_FILLED', + OrderFillAmountZero = 'ORDER_FILL_AMOUNT_ZERO', OrderRemainingFillAmountZero = 'ORDER_REMAINING_FILL_AMOUNT_ZERO', OrderFillRoundingError = 'ORDER_FILL_ROUNDING_ERROR', FillBalanceAllowanceError = 'FILL_BALANCE_ALLOWANCE_ERROR', diff --git a/src/utils/order_validation_utils.ts b/src/utils/order_validation_utils.ts index 8a737040c..445ad43f9 100644 --- a/src/utils/order_validation_utils.ts +++ b/src/utils/order_validation_utils.ts @@ -1,10 +1,75 @@ -import {ExchangeContractErrs, SignedOrder} from '../types'; +import {ExchangeContractErrs, SignedOrder, Order} from '../types'; import {TokenWrapper} from '../contract_wrappers/token_wrapper'; +import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper'; +import {utils} from '../utils/utils'; +import {constants} from '../utils/constants'; export class OrderValidationUtils { private tokenWrapper: TokenWrapper; - constructor(tokenWrapper: TokenWrapper) { + private exchangeWrapper: ExchangeWrapper; + constructor(tokenWrapper: TokenWrapper, exchangeWrapper: ExchangeWrapper) { this.tokenWrapper = tokenWrapper; + this.exchangeWrapper = exchangeWrapper; + } + public async validateFillOrderThrowIfInvalidAsync(signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber.BigNumber, + takerAddress: string, + zrxTokenAddress: string): Promise<void> { + if (fillTakerTokenAmount.eq(0)) { + throw new Error(ExchangeContractErrs.OrderFillAmountZero); + } + const orderHash = utils.getOrderHashHex(signedOrder); + const unavailableTakerTokenAmount = await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHash); + if (signedOrder.makerTokenAmount.eq(unavailableTakerTokenAmount)) { + throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); + } + if (signedOrder.taker !== constants.NULL_ADDRESS && signedOrder.taker !== takerAddress) { + throw new Error(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker); + } + const currentUnixTimestampSec = utils.getCurrentUnixTimestamp(); + if (signedOrder.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) { + throw new Error(ExchangeContractErrs.OrderFillExpired); + } + await this.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress, + ); + + const wouldRoundingErrorOccur = await this.exchangeWrapper.isRoundingErrorAsync( + fillTakerTokenAmount, signedOrder.takerTokenAmount, signedOrder.makerTokenAmount, + ); + if (wouldRoundingErrorOccur) { + throw new Error(ExchangeContractErrs.OrderFillRoundingError); + } + } + public async validateFillOrKillOrderThrowIfInvalidAsync(signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber.BigNumber, + takerAddress: string, + zrxTokenAddress: string): Promise<void> { + await this.validateFillOrderThrowIfInvalidAsync( + signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress, + ); + // Check that fillValue available >= fillTakerAmount + const orderHashHex = utils.getOrderHashHex(signedOrder); + const unavailableTakerAmount = await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHashHex); + const remainingTakerAmount = signedOrder.takerTokenAmount.minus(unavailableTakerAmount); + if (remainingTakerAmount < fillTakerTokenAmount) { + throw new Error(ExchangeContractErrs.InsufficientRemainingFillAmount); + } + } + public async validateCancelOrderThrowIfInvalidAsync(order: Order, + cancelTakerTokenAmount: BigNumber.BigNumber, + unavailableTakerTokenAmount: BigNumber.BigNumber, + ): Promise<void> { + if (cancelTakerTokenAmount.eq(0)) { + throw new Error(ExchangeContractErrs.OrderCancelAmountZero); + } + if (order.takerTokenAmount.eq(unavailableTakerTokenAmount)) { + throw new Error(ExchangeContractErrs.OrderAlreadyCancelledOrFilled); + } + const currentUnixTimestampSec = utils.getCurrentUnixTimestamp(); + if (order.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) { + throw new Error(ExchangeContractErrs.OrderCancelExpired); + } } public async validateFillOrderBalancesAllowancesThrowIfInvalidAsync( signedOrder: SignedOrder, fillTakerAmount: BigNumber.BigNumber, senderAddress: string, zrxTokenAddress: string, diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts index a9c237d0e..080be774a 100644 --- a/test/exchange_wrapper_test.ts +++ b/test/exchange_wrapper_test.ts @@ -95,24 +95,6 @@ describe('ExchangeWrapper', () => { }); }); describe('#fillOrKillOrderAsync', () => { - describe('failed fillOrKill', () => { - it('should throw if remaining fillAmount is less then the desired fillAmount', async () => { - const fillableAmount = new BigNumber(5); - const signedOrder = await fillScenarios.createFillableSignedOrderAsync( - makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, - ); - const tooLargeFillAmount = new BigNumber(7); - const fillAmountDifference = tooLargeFillAmount.minus(fillableAmount); - await zeroEx.token.transferAsync(takerTokenAddress, coinbase, takerAddress, fillAmountDifference); - await zeroEx.token.setProxyAllowanceAsync(takerTokenAddress, takerAddress, tooLargeFillAmount); - await zeroEx.token.transferAsync(makerTokenAddress, coinbase, makerAddress, fillAmountDifference); - await zeroEx.token.setProxyAllowanceAsync(makerTokenAddress, makerAddress, tooLargeFillAmount); - - return expect(zeroEx.exchange.fillOrKillOrderAsync( - signedOrder, tooLargeFillAmount, takerAddress, - )).to.be.rejectedWith(ExchangeContractErrs.InsufficientRemainingFillAmount); - }); - }); describe('successful fills', () => { it('should fill a valid order', async () => { const fillableAmount = new BigNumber(5); @@ -174,49 +156,6 @@ describe('ExchangeWrapper', () => { takerTokenAddress = takerToken.address; }); describe('#fillOrderAsync', () => { - describe('failed fills', () => { - it('should throw when the fill amount is zero', async () => { - const signedOrder = await fillScenarios.createFillableSignedOrderAsync( - makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, - ); - const zeroFillAmount = new BigNumber(0); - return expect(zeroEx.exchange.fillOrderAsync( - signedOrder, zeroFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, - )).to.be.rejectedWith(ExchangeContractErrs.OrderRemainingFillAmountZero); - }); - it('should throw when sender is not a taker', async () => { - const signedOrder = await fillScenarios.createFillableSignedOrderAsync( - makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, - ); - const nonTakerAddress = userAddresses[6]; - return expect(zeroEx.exchange.fillOrderAsync( - signedOrder, fillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, nonTakerAddress, - )).to.be.rejectedWith(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker); - }); - it('should throw when order is expired', async () => { - const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017 - const signedOrder = await fillScenarios.createFillableSignedOrderAsync( - makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, - fillableAmount, expirationInPast, - ); - return expect(zeroEx.exchange.fillOrderAsync( - signedOrder, fillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, - )).to.be.rejectedWith(ExchangeContractErrs.OrderFillExpired); - }); - it('should throw when there a rounding error would have occurred', async () => { - const makerAmount = new BigNumber(3); - const takerAmount = new BigNumber(5); - const signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync( - makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, - makerAmount, takerAmount, - ); - const fillTakerAmountThatCausesRoundingError = new BigNumber(3); - return expect(zeroEx.exchange.fillOrderAsync( - signedOrder, fillTakerAmountThatCausesRoundingError, - shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, - )).to.be.rejectedWith(ExchangeContractErrs.OrderFillRoundingError); - }); - }); describe('successful fills', () => { it('should fill a valid order', async () => { const signedOrder = await fillScenarios.createFillableSignedOrderAsync( @@ -403,28 +342,6 @@ describe('ExchangeWrapper', () => { orderHashHex = ZeroEx.getOrderHashHex(signedOrder); }); describe('#cancelOrderAsync', () => { - describe('failed cancels', () => { - it('should throw when cancel amount is zero', async () => { - const zeroCancelAmount = new BigNumber(0); - return expect(zeroEx.exchange.cancelOrderAsync(signedOrder, zeroCancelAmount)) - .to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); - }); - it('should throw when order is expired', async () => { - const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017 - const expiredSignedOrder = await fillScenarios.createFillableSignedOrderAsync( - makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, - fillableAmount, expirationInPast, - ); - orderHashHex = ZeroEx.getOrderHashHex(expiredSignedOrder); - return expect(zeroEx.exchange.cancelOrderAsync(expiredSignedOrder, cancelAmount)) - .to.be.rejectedWith(ExchangeContractErrs.OrderCancelExpired); - }); - it('should throw when order is already cancelled or filled', async () => { - await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount); - return expect(zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount)) - .to.be.rejectedWith(ExchangeContractErrs.OrderAlreadyCancelledOrFilled); - }); - }); describe('successful cancels', () => { it('should cancel an order', async () => { await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmount); diff --git a/test/order_validation_test.ts b/test/order_validation_test.ts index 826b44077..8a6303f3a 100644 --- a/test/order_validation_test.ts +++ b/test/order_validation_test.ts @@ -8,12 +8,13 @@ import {ZeroEx, SignedOrder, Token, ExchangeContractErrs} from '../src'; import {TokenUtils} from './utils/token_utils'; import {BlockchainLifecycle} from './utils/blockchain_lifecycle'; import {FillScenarios} from './utils/fill_scenarios'; +import {OrderValidationUtils} from '../src/utils/order_validation_utils'; chaiSetup.configure(); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(); -describe('OrderValidationUtils', () => { +describe('OrderValidation', () => { let web3: Web3; let zeroEx: ZeroEx; let userAddresses: string[]; @@ -28,6 +29,7 @@ describe('OrderValidationUtils', () => { let makerAddress: string; let takerAddress: string; let feeRecipient: string; + let orderValidationUtils: OrderValidationUtils; const fillableAmount = new BigNumber(5); const fillTakerAmount = new BigNumber(5); const shouldThrowOnInsufficientBalanceOrAllowance = false; @@ -44,6 +46,7 @@ describe('OrderValidationUtils', () => { const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens(); makerTokenAddress = makerToken.address; takerTokenAddress = takerToken.address; + orderValidationUtils = new OrderValidationUtils(zeroEx.token, zeroEx.exchange); }); beforeEach(async () => { await blockchainLifecycle.startAsync(); @@ -51,9 +54,113 @@ describe('OrderValidationUtils', () => { afterEach(async () => { await blockchainLifecycle.revertAsync(); }); - describe('#validateFillOrderBalancesAndAllowancesAndThrowIfInvalidAsync', () => { + describe('validateFillOrderAndThrowIfInvalidAsync', () => { + it('should throw when the fill amount is zero', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + const zeroFillAmount = new BigNumber(0); + return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, zeroFillAmount, takerAddress, + )).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should throw when the order is fully filled or cancelled', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount); + return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, fillableAmount, takerAddress, + )).to.be.rejectedWith(ExchangeContractErrs.OrderRemainingFillAmountZero); + }); + it('should throw when sender is not a taker', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + const nonTakerAddress = userAddresses[6]; + return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, fillTakerAmount, nonTakerAddress, + )).to.be.rejectedWith(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker); + }); + it('should throw when order is expired', async () => { + const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017 + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, + fillableAmount, expirationInPast, + ); + return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, fillTakerAmount, takerAddress, + )).to.be.rejectedWith(ExchangeContractErrs.OrderFillExpired); + }); + it('should throw when there a rounding error would have occurred', async () => { + const makerAmount = new BigNumber(3); + const takerAmount = new BigNumber(5); + const signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, + makerAmount, takerAmount, + ); + const fillTakerAmountThatCausesRoundingError = new BigNumber(3); + return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, fillTakerAmountThatCausesRoundingError, takerAddress, + )).to.be.rejectedWith(ExchangeContractErrs.OrderFillRoundingError); + }); + }); + describe('#validateFillOrKillOrderAndThrowIfInvalidAsync', () => { + it('should throw if remaining fillAmount is less then the desired fillAmount', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + const tooLargeFillAmount = new BigNumber(7); + const fillAmountDifference = tooLargeFillAmount.minus(fillableAmount); + await zeroEx.token.transferAsync(takerTokenAddress, coinbase, takerAddress, fillAmountDifference); + await zeroEx.token.setProxyAllowanceAsync(takerTokenAddress, takerAddress, tooLargeFillAmount); + await zeroEx.token.transferAsync(makerTokenAddress, coinbase, makerAddress, fillAmountDifference); + await zeroEx.token.setProxyAllowanceAsync(makerTokenAddress, makerAddress, tooLargeFillAmount); + + return expect(zeroEx.exchange.validateFillOrKillOrderThrowIfInvalidAsync( + signedOrder, tooLargeFillAmount, takerAddress, + )).to.be.rejectedWith(ExchangeContractErrs.InsufficientRemainingFillAmount); + }); + }); + describe('validateCancelOrderAndThrowIfInvalidAsync', () => { + let signedOrder: SignedOrder; + let orderHashHex: string; + const cancelAmount = new BigNumber(3); + beforeEach(async () => { + [coinbase, makerAddress, takerAddress] = userAddresses; + const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + orderHashHex = ZeroEx.getOrderHashHex(signedOrder); + }); + it('should throw when cancel amount is zero', async () => { + const zeroCancelAmount = new BigNumber(0); + return expect(zeroEx.exchange.validateCancelOrderThrowIfInvalidAsync(signedOrder, zeroCancelAmount)) + .to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + it('should throw when order is expired', async () => { + const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017 + const expiredSignedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, + fillableAmount, expirationInPast, + ); + orderHashHex = ZeroEx.getOrderHashHex(expiredSignedOrder); + return expect(zeroEx.exchange.validateCancelOrderThrowIfInvalidAsync(expiredSignedOrder, cancelAmount)) + .to.be.rejectedWith(ExchangeContractErrs.OrderCancelExpired); + }); + it('should throw when order is already cancelled or filled', async () => { + await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount); + return expect(zeroEx.exchange.validateCancelOrderThrowIfInvalidAsync(signedOrder, fillableAmount)) + .to.be.rejectedWith(ExchangeContractErrs.OrderAlreadyCancelledOrFilled); + }); + }); + describe('#validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync', () => { describe('should throw when not enough balance or allowance to fulfill the order', () => { const balanceToSubtractFromMaker = new BigNumber(3); + const balanceToSubtractFromTaker = new BigNumber(3); const lackingAllowance = new BigNumber(3); let signedOrder: SignedOrder; beforeEach('create fillable signed order', async () => { @@ -63,34 +170,34 @@ describe('OrderValidationUtils', () => { }); it('should throw when taker balance is less than fill amount', async () => { await zeroEx.token.transferAsync( - takerTokenAddress, takerAddress, coinbase, balanceToSubtractFromMaker, + takerTokenAddress, takerAddress, coinbase, balanceToSubtractFromTaker, ); - return expect(zeroEx.exchange.fillOrderAsync( - signedOrder, fillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync( + signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerBalance); }); it('should throw when taker allowance is less than fill amount', async () => { const newAllowanceWhichIsLessThanFillAmount = fillTakerAmount.minus(lackingAllowance); await zeroEx.token.setProxyAllowanceAsync(takerTokenAddress, takerAddress, newAllowanceWhichIsLessThanFillAmount); - return expect(zeroEx.exchange.fillOrderAsync( - signedOrder, fillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync( + signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerAllowance); }); it('should throw when maker balance is less than maker fill amount', async () => { await zeroEx.token.transferAsync( makerTokenAddress, makerAddress, coinbase, balanceToSubtractFromMaker, ); - return expect(zeroEx.exchange.fillOrderAsync( - signedOrder, fillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync( + signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerBalance); }); it('should throw when maker allowance is less than maker fill amount', async () => { const newAllowanceWhichIsLessThanFillAmount = fillTakerAmount.minus(lackingAllowance); await zeroEx.token.setProxyAllowanceAsync(makerTokenAddress, makerAddress, newAllowanceWhichIsLessThanFillAmount); - return expect(zeroEx.exchange.fillOrderAsync( - signedOrder, fillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync( + signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerAllowance); }); }); @@ -109,16 +216,16 @@ describe('OrderValidationUtils', () => { await zeroEx.token.transferAsync( zrxTokenAddress, makerAddress, coinbase, balanceToSubtractFromMaker, ); - return expect(zeroEx.exchange.fillOrderAsync( - signedOrder, fillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync( + signedOrder, fillTakerAmount, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerFeeBalance); }); it('should throw when maker doesn\'t have enough allowance to pay fees', async () => { const newAllowanceWhichIsLessThanFees = makerFee.minus(1); await zeroEx.token.setProxyAllowanceAsync(zrxTokenAddress, makerAddress, newAllowanceWhichIsLessThanFees); - return expect(zeroEx.exchange.fillOrderAsync( - signedOrder, fillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync( + signedOrder, fillTakerAmount, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerFeeAllowance); }); it('should throw when taker doesn\'t have enough balance to pay fees', async () => { @@ -126,16 +233,16 @@ describe('OrderValidationUtils', () => { await zeroEx.token.transferAsync( zrxTokenAddress, takerAddress, coinbase, balanceToSubtractFromTaker, ); - return expect(zeroEx.exchange.fillOrderAsync( - signedOrder, fillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync( + signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerFeeBalance); }); it('should throw when taker doesn\'t have enough allowance to pay fees', async () => { const newAllowanceWhichIsLessThanFees = makerFee.minus(1); await zeroEx.token.setProxyAllowanceAsync(zrxTokenAddress, takerAddress, newAllowanceWhichIsLessThanFees); - return expect(zeroEx.exchange.fillOrderAsync( - signedOrder, fillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync( + signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerFeeAllowance); }); }); @@ -155,8 +262,8 @@ describe('OrderValidationUtils', () => { await zeroEx.token.transferAsync( zrxTokenAddress, makerAddress, coinbase, balanceToSubtractFromMaker, ); - return expect(zeroEx.exchange.fillOrderAsync( - signedOrder, fillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync( + signedOrder, fillTakerAmount, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerBalance); }); it('should throw on insufficient allowance when makerToken is ZRX', async () => { @@ -164,8 +271,8 @@ describe('OrderValidationUtils', () => { const newAllowanceWhichIsInsufficient = oldAllowance.minus(1); await zeroEx.token.setProxyAllowanceAsync( zrxTokenAddress, makerAddress, newAllowanceWhichIsInsufficient); - return expect(zeroEx.exchange.fillOrderAsync( - signedOrder, fillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync( + signedOrder, fillTakerAmount, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerAllowance); }); }); @@ -185,8 +292,8 @@ describe('OrderValidationUtils', () => { await zeroEx.token.transferAsync( zrxTokenAddress, takerAddress, coinbase, balanceToSubtractFromTaker, ); - return expect(zeroEx.exchange.fillOrderAsync( - signedOrder, fillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync( + signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerBalance); }); it('should throw on insufficient allowance when takerToken is ZRX', async () => { @@ -194,8 +301,8 @@ describe('OrderValidationUtils', () => { const newAllowanceWhichIsInsufficient = oldAllowance.minus(1); await zeroEx.token.setProxyAllowanceAsync( zrxTokenAddress, takerAddress, newAllowanceWhichIsInsufficient); - return expect(zeroEx.exchange.fillOrderAsync( - signedOrder, fillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync( + signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerAllowance); }); }); |