Merge pull request #170 from 0xProject/addOrderValidation

Add validateOrderFillableOrThrowAsync Method
This commit is contained in:
Fabio Berger
2017-09-26 16:07:42 +02:00
committed by GitHub
6 changed files with 109 additions and 8 deletions

View File

@@ -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_

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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(

View File

@@ -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(