aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/contract_wrappers/exchange_wrapper.ts113
-rw-r--r--src/schemas/basic_type_schemas.ts11
-rw-r--r--src/schemas/order_fill_or_kill_requests_schema.ts12
-rw-r--r--src/schemas/order_schemas.ts12
-rw-r--r--src/types.ts11
-rw-r--r--src/utils/schema_validator.ts7
-rw-r--r--test/exchange_wrapper_test.ts127
-rw-r--r--test/schema_test.ts2
8 files changed, 205 insertions, 90 deletions
diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts
index 72ae0d07e..73ec0886c 100644
--- a/src/contract_wrappers/exchange_wrapper.ts
+++ b/src/contract_wrappers/exchange_wrapper.ts
@@ -10,6 +10,7 @@ import {
OrderValues,
OrderAddresses,
Order,
+ OrderFillOrKillRequest,
SignedOrder,
ContractEvent,
ExchangeEvents,
@@ -27,6 +28,7 @@ import {utils} from '../utils/utils';
import {ContractWrapper} from './contract_wrapper';
import * as ExchangeArtifacts from '../artifacts/Exchange.json';
import {ecSignatureSchema} from '../schemas/ec_signature_schema';
+import {orderFillOrKillRequestsSchema} from '../schemas/order_fill_or_kill_requests_schema';
import {signedOrderSchema, orderSchema} from '../schemas/order_schemas';
import {SchemaValidator} from '../utils/schema_validator';
import {constants} from '../utils/constants';
@@ -71,23 +73,6 @@ export class ExchangeWrapper extends ContractWrapper {
await this.stopWatchingExchangeLogEventsAsync();
delete this.exchangeContractIfExists;
}
- private async isValidSignatureUsingContractCallAsync(dataHex: string, ecSignature: ECSignature,
- signerAddressHex: string): Promise<boolean> {
- assert.isHexString('dataHex', dataHex);
- assert.doesConformToSchema('ecSignature', ecSignature, ecSignatureSchema);
- assert.isETHAddressHex('signerAddressHex', signerAddressHex);
-
- const exchangeInstance = await this.getExchangeContractAsync();
-
- const isValidSignature = await exchangeInstance.isValidSignature.call(
- signerAddressHex,
- dataHex,
- ecSignature.v,
- ecSignature.r,
- ecSignature.s,
- );
- return isValidSignature;
- }
/**
* Returns the unavailable takerAmount of an order. Unavailable amount is defined as the total
* amount that has been filled or cancelled. The remaining takerAmount can be calculated by
@@ -255,13 +240,8 @@ export class ExchangeWrapper extends ContractWrapper {
const exchangeInstance = await this.getExchangeContractAsync();
await this.validateFillOrderAndThrowIfInvalidAsync(signedOrder, fillTakerAmount, takerAddress);
- // Check that fillValue available >= fillTakerAmount
- const orderHashHex = await this.getOrderHashHexAsync(signedOrder);
- const unavailableTakerAmount = await this.getUnavailableTakerAmountAsync(orderHashHex);
- const remainingTakerAmount = signedOrder.takerTokenAmount.minus(unavailableTakerAmount);
- if (remainingTakerAmount < fillTakerAmount) {
- throw new Error(ExchangeContractErrs.INSUFFICIENT_REMAINING_FILL_AMOUNT);
- }
+ await this.validateFillOrKillOrderAndThrowIfInvalidAsync(signedOrder, exchangeInstance.address,
+ fillTakerAmount);
const [orderAddresses, orderValues] = ExchangeWrapper.getOrderAddressesAndValues(signedOrder);
@@ -291,6 +271,63 @@ export class ExchangeWrapper extends ContractWrapper {
this.throwErrorLogsAsErrors(response.logs);
}
/**
+ * Batch version of fillOrKill. Allows a taker to specify a batch of orders that will either be atomically
+ * filled (each to the specified fillAmount) or aborted.
+ */
+ public async batchFillOrKillAsync(orderFillOrKillRequests: OrderFillOrKillRequest[],
+ takerAddress: string) {
+ await assert.isSenderAddressAsync('takerAddress', takerAddress, this.web3Wrapper);
+ assert.doesConformToSchema('orderFillOrKillRequests',
+ SchemaValidator.convertToJSONSchemaCompatibleObject(orderFillOrKillRequests),
+ orderFillOrKillRequestsSchema,
+ );
+ const exchangeInstance = await this.getExchangeContractAsync();
+ _.each(orderFillOrKillRequests, request => {
+ this.validateFillOrKillOrderAndThrowIfInvalidAsync(request.signedOrder,
+ exchangeInstance.address,
+ request.fillTakerAmount);
+ });
+
+ const orderAddressesValuesAndTakerTokenFillAmounts = _.map(orderFillOrKillRequests, request => {
+ return [
+ ...ExchangeWrapper.getOrderAddressesAndValues(request.signedOrder),
+ request.fillTakerAmount,
+ request.signedOrder.ecSignature.v,
+ request.signedOrder.ecSignature.r,
+ request.signedOrder.ecSignature.s,
+ ];
+ });
+
+ // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'(
+ const [orderAddresses, orderValues, fillTakerAmounts, vParams, rParams, sParams] =
+ _.unzip<any>(orderAddressesValuesAndTakerTokenFillAmounts);
+
+ const gas = await exchangeInstance.batchFillOrKill.estimateGas(
+ orderAddresses,
+ orderValues,
+ fillTakerAmounts,
+ vParams,
+ rParams,
+ sParams,
+ {
+ from: takerAddress,
+ },
+ );
+ const response: ContractResponse = await exchangeInstance.batchFillOrKill(
+ orderAddresses,
+ orderValues,
+ fillTakerAmounts,
+ vParams,
+ rParams,
+ sParams,
+ {
+ from: takerAddress,
+ gas,
+ },
+ );
+ this.throwErrorLogsAsErrors(response.logs);
+ }
+ /**
* Cancel a given fill amount of an order. Cancellations are cumulative.
*/
public async cancelOrderAsync(
@@ -403,6 +440,23 @@ export class ExchangeWrapper extends ContractWrapper {
logEventObj.watch(callback);
this.exchangeLogEventObjs.push(logEventObj);
}
+ private async isValidSignatureUsingContractCallAsync(dataHex: string, ecSignature: ECSignature,
+ signerAddressHex: string): Promise<boolean> {
+ assert.isHexString('dataHex', dataHex);
+ assert.doesConformToSchema('ecSignature', ecSignature, ecSignatureSchema);
+ assert.isETHAddressHex('signerAddressHex', signerAddressHex);
+
+ const exchangeInstance = await this.getExchangeContractAsync();
+
+ const isValidSignature = await exchangeInstance.isValidSignature.call(
+ signerAddressHex,
+ dataHex,
+ ecSignature.v,
+ ecSignature.r,
+ ecSignature.s,
+ );
+ return isValidSignature;
+ }
private async getOrderHashHexAsync(order: Order|SignedOrder): Promise<string> {
const exchangeInstance = await this.getExchangeContractAsync();
const orderHashHex = utils.getOrderHashHex(order, exchangeInstance.address);
@@ -460,6 +514,17 @@ export class ExchangeWrapper extends ContractWrapper {
throw new Error(ExchangeContractErrs.ORDER_CANCEL_EXPIRED);
}
}
+ private async validateFillOrKillOrderAndThrowIfInvalidAsync(signedOrder: SignedOrder,
+ exchangeAddress: string,
+ fillTakerAmount: BigNumber.BigNumber) {
+ // Check that fillValue available >= fillTakerAmount
+ const orderHashHex = utils.getOrderHashHex(signedOrder, exchangeAddress);
+ const unavailableTakerAmount = await this.getUnavailableTakerAmountAsync(orderHashHex);
+ const remainingTakerAmount = signedOrder.takerTokenAmount.minus(unavailableTakerAmount);
+ if (remainingTakerAmount < fillTakerAmount) {
+ throw new Error(ExchangeContractErrs.INSUFFICIENT_REMAINING_FILL_AMOUNT);
+ }
+ }
/**
* This method does not currently validate the edge-case where the makerToken or takerToken is also the token used
* to pay fees (ZRX). It is possible for them to have enough for fees and the transfer but not both.
diff --git a/src/schemas/basic_type_schemas.ts b/src/schemas/basic_type_schemas.ts
new file mode 100644
index 000000000..c3b81185d
--- /dev/null
+++ b/src/schemas/basic_type_schemas.ts
@@ -0,0 +1,11 @@
+export const addressSchema = {
+ id: '/addressSchema',
+ type: 'string',
+ pattern: '^0[xX][0-9A-Fa-f]{40}$',
+};
+
+export const numberSchema = {
+ id: '/numberSchema',
+ type: 'string',
+ pattern: '^\\d+(\\.\\d+)?$',
+};
diff --git a/src/schemas/order_fill_or_kill_requests_schema.ts b/src/schemas/order_fill_or_kill_requests_schema.ts
new file mode 100644
index 000000000..4db7113de
--- /dev/null
+++ b/src/schemas/order_fill_or_kill_requests_schema.ts
@@ -0,0 +1,12 @@
+export const orderFillOrKillRequestsSchema = {
+ id: '/OrderFillOrKillRequests',
+ type: 'array',
+ items: {
+ properties: {
+ signedOrder: {$ref: '/signedOrderSchema'},
+ fillTakerAmount: {type: '/numberSchema'},
+ },
+ required: ['signedOrder', 'fillTakerAmount'],
+ type: 'object',
+ },
+};
diff --git a/src/schemas/order_schemas.ts b/src/schemas/order_schemas.ts
index 4999f3e9d..133736b3d 100644
--- a/src/schemas/order_schemas.ts
+++ b/src/schemas/order_schemas.ts
@@ -1,15 +1,3 @@
-export const addressSchema = {
- id: '/addressSchema',
- type: 'string',
- pattern: '^0[xX][0-9A-Fa-f]{40}$',
-};
-
-export const numberSchema = {
- id: '/numberSchema',
- type: 'string',
- pattern: '^\\d+(\\.\\d+)?$',
-};
-
export const orderSchema = {
id: '/orderSchema',
properties: {
diff --git a/src/types.ts b/src/types.ts
index edd7f2d33..dc5a5e6b3 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -92,6 +92,12 @@ export interface ExchangeContract extends ContractInstance {
estimateGas: (orderAddresses: OrderAddresses, orderValues: OrderValues, fillAmount: BigNumber.BigNumber,
v: number, r: string, s: string, txOpts?: TxOpts) => number;
};
+ batchFillOrKill: {
+ (orderAddresses: OrderAddresses[], orderValues: OrderValues[], fillValuesT: BigNumber.BigNumber[],
+ v: number[], r: string[], s: string[], txOpts: TxOpts): ContractResponse;
+ estimateGas: (orderAddresses: OrderAddresses[], orderValues: OrderValues[], fillValuesT: BigNumber.BigNumber[],
+ v: number[], r: string[], s: string[], txOpts?: TxOpts) => number;
+ };
filled: {
call: (orderHash: string) => BigNumber.BigNumber;
};
@@ -236,6 +242,11 @@ export interface SubscriptionOpts {
export type DoneCallback = (err?: Error) => void;
+export interface OrderFillOrKillRequest {
+ signedOrder: SignedOrder;
+ fillTakerAmount: BigNumber.BigNumber;
+}
+
export interface OrderCancellationRequest {
order: Order|SignedOrder;
takerTokenCancelAmount: BigNumber.BigNumber;
diff --git a/src/utils/schema_validator.ts b/src/utils/schema_validator.ts
index 932ddf62a..72f6afffa 100644
--- a/src/utils/schema_validator.ts
+++ b/src/utils/schema_validator.ts
@@ -1,7 +1,9 @@
import {Validator, ValidatorResult} from 'jsonschema';
import {ecSignatureSchema, ecSignatureParameter} from '../schemas/ec_signature_schema';
-import {addressSchema, numberSchema, orderSchema, signedOrderSchema} from '../schemas/order_schemas';
+import {orderSchema, signedOrderSchema} from '../schemas/order_schemas';
+import {addressSchema, numberSchema} from '../schemas/basic_type_schemas';
import {tokenSchema} from '../schemas/token_schema';
+import {orderFillOrKillRequestsSchema} from '../schemas/order_fill_or_kill_requests_schema';
export class SchemaValidator {
private validator: Validator;
@@ -9,7 +11,7 @@ export class SchemaValidator {
// sub-types (e.g BigNumber) with a simpler string representation. Since BigNumber and other
// complex types implement the `toString` method, we can stringify the object and
// then parse it. The resultant object can then be checked using jsonschema.
- public static convertToJSONSchemaCompatibleObject(obj: object): object {
+ public static convertToJSONSchemaCompatibleObject(obj: any): any {
return JSON.parse(JSON.stringify(obj));
}
constructor() {
@@ -21,6 +23,7 @@ export class SchemaValidator {
this.validator.addSchema(ecSignatureSchema, ecSignatureSchema.id);
this.validator.addSchema(signedOrderSchema, signedOrderSchema.id);
this.validator.addSchema(ecSignatureParameter, ecSignatureParameter.id);
+ this.validator.addSchema(orderFillOrKillRequestsSchema, orderFillOrKillRequestsSchema.id);
}
public validate(instance: object, schema: Schema): ValidatorResult {
return this.validator.validate(instance, schema);
diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts
index e3d8be1c0..2cd8af7f0 100644
--- a/test/exchange_wrapper_test.ts
+++ b/test/exchange_wrapper_test.ts
@@ -52,7 +52,7 @@ describe('ExchangeWrapper', () => {
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
- describe('#fillOrKillOrderAsync', () => {
+ describe('fillOrKill order(s)', () => {
let makerTokenAddress: string;
let takerTokenAddress: string;
let coinbase: string;
@@ -67,63 +67,88 @@ describe('ExchangeWrapper', () => {
makerTokenAddress = makerToken.address;
takerTokenAddress = takerToken.address;
});
- describe('failed fillOrKill', () => {
- it('should throw if remaining fillAmount is less then the desired fillAmount', async () => {
+ describe('#batchFillOrKillAsync', () => {
+ it('successfuly batch fillOrKill', async () => {
const fillableAmount = new BigNumber(5);
+ const partialFillTakerAmount = new BigNumber(2);
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.INSUFFICIENT_REMAINING_FILL_AMOUNT);
- });
- });
- describe('successful fills', () => {
- it('should fill a valid order', async () => {
- const fillableAmount = new BigNumber(5);
- const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ const anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
);
- expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
- .to.be.bignumber.equal(fillableAmount);
- expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
- .to.be.bignumber.equal(0);
- expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
- .to.be.bignumber.equal(0);
- expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
- .to.be.bignumber.equal(fillableAmount);
- await zeroEx.exchange.fillOrKillOrderAsync(signedOrder, fillTakerAmount, takerAddress);
- expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
- .to.be.bignumber.equal(fillableAmount.minus(fillTakerAmount));
- expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
- .to.be.bignumber.equal(fillTakerAmount);
- expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
- .to.be.bignumber.equal(fillTakerAmount);
- expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
- .to.be.bignumber.equal(fillableAmount.minus(fillTakerAmount));
+ const orderFillOrKillRequests = [
+ {
+ signedOrder,
+ fillTakerAmount: partialFillTakerAmount,
+ },
+ {
+ signedOrder: anotherSignedOrder,
+ fillTakerAmount: partialFillTakerAmount,
+ },
+ ];
+ await zeroEx.exchange.batchFillOrKillAsync(orderFillOrKillRequests, takerAddress);
});
- it('should partially fill a valid order', async () => {
- const fillableAmount = new BigNumber(5);
- const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
- makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
- );
- const partialFillAmount = new BigNumber(3);
- await zeroEx.exchange.fillOrKillOrderAsync(signedOrder, partialFillAmount, takerAddress);
- expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
- .to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
- expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
- .to.be.bignumber.equal(partialFillAmount);
- expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
- .to.be.bignumber.equal(partialFillAmount);
- expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
- .to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
+ });
+ 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.INSUFFICIENT_REMAINING_FILL_AMOUNT);
+ });
+ });
+ describe('successful fills', () => {
+ it('should fill a valid order', async () => {
+ const fillableAmount = new BigNumber(5);
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(0);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(0);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillableAmount);
+ await zeroEx.exchange.fillOrKillOrderAsync(signedOrder, fillTakerAmount, takerAddress);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(fillTakerAmount));
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillTakerAmount);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillTakerAmount);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(fillTakerAmount));
+ });
+ it('should partially fill a valid order', async () => {
+ const fillableAmount = new BigNumber(5);
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ const partialFillAmount = new BigNumber(3);
+ await zeroEx.exchange.fillOrKillOrderAsync(signedOrder, partialFillAmount, takerAddress);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(partialFillAmount);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(partialFillAmount);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
+ });
});
});
});
diff --git a/test/schema_test.ts b/test/schema_test.ts
index 1c4216496..d35ed4516 100644
--- a/test/schema_test.ts
+++ b/test/schema_test.ts
@@ -3,7 +3,7 @@ import * as _ from 'lodash';
import * as chai from 'chai';
import * as BigNumber from 'bignumber.js';
import promisify = require('es6-promisify');
-import {numberSchema} from '../src/schemas/order_schemas';
+import {numberSchema} from '../src/schemas/basic_type_schemas';
import {SchemaValidator} from '../src/utils/schema_validator';
chai.config.includeStack = true;