diff options
-rw-r--r-- | src/contract_wrappers/exchange_wrapper.ts | 74 | ||||
-rw-r--r-- | src/types.ts | 7 | ||||
-rw-r--r-- | test/exchange_wrapper_test.ts | 34 |
3 files changed, 111 insertions, 4 deletions
diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index 72ae0d07e..afaa89c94 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -176,6 +176,72 @@ export class ExchangeWrapper extends ContractWrapper { this.throwErrorLogsAsErrors(response.logs); } /** + * Sequentially and atomically fills signedOrders up to takerTokenFillAmount. + * If fill amount is reached - it succeeds and doesn't fill the rest of the orders. + * If fill amount is not reached - it just fills all the orders. + */ + public async fillOrdersUpToAsync(signedOrders: SignedOrder[], takerTokenFillAmount: BigNumber.BigNumber, + shouldCheckTransfer: boolean, takerAddress: string): Promise<void> { + const takerTokenAddresses = _.map(signedOrders, signedOrder => signedOrder.takerTokenAddress); + assert.assert(_.uniq(takerTokenAddresses).length <= 1, + ExchangeContractErrs.MULTIPLE_TAKER_TOKENS_IN_FILL_UP_TO); + assert.isBigNumber('takerTokenFillAmount', takerTokenFillAmount); + assert.isBoolean('shouldCheckTransfer', shouldCheckTransfer); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this.web3Wrapper); + _.forEach(signedOrders, + async (signedOrder: SignedOrder, i: number) => { + assert.doesConformToSchema(`signedOrder[${i}]`, + SchemaValidator.convertToJSONSchemaCompatibleObject(signedOrder as object), + signedOrderSchema); + await this.validateFillOrderAndThrowIfInvalidAsync( + signedOrder, takerTokenFillAmount, takerAddress); + }); + if (_.isEmpty(signedOrders)) { + return; // no-op + } + + const orderAddressesValuesAndSignatureArray = _.map(signedOrders, signedOrder => { + return [ + ...ExchangeWrapper.getOrderAddressesAndValues(signedOrder), + signedOrder.ecSignature.v, + signedOrder.ecSignature.r, + signedOrder.ecSignature.s, + ]; + }); + // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'( + const [orderAddressesArray, orderValuesArray, vArray, rArray, sArray] = _.unzip<any>( + orderAddressesValuesAndSignatureArray, + ); + + const exchangeInstance = await this.getExchangeContractAsync(); + const gas = await exchangeInstance.fillUpTo.estimateGas( + orderAddressesArray, + orderValuesArray, + takerTokenFillAmount, + shouldCheckTransfer, + vArray, + rArray, + sArray, + { + from: takerAddress, + }, + ); + const response: ContractResponse = await exchangeInstance.fillUpTo( + orderAddressesArray, + orderValuesArray, + takerTokenFillAmount, + shouldCheckTransfer, + vArray, + rArray, + sArray, + { + from: takerAddress, + gas, + }, + ); + this.throwErrorLogsAsErrors(response.logs); + } + /** * Batch version of fillOrderAsync. * Executes multiple fills atomically in a single transaction. * If shouldCheckTransfer is set to true, it will continue filling subsequent orders even when earlier ones fail. @@ -329,11 +395,8 @@ export class ExchangeWrapper extends ContractWrapper { * 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); + assert.assert(_.uniq(makers).length <= 1, ExchangeContractErrs.MULTIPLE_MAKERS_IN_SINGLE_CANCEL_BATCH); const maker = makers[0]; await assert.isSenderAddressAsync('maker', maker, this.web3Wrapper); _.forEach(orderCancellationRequests, @@ -348,6 +411,9 @@ export class ExchangeWrapper extends ContractWrapper { cancellationRequest.order, cancellationRequest.takerTokenCancelAmount, ); }); + if (_.isEmpty(orderCancellationRequests)) { + return; // no-op + } const exchangeInstance = await this.getExchangeContractAsync(); const orderAddressesValuesAndTakerTokenCancelAmounts = _.map(orderCancellationRequests, cancellationRequest => { return [ diff --git a/src/types.ts b/src/types.ts index edd7f2d33..2dabfaa3c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -74,6 +74,12 @@ export interface ExchangeContract extends ContractInstance { estimateGas: (orderAddresses: OrderAddresses[], orderValues: OrderValues[], fillAmounts: BigNumber.BigNumber[], shouldCheckTransfer: boolean, v: number[], r: string[], s: string[], txOpts?: TxOpts) => number; }; + fillUpTo: { + (orderAddresses: OrderAddresses[], orderValues: OrderValues[], fillAmount: BigNumber.BigNumber, + shouldCheckTransfer: boolean, v: number[], r: string[], s: string[], txOpts?: TxOpts): ContractResponse; + estimateGas: (orderAddresses: OrderAddresses[], orderValues: OrderValues[], fillAmount: BigNumber.BigNumber, + shouldCheckTransfer: boolean, v: number[], r: string[], s: string[], txOpts?: TxOpts) => number; + }; cancel: { (orderAddresses: OrderAddresses, orderValues: OrderValues, cancelAmount: BigNumber.BigNumber, txOpts?: TxOpts): ContractResponse; @@ -159,6 +165,7 @@ export const ExchangeContractErrs = strEnum([ 'TRANSACTION_SENDER_IS_NOT_FILL_ORDER_TAKER', 'MULTIPLE_MAKERS_IN_SINGLE_CANCEL_BATCH', 'INSUFFICIENT_REMAINING_FILL_AMOUNT', + 'MULTIPLE_TAKER_TOKENS_IN_FILL_UP_TO', ]); export type ExchangeContractErrs = keyof typeof ExchangeContractErrs; diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts index e3d8be1c0..4a1dbaed6 100644 --- a/test/exchange_wrapper_test.ts +++ b/test/exchange_wrapper_test.ts @@ -367,6 +367,40 @@ describe('ExchangeWrapper', () => { }); }); }); + describe('#fillOrdersUpTo', () => { + let signedOrder: SignedOrder; + let signedOrderHashHex: string; + let anotherSignedOrder: SignedOrder; + let anotherOrderHashHex: string; + let signedOrders: SignedOrder[]; + const fillUpToAmount = fillableAmount.plus(fillableAmount).minus(1); + beforeEach(async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + signedOrderHashHex = await zeroEx.getOrderHashHexAsync(signedOrder); + anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + anotherOrderHashHex = await zeroEx.getOrderHashHexAsync(anotherSignedOrder); + signedOrders = [signedOrder, anotherSignedOrder]; + }); + describe('successful batch fills', () => { + it('should no-op for an empty batch', async () => { + await zeroEx.exchange.fillOrdersUpToAsync([], fillUpToAmount, shouldCheckTransfer, takerAddress); + }); + it('should successfully fill up to specified amount', async () => { + await zeroEx.exchange.fillOrdersUpToAsync( + signedOrders, fillUpToAmount, shouldCheckTransfer, takerAddress, + ); + const filledAmount = await zeroEx.exchange.getFilledTakerAmountAsync(signedOrderHashHex); + const anotherFilledAmount = await zeroEx.exchange.getFilledTakerAmountAsync(anotherOrderHashHex); + expect(filledAmount).to.be.bignumber.equal(fillableAmount); + const remainingFillAmount = fillUpToAmount.minus(filledAmount); + expect(anotherFilledAmount).to.be.bignumber.equal(remainingFillAmount); + }); + }); + }); }); describe('cancel order(s)', () => { let makerTokenAddress: string; |