aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/contract_wrappers/exchange_wrapper.ts103
-rw-r--r--src/schemas/order_cancel_schema.ts12
-rw-r--r--src/schemas/order_fill_requests_schema.ts12
-rw-r--r--src/schemas/signed_orders_schema.ts5
-rw-r--r--src/types.ts9
-rw-r--r--src/utils/assert.ts3
-rw-r--r--test/exchange_wrapper_test.ts34
7 files changed, 157 insertions, 21 deletions
diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts
index b1b21f873..214a19df9 100644
--- a/src/contract_wrappers/exchange_wrapper.ts
+++ b/src/contract_wrappers/exchange_wrapper.ts
@@ -28,6 +28,9 @@ 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 {signedOrdersSchema} from '../schemas/signed_orders_schema';
+import {orderFillRequestsSchema} from '../schemas/order_fill_requests_schema';
+import {orderCancellationRequestsSchema} from '../schemas/order_cancel_schema';
import {orderFillOrKillRequestsSchema} from '../schemas/order_fill_or_kill_requests_schema';
import {signedOrderSchema, orderSchema} from '../schemas/order_schemas';
import {SchemaValidator} from '../utils/schema_validator';
@@ -161,6 +164,72 @@ export class ExchangeWrapper extends ContractWrapper {
this.throwErrorLogsAsErrors(response.logs);
}
/**
+ * Sequentially and atomically fills signedOrders up to the specified takerTokenFillAmount.
+ * If the fill amount is reached - it succeeds and does not fill the rest of the orders.
+ * If fill amount is not reached - it fills as much of the fill amount as possible and succeeds.
+ */
+ public async fillOrdersUpToAsync(signedOrders: SignedOrder[], takerTokenFillAmount: BigNumber.BigNumber,
+ shouldCheckTransfer: boolean, takerAddress: string): Promise<void> {
+ const takerTokenAddresses = _.map(signedOrders, signedOrder => signedOrder.takerTokenAddress);
+ assert.hasAtMostOneUniqueValue(takerTokenAddresses,
+ ExchangeContractErrs.MULTIPLE_TAKER_TOKENS_IN_FILL_UP_TO_DISALLOWED);
+ assert.isBigNumber('takerTokenFillAmount', takerTokenFillAmount);
+ assert.isBoolean('shouldCheckTransfer', shouldCheckTransfer);
+ assert.doesConformToSchema(
+ 'signedOrders', SchemaValidator.convertToJSONSchemaCompatibleObject(signedOrders), signedOrdersSchema
+ );
+ await assert.isSenderAddressAsync('takerAddress', takerAddress, this.web3Wrapper);
+ _.forEach(signedOrders,
+ async (signedOrder: SignedOrder, i: number) => {
+ 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.
@@ -170,12 +239,10 @@ export class ExchangeWrapper extends ContractWrapper {
shouldCheckTransfer: boolean, takerAddress: string): Promise<void> {
assert.isBoolean('shouldCheckTransfer', shouldCheckTransfer);
await assert.isSenderAddressAsync('takerAddress', takerAddress, this.web3Wrapper);
- _.forEach(orderFillRequests,
- async (orderFillRequest: OrderFillRequest, i: number) => {
- assert.doesConformToSchema(`orderFillRequests[${i}].signedOrder`,
- SchemaValidator.convertToJSONSchemaCompatibleObject(orderFillRequest.signedOrder as object),
- signedOrderSchema);
- assert.isBigNumber(`orderFillRequests[${i}].takerTokenFillAmount`, orderFillRequest.takerTokenFillAmount);
+ assert.doesConformToSchema('orderFillRequests',
+ SchemaValidator.convertToJSONSchemaCompatibleObject(orderFillRequests as object),
+ orderFillRequestsSchema);
+ _.forEach(orderFillRequests, async (orderFillRequest: OrderFillRequest) => {
await this.validateFillOrderAndThrowIfInvalidAsync(
orderFillRequest.signedOrder, orderFillRequest.takerTokenFillAmount, takerAddress);
});
@@ -332,8 +399,8 @@ export class ExchangeWrapper extends ContractWrapper {
public async cancelOrderAsync(
order: Order|SignedOrder, takerTokenCancelAmount: BigNumber.BigNumber): Promise<void> {
assert.doesConformToSchema('order',
- SchemaValidator.convertToJSONSchemaCompatibleObject(order as object),
- orderSchema);
+ SchemaValidator.convertToJSONSchemaCompatibleObject(order),
+ orderSchema);
assert.isBigNumber('takerTokenCancelAmount', takerTokenCancelAmount);
await assert.isSenderAddressAsync('order.maker', order.maker, this.web3Wrapper);
@@ -365,25 +432,21 @@ 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.hasAtMostOneUniqueValue(makers, ExchangeContractErrs.MULTIPLE_MAKERS_IN_SINGLE_CANCEL_BATCH_DISALLOWED);
const maker = makers[0];
await assert.isSenderAddressAsync('maker', maker, this.web3Wrapper);
- _.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,
- );
+ assert.doesConformToSchema('orderCancellationRequests',
+ SchemaValidator.convertToJSONSchemaCompatibleObject(orderCancellationRequests),
+ orderCancellationRequestsSchema);
+ _.forEach(orderCancellationRequests, async (cancellationRequest: OrderCancellationRequest) => {
await this.validateCancelOrderAndThrowIfInvalidAsync(
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/schemas/order_cancel_schema.ts b/src/schemas/order_cancel_schema.ts
new file mode 100644
index 000000000..5cc8f745c
--- /dev/null
+++ b/src/schemas/order_cancel_schema.ts
@@ -0,0 +1,12 @@
+export const orderCancellationRequestsSchema = {
+ id: '/OrderCancellationRequests',
+ type: 'array',
+ items: {
+ properties: {
+ order: {$ref: '/orderSchema'},
+ takerTokenCancelAmount: {type: '/numberSchema'},
+ },
+ required: ['order', 'takerTokenCancelAmount'],
+ type: 'object',
+ },
+};
diff --git a/src/schemas/order_fill_requests_schema.ts b/src/schemas/order_fill_requests_schema.ts
new file mode 100644
index 000000000..44f4e33e2
--- /dev/null
+++ b/src/schemas/order_fill_requests_schema.ts
@@ -0,0 +1,12 @@
+export const orderFillRequestsSchema = {
+ id: '/OrderFillRequests',
+ type: 'array',
+ items: {
+ properties: {
+ signedOrder: {$ref: '/signedOrderSchema'},
+ takerTokenFillAmount: {type: '/numberSchema'},
+ },
+ required: ['signedOrder', 'takerTokenFillAmount'],
+ type: 'object',
+ },
+};
diff --git a/src/schemas/signed_orders_schema.ts b/src/schemas/signed_orders_schema.ts
new file mode 100644
index 000000000..3b3b23649
--- /dev/null
+++ b/src/schemas/signed_orders_schema.ts
@@ -0,0 +1,5 @@
+export const signedOrdersSchema = {
+ id: '/signedOrdersSchema',
+ type: 'array',
+ items: {$ref: '/signedOrderSchema'},
+};
diff --git a/src/types.ts b/src/types.ts
index dc5a5e6b3..5237fdb1b 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;
@@ -163,8 +169,9 @@ 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',
+ 'MULTIPLE_MAKERS_IN_SINGLE_CANCEL_BATCH_DISALLOWED',
'INSUFFICIENT_REMAINING_FILL_AMOUNT',
+ 'MULTIPLE_TAKER_TOKENS_IN_FILL_UP_TO_DISALLOWED',
]);
export type ExchangeContractErrs = keyof typeof ExchangeContractErrs;
diff --git a/src/utils/assert.ts b/src/utils/assert.ts
index d93ac71f7..94b119d5a 100644
--- a/src/utils/assert.ts
+++ b/src/utils/assert.ts
@@ -38,6 +38,9 @@ export const assert = {
const availableAddresses = await web3Wrapper.getAvailableAddressesAsync();
this.assert(!_.isEmpty(availableAddresses), 'No addresses were available on the provided web3 instance');
},
+ hasAtMostOneUniqueValue(value: any[], errMsg: string): void {
+ this.assert(_.uniq(value).length <= 1, errMsg);
+ },
isNumber(variableName: string, value: number): void {
this.assert(_.isFinite(value), this.typeAssertionMessage(variableName, 'number', value));
},
diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts
index 2cd8af7f0..f159de759 100644
--- a/test/exchange_wrapper_test.ts
+++ b/test/exchange_wrapper_test.ts
@@ -392,6 +392,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 = fillableAmount.minus(1);
+ expect(anotherFilledAmount).to.be.bignumber.equal(remainingFillAmount);
+ });
+ });
+ });
});
describe('cancel order(s)', () => {
let makerTokenAddress: string;