diff options
-rw-r--r-- | src/contract_wrappers/exchange_wrapper.ts | 20 | ||||
-rw-r--r-- | src/types.ts | 10 | ||||
-rw-r--r-- | src/utils/order_validation_utils.ts | 42 | ||||
-rw-r--r-- | test/order_validation_test.ts | 40 | ||||
-rw-r--r-- | test/utils/fill_scenarios.ts | 4 |
5 files changed, 108 insertions, 8 deletions
diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index 694f3b71b..d2a3bcfa3 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -28,6 +28,7 @@ import { LogCancelContractEventArgs, LogWithDecodedArgs, MethodOpts, + ValidateOrderFillableOpts, } from '../types'; import {assert} from '../utils/assert'; import {utils} from '../utils/utils'; @@ -624,6 +625,25 @@ export class ExchangeWrapper extends ContractWrapper { return exchangeAddress; } /** + * Checks if order is still fillable and throws an error otherwise. Useful for orderbook + * pruning where you want to remove stale orders without knowing who the taker will be. + * @param signedOrder An object that conforms to the SignedOrder interface. The + * signedOrder you wish to validate. + * @param opts An object that conforms to the ValidateOrderFillableOpts + * interface. Allows specifying a specific fillTakerTokenAmount + * to validate for. + */ + public async validateOrderFillableOrThrowAsync( + signedOrder: SignedOrder, opts?: ValidateOrderFillableOpts, + ): Promise<void> { + assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + const zrxTokenAddress = await this._getZRXTokenAddressAsync(); + const expectedFillTakerTokenAmount = !_.isUndefined(opts) ? opts.expectedFillTakerTokenAmount : undefined; + await this._orderValidationUtils.validateOrderFillableOrThrowAsync( + signedOrder, zrxTokenAddress, expectedFillTakerTokenAmount, + ); + } + /** * 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. diff --git a/src/types.ts b/src/types.ts index 29fb40e73..2d069f596 100644 --- a/src/types.ts +++ b/src/types.ts @@ -434,6 +434,16 @@ export interface Artifact { } /* + * expectedFillTakerTokenAmount: If specified, the validation method will ensure that the + * supplied order maker has a sufficient allowance/balance to fill this amount of the order's + * takerTokenAmount. If not specified, the validation method ensures that the maker has a sufficient + * allowance/balance to fill the entire remaining order amount. + */ +export interface ValidateOrderFillableOpts { + expectedFillTakerTokenAmount?: BigNumber.BigNumber; +} + +/* * defaultBlock: The block up to which to query the blockchain state. Setting this to a historical block number * let's the user query the blockchain's state at an arbitrary point in time. In order for this to work, the * backing Ethereum node must keep the entire historical state of the chain (e.g setting `--pruning=archive` diff --git a/src/utils/order_validation_utils.ts b/src/utils/order_validation_utils.ts index 815cd0115..6f7522c41 100644 --- a/src/utils/order_validation_utils.ts +++ b/src/utils/order_validation_utils.ts @@ -1,3 +1,4 @@ +import * as _ from 'lodash'; import {ExchangeContractErrs, SignedOrder, Order, ZeroExError} from '../types'; import {ZeroEx} from '../0x'; import {TokenWrapper} from '../contract_wrappers/token_wrapper'; @@ -12,6 +13,23 @@ export class OrderValidationUtils { this.tokenWrapper = tokenWrapper; this.exchangeWrapper = exchangeWrapper; } + public async validateOrderFillableOrThrowAsync( + signedOrder: SignedOrder, zrxTokenAddress: string, expectedFillTakerTokenAmount?: BigNumber.BigNumber, + ): Promise<void> { + const orderHash = utils.getOrderHashHex(signedOrder); + const unavailableTakerTokenAmount = await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHash); + this.validateRemainingFillAmountNotZeroOrThrow( + signedOrder.takerTokenAmount, unavailableTakerTokenAmount, + ); + this.validateOrderNotExpiredOrThrow(signedOrder.expirationUnixTimestampSec); + let fillTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount); + if (!_.isUndefined(expectedFillTakerTokenAmount)) { + fillTakerTokenAmount = expectedFillTakerTokenAmount; + } + await this.validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync( + signedOrder, fillTakerTokenAmount, zrxTokenAddress, + ); + } public async validateFillOrderThrowIfInvalidAsync(signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber.BigNumber, takerAddress: string, @@ -24,16 +42,13 @@ export class OrderValidationUtils { throw new Error(ZeroExError.InvalidSignature); } const unavailableTakerTokenAmount = await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHash); - if (signedOrder.makerTokenAmount.eq(unavailableTakerTokenAmount)) { - throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); - } + this.validateRemainingFillAmountNotZeroOrThrow( + signedOrder.takerTokenAmount, unavailableTakerTokenAmount, + ); 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); - } + this.validateOrderNotExpiredOrThrow(signedOrder.expirationUnixTimestampSec); await this.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress, ); @@ -147,4 +162,17 @@ export class OrderValidationUtils { } } } + private validateRemainingFillAmountNotZeroOrThrow( + takerTokenAmount: BigNumber.BigNumber, unavailableTakerTokenAmount: BigNumber.BigNumber, + ) { + if (takerTokenAmount.eq(unavailableTakerTokenAmount)) { + throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); + } + } + private validateOrderNotExpiredOrThrow(expirationUnixTimestampSec: BigNumber.BigNumber) { + const currentUnixTimestampSec = utils.getCurrentUnixTimestamp(); + if (expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) { + throw new Error(ExchangeContractErrs.OrderFillExpired); + } + } } diff --git a/test/order_validation_test.ts b/test/order_validation_test.ts index 9a621555c..022a39dd1 100644 --- a/test/order_validation_test.ts +++ b/test/order_validation_test.ts @@ -54,6 +54,46 @@ describe('OrderValidation', () => { afterEach(async () => { await blockchainLifecycle.revertAsync(); }); + describe('validateOrderFillableOrThrowAsync', () => { + it('should succeed if the order is fillable', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + await zeroEx.exchange.validateOrderFillableOrThrowAsync( + signedOrder, + ); + }); + it('should succeed if the order is asymmetric and fillable', async () => { + const makerFillableAmount = fillableAmount; + const takerFillableAmount = fillableAmount.minus(5000); + const signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, + makerFillableAmount, takerFillableAmount, + ); + await zeroEx.exchange.validateOrderFillableOrThrowAsync( + signedOrder, + ); + }); + 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.validateOrderFillableOrThrowAsync( + signedOrder, + )).to.be.rejectedWith(ExchangeContractErrs.OrderRemainingFillAmountZero); + }); + 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.validateOrderFillableOrThrowAsync( + signedOrder, + )).to.be.rejectedWith(ExchangeContractErrs.OrderFillExpired); + }); + }); describe('validateFillOrderAndThrowIfInvalidAsync', () => { it('should throw when the fill amount is zero', async () => { const signedOrder = await fillScenarios.createFillableSignedOrderAsync( diff --git a/test/utils/fill_scenarios.ts b/test/utils/fill_scenarios.ts index 563415a48..e305759f6 100644 --- a/test/utils/fill_scenarios.ts +++ b/test/utils/fill_scenarios.ts @@ -62,7 +62,9 @@ export class FillScenarios { fillableAmount, fillableAmount, ); const shouldThrowOnInsufficientBalanceOrAllowance = false; - await this.zeroEx.exchange.fillOrderAsync(signedOrder, partialFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress); + await this.zeroEx.exchange.fillOrderAsync( + signedOrder, partialFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + ); return signedOrder; } private async createAsymmetricFillableSignedOrderWithFeesAsync( |