diff options
-rw-r--r-- | src/contract_wrappers/exchange_wrapper.ts | 58 | ||||
-rw-r--r-- | src/types.ts | 12 | ||||
-rw-r--r-- | test/exchange_wrapper_test.ts | 95 |
3 files changed, 140 insertions, 25 deletions
diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index 4f132656e..d144d8aad 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -19,6 +19,7 @@ import { ContractEventObj, EventCallback, ContractResponse, + OrderCancellationRequest, } from '../types'; import {assert} from '../utils/assert'; import {utils} from '../utils/utils'; @@ -226,8 +227,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); @@ -258,6 +259,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 cc145dc2e..00ef17189 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; @@ -145,6 +151,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; @@ -222,3 +229,8 @@ export interface SubscriptionOpts { } export type DoneCallback = (err?: Error) => void; + +export interface OrderCancellationRequest { + order: Order|SignedOrder; + takerTokenCancelAmount: BigNumber.BigNumber; +} diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts index 1186345be..08936f1d2 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'; @@ -421,7 +422,7 @@ describe('ExchangeWrapper', () => { }); }); }); - describe('#cancelOrderAsync', () => { + describe('cancel order(s)', () => { let makerTokenAddress: string; let takerTokenAddress: string; let coinbase: string; @@ -441,32 +442,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); + }); }); }); }); |