aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/contract_wrappers/exchange_wrapper.ts58
-rw-r--r--src/types.ts12
-rw-r--r--test/exchange_wrapper_test.ts95
3 files changed, 140 insertions, 25 deletions
diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts
index 901ea4749..74928def0 100644
--- a/src/contract_wrappers/exchange_wrapper.ts
+++ b/src/contract_wrappers/exchange_wrapper.ts
@@ -20,6 +20,7 @@ import {
ContractEventObj,
EventCallback,
ContractResponse,
+ OrderCancellationRequest,
} from '../types';
import {assert} from '../utils/assert';
import {utils} from '../utils/utils';
@@ -278,8 +279,8 @@ export class ExchangeWrapper extends ContractWrapper {
/**
* Cancel a given fill amount of an order. Cancellations are cumulative.
*/
- public async cancelOrderAsync(order: Order|SignedOrder, takerTokenCancelAmount: BigNumber.BigNumber):
- Promise<void> {
+ public async cancelOrderAsync(
+ order: Order|SignedOrder, takerTokenCancelAmount: BigNumber.BigNumber): Promise<void> {
assert.doesConformToSchema('order',
SchemaValidator.convertToJSONSchemaCompatibleObject(order as object),
orderSchema);
@@ -310,6 +311,59 @@ export class ExchangeWrapper extends ContractWrapper {
this.throwErrorLogsAsErrors(response.logs);
}
/**
+ * Batch version of cancelOrderAsync. Atomically cancels multiple orders in a single transaction.
+ * All orders must be from the same maker.
+ */
+ public async batchCancelOrderAsync(orderCancellationRequests: OrderCancellationRequest[]): Promise<void> {
+ if (_.isEmpty(orderCancellationRequests)) {
+ return; // no-op
+ }
+ const makers = _.map(orderCancellationRequests, cancellationRequest => cancellationRequest.order.maker);
+ assert.assert(_.uniq(makers).length === 1, ExchangeContractErrs.MULTIPLE_MAKERS_IN_SINGLE_CANCEL_BATCH);
+ const maker = makers[0];
+ await assert.isSenderAddressAvailableAsync(this.web3Wrapper, 'maker', maker);
+ _.forEach(orderCancellationRequests,
+ async (cancellationRequest: OrderCancellationRequest, i: number) => {
+ assert.doesConformToSchema(`orderCancellationRequests[${i}].order`,
+ SchemaValidator.convertToJSONSchemaCompatibleObject(cancellationRequest.order as object), orderSchema,
+ );
+ assert.isBigNumber(`orderCancellationRequests[${i}].takerTokenCancelAmount`,
+ cancellationRequest.takerTokenCancelAmount,
+ );
+ await this.validateCancelOrderAndThrowIfInvalidAsync(
+ cancellationRequest.order, cancellationRequest.takerTokenCancelAmount,
+ );
+ });
+ const exchangeInstance = await this.getExchangeContractAsync();
+ const orderAddressesValuesAndTakerTokenCancelAmounts = _.map(orderCancellationRequests, cancellationRequest => {
+ return [
+ ...ExchangeWrapper.getOrderAddressesAndValues(cancellationRequest.order),
+ cancellationRequest.takerTokenCancelAmount,
+ ];
+ });
+ // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'(
+ const [orderAddresses, orderValues, takerTokenCancelAmounts] =
+ _.unzip<any>(orderAddressesValuesAndTakerTokenCancelAmounts);
+ const gas = await exchangeInstance.batchCancel.estimateGas(
+ orderAddresses,
+ orderValues,
+ takerTokenCancelAmounts,
+ {
+ from: maker,
+ },
+ );
+ const response: ContractResponse = await exchangeInstance.batchCancel(
+ orderAddresses,
+ orderValues,
+ takerTokenCancelAmounts,
+ {
+ from: maker,
+ gas,
+ },
+ );
+ this.throwErrorLogsAsErrors(response.logs);
+ }
+ /**
* Subscribe to an event type emitted by the Exchange smart contract
*/
public async subscribeAsync(eventName: ExchangeEvents, subscriptionOpts: SubscriptionOpts,
diff --git a/src/types.ts b/src/types.ts
index d19fb2d43..55bab6282 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -74,6 +74,12 @@ export interface ExchangeContract extends ContractInstance {
estimateGas: (orderAddresses: OrderAddresses, orderValues: OrderValues, cancelAmount: BigNumber.BigNumber,
txOpts?: TxOpts) => number;
};
+ batchCancel: {
+ (orderAddresses: OrderAddresses[], orderValues: OrderValues[], cancelAmount: BigNumber.BigNumber[],
+ txOpts?: TxOpts): ContractResponse;
+ estimateGas: (orderAddresses: OrderAddresses[], orderValues: OrderValues[], cancelAmount: BigNumber.BigNumber[],
+ txOpts?: TxOpts) => number;
+ };
fillOrKill: {
(orderAddresses: OrderAddresses, orderValues: OrderValues, fillAmount: BigNumber.BigNumber,
v: number, r: string, s: string, txOpts?: TxOpts): ContractResponse;
@@ -151,6 +157,7 @@ export const ExchangeContractErrs = strEnum([
'INSUFFICIENT_MAKER_FEE_BALANCE',
'INSUFFICIENT_MAKER_FEE_ALLOWANCE',
'TRANSACTION_SENDER_IS_NOT_FILL_ORDER_TAKER',
+ 'MULTIPLE_MAKERS_IN_SINGLE_CANCEL_BATCH',
'INSUFFICIENT_REMAINING_FILL_AMOUNT',
]);
export type ExchangeContractErrs = keyof typeof ExchangeContractErrs;
@@ -233,3 +240,8 @@ export interface OrderFillOrKillRequest {
signedOrder: SignedOrder;
fillTakerAmount: BigNumber.BigNumber;
}
+
+export interface OrderCancellationRequest {
+ order: Order|SignedOrder;
+ takerTokenCancelAmount: BigNumber.BigNumber;
+}
diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts
index 4a004fbcf..586fbac02 100644
--- a/test/exchange_wrapper_test.ts
+++ b/test/exchange_wrapper_test.ts
@@ -17,6 +17,7 @@ import {
ContractEvent,
DoneCallback,
ExchangeContractErrs,
+ OrderCancellationRequest,
} from '../src/types';
import {FillScenarios} from './utils/fill_scenarios';
import {TokenUtils} from './utils/token_utils';
@@ -446,7 +447,7 @@ describe('ExchangeWrapper', () => {
});
});
});
- describe('#cancelOrderAsync', () => {
+ describe('cancel order(s)', () => {
let makerTokenAddress: string;
let takerTokenAddress: string;
let coinbase: string;
@@ -466,32 +467,80 @@ describe('ExchangeWrapper', () => {
);
orderHashHex = await zeroEx.getOrderHashHexAsync(signedOrder);
});
- describe('failed cancels', () => {
- it('should throw when cancel amount is zero', async () => {
- const zeroCancelAmount = new BigNumber(0);
- return expect(zeroEx.exchange.cancelOrderAsync(signedOrder, zeroCancelAmount))
- .to.be.rejectedWith(ExchangeContractErrs.ORDER_CANCEL_AMOUNT_ZERO);
+ describe('#cancelOrderAsync', () => {
+ describe('failed cancels', () => {
+ it('should throw when cancel amount is zero', async () => {
+ const zeroCancelAmount = new BigNumber(0);
+ return expect(zeroEx.exchange.cancelOrderAsync(signedOrder, zeroCancelAmount))
+ .to.be.rejectedWith(ExchangeContractErrs.ORDER_CANCEL_AMOUNT_ZERO);
+ });
+ it('should throw when order is expired', async () => {
+ const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017
+ const expiredSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress,
+ fillableAmount, expirationInPast,
+ );
+ orderHashHex = await zeroEx.getOrderHashHexAsync(expiredSignedOrder);
+ return expect(zeroEx.exchange.cancelOrderAsync(expiredSignedOrder, cancelAmount))
+ .to.be.rejectedWith(ExchangeContractErrs.ORDER_CANCEL_EXPIRED);
+ });
+ it('should throw when order is already cancelled or filled', async () => {
+ await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount);
+ return expect(zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount))
+ .to.be.rejectedWith(ExchangeContractErrs.ORDER_ALREADY_CANCELLED_OR_FILLED);
+ });
});
- it('should throw when order is expired', async () => {
- const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017
- const expiredSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
- makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, expirationInPast,
+ describe('successful cancels', () => {
+ it('should cancel an order', async () => {
+ await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmount);
+ const cancelledAmount = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHashHex);
+ expect(cancelledAmount).to.be.bignumber.equal(cancelAmount);
+ });
+ });
+ });
+ describe('#batchCancelOrderAsync', () => {
+ let anotherSignedOrder: SignedOrder;
+ let anotherOrderHashHex: string;
+ let cancelBatch: OrderCancellationRequest[];
+ beforeEach(async () => {
+ anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
);
- orderHashHex = await zeroEx.getOrderHashHexAsync(expiredSignedOrder);
- return expect(zeroEx.exchange.cancelOrderAsync(expiredSignedOrder, cancelAmount))
- .to.be.rejectedWith(ExchangeContractErrs.ORDER_CANCEL_EXPIRED);
+ anotherOrderHashHex = await zeroEx.getOrderHashHexAsync(anotherSignedOrder);
+ cancelBatch = [
+ {
+ order: signedOrder,
+ takerTokenCancelAmount: cancelAmount,
+ },
+ {
+ order: anotherSignedOrder,
+ takerTokenCancelAmount: cancelAmount,
+ },
+ ];
});
- it('should throw when order is already cancelled or filled', async () => {
- await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount);
- return expect(zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount))
- .to.be.rejectedWith(ExchangeContractErrs.ORDER_ALREADY_CANCELLED_OR_FILLED);
+ describe('failed batch cancels', () => {
+ it('should throw when orders have different makers', async () => {
+ const signedOrderWithDifferentMaker = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, takerAddress, takerAddress, fillableAmount,
+ );
+ return expect(zeroEx.exchange.batchCancelOrderAsync([
+ cancelBatch[0],
+ {
+ order: signedOrderWithDifferentMaker,
+ takerTokenCancelAmount: cancelAmount,
+ },
+ ])).to.be.rejectedWith(ExchangeContractErrs.MULTIPLE_MAKERS_IN_SINGLE_CANCEL_BATCH);
+ });
});
- });
- describe('successful cancels', () => {
- it('should cancel an order', async () => {
- await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmount);
- const cancelledAmount = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHashHex);
- expect(cancelledAmount).to.be.bignumber.equal(cancelAmount);
+ describe('successful batch cancels', () => {
+ it('should cancel a batch of orders', async () => {
+ await zeroEx.exchange.batchCancelOrderAsync(cancelBatch);
+ const cancelledAmount = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHashHex);
+ const anotherCancelledAmount = await zeroEx.exchange.getCanceledTakerAmountAsync(
+ anotherOrderHashHex);
+ expect(cancelledAmount).to.be.bignumber.equal(cancelAmount);
+ expect(anotherCancelledAmount).to.be.bignumber.equal(cancelAmount);
+ });
});
});
});