diff options
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | src/0x.js.ts | 19 | ||||
-rw-r--r-- | src/contract_wrappers/exchange_wrapper.ts | 39 | ||||
-rw-r--r-- | src/contract_wrappers/token_wrapper.ts | 4 | ||||
-rw-r--r-- | src/types.ts | 9 | ||||
-rw-r--r-- | src/utils/assert.ts | 4 | ||||
-rw-r--r-- | src/utils/utils.ts | 4 | ||||
-rw-r--r-- | test/exchange_wrapper_test.ts | 86 | ||||
-rw-r--r-- | test/utils/fill_scenarios.ts | 18 | ||||
-rw-r--r-- | yarn.lock | 4 |
10 files changed, 171 insertions, 18 deletions
diff --git a/package.json b/package.json index 8b472f2a9..249d8906b 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "chai": "^3.5.0", "chai-as-promised": "^6.0.0", "chai-as-promised-typescript-typings": "0.0.2", - "chai-bignumber": "^2.0.0", + "chai-bignumber": "git+ssh://git@github.com:0xProject/chai-bignumber.git", "chai-typescript-typings": "^0.0.0", "copyfiles": "^1.2.0", "dirty-chai": "^1.2.2", diff --git a/src/0x.js.ts b/src/0x.js.ts index 7cf313666..40290467a 100644 --- a/src/0x.js.ts +++ b/src/0x.js.ts @@ -17,7 +17,7 @@ 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} from './types'; +import {Order, SignedOrder} from './types'; import {orderSchema} from './schemas/order_schemas'; import * as ExchangeArtifacts from './artifacts/Exchange.json'; @@ -69,11 +69,16 @@ export class ZeroEx { const salt = randomNumber.times(factor).round(); return salt; } - /** Checks if order hash is valid */ - public static isValidOrderHash(orderHash: string): boolean { - assert.isString('orderHash', orderHash); - const isValid = /^0x[0-9A-F]{64}$/i.test(orderHash); - return isValid; + /** + * Checks if the supplied hex encoded order hash is valid. + * Note: Valid means it has the expected format, not that an order with the orderHash exists. + */ + public static isValidOrderHash(orderHashHex: string): boolean { + // Since this method can be called to check if any arbitrary string conforms to an orderHash's + // format, we only assert that we were indeed passed a string. + assert.isString('orderHashHex', orderHashHex); + const isValidOrderHash = utils.isValidOrderHash(orderHashHex); + return isValidOrderHash; } /** * A unit amount is defined as the amount of a token above the specified decimal places (integer part). @@ -132,7 +137,7 @@ export class ZeroEx { /** * Computes the orderHash for a given order and returns it as a hex encoded string. */ - public async getOrderHashHexAsync(order: Order): Promise<string> { + public async getOrderHashHexAsync(order: Order|SignedOrder): Promise<string> { const exchangeContractAddr = await this.getExchangeAddressAsync(); assert.doesConformToSchema('order', SchemaValidator.convertToJSONSchemaCompatibleObject(order as object), diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index 4aa532bdd..cb869b498 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -1,4 +1,5 @@ import * as _ from 'lodash'; +import * as BigNumber from 'bignumber.js'; import {Web3Wrapper} from '../web3_wrapper'; import { ECSignature, @@ -60,6 +61,44 @@ export class ExchangeWrapper extends ContractWrapper { return isValidSignature; } /** + * Returns the unavailable takerAmount of an order. Unavailable amount is defined as the total + * amount that has been filled or cancelled. The remaining takerAmount can be calculated by + * subtracting the unavailable amount from the total order takerAmount. + */ + public async getUnavailableTakerAmountAsync(orderHashHex: string): Promise<BigNumber.BigNumber> { + assert.isValidOrderHash('orderHashHex', orderHashHex); + + const exchangeContract = await this.getExchangeContractAsync(); + let unavailableAmountInBaseUnits = await exchangeContract.getUnavailableValueT.call(orderHashHex); + // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber + unavailableAmountInBaseUnits = new BigNumber(unavailableAmountInBaseUnits); + return unavailableAmountInBaseUnits; + } + /** + * Retrieve the takerAmount of an order that has already been filled. + */ + public async getFilledTakerAmountAsync(orderHashHex: string): Promise<BigNumber.BigNumber> { + assert.isValidOrderHash('orderHashHex', orderHashHex); + + const exchangeContract = await this.getExchangeContractAsync(); + let fillAmountInBaseUnits = await exchangeContract.filled.call(orderHashHex); + // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber + fillAmountInBaseUnits = new BigNumber(fillAmountInBaseUnits); + return fillAmountInBaseUnits; + } + /** + * Retrieve the takerAmount of an order that has been cancelled. + */ + public async getCanceledTakerAmountAsync(orderHashHex: string): Promise<BigNumber.BigNumber> { + assert.isValidOrderHash('orderHashHex', orderHashHex); + + const exchangeContract = await this.getExchangeContractAsync(); + let cancelledAmountInBaseUnits = await exchangeContract.cancelled.call(orderHashHex); + // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber + cancelledAmountInBaseUnits = new BigNumber(cancelledAmountInBaseUnits); + return cancelledAmountInBaseUnits; + } + /** * Fills a signed order with a fillAmount denominated in baseUnits of the taker token. * Since the order in which transactions are included in the next block is indeterminate, race-conditions * could arise where a users balance or allowance changes before the fillOrder executes. Because of this, diff --git a/src/contract_wrappers/token_wrapper.ts b/src/contract_wrappers/token_wrapper.ts index 8bec39a52..c8b557d0d 100644 --- a/src/contract_wrappers/token_wrapper.ts +++ b/src/contract_wrappers/token_wrapper.ts @@ -28,8 +28,7 @@ export class TokenWrapper extends ContractWrapper { const tokenContract = await this.getTokenContractAsync(tokenAddress); let balance = await tokenContract.balanceOf.call(ownerAddress); - // The BigNumber instance returned by Web3 is of a much older version then our own, we therefore - // should always re-instantiate the returned BigNumber after retrieval. + // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber balance = new BigNumber(balance); return balance; } @@ -64,6 +63,7 @@ export class TokenWrapper extends ContractWrapper { const tokenContract = await this.getTokenContractAsync(tokenAddress); let allowanceInBaseUnits = await tokenContract.allowance.call(ownerAddress, spenderAddress); + // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber allowanceInBaseUnits = new BigNumber(allowanceInBaseUnits); return allowanceInBaseUnits; } diff --git a/src/types.ts b/src/types.ts index d17dadf29..9d3127325 100644 --- a/src/types.ts +++ b/src/types.ts @@ -37,6 +37,9 @@ export type OrderValues = [BigNumber.BigNumber, BigNumber.BigNumber, BigNumber.B export interface ExchangeContract { isValidSignature: any; + getUnavailableValueT: { + call: (orderHash: string) => BigNumber.BigNumber; + }; isRoundingError: { call: (takerTokenAmount: BigNumber.BigNumber, fillTakerAmount: BigNumber.BigNumber, makerTokenAmount: BigNumber.BigNumber, txOpts: TxOpts) => Promise<boolean>; @@ -47,6 +50,12 @@ export interface ExchangeContract { estimateGas: (orderAddresses: OrderAddresses, orderValues: OrderValues, fillAmount: BigNumber.BigNumber, shouldCheckTransfer: boolean, v: number, r: string, s: string, txOpts: TxOpts) => number; }; + filled: { + call: (orderHash: string) => BigNumber.BigNumber; + }; + cancelled: { + call: (orderHash: string) => BigNumber.BigNumber; + }; ZRX: { call: () => Promise<string>; }; diff --git a/src/utils/assert.ts b/src/utils/assert.ts index 446974b10..5a31e1b16 100644 --- a/src/utils/assert.ts +++ b/src/utils/assert.ts @@ -3,6 +3,7 @@ import * as BigNumber from 'bignumber.js'; import * as Web3 from 'web3'; import {Web3Wrapper} from '../web3_wrapper'; import {SchemaValidator} from './schema_validator'; +import {utils} from './utils'; const HEX_REGEX = /^0x[0-9A-F]*$/i; @@ -33,6 +34,9 @@ export const assert = { isNumber(variableName: string, value: number): void { this.assert(_.isFinite(value), this.typeAssertionMessage(variableName, 'number', value)); }, + isValidOrderHash(variableName: string, value: string): void { + this.assert(utils.isValidOrderHash(value), this.typeAssertionMessage(variableName, 'orderHash', value)); + }, isBoolean(variableName: string, value: boolean): void { this.assert(_.isBoolean(value), this.typeAssertionMessage(variableName, 'boolean', value)); }, diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 336eaf7bb..e6840a624 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -18,4 +18,8 @@ export const utils = { isParityNode(nodeVersion: string): boolean { return _.includes(nodeVersion, 'Parity'); }, + isValidOrderHash(orderHashHex: string) { + const isValid = /^0x[0-9A-F]{64}$/i.test(orderHashHex); + return isValid; + }, }; diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts index 607cdf052..0f683f811 100644 --- a/test/exchange_wrapper_test.ts +++ b/test/exchange_wrapper_test.ts @@ -9,7 +9,8 @@ import promisify = require('es6-promisify'); import {web3Factory} from './utils/web3_factory'; import {ZeroEx} from '../src/0x.js'; import {BlockchainLifecycle} from './utils/blockchain_lifecycle'; -import {ExchangeContractErrs, SignedOrder, Token} from '../src/types'; +import {orderFactory} from './utils/order_factory'; +import {Token, SignedOrder, ExchangeContractErrs} from '../src/types'; import {FillScenarios} from './utils/fill_scenarios'; import {TokenUtils} from './utils/token_utils'; @@ -19,14 +20,24 @@ chai.use(ChaiBigNumber()); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(); +const NON_EXISTENT_ORDER_HASH = '0x79370342234e7acd6bbeac335bd3bb1d368383294b64b8160a00f4060e4d3777'; + describe('ExchangeWrapper', () => { let web3: Web3; let zeroEx: ZeroEx; + let tokenUtils: TokenUtils; + let tokens: Token[]; let userAddresses: string[]; + let zrxTokenAddress: string; + let fillScenarios: FillScenarios; before(async () => { web3 = web3Factory.create(); zeroEx = new ZeroEx(web3); userAddresses = await promisify(web3.eth.getAccounts)(); + tokens = await zeroEx.tokenRegistry.getTokensAsync(); + tokenUtils = new TokenUtils(tokens); + zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address; + fillScenarios = new FillScenarios(zeroEx, userAddresses, tokens, zrxTokenAddress); }); beforeEach(async () => { await blockchainLifecycle.startAsync(); @@ -105,26 +116,19 @@ describe('ExchangeWrapper', () => { }); }); describe('#fillOrderAsync', () => { - let tokens: Token[]; let makerTokenAddress: string; let takerTokenAddress: string; - let fillScenarios: FillScenarios; let coinbase: string; let makerAddress: string; let takerAddress: string; let feeRecipient: string; - let zrxTokenAddress: string; const fillTakerAmount = new BigNumber(5); const shouldCheckTransfer = false; before(async () => { [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses; - tokens = await zeroEx.tokenRegistry.getTokensAsync(); - const tokenUtils = new TokenUtils(tokens); const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens(); makerTokenAddress = makerToken.address; takerTokenAddress = takerToken.address; - zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address; - fillScenarios = new FillScenarios(zeroEx, userAddresses, tokens, zrxTokenAddress); }); afterEach('reset default account', () => { zeroEx.setTransactionSenderAccount(userAddresses[0]); @@ -326,4 +330,70 @@ describe('ExchangeWrapper', () => { }); }); }); + describe('tests that require partially filled order', () => { + let makerTokenAddress: string; + let takerTokenAddress: string; + let takerAddress: string; + let fillableAmount: BigNumber.BigNumber; + let partialFillAmount: BigNumber.BigNumber; + let signedOrder: SignedOrder; + before(() => { + takerAddress = userAddresses[1]; + const [makerToken, takerToken] = tokens; + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + }); + beforeEach(async () => { + fillableAmount = new BigNumber(5); + partialFillAmount = new BigNumber(2); + signedOrder = await fillScenarios.createPartiallyFilledSignedOrderAsync( + makerTokenAddress, takerTokenAddress, takerAddress, fillableAmount, partialFillAmount, + ); + }); + describe('#getUnavailableTakerAmountAsync', () => { + it ('should throw if passed an invalid orderHash', async () => { + const invalidOrderHashHex = '0x123'; + expect(zeroEx.exchange.getUnavailableTakerAmountAsync(invalidOrderHashHex)).to.be.rejected(); + }); + it ('should return zero if passed a valid but non-existent orderHash', async () => { + const unavailableValueT = await zeroEx.exchange.getUnavailableTakerAmountAsync(NON_EXISTENT_ORDER_HASH); + expect(unavailableValueT).to.be.bignumber.equal(0); + }); + it ('should return the unavailableValueT for a valid and partially filled orderHash', async () => { + const orderHash = await zeroEx.getOrderHashHexAsync(signedOrder); + const unavailableValueT = await zeroEx.exchange.getUnavailableTakerAmountAsync(orderHash); + expect(unavailableValueT).to.be.bignumber.equal(partialFillAmount); + }); + }); + describe('#getFilledTakerAmountAsync', () => { + it ('should throw if passed an invalid orderHash', async () => { + const invalidOrderHashHex = '0x123'; + expect(zeroEx.exchange.getFilledTakerAmountAsync(invalidOrderHashHex)).to.be.rejected(); + }); + it ('should return zero if passed a valid but non-existent orderHash', async () => { + const filledValueT = await zeroEx.exchange.getFilledTakerAmountAsync(NON_EXISTENT_ORDER_HASH); + expect(filledValueT).to.be.bignumber.equal(0); + }); + it ('should return the filledValueT for a valid and partially filled orderHash', async () => { + const orderHash = await zeroEx.getOrderHashHexAsync(signedOrder); + const filledValueT = await zeroEx.exchange.getFilledTakerAmountAsync(orderHash); + expect(filledValueT).to.be.bignumber.equal(partialFillAmount); + }); + }); + describe('#getCanceledTakerAmountAsync', () => { + it ('should throw if passed an invalid orderHash', async () => { + const invalidOrderHashHex = '0x123'; + expect(zeroEx.exchange.getCanceledTakerAmountAsync(invalidOrderHashHex)).to.be.rejected(); + }); + it ('should return zero if passed a valid but non-existent orderHash', async () => { + const cancelledValueT = await zeroEx.exchange.getCanceledTakerAmountAsync(NON_EXISTENT_ORDER_HASH); + expect(cancelledValueT).to.be.bignumber.equal(0); + }); + it ('should return the cancelledValueT for a valid and partially filled orderHash', async () => { + const orderHash = await zeroEx.getOrderHashHexAsync(signedOrder); + const cancelledValueT = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHash); + expect(cancelledValueT).to.be.bignumber.equal(0); + }); + }); + }); }); diff --git a/test/utils/fill_scenarios.ts b/test/utils/fill_scenarios.ts index a44d6b18a..d186593b9 100644 --- a/test/utils/fill_scenarios.ts +++ b/test/utils/fill_scenarios.ts @@ -51,6 +51,24 @@ export class FillScenarios { makerFillableAmount, takerFillableAmount, feeRecepient, expirationUnixTimestampSec, ); } + public async createPartiallyFilledSignedOrderAsync(makerTokenAddress: string, takerTokenAddress: string, + takerAddress: string, fillableAmount: BigNumber.BigNumber, + partialFillAmount: BigNumber.BigNumber) { + const prevSenderAccount = await this.zeroEx.getTransactionSenderAccountIfExistsAsync(); + const [makerAddress] = this.userAddresses; + const signedOrder = await this.createAsymmetricFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, + fillableAmount, fillableAmount, + ); + + this.zeroEx.setTransactionSenderAccount(takerAddress); + const shouldCheckTransfer = false; + await this.zeroEx.exchange.fillOrderAsync(signedOrder, partialFillAmount, shouldCheckTransfer); + + // Re-set sender account so as to avoid introducing side-effects + this.zeroEx.setTransactionSenderAccount(prevSenderAccount as string); + return signedOrder; + } private async createAsymmetricFillableSignedOrderWithFeesAsync( makerTokenAddress: string, takerTokenAddress: string, makerFee: BigNumber.BigNumber, takerFee: BigNumber.BigNumber, @@ -914,6 +914,10 @@ chai-bignumber@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/chai-bignumber/-/chai-bignumber-2.0.0.tgz#0cbf9b81790801c3f24fb77f59fa1e17a9c6e3f2" +"chai-bignumber@git+ssh://git@github.com:0xProject/chai-bignumber.git": + version "2.0.0" + resolved "git+ssh://git@github.com/0xProject/chai-bignumber.git#9ef233f3e2ca3ab250b66149390c833c117c27e7" + chai-typescript-typings@^0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/chai-typescript-typings/-/chai-typescript-typings-0.0.0.tgz#52e076d72cf29129c94ab1dba6e33ce3828a0724" |