diff options
-rw-r--r-- | src/0x.js.ts | 33 | ||||
-rw-r--r-- | src/contract_wrappers/exchange_wrapper.ts | 95 | ||||
-rw-r--r-- | src/globals.d.ts | 4 | ||||
-rw-r--r-- | src/types.ts | 28 | ||||
-rw-r--r-- | src/utils/utils.ts | 28 | ||||
-rw-r--r-- | test/exchange_wrapper_test.ts | 53 | ||||
-rw-r--r-- | test/utils/order_factory.ts | 4 |
7 files changed, 185 insertions, 60 deletions
diff --git a/src/0x.js.ts b/src/0x.js.ts index 0f437e039..7b53b70ea 100644 --- a/src/0x.js.ts +++ b/src/0x.js.ts @@ -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; } /** diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index d3a53a9f7..6f62934dc 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -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, @@ -169,6 +173,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 */ public async subscribeAsync(eventName: ExchangeEvents, subscriptionOpts: SubscriptionOpts, @@ -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. diff --git a/src/globals.d.ts b/src/globals.d.ts index 164fc2386..567ba016d 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -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; diff --git a/src/types.ts b/src/types.ts index a02bd0252..5407b0121 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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', diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 114b46f6c..5786bab07 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -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); + }, }; diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts index 370e741b4..c0068c58a 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, @@ -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; 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( |