aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLeonid Logvinov <logvinov.leon@gmail.com>2017-06-08 17:31:40 +0800
committerLeonid Logvinov <logvinov.leon@gmail.com>2017-06-08 17:31:40 +0800
commitc1322717802d652090fb374ac706cb1abf6c26be (patch)
treeefc3905da0882f1e23b896f06a59e48122bd3532
parent5d464d87e07003c231fca55e87b95cc7e1e9e978 (diff)
parentc6645f9f4f470323086d9004cf603a68091d71a3 (diff)
downloaddexon-sol-tools-c1322717802d652090fb374ac706cb1abf6c26be.tar
dexon-sol-tools-c1322717802d652090fb374ac706cb1abf6c26be.tar.gz
dexon-sol-tools-c1322717802d652090fb374ac706cb1abf6c26be.tar.bz2
dexon-sol-tools-c1322717802d652090fb374ac706cb1abf6c26be.tar.lz
dexon-sol-tools-c1322717802d652090fb374ac706cb1abf6c26be.tar.xz
dexon-sol-tools-c1322717802d652090fb374ac706cb1abf6c26be.tar.zst
dexon-sol-tools-c1322717802d652090fb374ac706cb1abf6c26be.zip
Merge branch 'master' into batchFillAsync
-rw-r--r--src/0x.js.ts9
-rw-r--r--src/contract_wrappers/exchange_wrapper.ts131
-rw-r--r--src/types.ts9
-rw-r--r--test/0x.js_test.ts2
-rw-r--r--test/exchange_wrapper_test.ts84
5 files changed, 198 insertions, 37 deletions
diff --git a/src/0x.js.ts b/src/0x.js.ts
index 7b53b70ea..8f1178b2a 100644
--- a/src/0x.js.ts
+++ b/src/0x.js.ts
@@ -130,11 +130,12 @@ export class ZeroEx {
* Computes the orderHash for a given order and returns it as a hex encoded string.
*/
public async getOrderHashHexAsync(order: Order|SignedOrder): Promise<string> {
- assert.doesConformToSchema(
- 'order', SchemaValidator.convertToJSONSchemaCompatibleObject(order as object), orderSchema);
+ assert.doesConformToSchema('order', SchemaValidator.convertToJSONSchemaCompatibleObject(order as object),
+ orderSchema);
+
const exchangeContractAddr = await this.getExchangeAddressAsync();
- const hashHex = utils.getOrderHashHex(order, exchangeContractAddr);
- return hashHex;
+ const orderHashHex = utils.getOrderHashHex(order, exchangeContractAddr);
+ return orderHashHex;
}
/**
* Signs an orderHash and returns it's elliptic curve signature
diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts
index dffb9df13..8d95c9fae 100644
--- a/src/contract_wrappers/exchange_wrapper.ts
+++ b/src/contract_wrappers/exchange_wrapper.ts
@@ -18,7 +18,9 @@ import {
CreateContractEvent,
ContractEventObj,
EventCallback,
- ContractResponse, OrderCancellationRequest, OrderFillRequest,
+ ContractResponse,
+ OrderCancellationRequest,
+ OrderFillRequest,
} from '../types';
import {assert} from '../utils/assert';
import {utils} from '../utils/utils';
@@ -235,41 +237,121 @@ export class ExchangeWrapper extends ContractWrapper {
this.throwErrorLogsAsErrors(response.logs);
}
/**
+ * Attempts to fill a specific amount of an order. If the entire amount specified cannot be filled,
+ * the fill order is abandoned.
+ */
+ public async fillOrKillOrderAsync(signedOrder: SignedOrder, fillTakerAmount: BigNumber.BigNumber,
+ takerAddress: string) {
+ assert.doesConformToSchema('signedOrder',
+ SchemaValidator.convertToJSONSchemaCompatibleObject(signedOrder as object),
+ signedOrderSchema);
+ assert.isBigNumber('fillTakerAmount', fillTakerAmount);
+ await assert.isSenderAddressAsync('takerAddress', takerAddress, this.web3Wrapper);
+
+ 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);
+ }
+
+ const [orderAddresses, orderValues] = ExchangeWrapper.getOrderAddressesAndValues(signedOrder);
+
+ const gas = await exchangeInstance.fillOrKill.estimateGas(
+ orderAddresses,
+ orderValues,
+ fillTakerAmount,
+ signedOrder.ecSignature.v,
+ signedOrder.ecSignature.r,
+ signedOrder.ecSignature.s,
+ {
+ from: takerAddress,
+ },
+ );
+ const response: ContractResponse = await exchangeInstance.fillOrKill(
+ orderAddresses,
+ orderValues,
+ fillTakerAmount,
+ signedOrder.ecSignature.v,
+ signedOrder.ecSignature.r,
+ signedOrder.ecSignature.s,
+ {
+ from: takerAddress,
+ gas,
+ },
+ );
+ this.throwErrorLogsAsErrors(response.logs);
+ }
+ /**
* Cancel a given fill amount of an order. Cancellations are cumulative.
*/
public async cancelOrderAsync(
order: Order|SignedOrder, takerTokenCancelAmount: BigNumber.BigNumber): Promise<void> {
- await this.batchCancelOrderAsync([{
- order,
+ assert.doesConformToSchema('order',
+ SchemaValidator.convertToJSONSchemaCompatibleObject(order as object),
+ orderSchema);
+ assert.isBigNumber('takerTokenCancelAmount', takerTokenCancelAmount);
+ await assert.isSenderAddressAvailableAsync(this.web3Wrapper, 'order.maker', order.maker);
+
+ const exchangeInstance = await this.getExchangeContractAsync();
+ await this.validateCancelOrderAndThrowIfInvalidAsync(order, takerTokenCancelAmount);
+
+ const [orderAddresses, orderValues] = ExchangeWrapper.getOrderAddressesAndValues(order);
+ const gas = await exchangeInstance.cancel.estimateGas(
+ orderAddresses,
+ orderValues,
+ takerTokenCancelAmount,
+ {
+ from: order.maker,
+ },
+ );
+ const response: ContractResponse = await exchangeInstance.cancel(
+ orderAddresses,
+ orderValues,
takerTokenCancelAmount,
- }]);
+ {
+ from: order.maker,
+ gas,
+ },
+ );
+ 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(cancellationRequestsBatch: OrderCancellationRequest[]): Promise<void> {
- const makers = _.map(cancellationRequestsBatch, cancellationRequest => cancellationRequest.order.maker);
- assert.assert(!_.isEmpty(cancellationRequestsBatch), 'Can not cancel an empty batch');
- assert.assert(_.uniq(makers).length === 1, 'Can not cancel orders from multiple makers in a single batch');
+ 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];
- _.forEach(cancellationRequestsBatch,
- async (cancellationRequest: OrderCancellationRequest) => {
- assert.doesConformToSchema('order',
- SchemaValidator.convertToJSONSchemaCompatibleObject(cancellationRequest.order as object), orderSchema);
- assert.isBigNumber('takerTokenCancelAmount', cancellationRequest.takerTokenCancelAmount);
- await assert.isSenderAddressAvailableAsync(this.web3Wrapper, 'order.maker',
- cancellationRequest.order.maker);
+ 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);
+ cancellationRequest.order, cancellationRequest.takerTokenCancelAmount,
+ );
});
const exchangeInstance = await this.getExchangeContractAsync();
- const orderAddressesValuesAndTakerTokenCancelAmounts = _.map(cancellationRequestsBatch, cancellationRequest => {
+ const orderAddressesValuesAndTakerTokenCancelAmounts = _.map(orderCancellationRequests, cancellationRequest => {
return [
...ExchangeWrapper.getOrderAddressesAndValues(cancellationRequest.order),
cancellationRequest.takerTokenCancelAmount,
];
});
- // _.unzip doesn't type check if values have different types :'(
+ // 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(
@@ -317,11 +399,10 @@ export class ExchangeWrapper extends ContractWrapper {
logEventObj.watch(callback);
this.exchangeLogEventObjs.push(logEventObj);
}
- private async getOrderHashAsync(order: Order|SignedOrder): Promise<string> {
- const [orderAddresses, orderValues] = ExchangeWrapper.getOrderAddressesAndValues(order);
+ private async getOrderHashHexAsync(order: Order|SignedOrder): Promise<string> {
const exchangeInstance = await this.getExchangeContractAsync();
- const orderHash = utils.getOrderHashHex(order, exchangeInstance.address);
- return orderHash;
+ const orderHashHex = utils.getOrderHashHex(order, exchangeInstance.address);
+ return orderHashHex;
}
private async stopWatchingExchangeLogEventsAsync() {
const stopWatchingPromises = _.map(this.exchangeLogEventObjs, logEventObj => {
@@ -359,7 +440,7 @@ export class ExchangeWrapper extends ContractWrapper {
if (takerTokenCancelAmount.eq(0)) {
throw new Error(ExchangeContractErrs.ORDER_CANCEL_AMOUNT_ZERO);
}
- const orderHash = await this.getOrderHashAsync(order);
+ const orderHash = await this.getOrderHashHexAsync(order);
const unavailableAmount = await this.getUnavailableTakerAmountAsync(orderHash);
if (order.takerTokenAmount.minus(unavailableAmount).eq(0)) {
throw new Error(ExchangeContractErrs.ORDER_ALREADY_CANCELLED_OR_FILLED);
@@ -375,8 +456,8 @@ export class ExchangeWrapper extends ContractWrapper {
* Handling the edge-cases that arise when this happens would require making sure that the user has sufficient
* funds to pay both the fees and the transfer amount. We decided to punt on this for now as the contracts
* will throw for these edge-cases.
- * TODO: Throw errors before calling the smart contract for these edge-cases
- * TODO: in order to minimize the callers gas costs.
+ * TODO: Throw errors before calling the smart contract for these edge-cases in order to minimize
+ * the callers gas costs.
*/
private async validateFillOrderBalancesAndAllowancesAndThrowIfInvalidAsync(signedOrder: SignedOrder,
fillTakerAmount: BigNumber.BigNumber,
diff --git a/src/types.ts b/src/types.ts
index f3cfa440c..90e64bc46 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -86,6 +86,12 @@ export interface ExchangeContract extends ContractInstance {
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;
+ estimateGas: (orderAddresses: OrderAddresses, orderValues: OrderValues, fillAmount: BigNumber.BigNumber,
+ v: number, r: string, s: string, txOpts?: TxOpts) => number;
+ };
filled: {
call: (orderHash: string) => BigNumber.BigNumber;
};
@@ -151,7 +157,8 @@ 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;
diff --git a/test/0x.js_test.ts b/test/0x.js_test.ts
index 5096d5df2..58f259a11 100644
--- a/test/0x.js_test.ts
+++ b/test/0x.js_test.ts
@@ -151,7 +151,7 @@ describe('ZeroEx library', () => {
expect(baseUnitAmount).to.be.bignumber.equal(expectedUnitAmount);
});
});
- describe('#getOrderHashAsync', () => {
+ describe('#getOrderHashHexAsync', () => {
const exchangeContractAddress = constants.NULL_ADDRESS;
const expectedOrderHash = '0x103a5e97dab5dbeb8f385636f86a7d1e458a7ccbe1bd194727f0b2f85ab116c7';
const order: Order = {
diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts
index bd2536741..2d7810bcc 100644
--- a/test/exchange_wrapper_test.ts
+++ b/test/exchange_wrapper_test.ts
@@ -122,6 +122,81 @@ describe('ExchangeWrapper', () => {
expect(isValid).to.be.true();
});
});
+ describe('#fillOrKillOrderAsync', () => {
+ let makerTokenAddress: string;
+ let takerTokenAddress: string;
+ let coinbase: string;
+ let makerAddress: string;
+ let takerAddress: string;
+ let feeRecipient: string;
+ const fillTakerAmount = new BigNumber(5);
+ before(async () => {
+ [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses;
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ });
+ 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));
+ });
+ });
+ });
describe('fill order(s)', () => {
let makerTokenAddress: string;
let takerTokenAddress: string;
@@ -134,6 +209,7 @@ describe('ExchangeWrapper', () => {
const shouldCheckTransfer = false;
before(async () => {
[coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses;
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
makerTokenAddress = makerToken.address;
takerTokenAddress = takerToken.address;
@@ -434,18 +510,14 @@ describe('ExchangeWrapper', () => {
];
});
describe('failed batch cancels', () => {
- it('should throw when orders are empty', async () => {
- return expect(zeroEx.exchange.batchCancelOrderAsync([]))
- .to.be.rejectedWith('Can not cancel an empty batch');
- });
it('should throw when orders have different makers', async () => {
- const signedOrderWithADifferentMaker = await fillScenarios.createFillableSignedOrderAsync(
+ const signedOrderWithDifferentMaker = await fillScenarios.createFillableSignedOrderAsync(
makerTokenAddress, takerTokenAddress, takerAddress, takerAddress, fillableAmount,
);
return expect(zeroEx.exchange.batchCancelOrderAsync([
cancelBatch[0],
{
- order: signedOrderWithADifferentMaker,
+ order: signedOrderWithDifferentMaker,
takerTokenCancelAmount: cancelAmount,
},
])).to.be.rejectedWith('Can not cancel orders from multiple makers in a single batch');