Merge pull request #40 from 0xProject/cancelAsync

Add cancelOrderAsync
This commit is contained in:
Leonid
2017-06-07 12:22:05 +02:00
committed by GitHub
7 changed files with 185 additions and 60 deletions

View File

@@ -4,22 +4,20 @@ import {bigNumberConfigs} from './bignumber_config';
import * as ethUtil from 'ethereumjs-util';
import contract = require('truffle-contract');
import * as Web3 from 'web3';
import * as ethABI from 'ethereumjs-abi';
import findVersions = require('find-versions');
import compareVersions = require('compare-versions');
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} from './types';
import {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();
@@ -132,29 +130,10 @@ 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);
const exchangeContractAddr = await this.getExchangeAddressAsync();
assert.doesConformToSchema('order',
SchemaValidator.convertToJSONSchemaCompatibleObject(order as object),
orderSchema);
const orderParts = [
{value: exchangeContractAddr, type: SolidityTypes.address},
{value: order.maker, type: SolidityTypes.address},
{value: order.taker, type: SolidityTypes.address},
{value: order.makerTokenAddress, type: SolidityTypes.address},
{value: order.takerTokenAddress, type: SolidityTypes.address},
{value: order.feeRecipient, type: SolidityTypes.address},
{value: utils.bigNumberToBN(order.makerTokenAmount), type: SolidityTypes.uint256},
{value: utils.bigNumberToBN(order.takerTokenAmount), type: SolidityTypes.uint256},
{value: utils.bigNumberToBN(order.makerFee), type: SolidityTypes.uint256},
{value: utils.bigNumberToBN(order.takerFee), type: SolidityTypes.uint256},
{value: utils.bigNumberToBN(order.expirationUnixTimestampSec), type: SolidityTypes.uint256},
{value: utils.bigNumberToBN(order.salt), type: SolidityTypes.uint256},
];
const types = _.map(orderParts, o => o.type);
const values = _.map(orderParts, o => o.value);
const hashBuff = ethABI.soliditySHA3(types, values);
const hashHex = ethUtil.bufferToHex(hashBuff);
const hashHex = utils.getOrderHashHex(order, exchangeContractAddr);
return hashHex;
}
/**

View File

@@ -9,9 +9,9 @@ import {
ExchangeContractErrs,
OrderValues,
OrderAddresses,
Order,
SignedOrder,
ContractEvent,
ZeroExError,
ExchangeEvents,
SubscriptionOpts,
IndexFilterValues,
@@ -25,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';
@@ -42,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;
@@ -126,21 +144,7 @@ export class ExchangeWrapper extends ContractWrapper {
const exchangeInstance = await this.getExchangeContractAsync();
await this.validateFillOrderAndThrowIfInvalidAsync(signedOrder, fillTakerAmount, takerAddress);
const orderAddresses: OrderAddresses = [
signedOrder.maker,
signedOrder.taker,
signedOrder.makerTokenAddress,
signedOrder.takerTokenAddress,
signedOrder.feeRecipient,
];
const orderValues: OrderValues = [
signedOrder.makerTokenAmount,
signedOrder.takerTokenAmount,
signedOrder.makerFee,
signedOrder.takerFee,
signedOrder.expirationUnixTimestampSec,
signedOrder.salt,
];
const [orderAddresses, orderValues] = ExchangeWrapper.getOrderAddressesAndValues(signedOrder);
const gas = await exchangeInstance.fill.estimateGas(
orderAddresses,
orderValues,
@@ -168,6 +172,39 @@ export class ExchangeWrapper extends ContractWrapper {
);
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> {
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
*/
@@ -194,6 +231,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)();
@@ -210,7 +253,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);
}
@@ -225,7 +268,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.

4
src/globals.d.ts vendored
View File

@@ -47,7 +47,9 @@ declare module 'ethereumjs-util' {
}
// truffle-contract declarations
declare interface ContractInstance {}
declare interface ContractInstance {
address: string;
}
declare interface ContractFactory {
setProvider: (providerObj: any) => void;
deployed: () => ContractInstance;

View File

@@ -44,7 +44,7 @@ export interface ContractEventObj {
}
export type CreateContractEvent = (indexFilterValues: IndexFilterValues,
subscriptionOpts: SubscriptionOpts) => ContractEventObj;
export interface ExchangeContract {
export interface ExchangeContract extends ContractInstance {
isValidSignature: {
call: (signerAddressHex: string, dataHex: string, v: number, r: string, s: string,
txOpts?: TxOpts) => Promise<boolean>;
@@ -64,9 +64,15 @@ export interface ExchangeContract {
};
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;
};
filled: {
call: (orderHash: string) => BigNumber.BigNumber;
@@ -74,22 +80,25 @@ export interface ExchangeContract {
cancelled: {
call: (orderHash: string) => BigNumber.BigNumber;
};
getOrderHash: {
call: (orderAddresses: OrderAddresses, orderValues: OrderValues) => string;
};
}
export interface TokenContract {
export interface TokenContract extends ContractInstance {
balanceOf: {
call: (address: string) => Promise<BigNumber.BigNumber>;
};
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 {
export interface TokenRegistryContract extends ContractInstance {
getTokenMetaData: {
call: (address: string) => Promise<TokenMetadata>;
};
@@ -115,6 +124,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',

View File

@@ -1,5 +1,9 @@
import * as _ from 'lodash';
import * as BN from 'bn.js';
import * as ethABI from 'ethereumjs-abi';
import * as ethUtil from 'ethereumjs-util';
import {Order, SignedOrder, SolidityTypes} from '../types';
import * as BigNumber from 'bignumber.js';
export const utils = {
/**
@@ -25,4 +29,28 @@ export const utils = {
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},
{value: order.taker, type: SolidityTypes.address},
{value: order.makerTokenAddress, type: SolidityTypes.address},
{value: order.takerTokenAddress, type: SolidityTypes.address},
{value: order.feeRecipient, type: SolidityTypes.address},
{value: utils.bigNumberToBN(order.makerTokenAmount), type: SolidityTypes.uint256},
{value: utils.bigNumberToBN(order.takerTokenAmount), type: SolidityTypes.uint256},
{value: utils.bigNumberToBN(order.makerFee), type: SolidityTypes.uint256},
{value: utils.bigNumberToBN(order.takerFee), type: SolidityTypes.uint256},
{value: utils.bigNumberToBN(order.expirationUnixTimestampSec), type: SolidityTypes.uint256},
{value: utils.bigNumberToBN(order.salt), type: SolidityTypes.uint256},
];
const types = _.map(orderParts, o => o.type);
const values = _.map(orderParts, o => o.value);
const hashBuff = ethABI.soliditySHA3(types, values);
const hashHex = ethUtil.bufferToHex(hashBuff);
return hashHex;
},
getCurrentUnixTimestamp(): BigNumber.BigNumber {
return new BigNumber(Date.now() / 1000);
},
};

View File

@@ -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,
@@ -158,7 +158,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,
@@ -323,6 +323,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;

View File

@@ -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(