diff options
-rw-r--r-- | CHANGELOG.md | 9 | ||||
-rw-r--r-- | package-lock.json | 2 | ||||
-rw-r--r-- | package.json | 4 | ||||
-rw-r--r-- | src/artifacts/EtherToken.json | 51 | ||||
-rw-r--r-- | src/artifacts/Exchange.json | 135 | ||||
-rw-r--r-- | src/artifacts/TokenRegistry.json | 171 | ||||
-rw-r--r-- | src/contract_wrappers/exchange_wrapper.ts | 150 | ||||
-rw-r--r-- | src/types.ts | 18 | ||||
-rw-r--r-- | src/utils/order_validation_utils.ts | 42 | ||||
-rw-r--r-- | test/artifacts_test.ts | 17 | ||||
-rw-r--r-- | test/exchange_wrapper_test.ts | 216 | ||||
-rw-r--r-- | test/order_validation_test.ts | 40 | ||||
-rw-r--r-- | test/utils/constants.ts | 3 | ||||
-rw-r--r-- | test/utils/fill_scenarios.ts | 4 |
14 files changed, 792 insertions, 70 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 61031c089..ac2588278 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # CHANGELOG +v0.18.1 - _September 28, 2017_ + * Added Ropsten testnet support (#173) + +v0.18.0 - _September 26, 2017_ + * Added `zeroEx.exchange.validateOrderFillableOrThrowAsync` to simplify orderbook pruning (#170) + +v0.17.0 - _September 26, 2017_ + * Made `zeroEx.exchange.getZRXTokenAddressAsync` public (#171) + v0.16.0 - _September 20, 2017_ ------------------------ * Added the ability to specify custom contract addresses to be used with 0x.js (#165) diff --git a/package-lock.json b/package-lock.json index 6d529d46e..600573a10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "0x.js", - "version": "0.16.0", + "version": "0.18.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 41ff5eefe..a87b3d8bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "0x.js", - "version": "0.16.0", + "version": "0.18.0", "description": "A javascript library for interacting with the 0x protocol", "keywords": [ "0x.js", @@ -64,7 +64,7 @@ "chai-bignumber": "^2.0.1", "chai-typescript-typings": "^0.0.0", "copyfiles": "^1.2.0", - "coveralls": "^2.13.1", + "coveralls": "^3.0.0", "dirty-chai": "^2.0.1", "ethereumjs-testrpc": "4.0.1", "json-loader": "^0.5.4", diff --git a/src/artifacts/EtherToken.json b/src/artifacts/EtherToken.json index 54b5a032e..91b23bc94 100644 --- a/src/artifacts/EtherToken.json +++ b/src/artifacts/EtherToken.json @@ -286,6 +286,57 @@ "updated_at": 1502488087000, "address": "0x2956356cd2a2bf3202f771f50d3d14a367b48070" }, + "3": { + "links": {}, + "events": { + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef": { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_from", + "type": "address" + }, + { + "indexed": true, + "name": "_to", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925": { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_owner", + "type": "address" + }, + { + "indexed": true, + "name": "_spender", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + } + }, + "updated_at": 1506602007000, + "address": "0xc00fd9820cd2898cc4c054b7bf142de637ad129a" + }, "42": { "links": {}, "events": { diff --git a/src/artifacts/Exchange.json b/src/artifacts/Exchange.json index 2445f13ad..734c8f9c7 100644 --- a/src/artifacts/Exchange.json +++ b/src/artifacts/Exchange.json @@ -725,6 +725,139 @@ "updated_at": 1502480340000, "address": "0x12459c951127e0c374ff9105dda097662a027093" }, + "3": { + "links": {}, + "events": { + "0x0d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb3": { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "maker", + "type": "address" + }, + { + "indexed": false, + "name": "taker", + "type": "address" + }, + { + "indexed": true, + "name": "feeRecipient", + "type": "address" + }, + { + "indexed": false, + "name": "makerToken", + "type": "address" + }, + { + "indexed": false, + "name": "takerToken", + "type": "address" + }, + { + "indexed": false, + "name": "filledMakerTokenAmount", + "type": "uint256" + }, + { + "indexed": false, + "name": "filledTakerTokenAmount", + "type": "uint256" + }, + { + "indexed": false, + "name": "paidMakerFee", + "type": "uint256" + }, + { + "indexed": false, + "name": "paidTakerFee", + "type": "uint256" + }, + { + "indexed": true, + "name": "tokens", + "type": "bytes32" + }, + { + "indexed": false, + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "LogFill", + "type": "event" + }, + "0x67d66f160bc93d925d05dae1794c90d2d6d6688b29b84ff069398a9b04587131": { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "maker", + "type": "address" + }, + { + "indexed": true, + "name": "feeRecipient", + "type": "address" + }, + { + "indexed": false, + "name": "makerToken", + "type": "address" + }, + { + "indexed": false, + "name": "takerToken", + "type": "address" + }, + { + "indexed": false, + "name": "cancelledMakerTokenAmount", + "type": "uint256" + }, + { + "indexed": false, + "name": "cancelledTakerTokenAmount", + "type": "uint256" + }, + { + "indexed": true, + "name": "tokens", + "type": "bytes32" + }, + { + "indexed": false, + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "LogCancel", + "type": "event" + }, + "0x36d86c59e00bd73dc19ba3adfe068e4b64ac7e92be35546adeddf1b956a87e90": { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "errorId", + "type": "uint8" + }, + { + "indexed": true, + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "LogError", + "type": "event" + } + }, + "updated_at": 1506602007000, + "address": "0x479cc461fecd078f766ecc58533d6f69580cf3ac" + }, "42": { "links": {}, "events": { @@ -994,4 +1127,4 @@ }, "schema_version": "0.0.5", "updated_at": 1503318938231 -}
\ No newline at end of file +} diff --git a/src/artifacts/TokenRegistry.json b/src/artifacts/TokenRegistry.json index e244a496a..5a0564a69 100644 --- a/src/artifacts/TokenRegistry.json +++ b/src/artifacts/TokenRegistry.json @@ -698,6 +698,175 @@ "updated_at": 1502488442000, "address": "0x926a74c5c36adf004c87399e65f75628b0f98d2c" }, + "3": { + "links": {}, + "events": { + "0xd8d928b0b50ca11d9dc273236b46f3526515b03602f71f3a6af4f45bd9fa9144": { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "token", + "type": "address" + }, + { + "indexed": false, + "name": "name", + "type": "string" + }, + { + "indexed": false, + "name": "symbol", + "type": "string" + }, + { + "indexed": false, + "name": "decimals", + "type": "uint8" + }, + { + "indexed": false, + "name": "ipfsHash", + "type": "bytes" + }, + { + "indexed": false, + "name": "swarmHash", + "type": "bytes" + } + ], + "name": "LogAddToken", + "type": "event" + }, + "0x32c54f1e2ea75844ded7517e7dbcd3895da7cd0c28f9ab9f9cf6ecf5f83762c6": { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "token", + "type": "address" + }, + { + "indexed": false, + "name": "name", + "type": "string" + }, + { + "indexed": false, + "name": "symbol", + "type": "string" + }, + { + "indexed": false, + "name": "decimals", + "type": "uint8" + }, + { + "indexed": false, + "name": "ipfsHash", + "type": "bytes" + }, + { + "indexed": false, + "name": "swarmHash", + "type": "bytes" + } + ], + "name": "LogRemoveToken", + "type": "event" + }, + "0x4a6dbfc867b179991dec22ff19960f0a94d8d9d891fc556f547764670340e8ae": { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "token", + "type": "address" + }, + { + "indexed": false, + "name": "oldName", + "type": "string" + }, + { + "indexed": false, + "name": "newName", + "type": "string" + } + ], + "name": "LogTokenNameChange", + "type": "event" + }, + "0x53d878a6530e56c9bc96548fa0a8cae4f1d1f49c86b0e934c086b992ebb6998f": { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "token", + "type": "address" + }, + { + "indexed": false, + "name": "oldSymbol", + "type": "string" + }, + { + "indexed": false, + "name": "newSymbol", + "type": "string" + } + ], + "name": "LogTokenSymbolChange", + "type": "event" + }, + "0x5b19f79ac4e8cfa820815502e11615f1a449e28155dc289ec5cac1a11f908694": { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "token", + "type": "address" + }, + { + "indexed": false, + "name": "oldIpfsHash", + "type": "bytes" + }, + { + "indexed": false, + "name": "newIpfsHash", + "type": "bytes" + } + ], + "name": "LogTokenIpfsHashChange", + "type": "event" + }, + "0xc3168fdc13112e44a031057dbf6c609b33353addb4d8037d24543e22cbfe2acd": { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "token", + "type": "address" + }, + { + "indexed": false, + "name": "oldSwarmHash", + "type": "bytes" + }, + { + "indexed": false, + "name": "newSwarmHash", + "type": "bytes" + } + ], + "name": "LogTokenSwarmHashChange", + "type": "event" + } + }, + "updated_at": 1506602007000, + "address": "0x6b1a50f0bb5a7995444bd3877b22dc89c62843ed" + }, "42": { "links": {}, "events": { @@ -1039,4 +1208,4 @@ }, "schema_version": "0.0.5", "updated_at": 1503318938228 -}
\ No newline at end of file +} diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index 54d7f62d5..d02a6e642 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -28,6 +28,8 @@ import { LogCancelContractEventArgs, LogWithDecodedArgs, MethodOpts, + ValidateOrderFillableOpts, + OrderTransactionOpts, } from '../types'; import {assert} from '../utils/assert'; import {utils} from '../utils/utils'; @@ -39,6 +41,8 @@ import {TokenWrapper} from './token_wrapper'; import {decorators} from '../utils/decorators'; import {artifacts} from '../artifacts'; +const SHOULD_VALIDATE_BY_DEFAULT = true; + /** * This class includes all the functionality related to calling methods and subscribing to * events of the 0x Exchange smart contract. @@ -153,19 +157,26 @@ export class ExchangeWrapper extends ContractWrapper { * @param takerAddress The user Ethereum address who would like to fill this order. * Must be available via the supplied Web3.Provider * passed to 0x.js. - * @return Transaction hash. + * @param orderTransactionOpts Optional arguments this method accepts. + * @return Transaction hash. */ @decorators.contractCallErrorHandler public async fillOrderAsync(signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber.BigNumber, shouldThrowOnInsufficientBalanceOrAllowance: boolean, - takerAddress: string): Promise<string> { + takerAddress: string, + orderTransactionOpts?: OrderTransactionOpts): Promise<string> { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount); assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const exchangeInstance = await this._getExchangeContractAsync(); - await this.validateFillOrderThrowIfInvalidAsync(signedOrder, fillTakerTokenAmount, takerAddress); + const shouldValidate = _.isUndefined(orderTransactionOpts) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; + if (shouldValidate) { + await this.validateFillOrderThrowIfInvalidAsync(signedOrder, fillTakerTokenAmount, takerAddress); + } const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(signedOrder); @@ -210,12 +221,14 @@ export class ExchangeWrapper extends ContractWrapper { * @param takerAddress The user Ethereum address who would like to fill these * orders. Must be available via the supplied Web3.Provider * passed to 0x.js. - * @return Transaction hash. + * @param orderTransactionOpts Optional arguments this method accepts. + * @return Transaction hash. */ @decorators.contractCallErrorHandler public async fillOrdersUpToAsync(signedOrders: SignedOrder[], fillTakerTokenAmount: BigNumber.BigNumber, shouldThrowOnInsufficientBalanceOrAllowance: boolean, - takerAddress: string): Promise<string> { + takerAddress: string, + orderTransactionOpts?: OrderTransactionOpts): Promise<string> { assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); const takerTokenAddresses = _.map(signedOrders, signedOrder => signedOrder.takerTokenAddress); assert.hasAtMostOneUniqueValue(takerTokenAddresses, @@ -226,10 +239,15 @@ export class ExchangeWrapper extends ContractWrapper { assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount); assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); - for (const signedOrder of signedOrders) { - await this.validateFillOrderThrowIfInvalidAsync( - signedOrder, fillTakerTokenAmount, takerAddress); + + const shouldValidate = _.isUndefined(orderTransactionOpts) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; + if (shouldValidate) { + await Promise.all(signedOrders.map(signedOrder => this.validateFillOrderThrowIfInvalidAsync( + signedOrder, fillTakerTokenAmount, takerAddress))); } + if (_.isEmpty(signedOrders)) { throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem); } @@ -291,12 +309,14 @@ export class ExchangeWrapper extends ContractWrapper { * @param takerAddress The user Ethereum address who would like to fill * these orders. Must be available via the supplied * Web3.Provider passed to 0x.js. - * @return Transaction hash. + * @param orderTransactionOpts Optional arguments this method accepts. + * @return Transaction hash. */ @decorators.contractCallErrorHandler public async batchFillOrdersAsync(orderFillRequests: OrderFillRequest[], shouldThrowOnInsufficientBalanceOrAllowance: boolean, - takerAddress: string): Promise<string> { + takerAddress: string, + orderTransactionOpts?: OrderTransactionOpts): Promise<string> { assert.doesConformToSchema('orderFillRequests', orderFillRequests, schemas.orderFillRequestsSchema); const exchangeContractAddresses = _.map( orderFillRequests, @@ -306,9 +326,13 @@ export class ExchangeWrapper extends ContractWrapper { ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress); assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); - for (const orderFillRequest of orderFillRequests) { - await this.validateFillOrderThrowIfInvalidAsync( - orderFillRequest.signedOrder, orderFillRequest.takerTokenFillAmount, takerAddress); + const shouldValidate = _.isUndefined(orderTransactionOpts) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; + if (shouldValidate) { + await Promise.all(orderFillRequests.map(orderFillRequest => this.validateFillOrderThrowIfInvalidAsync( + orderFillRequest.signedOrder, orderFillRequest.takerTokenFillAmount, takerAddress)), + ); } if (_.isEmpty(orderFillRequests)) { throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem); @@ -364,18 +388,25 @@ export class ExchangeWrapper extends ContractWrapper { * @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill. * @param takerAddress The user Ethereum address who would like to fill this order. * Must be available via the supplied Web3.Provider passed to 0x.js. - * @return Transaction hash. + * @param orderTransactionOpts Optional arguments this method accepts. + * @return Transaction hash. */ @decorators.contractCallErrorHandler public async fillOrKillOrderAsync(signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber.BigNumber, - takerAddress: string): Promise<string> { + takerAddress: string, + orderTransactionOpts?: OrderTransactionOpts): Promise<string> { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const exchangeInstance = await this._getExchangeContractAsync(); - await this.validateFillOrKillOrderThrowIfInvalidAsync(signedOrder, fillTakerTokenAmount, takerAddress); + const shouldValidate = _.isUndefined(orderTransactionOpts) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; + if (shouldValidate) { + await this.validateFillOrKillOrderThrowIfInvalidAsync(signedOrder, fillTakerTokenAmount, takerAddress); + } const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(signedOrder); @@ -410,11 +441,13 @@ export class ExchangeWrapper extends ContractWrapper { * @param orderFillOrKillRequests An array of objects that conform to the OrderFillOrKillRequest interface. * @param takerAddress The user Ethereum address who would like to fill there orders. * Must be available via the supplied Web3.Provider passed to 0x.js. - * @return Transaction hash. + * @param orderTransactionOpts Optional arguments this method accepts. + * @return Transaction hash. */ @decorators.contractCallErrorHandler public async batchFillOrKillAsync(orderFillOrKillRequests: OrderFillOrKillRequest[], - takerAddress: string): Promise<string> { + takerAddress: string, + orderTransactionOpts?: OrderTransactionOpts): Promise<string> { assert.doesConformToSchema('orderFillOrKillRequests', orderFillOrKillRequests, schemas.orderFillOrKillRequestsSchema); const exchangeContractAddresses = _.map( @@ -428,9 +461,14 @@ export class ExchangeWrapper extends ContractWrapper { throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem); } const exchangeInstance = await this._getExchangeContractAsync(); - for (const request of orderFillOrKillRequests) { - await this.validateFillOrKillOrderThrowIfInvalidAsync( - request.signedOrder, request.fillTakerAmount, takerAddress); + + const shouldValidate = _.isUndefined(orderTransactionOpts) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; + if (shouldValidate) { + await Promise.all(orderFillOrKillRequests.map(request => this.validateFillOrKillOrderThrowIfInvalidAsync( + request.signedOrder, request.fillTakerAmount, takerAddress)), + ); } const orderAddressesValuesAndTakerTokenFillAmounts = _.map(orderFillOrKillRequests, request => { @@ -477,17 +515,25 @@ export class ExchangeWrapper extends ContractWrapper { * @param order An object that conforms to the Order or SignedOrder interface. * The order you would like to cancel. * @param cancelTakerTokenAmount The amount (specified in taker tokens) that you would like to cancel. - * @return Transaction hash. + * @param transactionOpts Optional arguments this method accepts. + * @return Transaction hash. */ @decorators.contractCallErrorHandler - public async cancelOrderAsync( - order: Order|SignedOrder, cancelTakerTokenAmount: BigNumber.BigNumber): Promise<string> { + public async cancelOrderAsync(order: Order|SignedOrder, + cancelTakerTokenAmount: BigNumber.BigNumber, + orderTransactionOpts?: OrderTransactionOpts): Promise<string> { assert.doesConformToSchema('order', order, schemas.orderSchema); assert.isBigNumber('takerTokenCancelAmount', cancelTakerTokenAmount); await assert.isSenderAddressAsync('order.maker', order.maker, this._web3Wrapper); const exchangeInstance = await this._getExchangeContractAsync(); - await this.validateCancelOrderThrowIfInvalidAsync(order, cancelTakerTokenAmount); + + const shouldValidate = _.isUndefined(orderTransactionOpts) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; + if (shouldValidate) { + await this.validateCancelOrderThrowIfInvalidAsync(order, cancelTakerTokenAmount); + } const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(order); const gas = await exchangeInstance.cancelOrder.estimateGasAsync( @@ -514,10 +560,12 @@ export class ExchangeWrapper extends ContractWrapper { * All orders must be from the same maker. * @param orderCancellationRequests An array of objects that conform to the OrderCancellationRequest * interface. - * @return Transaction hash. + * @param transactionOpts Optional arguments this method accepts. + * @return Transaction hash. */ @decorators.contractCallErrorHandler - public async batchCancelOrdersAsync(orderCancellationRequests: OrderCancellationRequest[]): Promise<string> { + public async batchCancelOrdersAsync(orderCancellationRequests: OrderCancellationRequest[], + orderTransactionOpts?: OrderTransactionOpts): Promise<string> { assert.doesConformToSchema('orderCancellationRequests', orderCancellationRequests, schemas.orderCancellationRequestsSchema); const exchangeContractAddresses = _.map( @@ -530,10 +578,13 @@ export class ExchangeWrapper extends ContractWrapper { assert.hasAtMostOneUniqueValue(makers, ExchangeContractErrs.MultipleMakersInSingleCancelBatchDisallowed); const maker = makers[0]; await assert.isSenderAddressAsync('maker', maker, this._web3Wrapper); - for (const cancellationRequest of orderCancellationRequests) { - await this.validateCancelOrderThrowIfInvalidAsync( - cancellationRequest.order, cancellationRequest.takerTokenCancelAmount, - ); + const shouldValidate = _.isUndefined(orderTransactionOpts) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; + if (shouldValidate) { + await Promise.all(orderCancellationRequests.map(cancellationRequest => + this.validateCancelOrderThrowIfInvalidAsync( + cancellationRequest.order, cancellationRequest.takerTokenCancelAmount))); } if (_.isEmpty(orderCancellationRequests)) { throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem); @@ -624,6 +675,25 @@ export class ExchangeWrapper extends ContractWrapper { return exchangeAddress; } /** + * Checks if order is still fillable and throws an error otherwise. Useful for orderbook + * pruning where you want to remove stale orders without knowing who the taker will be. + * @param signedOrder An object that conforms to the SignedOrder interface. The + * signedOrder you wish to validate. + * @param opts An object that conforms to the ValidateOrderFillableOpts + * interface. Allows specifying a specific fillTakerTokenAmount + * to validate for. + */ + public async validateOrderFillableOrThrowAsync( + signedOrder: SignedOrder, opts?: ValidateOrderFillableOpts, + ): Promise<void> { + assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + const zrxTokenAddress = await this.getZRXTokenAddressAsync(); + const expectedFillTakerTokenAmount = !_.isUndefined(opts) ? opts.expectedFillTakerTokenAmount : undefined; + await this._orderValidationUtils.validateOrderFillableOrThrowAsync( + signedOrder, zrxTokenAddress, expectedFillTakerTokenAmount, + ); + } + /** * Checks if order fill will succeed and throws an error otherwise. * @param signedOrder An object that conforms to the SignedOrder interface. The * signedOrder you wish to fill. @@ -637,7 +707,7 @@ export class ExchangeWrapper extends ContractWrapper { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); - const zrxTokenAddress = await this._getZRXTokenAddressAsync(); + const zrxTokenAddress = await this.getZRXTokenAddressAsync(); await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync( signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress); } @@ -670,7 +740,7 @@ export class ExchangeWrapper extends ContractWrapper { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); - const zrxTokenAddress = await this._getZRXTokenAddressAsync(); + const zrxTokenAddress = await this.getZRXTokenAddressAsync(); await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync( signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress); } @@ -708,6 +778,15 @@ export class ExchangeWrapper extends ContractWrapper { throw new Error(errMessage); } } + /** + * Returns the ZRX token address used by the exchange contract. + * @return Address of ZRX token + */ + public async getZRXTokenAddressAsync(): Promise<string> { + const exchangeInstance = await this._getExchangeContractAsync(); + const ZRXtokenAddress = await exchangeInstance.ZRX_TOKEN_CONTRACT.callAsync(); + return ZRXtokenAddress; + } private async _invalidateContractInstancesAsync(): Promise<void> { await this.stopWatchingAllEventsAsync(); delete this._exchangeContractIfExists; @@ -745,11 +824,6 @@ export class ExchangeWrapper extends ContractWrapper { this._exchangeContractIfExists = contractInstance as ExchangeContract; return this._exchangeContractIfExists; } - private async _getZRXTokenAddressAsync(): Promise<string> { - const exchangeInstance = await this._getExchangeContractAsync(); - const ZRXtokenAddress = await exchangeInstance.ZRX_TOKEN_CONTRACT.callAsync(); - return ZRXtokenAddress; - } private async _getTokenTransferProxyAddressAsync(): Promise<string> { const exchangeInstance = await this._getExchangeContractAsync(); const tokenTransferProxyAddress = await exchangeInstance.TOKEN_TRANSFER_PROXY_CONTRACT.callAsync(); diff --git a/src/types.ts b/src/types.ts index 29fb40e73..02230b0ab 100644 --- a/src/types.ts +++ b/src/types.ts @@ -434,6 +434,16 @@ export interface Artifact { } /* + * expectedFillTakerTokenAmount: If specified, the validation method will ensure that the + * supplied order maker has a sufficient allowance/balance to fill this amount of the order's + * takerTokenAmount. If not specified, the validation method ensures that the maker has a sufficient + * allowance/balance to fill the entire remaining order amount. + */ +export interface ValidateOrderFillableOpts { + expectedFillTakerTokenAmount?: BigNumber.BigNumber; +} + +/* * defaultBlock: The block up to which to query the blockchain state. Setting this to a historical block number * let's the user query the blockchain's state at an arbitrary point in time. In order for this to work, the * backing Ethereum node must keep the entire historical state of the chain (e.g setting `--pruning=archive` @@ -442,3 +452,11 @@ export interface Artifact { export interface MethodOpts { defaultBlock?: Web3.BlockParam; } + +/* + * shouldValidate: Flag indicating whether the library should make attempts to validate a transaction before + * broadcasting it. For example, order has a valid signature, maker has sufficient funds, etc. + */ +export interface OrderTransactionOpts { + shouldValidate: boolean; +} diff --git a/src/utils/order_validation_utils.ts b/src/utils/order_validation_utils.ts index 815cd0115..6f7522c41 100644 --- a/src/utils/order_validation_utils.ts +++ b/src/utils/order_validation_utils.ts @@ -1,3 +1,4 @@ +import * as _ from 'lodash'; import {ExchangeContractErrs, SignedOrder, Order, ZeroExError} from '../types'; import {ZeroEx} from '../0x'; import {TokenWrapper} from '../contract_wrappers/token_wrapper'; @@ -12,6 +13,23 @@ export class OrderValidationUtils { this.tokenWrapper = tokenWrapper; this.exchangeWrapper = exchangeWrapper; } + public async validateOrderFillableOrThrowAsync( + signedOrder: SignedOrder, zrxTokenAddress: string, expectedFillTakerTokenAmount?: BigNumber.BigNumber, + ): Promise<void> { + const orderHash = utils.getOrderHashHex(signedOrder); + const unavailableTakerTokenAmount = await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHash); + this.validateRemainingFillAmountNotZeroOrThrow( + signedOrder.takerTokenAmount, unavailableTakerTokenAmount, + ); + this.validateOrderNotExpiredOrThrow(signedOrder.expirationUnixTimestampSec); + let fillTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount); + if (!_.isUndefined(expectedFillTakerTokenAmount)) { + fillTakerTokenAmount = expectedFillTakerTokenAmount; + } + await this.validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync( + signedOrder, fillTakerTokenAmount, zrxTokenAddress, + ); + } public async validateFillOrderThrowIfInvalidAsync(signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber.BigNumber, takerAddress: string, @@ -24,16 +42,13 @@ export class OrderValidationUtils { throw new Error(ZeroExError.InvalidSignature); } const unavailableTakerTokenAmount = await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHash); - if (signedOrder.makerTokenAmount.eq(unavailableTakerTokenAmount)) { - throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); - } + this.validateRemainingFillAmountNotZeroOrThrow( + signedOrder.takerTokenAmount, unavailableTakerTokenAmount, + ); if (signedOrder.taker !== constants.NULL_ADDRESS && signedOrder.taker !== takerAddress) { throw new Error(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker); } - const currentUnixTimestampSec = utils.getCurrentUnixTimestamp(); - if (signedOrder.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) { - throw new Error(ExchangeContractErrs.OrderFillExpired); - } + this.validateOrderNotExpiredOrThrow(signedOrder.expirationUnixTimestampSec); await this.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress, ); @@ -147,4 +162,17 @@ export class OrderValidationUtils { } } } + private validateRemainingFillAmountNotZeroOrThrow( + takerTokenAmount: BigNumber.BigNumber, unavailableTakerTokenAmount: BigNumber.BigNumber, + ) { + if (takerTokenAmount.eq(unavailableTakerTokenAmount)) { + throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); + } + } + private validateOrderNotExpiredOrThrow(expirationUnixTimestampSec: BigNumber.BigNumber) { + const currentUnixTimestampSec = utils.getCurrentUnixTimestamp(); + if (expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) { + throw new Error(ExchangeContractErrs.OrderFillExpired); + } + } } diff --git a/test/artifacts_test.ts b/test/artifacts_test.ts index bd34a6b91..b2866a1d6 100644 --- a/test/artifacts_test.ts +++ b/test/artifacts_test.ts @@ -29,4 +29,21 @@ describe('Artifacts', () => { await zeroEx.exchange.getContractAddressAsync(); }).timeout(TIMEOUT); }); + describe('contracts are deployed on ropsten', () => { + const ropstenRpcUrl = constants.ROPSTEN_RPC_URL; + const packageJSONContent = fs.readFileSync('package.json', 'utf-8'); + const packageJSON = JSON.parse(packageJSONContent); + const mnemonic = packageJSON.config.mnemonic; + const web3Provider = new HDWalletProvider(mnemonic, ropstenRpcUrl); + const zeroEx = new ZeroEx(web3Provider); + it('token registry contract is deployed', async () => { + await (zeroEx.tokenRegistry as any)._getTokenRegistryContractAsync(); + }).timeout(TIMEOUT); + it('proxy contract is deployed', async () => { + await (zeroEx.token as any)._getTokenTransferProxyAddressAsync(); + }).timeout(TIMEOUT); + it('exchange contract is deployed', async () => { + await zeroEx.exchange.getContractAddressAsync(); + }).timeout(TIMEOUT); + }); }); diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts index f72e7f64f..45a2d3907 100644 --- a/test/exchange_wrapper_test.ts +++ b/test/exchange_wrapper_test.ts @@ -18,6 +18,7 @@ import { OrderCancellationRequest, OrderFillRequest, LogFillContractEventArgs, + OrderFillOrKillRequest, } from '../src'; import {DoneCallback} from '../src/types'; import {FillScenarios} from './utils/fill_scenarios'; @@ -93,14 +94,47 @@ describe('ExchangeWrapper', () => { ]; await zeroEx.exchange.batchFillOrKillAsync(orderFillOrKillRequests, takerAddress); }); + describe('order transaction options', () => { + let signedOrder: SignedOrder; + let orderFillOrKillRequests: OrderFillOrKillRequest[]; + const fillableAmount = new BigNumber(5); + beforeEach(async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + orderFillOrKillRequests = [ + { + signedOrder, + fillTakerAmount: new BigNumber(0), + }, + ]; + }); + it('should validate when orderTransactionOptions are not present', async () => { + return expect(zeroEx.exchange.batchFillOrKillAsync(orderFillOrKillRequests, takerAddress)) + .to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect(zeroEx.exchange.batchFillOrKillAsync(orderFillOrKillRequests, takerAddress, { + shouldValidate: true, + })).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect(zeroEx.exchange.batchFillOrKillAsync(orderFillOrKillRequests, takerAddress, { + shouldValidate: false, + })).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + }); }); describe('#fillOrKillOrderAsync', () => { + let signedOrder: SignedOrder; + const fillableAmount = new BigNumber(5); + beforeEach(async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + }); 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)) @@ -120,10 +154,6 @@ describe('ExchangeWrapper', () => { .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)) @@ -136,6 +166,23 @@ describe('ExchangeWrapper', () => { .to.be.bignumber.equal(fillableAmount.minus(partialFillAmount)); }); }); + describe('order transaction options', () => { + const emptyFillableAmount = new BigNumber(0); + it('should validate when orderTransactionOptions are not present', async () => { + return expect(zeroEx.exchange.fillOrKillOrderAsync(signedOrder, emptyFillableAmount, takerAddress)) + .to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect(zeroEx.exchange.fillOrKillOrderAsync(signedOrder, emptyFillableAmount, takerAddress, { + shouldValidate: true, + })).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect(zeroEx.exchange.fillOrKillOrderAsync(signedOrder, emptyFillableAmount, takerAddress, { + shouldValidate: false, + })).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + }); }); }); describe('fill order(s)', () => { @@ -212,6 +259,32 @@ describe('ExchangeWrapper', () => { .to.be.bignumber.equal(makerFee.plus(takerFee)); }); }); + describe('order transaction options', () => { + let signedOrder: SignedOrder; + const emptyFillTakerAmount = new BigNumber(0); + beforeEach(async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + }); + it('should validate when orderTransactionOptions are not present', async () => { + return expect(zeroEx.exchange.fillOrderAsync( + signedOrder, emptyFillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + )).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect(zeroEx.exchange.fillOrderAsync( + signedOrder, emptyFillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, { + shouldValidate: true, + })).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect(zeroEx.exchange.fillOrderAsync( + signedOrder, emptyFillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, { + shouldValidate: false, + })).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + }); }); describe('#batchFillOrdersAsync', () => { let signedOrder: SignedOrder; @@ -228,18 +301,20 @@ describe('ExchangeWrapper', () => { makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, ); anotherOrderHashHex = ZeroEx.getOrderHashHex(anotherSignedOrder); - orderFillBatch = [ - { - signedOrder, - takerTokenFillAmount: fillTakerAmount, - }, - { - signedOrder: anotherSignedOrder, - takerTokenFillAmount: fillTakerAmount, - }, - ]; }); describe('successful batch fills', () => { + beforeEach(() => { + orderFillBatch = [ + { + signedOrder, + takerTokenFillAmount: fillTakerAmount, + }, + { + signedOrder: anotherSignedOrder, + takerTokenFillAmount: fillTakerAmount, + }, + ]; + }); it('should throw if a batch is empty', async () => { return expect(zeroEx.exchange.batchFillOrdersAsync( [], shouldThrowOnInsufficientBalanceOrAllowance, takerAddress), @@ -255,6 +330,38 @@ describe('ExchangeWrapper', () => { expect(anotherFilledAmount).to.be.bignumber.equal(fillTakerAmount); }); }); + describe('order transaction options', () => { + beforeEach(async () => { + const emptyFillTakerAmount = new BigNumber(0); + orderFillBatch = [ + { + signedOrder, + takerTokenFillAmount: emptyFillTakerAmount, + }, + { + signedOrder: anotherSignedOrder, + takerTokenFillAmount: emptyFillTakerAmount, + }, + ]; + }); + it('should validate when orderTransactionOptions are not present', async () => { + return expect(zeroEx.exchange.batchFillOrdersAsync( + orderFillBatch, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect(zeroEx.exchange.batchFillOrdersAsync( + orderFillBatch, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, { + shouldValidate: true, + })).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect(zeroEx.exchange.batchFillOrdersAsync( + orderFillBatch, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, { + shouldValidate: false, + })).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + }); }); describe('#fillOrdersUpTo', () => { let signedOrder: SignedOrder; @@ -292,6 +399,26 @@ describe('ExchangeWrapper', () => { expect(anotherFilledAmount).to.be.bignumber.equal(remainingFillAmount); }); }); + describe('order transaction options', () => { + const emptyFillUpToAmount = new BigNumber(0); + it('should validate when orderTransactionOptions are not present', async () => { + return expect(zeroEx.exchange.fillOrdersUpToAsync( + signedOrders, emptyFillUpToAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + )).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect(zeroEx.exchange.fillOrdersUpToAsync( + signedOrders, emptyFillUpToAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, { + shouldValidate: true, + })).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect(zeroEx.exchange.fillOrdersUpToAsync( + signedOrders, emptyFillUpToAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, { + shouldValidate: false, + })).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + }); }); }); describe('cancel order(s)', () => { @@ -323,6 +450,23 @@ describe('ExchangeWrapper', () => { expect(cancelledAmount).to.be.bignumber.equal(cancelAmount); }); }); + describe('order transaction options', () => { + const emptyCancelTakerTokenAmount = new BigNumber(0); + it('should validate when orderTransactionOptions are not present', async () => { + return expect(zeroEx.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount)) + .to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect(zeroEx.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount, { + shouldValidate: true, + })).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect(zeroEx.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount, { + shouldValidate: false, + })).to.not.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + }); }); describe('#batchCancelOrdersAsync', () => { let anotherSignedOrder: SignedOrder; @@ -369,6 +513,35 @@ describe('ExchangeWrapper', () => { expect(anotherCancelledAmount).to.be.bignumber.equal(cancelAmount); }); }); + describe('order transaction options', () => { + beforeEach(async () => { + const emptyTakerTokenCancelAmount = new BigNumber(0); + cancelBatch = [ + { + order: signedOrder, + takerTokenCancelAmount: emptyTakerTokenCancelAmount, + }, + { + order: anotherSignedOrder, + takerTokenCancelAmount: emptyTakerTokenCancelAmount, + }, + ]; + }); + it('should validate when orderTransactionOptions are not present', async () => { + return expect(zeroEx.exchange.batchCancelOrdersAsync(cancelBatch)) + .to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect(zeroEx.exchange.batchCancelOrdersAsync(cancelBatch, { + shouldValidate: true, + })).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect(zeroEx.exchange.batchCancelOrdersAsync(cancelBatch, { + shouldValidate: false, + })).to.not.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + }); }); }); describe('tests that require partially filled order', () => { @@ -591,4 +764,11 @@ describe('ExchangeWrapper', () => { expect(orderHash).to.equal(orderHashFromContract); }); }); + describe('#getZRXTokenAddressAsync', () => { + it('gets the same token as is in token registry', async () => { + const zrxAddress = await zeroEx.exchange.getZRXTokenAddressAsync(); + const zrxToken = tokenUtils.getProtocolTokenOrThrow(); + expect(zrxAddress).to.equal(zrxToken.address); + }); + }); }); diff --git a/test/order_validation_test.ts b/test/order_validation_test.ts index 9a621555c..f625433eb 100644 --- a/test/order_validation_test.ts +++ b/test/order_validation_test.ts @@ -54,6 +54,46 @@ describe('OrderValidation', () => { afterEach(async () => { await blockchainLifecycle.revertAsync(); }); + describe('validateOrderFillableOrThrowAsync', () => { + it('should succeed if the order is fillable', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + await zeroEx.exchange.validateOrderFillableOrThrowAsync( + signedOrder, + ); + }); + it('should succeed if the order is asymmetric and fillable', async () => { + const makerFillableAmount = fillableAmount; + const takerFillableAmount = fillableAmount.minus(4); + const signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, + makerFillableAmount, takerFillableAmount, + ); + await zeroEx.exchange.validateOrderFillableOrThrowAsync( + signedOrder, + ); + }); + it('should throw when the order is fully filled or cancelled', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount); + return expect(zeroEx.exchange.validateOrderFillableOrThrowAsync( + signedOrder, + )).to.be.rejectedWith(ExchangeContractErrs.OrderRemainingFillAmountZero); + }); + it('should throw when order is expired', async () => { + const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017 + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, + fillableAmount, expirationInPast, + ); + return expect(zeroEx.exchange.validateOrderFillableOrThrowAsync( + signedOrder, + )).to.be.rejectedWith(ExchangeContractErrs.OrderFillExpired); + }); + }); describe('validateFillOrderAndThrowIfInvalidAsync', () => { it('should throw when the fill amount is zero', async () => { const signedOrder = await fillScenarios.createFillableSignedOrderAsync( diff --git a/test/utils/constants.ts b/test/utils/constants.ts index b677d7361..c7d3aebca 100644 --- a/test/utils/constants.ts +++ b/test/utils/constants.ts @@ -3,5 +3,6 @@ export const constants = { RPC_HOST: 'localhost', RPC_PORT: 8545, TESTRPC_NETWORK_ID: 50, - KOVAN_RPC_URL: 'https://kovan.0xproject.com', + KOVAN_RPC_URL: 'https://kovan.infura.io', + ROPSTEN_RPC_URL: 'https://ropsten.infura.io', }; diff --git a/test/utils/fill_scenarios.ts b/test/utils/fill_scenarios.ts index 563415a48..e305759f6 100644 --- a/test/utils/fill_scenarios.ts +++ b/test/utils/fill_scenarios.ts @@ -62,7 +62,9 @@ export class FillScenarios { fillableAmount, fillableAmount, ); const shouldThrowOnInsufficientBalanceOrAllowance = false; - await this.zeroEx.exchange.fillOrderAsync(signedOrder, partialFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress); + await this.zeroEx.exchange.fillOrderAsync( + signedOrder, partialFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + ); return signedOrder; } private async createAsymmetricFillableSignedOrderWithFeesAsync( |