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 # CHANGELOG
v0.17.0 - _September 26, 2017_ v0.17.0 - _September 26, 2017_
* Added `zeroEx.exchange.validateOrderFillableOrThrowAsync` to simplify orderbook pruning (#170)
* Made `zeroEx.exchange.getZRXTokenAddressAsync` public (#171) * Made `zeroEx.exchange.getZRXTokenAddressAsync` public (#171)
v0.16.0 - _September 20, 2017_ v0.16.0 - _September 20, 2017_

View File

@@ -28,6 +28,7 @@ import {
LogCancelContractEventArgs, LogCancelContractEventArgs,
LogWithDecodedArgs, LogWithDecodedArgs,
MethodOpts, MethodOpts,
ValidateOrderFillableOpts,
} from '../types'; } from '../types';
import {assert} from '../utils/assert'; import {assert} from '../utils/assert';
import {utils} from '../utils/utils'; import {utils} from '../utils/utils';
@@ -623,6 +624,25 @@ export class ExchangeWrapper extends ContractWrapper {
const exchangeAddress = exchangeInstance.address; const exchangeAddress = exchangeInstance.address;
return exchangeAddress; 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. * Checks if order fill will succeed and throws an error otherwise.
* @param signedOrder An object that conforms to the SignedOrder interface. The * @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 * 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 * 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 {ExchangeContractErrs, SignedOrder, Order, ZeroExError} from '../types';
import {ZeroEx} from '../0x'; import {ZeroEx} from '../0x';
import {TokenWrapper} from '../contract_wrappers/token_wrapper'; import {TokenWrapper} from '../contract_wrappers/token_wrapper';
@@ -12,6 +13,23 @@ export class OrderValidationUtils {
this.tokenWrapper = tokenWrapper; this.tokenWrapper = tokenWrapper;
this.exchangeWrapper = exchangeWrapper; 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, public async validateFillOrderThrowIfInvalidAsync(signedOrder: SignedOrder,
fillTakerTokenAmount: BigNumber.BigNumber, fillTakerTokenAmount: BigNumber.BigNumber,
takerAddress: string, takerAddress: string,
@@ -24,16 +42,13 @@ export class OrderValidationUtils {
throw new Error(ZeroExError.InvalidSignature); throw new Error(ZeroExError.InvalidSignature);
} }
const unavailableTakerTokenAmount = await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHash); const unavailableTakerTokenAmount = await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHash);
if (signedOrder.makerTokenAmount.eq(unavailableTakerTokenAmount)) { this.validateRemainingFillAmountNotZeroOrThrow(
throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); signedOrder.takerTokenAmount, unavailableTakerTokenAmount,
} );
if (signedOrder.taker !== constants.NULL_ADDRESS && signedOrder.taker !== takerAddress) { if (signedOrder.taker !== constants.NULL_ADDRESS && signedOrder.taker !== takerAddress) {
throw new Error(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker); throw new Error(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker);
} }
const currentUnixTimestampSec = utils.getCurrentUnixTimestamp(); this.validateOrderNotExpiredOrThrow(signedOrder.expirationUnixTimestampSec);
if (signedOrder.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) {
throw new Error(ExchangeContractErrs.OrderFillExpired);
}
await this.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( await this.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress, 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 () => { afterEach(async () => {
await blockchainLifecycle.revertAsync(); 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', () => { describe('validateFillOrderAndThrowIfInvalidAsync', () => {
it('should throw when the fill amount is zero', async () => { it('should throw when the fill amount is zero', async () => {
const signedOrder = await fillScenarios.createFillableSignedOrderAsync( const signedOrder = await fillScenarios.createFillableSignedOrderAsync(

View File

@@ -62,7 +62,9 @@ export class FillScenarios {
fillableAmount, fillableAmount, fillableAmount, fillableAmount,
); );
const shouldThrowOnInsufficientBalanceOrAllowance = false; const shouldThrowOnInsufficientBalanceOrAllowance = false;
await this.zeroEx.exchange.fillOrderAsync(signedOrder, partialFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress); await this.zeroEx.exchange.fillOrderAsync(
signedOrder, partialFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
);
return signedOrder; return signedOrder;
} }
private async createAsymmetricFillableSignedOrderWithFeesAsync( private async createAsymmetricFillableSignedOrderWithFeesAsync(