Merge pull request #170 from 0xProject/addOrderValidation
Add validateOrderFillableOrThrowAsync Method
This commit is contained in:
		@@ -1,6 +1,7 @@
 | 
			
		||||
# CHANGELOG
 | 
			
		||||
 | 
			
		||||
v0.17.0 - _September 26, 2017_
 | 
			
		||||
    * Added `zeroEx.exchange.validateOrderFillableOrThrowAsync` to simplify orderbook pruning (#170)
 | 
			
		||||
    * Made `zeroEx.exchange.getZRXTokenAddressAsync` public (#171)
 | 
			
		||||
 | 
			
		||||
v0.16.0 - _September 20, 2017_
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,7 @@ import {
 | 
			
		||||
    LogCancelContractEventArgs,
 | 
			
		||||
    LogWithDecodedArgs,
 | 
			
		||||
    MethodOpts,
 | 
			
		||||
    ValidateOrderFillableOpts,
 | 
			
		||||
} from '../types';
 | 
			
		||||
import {assert} from '../utils/assert';
 | 
			
		||||
import {utils} from '../utils/utils';
 | 
			
		||||
@@ -623,6 +624,25 @@ export class ExchangeWrapper extends ContractWrapper {
 | 
			
		||||
        const exchangeAddress = exchangeInstance.address;
 | 
			
		||||
        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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								src/types.ts
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								src/types.ts
									
									
									
									
									
								
							@@ -433,6 +433,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
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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(4);
 | 
			
		||||
            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(
 | 
			
		||||
 
 | 
			
		||||
@@ -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(
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user