aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFabio Berger <me@fabioberger.com>2017-06-07 18:48:26 +0800
committerFabio Berger <me@fabioberger.com>2017-06-07 18:48:26 +0800
commit4eee0b52f13cb382329cc7061d4377561ac1cbd3 (patch)
treec560b5b90b0ec8bee67094711b17f0c16e53e23f
parent918315e89f3408124d2e78bbd1acb58ed42d1766 (diff)
parent28d3528e42563f95255cee3bd7f85cc03141522e (diff)
downloaddexon-sol-tools-4eee0b52f13cb382329cc7061d4377561ac1cbd3.tar
dexon-sol-tools-4eee0b52f13cb382329cc7061d4377561ac1cbd3.tar.gz
dexon-sol-tools-4eee0b52f13cb382329cc7061d4377561ac1cbd3.tar.bz2
dexon-sol-tools-4eee0b52f13cb382329cc7061d4377561ac1cbd3.tar.lz
dexon-sol-tools-4eee0b52f13cb382329cc7061d4377561ac1cbd3.tar.xz
dexon-sol-tools-4eee0b52f13cb382329cc7061d4377561ac1cbd3.tar.zst
dexon-sol-tools-4eee0b52f13cb382329cc7061d4377561ac1cbd3.zip
merge master
-rw-r--r--src/0x.js.ts6
-rw-r--r--src/contract_wrappers/exchange_wrapper.ts110
-rw-r--r--src/types.ts22
-rw-r--r--src/utils/utils.ts14
-rw-r--r--test/exchange_wrapper_test.ts53
-rw-r--r--test/utils/order_factory.ts4
6 files changed, 160 insertions, 49 deletions
diff --git a/src/0x.js.ts b/src/0x.js.ts
index 6d66c9d86..7be6922fc 100644
--- a/src/0x.js.ts
+++ b/src/0x.js.ts
@@ -10,14 +10,14 @@ import {Web3Wrapper} from './web3_wrapper';
import {constants} from './utils/constants';
import {utils} from './utils/utils';
import {assert} from './utils/assert';
-import {SchemaValidator} from './utils/schema_validator';
import {ExchangeWrapper} from './contract_wrappers/exchange_wrapper';
import {TokenRegistryWrapper} from './contract_wrappers/token_registry_wrapper';
import {ecSignatureSchema} from './schemas/ec_signature_schema';
import {TokenWrapper} from './contract_wrappers/token_wrapper';
-import {SolidityTypes, ECSignature, ZeroExError, Order, SignedOrder} from './types';
-import {orderSchema} from './schemas/order_schemas';
+import {ECSignature, ZeroExError, Order, SignedOrder} from './types';
import * as ExchangeArtifacts from './artifacts/Exchange.json';
+import {SchemaValidator} from './utils/schema_validator';
+import {orderSchema} from './schemas/order_schemas';
// Customize our BigNumber instances
bigNumberConfigs.configure();
diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts
index 55ff9068e..c24a518a4 100644
--- a/src/contract_wrappers/exchange_wrapper.ts
+++ b/src/contract_wrappers/exchange_wrapper.ts
@@ -7,12 +7,11 @@ import {
ExchangeContract,
ExchangeContractErrCodes,
ExchangeContractErrs,
- Order,
OrderValues,
OrderAddresses,
+ Order,
SignedOrder,
ContractEvent,
- ZeroExError,
ExchangeEvents,
SubscriptionOpts,
IndexFilterValues,
@@ -26,7 +25,7 @@ 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 {signedOrderSchema} from '../schemas/order_schemas';
+import {signedOrderSchema, orderSchema} from '../schemas/order_schemas';
import {SchemaValidator} from '../utils/schema_validator';
import {constants} from '../utils/constants';
import {TokenWrapper} from './token_wrapper';
@@ -43,6 +42,24 @@ export class ExchangeWrapper extends ContractWrapper {
private exchangeContractIfExists?: ExchangeContract;
private exchangeLogEventObjs: ContractEventObj[];
private tokenWrapper: TokenWrapper;
+ private static getOrderAddressesAndValues(order: Order): [OrderAddresses, OrderValues] {
+ const orderAddresses: OrderAddresses = [
+ order.maker,
+ order.taker,
+ order.makerTokenAddress,
+ order.takerTokenAddress,
+ order.feeRecipient,
+ ];
+ const orderValues: OrderValues = [
+ order.makerTokenAmount,
+ order.takerTokenAmount,
+ order.makerFee,
+ order.takerFee,
+ order.expirationUnixTimestampSec,
+ order.salt,
+ ];
+ return [orderAddresses, orderValues];
+ }
constructor(web3Wrapper: Web3Wrapper, tokenWrapper: TokenWrapper) {
super(web3Wrapper);
this.tokenWrapper = tokenWrapper;
@@ -127,8 +144,7 @@ export class ExchangeWrapper extends ContractWrapper {
const exchangeInstance = await this.getExchangeContractAsync();
await this.validateFillOrderAndThrowIfInvalidAsync(signedOrder, fillTakerAmount, takerAddress);
- const orderAddresses = this.getOrderAddresses(signedOrder);
- const orderValues = this.getOrderValues(signedOrder);
+ const [orderAddresses, orderValues] = ExchangeWrapper.getOrderAddressesAndValues(signedOrder);
const gas = await exchangeInstance.fill.estimateGas(
orderAddresses,
@@ -181,8 +197,7 @@ export class ExchangeWrapper extends ContractWrapper {
throw new Error(ExchangeContractErrs.INSUFFICIENT_REMAINING_FILL_AMOUNT);
}
- const orderAddresses = this.getOrderAddresses(signedOrder);
- const orderValues = this.getOrderValues(signedOrder);
+ const [orderAddresses, orderValues] = ExchangeWrapper.getOrderAddressesAndValues(signedOrder);
const gas = await exchangeInstance.fillOrKill.estimateGas(
orderAddresses,
@@ -214,11 +229,45 @@ export class ExchangeWrapper extends ContractWrapper {
// fillAmount is available, but by the time the transaction gets mined, it no longer is. Instead of
// throwing an invalid jump exception, we would rather give the user a more helpful error message.
if (_.includes(err, constants.INVALID_JUMP_IDENTIFIER)) {
- throw new Error(ZeroExError.INSUFFICIENT_REMAINING_FILL_AMOUNT);
+ throw new Error(ExchangeContractErrs.INSUFFICIENT_REMAINING_FILL_AMOUNT);
}
}
}
/**
+ * Cancel a given fill amount of an order. Cancellations are cumulative.
+ */
+ public async cancelOrderAsync(order: Order|SignedOrder, takerTokenCancelAmount: BigNumber.BigNumber):
+ Promise<void> {
+ 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);
+ }
+ /**
* Subscribe to an event type emitted by the Exchange smart contract
*/
public async subscribeAsync(eventName: ExchangeEvents, subscriptionOpts: SubscriptionOpts,
@@ -244,6 +293,12 @@ 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);
+ const exchangeInstance = await this.getExchangeContractAsync();
+ const orderHash = utils.getOrderHashHex(order, exchangeInstance.address);
+ return orderHash;
+ }
private async stopWatchingExchangeLogEventsAsync() {
const stopWatchingPromises = _.map(this.exchangeLogEventObjs, logEventObj => {
return promisify(logEventObj.stopWatching, logEventObj)();
@@ -260,7 +315,7 @@ export class ExchangeWrapper extends ContractWrapper {
if (signedOrder.taker !== constants.NULL_ADDRESS && signedOrder.taker !== senderAddress) {
throw new Error(ExchangeContractErrs.TRANSACTION_SENDER_IS_NOT_FILL_ORDER_TAKER);
}
- const currentUnixTimestampSec = Date.now() / 1000;
+ const currentUnixTimestampSec = utils.getCurrentUnixTimestamp();
if (signedOrder.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) {
throw new Error(ExchangeContractErrs.ORDER_FILL_EXPIRED);
}
@@ -275,7 +330,21 @@ export class ExchangeWrapper extends ContractWrapper {
throw new Error(ExchangeContractErrs.ORDER_FILL_ROUNDING_ERROR);
}
}
-
+ private async validateCancelOrderAndThrowIfInvalidAsync(
+ order: Order, takerTokenCancelAmount: BigNumber.BigNumber): Promise<void> {
+ if (takerTokenCancelAmount.eq(0)) {
+ throw new Error(ExchangeContractErrs.ORDER_CANCEL_AMOUNT_ZERO);
+ }
+ const orderHash = await this.getOrderHashAsync(order);
+ const unavailableAmount = await this.getUnavailableTakerAmountAsync(orderHash);
+ if (order.takerTokenAmount.minus(unavailableAmount).eq(0)) {
+ throw new Error(ExchangeContractErrs.ORDER_ALREADY_CANCELLED_OR_FILLED);
+ }
+ const currentUnixTimestampSec = utils.getCurrentUnixTimestamp();
+ if (order.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) {
+ throw new Error(ExchangeContractErrs.ORDER_CANCEL_EXPIRED);
+ }
+ }
/**
* This method does not currently validate the edge-case where the makerToken or takerToken is also the token used
* to pay fees (ZRX). It is possible for them to have enough for fees and the transfer but not both.
@@ -366,25 +435,4 @@ export class ExchangeWrapper extends ContractWrapper {
const exchangeInstance = await this.getExchangeContractAsync();
return exchangeInstance.ZRX.call();
}
- private getOrderAddresses(order: Order|SignedOrder) {
- const orderAddresses: OrderAddresses = [
- order.maker,
- order.taker,
- order.makerTokenAddress,
- order.takerTokenAddress,
- order.feeRecipient,
- ];
- return orderAddresses;
- }
- private getOrderValues(order: Order|SignedOrder) {
- const orderValues: OrderValues = [
- order.makerTokenAmount,
- order.takerTokenAmount,
- order.makerFee,
- order.takerFee,
- order.expirationUnixTimestampSec,
- order.salt,
- ];
- return orderValues;
- }
}
diff --git a/src/types.ts b/src/types.ts
index 49d8365d1..1ee8a5bd6 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -65,9 +65,15 @@ export interface ExchangeContract extends ContractInstance {
};
fill: {
(orderAddresses: OrderAddresses, orderValues: OrderValues, fillAmount: BigNumber.BigNumber,
- shouldCheckTransfer: boolean, v: number, r: string, s: string, txOpts: TxOpts): ContractResponse;
+ 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;
+ shouldCheckTransfer: boolean, v: number, r: string, s: string, txOpts?: TxOpts) => number;
+ };
+ cancel: {
+ (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,
@@ -81,6 +87,9 @@ export interface ExchangeContract extends ContractInstance {
cancelled: {
call: (orderHash: string) => BigNumber.BigNumber;
};
+ getOrderHash: {
+ call: (orderAddresses: OrderAddresses, orderValues: OrderValues) => string;
+ };
}
export interface TokenContract extends ContractInstance {
@@ -90,10 +99,10 @@ export interface TokenContract extends ContractInstance {
allowance: {
call: (ownerAddress: string, allowedAddress: string) => Promise<BigNumber.BigNumber>;
};
- transfer: (toAddress: string, amountInBaseUnits: BigNumber.BigNumber, txOpts: TxOpts) => Promise<boolean>;
+ transfer: (toAddress: string, amountInBaseUnits: BigNumber.BigNumber, txOpts?: TxOpts) => Promise<boolean>;
transferFrom: (fromAddress: string, toAddress: string, amountInBaseUnits: BigNumber.BigNumber,
- txOpts: TxOpts) => Promise<boolean>;
- approve: (proxyAddress: string, amountInBaseUnits: BigNumber.BigNumber, txOpts: TxOpts) => void;
+ txOpts?: TxOpts) => Promise<boolean>;
+ approve: (proxyAddress: string, amountInBaseUnits: BigNumber.BigNumber, txOpts?: TxOpts) => void;
}
export interface TokenRegistryContract extends ContractInstance {
@@ -122,6 +131,9 @@ export enum ExchangeContractErrCodes {
export const ExchangeContractErrs = strEnum([
'ORDER_FILL_EXPIRED',
+ 'ORDER_CANCEL_EXPIRED',
+ 'ORDER_CANCEL_AMOUNT_ZERO',
+ 'ORDER_ALREADY_CANCELLED_OR_FILLED',
'ORDER_REMAINING_FILL_AMOUNT_ZERO',
'ORDER_FILL_ROUNDING_ERROR',
'FILL_BALANCE_ALLOWANCE_ERROR',
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index 1d2e2f908..5786bab07 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -1,8 +1,9 @@
import * as _ from 'lodash';
import * as BN from 'bn.js';
-import * as ethUtil from 'ethereumjs-util';
import * as ethABI from 'ethereumjs-abi';
-import {SolidityTypes, Order} from '../types';
+import * as ethUtil from 'ethereumjs-util';
+import {Order, SignedOrder, SolidityTypes} from '../types';
+import * as BigNumber from 'bignumber.js';
export const utils = {
/**
@@ -25,7 +26,10 @@ export const utils = {
const isValid = /^0x[0-9A-F]{64}$/i.test(orderHashHex);
return isValid;
},
- getOrderHashHex(order: Order, exchangeContractAddr: string) {
+ spawnSwitchErr(name: string, value: any) {
+ return new Error(`Unexpected switch value: ${value} encountered for ${name}`);
+ },
+ getOrderHashHex(order: Order|SignedOrder, exchangeContractAddr: string): string {
const orderParts = [
{value: exchangeContractAddr, type: SolidityTypes.address},
{value: order.maker, type: SolidityTypes.address},
@@ -46,7 +50,7 @@ export const utils = {
const hashHex = ethUtil.bufferToHex(hashBuff);
return hashHex;
},
- spawnSwitchErr(name: string, value: any) {
- return new Error(`Unexpected switch value: ${value} encountered for ${name}`);
+ getCurrentUnixTimestamp(): BigNumber.BigNumber {
+ return new BigNumber(Date.now() / 1000);
},
};
diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts
index 9ef20736f..ff2121de1 100644
--- a/test/exchange_wrapper_test.ts
+++ b/test/exchange_wrapper_test.ts
@@ -4,13 +4,13 @@ import * as Web3 from 'web3';
import * as BigNumber from 'bignumber.js';
import {chaiSetup} from './utils/chai_setup';
import ChaiBigNumber = require('chai-bignumber');
-import * as chaiAsPromised from 'chai-as-promised';
import promisify = require('es6-promisify');
import {web3Factory} from './utils/web3_factory';
import {ZeroEx} from '../src/0x.js';
import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
import {
Token,
+ Order,
SignedOrder,
SubscriptionOpts,
ExchangeEvents,
@@ -236,7 +236,7 @@ describe('ExchangeWrapper', () => {
)).to.be.rejectedWith(ExchangeContractErrs.TRANSACTION_SENDER_IS_NOT_FILL_ORDER_TAKER);
});
it('should throw when order is expired', async () => {
- const expirationInPast = new BigNumber(42);
+ const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017
const fillableAmount = new BigNumber(5);
const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, expirationInPast,
@@ -424,6 +424,55 @@ describe('ExchangeWrapper', () => {
});
});
});
+ describe('#cancelOrderAsync', () => {
+ let makerTokenAddress: string;
+ let takerTokenAddress: string;
+ let coinbase: string;
+ let makerAddress: string;
+ let takerAddress: string;
+ const fillableAmount = new BigNumber(5);
+ let signedOrder: SignedOrder;
+ let orderHashHex: string;
+ const cancelAmount = new BigNumber(3);
+ beforeEach(async () => {
+ [coinbase, makerAddress, takerAddress] = userAddresses;
+ const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ 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);
+ });
+ 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);
+ });
+ });
+ 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('tests that require partially filled order', () => {
let makerTokenAddress: string;
let takerTokenAddress: string;
diff --git a/test/utils/order_factory.ts b/test/utils/order_factory.ts
index 6f5fa7286..a1cc243c6 100644
--- a/test/utils/order_factory.ts
+++ b/test/utils/order_factory.ts
@@ -1,9 +1,7 @@
import * as _ from 'lodash';
import * as BigNumber from 'bignumber.js';
-import {SignedOrder, Token} from '../../src/types';
+import {SignedOrder} from '../../src/types';
import {ZeroEx} from '../../src/0x.js';
-import {constants} from './constants';
-import * as ExchangeArtifacts from '../../src/artifacts/Exchange.json';
export const orderFactory = {
async createSignedOrderAsync(