diff options
58 files changed, 2465 insertions, 141 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml index 330420db4..9a39dd33c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,7 +24,5 @@ jobs: name: testrpc command: npm run testrpc -- --db testrpc_snapshot background: true - - run: yarn lerna:run test:coverage - - run: yarn lerna:run report_test_coverage - - run: if [ $CIRCLE_BRANCH = "development" ]; then yarn lerna:run test:umd; fi + - run: yarn lerna:run test:circleci - run: yarn lerna:run lint @@ -13,3 +13,26 @@ This repository contains all the 0x developer tools written in TypeScript. Our h [![Join the chat at https://gitter.im/0xProject/Lobby](https://badges.gitter.im/0xProject/Lobby.svg)](https://gitter.im/0xProject/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Greenkeeper badge](https://badges.greenkeeper.io/0xProject/0x.js.svg?token=7c22e5c72acf39d3ead8d29c5d9bb38f9096df3e643024dcedd53ab732847be1&ts=1496426342666)](https://greenkeeper.io/) + +Instructions +------------ + +Make sure you have `yarn@1.x` installed locally. + +### Creating a new sub-package + +1. Make sure the `name` field in the sub-package's `package.json` starts with `@0xproject/` and has a unique name (e.g `@0xproject/assert`). + +2. Run `yarn install` to install all it's dependencies. + +### How to add a sub-package as a dependency to another sub-package: + +1. Add the sub-packages name (declared in it's `package.json`) to your sub-packages `package.json` under `dependencies` or `devDependencies`. + +2. Run `yarn install` from anywhere in the mono repo. + +3. Import the sub-package as: + +``` +import {myPkg} from '@0xproject/myPkg'; +``` diff --git a/packages/0x.js/CHANGELOG.md b/packages/0x.js/CHANGELOG.md new file mode 100644 index 000000000..b31e05923 --- /dev/null +++ b/packages/0x.js/CHANGELOG.md @@ -0,0 +1,221 @@ +# CHANGELOG + +v0.24.0 - _November 13, 2017_ +------------------------ + * Standardise on Cancelled over Canceled + * Add missing `DecodedLogEvent` type to exported types + +v0.23.0 - _November 12, 2017_ +------------------------ + * Fixed unhandled promise rejection error in subscribe methods (#209) + * Subscribe callbacks now receive an error object as their first argument + +v0.22.6 - _November 10, 2017_ +------------------------ + * Add a timeout parameter to transaction awaiting (#206) + +v0.22.5 - _November 7, 2017_ +------------------------ + * Re-publish v0.22.4 to fix publishing issue + +v0.22.4 - _October 25, 2017_ +------------------------ + * Upgraded bignumber.js to a new version that ships with native typings + +v0.22.3 - _October 25, 2017_ +------------------------ + * Fixed an issue with new version of testrpc and unlimited proxy allowance (#199) + +v0.22.2 - _October 24, 2017_ +------------------------ + * Fixed rounding of maker fill amount and incorrect validation of partial fees (#197) + +v0.22.0 - _October 16, 2017_ +------------------------ + * Started using `OrderFillRequest` interface instead of `OrderFillOrKillRequest` interface for `zeroEx.exchange.batchFillOrKill` (#187) + * Removed `OrderFillOrKillRequest` (#187) + +v0.21.4 - _October 13, 2017_ +------------------------ + * Made 0x.js more type-safe by making `getLogsAsync` and `subscribe/subscribeAsync` generics parametrized with arg type (#194) + +v0.21.3 - _October 12, 2017_ +------------------------ + * Fixed a bug causing order fills to throw `INSUFFICIENT_TAKER_ALLOWANCE` (#193) + +v0.21.2 - _October 11, 2017_ +------------------------ + * Exported `ContractEventArg` as a public type (#190) + +v0.21.1 - _October 11, 2017_ +------------------------ + * Fixed a bug in subscriptions (#189) + +v0.21.0 - _October 10, 2017_ +------------------------ + * Complete rewrite of subscription logic (#182) + * Subscriptions no longer return historical logs. If you want them - use `getLogsAsync` + * Subscriptions now use [ethereumjs-blockstream](https://github.com/ethereumjs/ethereumjs-blockstream) under the hood + * Subscriptions correctly handle block re-orgs (forks) + * Subscriptions correctly backfill logs (connection problems) + * They no longer setup filters on the underlying nodes, so you can use them with infura without a filter Subprovider + * Removed `ContractEventEmitter` and added `LogEvent` + * Renamed `zeroEx.token.subscribeAsync` to `zeroEx.token.subscribe` + * Added `zeroEx.token.unsubscribe` and `zeroEx.exchange.unsubscribe` + * Renamed `zeroEx.exchange.stopWatchingAllEventsAsync` to `zeroEx.exhange.unsubscribeAll` + * Renamed `zeroEx.token.stopWatchingAllEventsAsync` to `zeroEx.token.unsubscribeAll` + * Fixed the batch fills validation by emulating all balance & proxy allowance changes (#185) + +v0.20.0 - _October 5, 2017_ +------------------------ + * Add `zeroEx.token.getLogsAsync` (#178) + * Add `zeroEx.exchange.getLogsAsync` (#178) + * Fixed fees validation when one of the tokens transferred is ZRX (#181) + +v0.19.0 - _September 29, 2017_ +------------------------ + * Made order validation optional (#172) + * Added Ropsten testnet support (#173) + * Fixed a bug causing awaitTransactionMinedAsync to DDos backend nodes (#175) + +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) + * ZeroExConfig.exchangeContractAddress + * ZeroExConfig.tokenRegistryContractAddress + * ZeroExConfig.etherTokenContractAddress + * Added `zeroEx.tokenRegistry.getContractAddressAsync` (#165) + +v0.15.0 - _September 8, 2017_ +------------------------ + * Added the ability to specify a historical `blockNumber` at which to query the blockchain's state when calling a token or exchange method (#161) + +v0.14.2 - _September 7, 2017_ +------------------------ + * Fixed an issue with bignumber.js types not found (#160) + +v0.14.1 - _September 7, 2017_ +------------------------ + * Fixed an issue with Artifact type not found (#159) + +v0.14.0 - _September 6, 2017_ +------------------------ + * Added `zeroEx.exchange.throwLogErrorsAsErrors` method to public interface (#157) + * Fixed an issue with overlapping async intervals in `zeroEx.awaitTransactionMinedAsync` (#157) + * Fixed an issue with log decoder returning `BigNumber`s as `strings` (#157) + +v0.13.0 - _September 6, 2017_ +------------------------ + * Made all the functions submitting transactions to the network to immediately return transaction hash (#151) + * Added `zeroEx.awaitTransactionMinedAsync` (#151) + * Added `TransactionReceiptWithDecodedLogs`, `LogWithDecodedArgs`, `DecodedLogArgs` to public types (#151) + * Added signature validation to `validateFillOrderThrowIfInvalidAsync` (#152) + +v0.12.1 - _September 2, 2017_ +------------------------ + * Added the support for web3@1.x.x provider (#142) + * Added the optional `zeroExConfig` parameter to the constructor of `ZeroEx` (#139) + * Added the ability to specify `gasPrice` when instantiating `ZeroEx` (#139) + +v0.11.0 - _August 24, 2017_ +------------------------ + * Added `zeroEx.token.setUnlimitedProxyAllowanceAsync` (#137) + * Added `zeroEx.token.setUnlimitedAllowanceAsync` (#137) + * Added `zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS` (#137) + +v0.10.4 - _Aug 24, 2017_ +------------------------ + * Fixed a bug where checksummed addresses were being pulled from artifacts and not lower-cased. (#135) + +v0.10.1 - _Aug 24, 2017_ +------------------------ + * Added `zeroEx.exchange.validateFillOrderThrowIfInvalidAsync` (#128) + * Added `zeroEx.exchange.validateFillOrKillOrderThrowIfInvalidAsync` (#128) + * Added `zeroEx.exchange.validateCancelOrderThrowIfInvalidAsync` (#128) + * Added `zeroEx.exchange.isRoundingErrorAsync` (#128) + * Added `zeroEx.proxy.getContractAddressAsync` (#130) + * Added `zeroEx.tokenRegistry.getTokenAddressesAsync` (#132) + * Added `zeroEx.tokenRegistry.getTokenAddressBySymbolIfExistsAsync` (#132) + * Added `zeroEx.tokenRegistry.getTokenAddressByNameIfExistsAsync` (#132) + * Added `zeroEx.tokenRegistry.getTokenBySymbolIfExistsAsync` (#132) + * Added `zeroEx.tokenRegistry.getTokenByNameIfExistsAsync` (#132) + * Added clear error message when checksummed address is passed to a public method (#124) + * Fixes the description of `shouldThrowOnInsufficientBalanceOrAllowance` in docs (#127) + +v0.9.3 - _Aug 22, 2017_ +------------------------ + * Update contract artifacts to include latest Kovan and Mainnet deploys (#118) + +v0.9.2 - _Aug 21, 2017_ +------------------------ + * *This version was unpublished because of a publishing issue.* + * Update contract artifacts to include latest Kovan and Mainnet deploys (#118) + +v0.9.1 - _Aug. 16, 2017_ +------------------------ + * Fixed the bug causing `zeroEx.token.getBalanceAsync()` to fail if no addresses available (#120) + +v0.9.0 - _Jul. 26, 2017_ +------------------------ + * Migrated to the new version of smart contracts (#101) + * Removed the ability to call methods on multiple authorized Exchange smart contracts (#106) + * Made `zeroEx.getOrderHashHex` a static method (#107) + * Cached `net_version` requests and invalidate the cache on calls to `setProvider` (#95) + * Renamed `zeroEx.exchange.batchCancelOrderAsync` to `zeroEx.exchange.batchCancelOrdersAsync` + * Renamed `zeroEx.exchange.batchFillOrderAsync` to `zeroEx.exchange.batchFillOrdersAsync` + * Updated to typescript v2.4 (#104) + * Fixed an issue with incorrect balance/allowance validation when ZRX is one of the tokens traded (#109) + +v0.8.0 - _Jul. 4, 2017_ +------------------------ + * Added the ability to call methods on different authorized versions of the Exchange smart contract (#82) + * Updated contract artifacts to reflect latest changes to the smart contracts (0xproject/contracts#59) + * Added `zeroEx.proxy.isAuthorizedAsync` and `zeroEx.proxy.getAuthorizedAddressesAsync` (#89) + * Added `zeroEx.token.subscribeAsync` (#90) + * Made contract invalidation functions private (#90) + * `zeroEx.token.invalidateContractInstancesAsync` + * `zeroEx.exchange.invalidateContractInstancesAsync` + * `zeroEx.proxy.invalidateContractInstance` + * `zeroEx.tokenRegistry.invalidateContractInstance` + * Fixed the bug where `zeroEx.setProviderAsync` didn't invalidate etherToken contract's instance + +v0.7.1 - _Jun. 26, 2017_ +------------------------ + * Added the ability to convert Ether to wrapped Ether tokens and back via `zeroEx.etherToken.depostAsync` and `zeroEx.etherToken.withdrawAsync` (#81) + +v0.7.0 - _Jun. 22, 2017_ +------------------------ + * Added Kovan smart contract artifacts (#78) + * Started returning fillAmount from `fillOrderAsync` and `fillUpToAsync` (#72) + * Started returning cancelledAmount from `cancelOrderAsync` (#72) + * Renamed type `LogCancelArgs` to `LogCancelContractEventArgs` and `LogFillArgs` to `LogFillContractEventArgs` + +v0.6.2 - _Jun. 21, 2017_ +------------------------ + * Reduced bundle size + * Improved documentation + +v0.6.1 - _Jun. 19, 2017_ +------------------------ + * Improved documentation + +v0.6.0 - _Jun. 19, 2017_ +------------------------ + * Made `ZeroEx` class accept `Web3Provider` instance instead of `Web3` instance + * Added types for contract event arguments + +v0.5.2 - _Jun. 15, 2017_ +------------------------ + * Fixed the bug in `postpublish` script that caused that only unminified UMD bundle was uploaded to release page + +v0.5.1 - _Jun. 15, 2017_ +------------------------ + * Added `postpublish` script to publish to Github Releases with assets. diff --git a/packages/0x.js/package.json b/packages/0x.js/package.json index 6e30df612..0204d723f 100644 --- a/packages/0x.js/package.json +++ b/packages/0x.js/package.json @@ -1,6 +1,6 @@ { "name": "0x.js", - "version": "0.23.0", + "version": "0.25.1", "description": "A javascript library for interacting with the 0x protocol", "keywords": [ "0x.js", @@ -13,12 +13,13 @@ "types": "lib/src/index.d.ts", "scripts": { "prebuild": "npm run clean", - "build": "run-p build:umd:prod build:commonjs", + "build": "run-p build:umd:prod build:commonjs; exit 0;", "prepublishOnly": "run-p build", "postpublish": "run-s release docs:json upload_docs_json", "release": "publish-release --assets _bundles/index.js,_bundles/index.min.js --tag $(git describe --tags) --owner 0xProject --repo 0x.js", "upload_docs_json": "aws s3 cp docs/index.json s3://0xjs-docs-jsons/$(git describe --tags).json --profile 0xproject --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type aplication/json", "lint": "tslint src/**/*.ts test/**/*.ts", + "test:circleci": "run-s test:coverage report_test_coverage; if [ $CIRCLE_BRANCH = \"development\" ]; then yarn test:umd; fi", "test": "run-s clean test:commonjs", "test:umd": "./scripts/test_umd.sh", "test:coverage": "nyc npm run test --all", @@ -48,6 +49,7 @@ "node": ">=6.0.0" }, "devDependencies": { + "@0xproject/tslint-config": "^0.1.0", "@types/jsonschema": "^1.1.1", "@types/lodash": "^4.14.64", "@types/mocha": "^2.2.41", @@ -75,8 +77,7 @@ "sinon": "^4.0.0", "source-map-support": "^0.5.0", "truffle-hdwallet-provider": "^0.0.3", - "tslint": "~5.5.0", - "tslint-config-0xproject": "^0.0.2", + "tslint": "5.8.0", "typedoc": "~0.8.0", "types-bn": "^0.0.1", "types-ethereumjs-util": "0xProject/types-ethereumjs-util", @@ -86,8 +87,10 @@ "webpack": "^3.1.0" }, "dependencies": { - "0x-json-schemas": "^0.6.1", - "bignumber.js": "^4.1.0", + "@0xproject/assert": "^0.0.4", + "@0xproject/json-schemas": "^0.6.7", + "bignumber.js": "~4.1.0", + "bn.js": "4.11.8", "compare-versions": "^3.0.1", "es6-promisify": "^5.0.0", "ethereumjs-abi": "^0.6.4", diff --git a/packages/0x.js/src/0x.ts b/packages/0x.js/src/0x.ts index fe765bbbe..85c2b7724 100644 --- a/packages/0x.js/src/0x.ts +++ b/packages/0x.js/src/0x.ts @@ -1,6 +1,6 @@ import * as _ from 'lodash'; import BigNumber from 'bignumber.js'; -import {SchemaValidator, schemas} from '0x-json-schemas'; +import {SchemaValidator, schemas} from '@0xproject/json-schemas'; import {bigNumberConfigs} from './bignumber_config'; import * as ethUtil from 'ethereumjs-util'; import {Web3Wrapper} from './web3_wrapper'; diff --git a/packages/0x.js/src/contract.ts b/packages/0x.js/src/contract.ts index 1aacc65dc..7ccd336d6 100644 --- a/packages/0x.js/src/contract.ts +++ b/packages/0x.js/src/contract.ts @@ -1,7 +1,7 @@ import * as Web3 from 'web3'; import * as _ from 'lodash'; import promisify = require('es6-promisify'); -import {SchemaValidator, schemas} from '0x-json-schemas'; +import {SchemaValidator, schemas} from '@0xproject/json-schemas'; import {AbiType} from './types'; export class Contract implements Web3.ContractInstance { diff --git a/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts b/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts index 654637a38..3e631b73e 100644 --- a/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts +++ b/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts @@ -1,7 +1,7 @@ import * as _ from 'lodash'; import * as Web3 from 'web3'; import BigNumber from 'bignumber.js'; -import {schemas} from '0x-json-schemas'; +import {schemas} from '@0xproject/json-schemas'; import {Web3Wrapper} from '../web3_wrapper'; import { ECSignature, @@ -95,7 +95,7 @@ export class ExchangeWrapper extends ContractWrapper { * @param orderHash The hex encoded orderHash for which you would like to retrieve the * unavailable takerAmount. * @param methodOpts Optional arguments this method accepts. - * @return The amount of the order (in taker tokens) that has either been filled or canceled. + * @return The amount of the order (in taker tokens) that has either been filled or cancelled. */ public async getUnavailableTakerAmountAsync(orderHash: string, methodOpts?: MethodOpts): Promise<BigNumber> { @@ -133,7 +133,7 @@ export class ExchangeWrapper extends ContractWrapper { * @param methodOpts Optional arguments this method accepts. * @return The amount of the order (in taker tokens) that has been cancelled. */ - public async getCanceledTakerAmountAsync(orderHash: string, methodOpts?: MethodOpts): Promise<BigNumber> { + public async getCancelledTakerAmountAsync(orderHash: string, methodOpts?: MethodOpts): Promise<BigNumber> { assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); const exchangeContract = await this._getExchangeContractAsync(); diff --git a/packages/0x.js/src/contract_wrappers/token_wrapper.ts b/packages/0x.js/src/contract_wrappers/token_wrapper.ts index 614ac19d4..4b89d3cfc 100644 --- a/packages/0x.js/src/contract_wrappers/token_wrapper.ts +++ b/packages/0x.js/src/contract_wrappers/token_wrapper.ts @@ -1,6 +1,6 @@ import * as _ from 'lodash'; import BigNumber from 'bignumber.js'; -import {schemas} from '0x-json-schemas'; +import {schemas} from '@0xproject/json-schemas'; import {Web3Wrapper} from '../web3_wrapper'; import {assert} from '../utils/assert'; import {constants} from '../utils/constants'; diff --git a/packages/0x.js/src/order_watcher/order_state_watcher.ts b/packages/0x.js/src/order_watcher/order_state_watcher.ts index 2b9d7997e..bafd7a994 100644 --- a/packages/0x.js/src/order_watcher/order_state_watcher.ts +++ b/packages/0x.js/src/order_watcher/order_state_watcher.ts @@ -1,5 +1,5 @@ import * as _ from 'lodash'; -import {schemas} from '0x-json-schemas'; +import {schemas} from '@0xproject/json-schemas'; import {ZeroEx} from '../0x'; import {EventWatcher} from './event_watcher'; import {assert} from '../utils/assert'; diff --git a/packages/0x.js/src/stores/order_filled_cancelled_lazy_store.ts b/packages/0x.js/src/stores/order_filled_cancelled_lazy_store.ts index 9d74da096..666d8363c 100644 --- a/packages/0x.js/src/stores/order_filled_cancelled_lazy_store.ts +++ b/packages/0x.js/src/stores/order_filled_cancelled_lazy_store.ts @@ -42,7 +42,7 @@ export class OrderFilledCancelledLazyStore { const methodOpts = { defaultBlock: BlockParamLiteral.Pending, }; - const cancelledTakerAmount = await this.exchange.getCanceledTakerAmountAsync(orderHash, methodOpts); + const cancelledTakerAmount = await this.exchange.getCancelledTakerAmountAsync(orderHash, methodOpts); this.setCancelledTakerAmount(orderHash, cancelledTakerAmount); } const cachedCancelled = this.cancelledTakerAmount[orderHash]; diff --git a/packages/0x.js/src/types.ts b/packages/0x.js/src/types.ts index da1971387..71089f9a1 100644 --- a/packages/0x.js/src/types.ts +++ b/packages/0x.js/src/types.ts @@ -488,7 +488,7 @@ export interface OrderRelevantState { makerFeeBalance: BigNumber; makerFeeProxyAllowance: BigNumber; filledTakerTokenAmount: BigNumber; - canceledTakerTokenAmount: BigNumber; + cancelledTakerTokenAmount: BigNumber; remainingFillableMakerTokenAmount: BigNumber; remainingFillableTakerTokenAmount: BigNumber; } diff --git a/packages/0x.js/src/utils/abi_decoder.ts b/packages/0x.js/src/utils/abi_decoder.ts index 840ad9be0..df0fb2d6f 100644 --- a/packages/0x.js/src/utils/abi_decoder.ts +++ b/packages/0x.js/src/utils/abi_decoder.ts @@ -34,7 +34,7 @@ export class AbiDecoder { value = this.padZeros(new BigNumber(value).toString(16)); } else if (param.type === SolidityTypes.Uint256 || param.type === SolidityTypes.Uint8 || - param.type === SolidityTypes.Uint ) { + param.type === SolidityTypes.Uint) { value = new BigNumber(value); } decodedParams[param.name] = value; diff --git a/packages/0x.js/src/utils/assert.ts b/packages/0x.js/src/utils/assert.ts index e5c9439f3..63d975c03 100644 --- a/packages/0x.js/src/utils/assert.ts +++ b/packages/0x.js/src/utils/assert.ts @@ -1,66 +1,24 @@ import * as _ from 'lodash'; import * as Web3 from 'web3'; import BigNumber from 'bignumber.js'; -import {SchemaValidator, Schema} from '0x-json-schemas'; +import {SchemaValidator, Schema} from '@0xproject/json-schemas'; +import {assert as sharedAssert} from '@0xproject/assert'; import {Web3Wrapper} from '../web3_wrapper'; import {signatureUtils} from '../utils/signature_utils'; import {ECSignature} from '../types'; const HEX_REGEX = /^0x[0-9A-F]*$/i; -export const assert = { - isBigNumber(variableName: string, value: BigNumber): void { - const isBigNumber = _.isObject(value) && (value as any).isBigNumber; - this.assert(isBigNumber, this.typeAssertionMessage(variableName, 'BigNumber', value)); - }, - isValidBaseUnitAmount(variableName: string, value: BigNumber) { - assert.isBigNumber(variableName, value); - const hasDecimals = value.decimalPlaces() !== 0; - this.assert( - !hasDecimals, `${variableName} should be in baseUnits (no decimals), found value: ${value.toNumber()}`, - ); - }, +export const assert = _.extend({}, sharedAssert, { isValidSignature(orderHash: string, ecSignature: ECSignature, signerAddress: string) { const isValidSignature = signatureUtils.isValidSignature(orderHash, ecSignature, signerAddress); this.assert(isValidSignature, `Expected order with hash '${orderHash}' to have a valid signature`); }, - isUndefined(value: any, variableName?: string): void { - this.assert(_.isUndefined(value), this.typeAssertionMessage(variableName, 'undefined', value)); - }, - isString(variableName: string, value: string): void { - this.assert(_.isString(value), this.typeAssertionMessage(variableName, 'string', value)); - }, - isFunction(variableName: string, value: any): void { - this.assert(_.isFunction(value), this.typeAssertionMessage(variableName, 'function', value)); - }, - isHexString(variableName: string, value: string): void { - this.assert(_.isString(value) && HEX_REGEX.test(value), - this.typeAssertionMessage(variableName, 'HexString', value)); - }, - isETHAddressHex(variableName: string, value: string): void { - const web3 = new Web3(); - this.assert(web3.isAddress(value), this.typeAssertionMessage(variableName, 'ETHAddressHex', value)); - this.assert( - web3.isAddress(value) && value.toLowerCase() === value, - `Checksummed addresses are not supported. Convert ${variableName} to lower case before passing`, - ); - }, - doesBelongToStringEnum(variableName: string, value: string, - stringEnum: any /* There is no base type for every string enum */): void { - const doesBelongToStringEnum = !_.isUndefined(stringEnum[value]); - const enumValues = _.keys(stringEnum); - const enumValuesAsStrings = _.map(enumValues, enumValue => `'${enumValue}'`); - const enumValuesAsString = enumValuesAsStrings.join(', '); - assert.assert( - doesBelongToStringEnum, - `Expected ${variableName} to be one of: ${enumValuesAsString}, encountered: ${value}`, - ); - }, async isSenderAddressAsync(variableName: string, senderAddressHex: string, web3Wrapper: Web3Wrapper): Promise<void> { - assert.isETHAddressHex(variableName, senderAddressHex); + sharedAssert.isETHAddressHex(variableName, senderAddressHex); const isSenderAddressAvailable = await web3Wrapper.isSenderAddressAvailableAsync(senderAddressHex); - assert.assert(isSenderAddressAvailable, + sharedAssert.assert(isSenderAddressAvailable, `Specified ${variableName} ${senderAddressHex} isn't available through the supplied web3 provider`, ); }, @@ -68,34 +26,4 @@ export const assert = { const availableAddresses = await web3Wrapper.getAvailableAddressesAsync(); this.assert(!_.isEmpty(availableAddresses), 'No addresses were available on the provided web3 provider'); }, - hasAtMostOneUniqueValue(value: any[], errMsg: string): void { - this.assert(_.uniq(value).length <= 1, errMsg); - }, - isNumber(variableName: string, value: number): void { - this.assert(_.isFinite(value), this.typeAssertionMessage(variableName, 'number', value)); - }, - isBoolean(variableName: string, value: boolean): void { - this.assert(_.isBoolean(value), this.typeAssertionMessage(variableName, 'boolean', value)); - }, - isWeb3Provider(variableName: string, value: Web3.Provider): void { - const isWeb3Provider = _.isFunction((value as any).send) || _.isFunction((value as any).sendAsync); - this.assert(isWeb3Provider, this.typeAssertionMessage(variableName, 'Web3.Provider', value)); - }, - doesConformToSchema(variableName: string, value: any, schema: Schema): void { - const schemaValidator = new SchemaValidator(); - const validationResult = schemaValidator.validate(value, schema); - const hasValidationErrors = validationResult.errors.length > 0; - const msg = `Expected ${variableName} to conform to schema ${schema.id} -Encountered: ${JSON.stringify(value, null, '\t')} -Validation errors: ${validationResult.errors.join(', ')}`; - this.assert(!hasValidationErrors, msg); - }, - assert(condition: boolean, message: string): void { - if (!condition) { - throw new Error(message); - } - }, - typeAssertionMessage(variableName: string, type: string, value: any): string { - return `Expected ${variableName} to be of type ${type}, encountered: ${value}`; - }, -}; +}); diff --git a/packages/0x.js/src/utils/order_state_utils.ts b/packages/0x.js/src/utils/order_state_utils.ts index af6392c81..123584f90 100644 --- a/packages/0x.js/src/utils/order_state_utils.ts +++ b/packages/0x.js/src/utils/order_state_utils.ts @@ -69,7 +69,7 @@ export class OrderStateUtils { zrxTokenAddress, signedOrder.maker, ); const filledTakerTokenAmount = await this.orderFilledCancelledLazyStore.getFilledTakerAmountAsync(orderHash); - const canceledTakerTokenAmount = await this.orderFilledCancelledLazyStore.getCancelledTakerAmountAsync( + const cancelledTakerTokenAmount = await this.orderFilledCancelledLazyStore.getCancelledTakerAmountAsync( orderHash, ); const unavailableTakerTokenAmount = await exchange.getUnavailableTakerAmountAsync(orderHash); @@ -90,14 +90,14 @@ export class OrderStateUtils { makerFeeBalance, makerFeeProxyAllowance, filledTakerTokenAmount, - canceledTakerTokenAmount, + cancelledTakerTokenAmount, remainingFillableMakerTokenAmount, remainingFillableTakerTokenAmount, }; return orderRelevantState; } private validateIfOrderIsValid(signedOrder: SignedOrder, orderRelevantState: OrderRelevantState): void { - const unavailableTakerTokenAmount = orderRelevantState.canceledTakerTokenAmount.add( + const unavailableTakerTokenAmount = orderRelevantState.cancelledTakerTokenAmount.add( orderRelevantState.filledTakerTokenAmount, ); const availableTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount); diff --git a/packages/0x.js/test/exchange_wrapper_test.ts b/packages/0x.js/test/exchange_wrapper_test.ts index 26b8c1e0e..add89a3b2 100644 --- a/packages/0x.js/test/exchange_wrapper_test.ts +++ b/packages/0x.js/test/exchange_wrapper_test.ts @@ -443,7 +443,7 @@ describe('ExchangeWrapper', () => { it('should cancel an order', async () => { const txHash = await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmount); await zeroEx.awaitTransactionMinedAsync(txHash); - const cancelledAmount = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHashHex); + const cancelledAmount = await zeroEx.exchange.getCancelledTakerAmountAsync(orderHashHex); expect(cancelledAmount).to.be.bignumber.equal(cancelAmount); }); }); @@ -502,8 +502,8 @@ describe('ExchangeWrapper', () => { describe('successful batch cancels', () => { it('should cancel a batch of orders', async () => { await zeroEx.exchange.batchCancelOrdersAsync(cancelBatch); - const cancelledAmount = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHashHex); - const anotherCancelledAmount = await zeroEx.exchange.getCanceledTakerAmountAsync( + const cancelledAmount = await zeroEx.exchange.getCancelledTakerAmountAsync(orderHashHex); + const anotherCancelledAmount = await zeroEx.exchange.getCancelledTakerAmountAsync( anotherOrderHashHex, ); expect(cancelledAmount).to.be.bignumber.equal(cancelAmount); @@ -592,23 +592,23 @@ describe('ExchangeWrapper', () => { expect(filledValueT).to.be.bignumber.equal(partialFillAmount); }); }); - describe('#getCanceledTakerAmountAsync', () => { + describe('#getCancelledTakerAmountAsync', () => { it('should throw if passed an invalid orderHash', async () => { const invalidOrderHashHex = '0x123'; - return expect(zeroEx.exchange.getCanceledTakerAmountAsync(invalidOrderHashHex)).to.be.rejected(); + return expect(zeroEx.exchange.getCancelledTakerAmountAsync(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); + const cancelledValueT = await zeroEx.exchange.getCancelledTakerAmountAsync(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 cancelledValueT = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHash); + const cancelledValueT = await zeroEx.exchange.getCancelledTakerAmountAsync(orderHash); expect(cancelledValueT).to.be.bignumber.equal(0); }); it('should return the cancelledValueT for a valid and cancelled orderHash', async () => { const cancelAmount = fillableAmount.minus(partialFillAmount); await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmount); - const cancelledValueT = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHash); + const cancelledValueT = await zeroEx.exchange.getCancelledTakerAmountAsync(orderHash); expect(cancelledValueT).to.be.bignumber.equal(cancelAmount); }); }); diff --git a/packages/0x.js/test/order_state_watcher_test.ts b/packages/0x.js/test/order_state_watcher_test.ts index 03b1f36a4..a112bac1d 100644 --- a/packages/0x.js/test/order_state_watcher_test.ts +++ b/packages/0x.js/test/order_state_watcher_test.ts @@ -61,6 +61,12 @@ describe('OrderStateWatcher', () => { [makerToken, takerToken] = tokenUtils.getNonProtocolTokens(); web3Wrapper = (zeroEx as any)._web3Wrapper; }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); describe('#removeOrder', async () => { it('should successfully remove existing order', async () => { signedOrder = await fillScenarios.createFillableSignedOrderAsync( @@ -377,7 +383,7 @@ describe('OrderStateWatcher', () => { const validOrderState = orderState as OrderStateValid; expect(validOrderState.orderHash).to.be.equal(orderHash); const orderRelevantState = validOrderState.orderRelevantState; - expect(orderRelevantState.canceledTakerTokenAmount).to.be.bignumber.equal(cancelAmountInBaseUnits); + expect(orderRelevantState.cancelledTakerTokenAmount).to.be.bignumber.equal(cancelAmountInBaseUnits); done(); }); zeroEx.orderStateWatcher.subscribe(callback); diff --git a/packages/0x.js/test/token_registry_wrapper_test.ts b/packages/0x.js/test/token_registry_wrapper_test.ts index 6b5dd517e..d3497451b 100644 --- a/packages/0x.js/test/token_registry_wrapper_test.ts +++ b/packages/0x.js/test/token_registry_wrapper_test.ts @@ -1,7 +1,7 @@ import * as _ from 'lodash'; import 'mocha'; import * as chai from 'chai'; -import {SchemaValidator, schemas} from '0x-json-schemas'; +import {SchemaValidator, schemas} from '@0xproject/json-schemas'; import {chaiSetup} from './utils/chai_setup'; import {web3Factory} from './utils/web3_factory'; import {ZeroEx, Token} from '../src'; diff --git a/packages/0x.js/tslint.json b/packages/0x.js/tslint.json index 5842a872a..a07795151 100644 --- a/packages/0x.js/tslint.json +++ b/packages/0x.js/tslint.json @@ -1,5 +1,5 @@ { "extends": [ - "tslint-config-0xproject" + "@0xproject/tslint-config" ] } diff --git a/packages/assert/README.md b/packages/assert/README.md new file mode 100644 index 000000000..0c60671ea --- /dev/null +++ b/packages/assert/README.md @@ -0,0 +1 @@ +Standard type and schema assertions to be used across all 0x projects and packages diff --git a/packages/assert/package.json b/packages/assert/package.json new file mode 100644 index 000000000..ed1d2a98b --- /dev/null +++ b/packages/assert/package.json @@ -0,0 +1,46 @@ +{ + "name": "@0xproject/assert", + "version": "0.0.4", + "description": "Provides a standard way of performing type and schema validation across 0x projects", + "main": "lib/src/index.js", + "types": "lib/src/index.d.ts", + "scripts": { + "build": "tsc", + "clean": "shx rm -rf _bundles lib test_temp", + "lint": "tslint src/**/*.ts test/**/*.ts", + "run_mocha": "mocha lib/test/**/*_test.js", + "prepublishOnly": "run-p build", + "test": "run-s clean build run_mocha", + "test:circleci": "yarn test" + }, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/0xProject/0x.js.git" + }, + "bugs": { + "url": "https://github.com/0xProject/0x.js/issues" + }, + "homepage": "https://github.com/0xProject/0x.js/packages/assert/README.md", + "devDependencies": { + "@0xproject/tslint-config": "^0.1.0", + "@types/lodash": "^4.14.78", + "@types/mocha": "^2.2.42", + "@types/valid-url": "^1.0.2", + "chai": "^4.0.1", + "chai-typescript-typings": "^0.0.1", + "dirty-chai": "^2.0.1", + "mocha": "^4.0.1", + "npm-run-all": "^4.1.1", + "shx": "^0.2.2", + "tslint": "5.8.0", + "typescript": "^2.4.2" + }, + "dependencies": { + "@0xproject/json-schemas": "^0.6.7", + "bignumber.js": "~4.1.0", + "ethereum-address": "^0.0.4", + "lodash": "^4.17.4", + "valid-url": "^1.0.9" + } +} diff --git a/packages/assert/src/globals.d.ts b/packages/assert/src/globals.d.ts new file mode 100644 index 000000000..cc47f3113 --- /dev/null +++ b/packages/assert/src/globals.d.ts @@ -0,0 +1,5 @@ +declare module 'dirty-chai'; + +declare module 'ethereum-address' { + const isAddress: (arg: any) => boolean; +} diff --git a/packages/assert/src/index.ts b/packages/assert/src/index.ts new file mode 100644 index 000000000..eb224223f --- /dev/null +++ b/packages/assert/src/index.ts @@ -0,0 +1,93 @@ +import BigNumber from 'bignumber.js'; +import * as ethereum_address from 'ethereum-address'; +import * as _ from 'lodash'; +import * as validUrl from 'valid-url'; +import { + SchemaValidator, + Schema, +} from '@0xproject/json-schemas'; + +const HEX_REGEX = /^0x[0-9A-F]*$/i; + +export const assert = { + isBigNumber(variableName: string, value: BigNumber): void { + const isBigNumber = _.isObject(value) && (value as any).isBigNumber; + this.assert(isBigNumber, this.typeAssertionMessage(variableName, 'BigNumber', value)); + }, + isValidBaseUnitAmount(variableName: string, value: BigNumber) { + assert.isBigNumber(variableName, value); + const hasDecimals = value.decimalPlaces() !== 0; + this.assert( + !hasDecimals, `${variableName} should be in baseUnits (no decimals), found value: ${value.toNumber()}`, + ); + }, + isUndefined(value: any, variableName?: string): void { + this.assert(_.isUndefined(value), this.typeAssertionMessage(variableName, 'undefined', value)); + }, + isString(variableName: string, value: string): void { + this.assert(_.isString(value), this.typeAssertionMessage(variableName, 'string', value)); + }, + isFunction(variableName: string, value: any): void { + this.assert(_.isFunction(value), this.typeAssertionMessage(variableName, 'function', value)); + }, + isHexString(variableName: string, value: string): void { + this.assert(_.isString(value) && HEX_REGEX.test(value), + this.typeAssertionMessage(variableName, 'HexString', value)); + }, + isETHAddressHex(variableName: string, value: string): void { + this.assert(ethereum_address.isAddress(value), this.typeAssertionMessage(variableName, 'ETHAddressHex', value)); + this.assert( + ethereum_address.isAddress(value) && value.toLowerCase() === value, + `Checksummed addresses are not supported. Convert ${variableName} to lower case before passing`, + ); + }, + doesBelongToStringEnum(variableName: string, value: string, + stringEnum: any /* There is no base type for every string enum */): void { + const doesBelongToStringEnum = !_.isUndefined(stringEnum[value]); + const enumValues = _.keys(stringEnum); + const enumValuesAsStrings = _.map(enumValues, enumValue => `'${enumValue}'`); + const enumValuesAsString = enumValuesAsStrings.join(', '); + assert.assert( + doesBelongToStringEnum, + `Expected ${variableName} to be one of: ${enumValuesAsString}, encountered: ${value}`, + ); + }, + hasAtMostOneUniqueValue(value: any[], errMsg: string): void { + this.assert(_.uniq(value).length <= 1, errMsg); + }, + isNumber(variableName: string, value: number): void { + this.assert(_.isFinite(value), this.typeAssertionMessage(variableName, 'number', value)); + }, + isBoolean(variableName: string, value: boolean): void { + this.assert(_.isBoolean(value), this.typeAssertionMessage(variableName, 'boolean', value)); + }, + isWeb3Provider(variableName: string, value: any): void { + const isWeb3Provider = _.isFunction((value as any).send) || _.isFunction((value as any).sendAsync); + this.assert(isWeb3Provider, this.typeAssertionMessage(variableName, 'Web3.Provider', value)); + }, + doesConformToSchema(variableName: string, value: any, schema: Schema): void { + const schemaValidator = new SchemaValidator(); + const validationResult = schemaValidator.validate(value, schema); + const hasValidationErrors = validationResult.errors.length > 0; + const msg = `Expected ${variableName} to conform to schema ${schema.id} +Encountered: ${JSON.stringify(value, null, '\t')} +Validation errors: ${validationResult.errors.join(', ')}`; + this.assert(!hasValidationErrors, msg); + }, + isHttpUrl(variableName: string, value: any): void { + const isValidUrl = validUrl.isWebUri(value); + this.assert(isValidUrl, this.typeAssertionMessage(variableName, 'http url', value)); + }, + isUri(variableName: string, value: any): void { + const isValidUri = validUrl.isUri(value); + this.assert(isValidUri, this.typeAssertionMessage(variableName, 'uri', value)); + }, + assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(message); + } + }, + typeAssertionMessage(variableName: string, type: string, value: any): string { + return `Expected ${variableName} to be of type ${type}, encountered: ${value}`; + }, +}; diff --git a/packages/assert/test/assert_test.ts b/packages/assert/test/assert_test.ts new file mode 100644 index 000000000..66fa4eb54 --- /dev/null +++ b/packages/assert/test/assert_test.ts @@ -0,0 +1,338 @@ +import 'mocha'; +import * as dirtyChai from 'dirty-chai'; +import * as chai from 'chai'; +import {BigNumber} from 'bignumber.js'; +import {schemas} from '@0xproject/json-schemas'; +import {assert} from '../src/index'; + +chai.config.includeStack = true; +chai.use(dirtyChai); +const expect = chai.expect; + +describe('Assertions', () => { + const variableName = 'variable'; + describe('#isBigNumber', () => { + it('should not throw for valid input', () => { + const validInputs = [ + new BigNumber(23), + new BigNumber('45'), + ]; + validInputs.forEach(input => expect(assert.isBigNumber.bind(assert, variableName, input)).to.not.throw()); + }); + it('should throw for invalid input', () => { + const invalidInputs = [ + 'test', + 42, + false, + { random: 'test' }, + undefined, + ]; + invalidInputs.forEach(input => expect(assert.isBigNumber.bind(assert, variableName, input)).to.throw()); + }); + }); + describe('#isUndefined', () => { + it('should not throw for valid input', () => { + const validInputs = [ + undefined, + ]; + validInputs.forEach(input => expect(assert.isUndefined.bind(assert, input, variableName)).to.not.throw()); + }); + it('should throw for invalid input', () => { + const invalidInputs = [ + 'test', + 42, + false, + { random: 'test' }, + ]; + invalidInputs.forEach(input => expect(assert.isUndefined.bind(assert, input, variableName)).to.throw()); + }); + }); + describe('#isString', () => { + it('should not throw for valid input', () => { + const validInputs = [ + 'hello', + 'goodbye', + ]; + validInputs.forEach(input => expect(assert.isString.bind(assert, variableName, input)).to.not.throw()); + }); + it('should throw for invalid input', () => { + const invalidInputs = [ + 42, + false, + { random: 'test' }, + undefined, + new BigNumber(45), + ]; + invalidInputs.forEach(input => expect(assert.isString.bind(assert, variableName, input)).to.throw()); + }); + }); + describe('#isFunction', () => { + it('should not throw for valid input', () => { + const validInputs = [ + BigNumber, + assert.isString.bind(this), + ]; + validInputs.forEach(input => expect(assert.isFunction.bind(assert, variableName, input)).to.not.throw()); + }); + it('should throw for invalid input', () => { + const invalidInputs = [ + 42, + false, + { random: 'test' }, + undefined, + new BigNumber(45), + ]; + invalidInputs.forEach(input => expect(assert.isFunction.bind(assert, variableName, input)).to.throw()); + }); + }); + describe('#isHexString', () => { + it('should not throw for valid input', () => { + const validInputs = [ + '0x61a3ed31B43c8780e905a260a35faefEc527be7516aa11c0256729b5b351bc33', + '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', + ]; + validInputs.forEach(input => expect(assert.isHexString.bind(assert, variableName, input)).to.not.throw()); + }); + it('should throw for invalid input', () => { + const invalidInputs = [ + 42, + false, + { random: 'test' }, + undefined, + new BigNumber(45), + '0x61a3ed31B43c8780e905a260a35faYfEc527be7516aa11c0256729b5b351bc33', + ]; + invalidInputs.forEach(input => expect(assert.isHexString.bind(assert, variableName, input)).to.throw()); + }); + }); + describe('#isETHAddressHex', () => { + it('should not throw for valid input', () => { + const validInputs = [ + '0x0000000000000000000000000000000000000000', + '0x6fffd0ae3f7d88c9b4925323f54c6e4b2918c5fd', + '0x12459c951127e0c374ff9105dda097662a027093', + ]; + validInputs.forEach(input => + expect(assert.isETHAddressHex.bind(assert, variableName, input)).to.not.throw(), + ); + }); + it('should throw for invalid input', () => { + const invalidInputs = [ + 42, + false, + { random: 'test' }, + undefined, + new BigNumber(45), + '0x6FFFd0ae3f7d88c9b4925323f54c6e4b2918c5fd', + '0x6FFFd0ae3f7d88c9b4925323f54c6e4', + ]; + invalidInputs.forEach(input => + expect(assert.isETHAddressHex.bind(assert, variableName, input)).to.throw(), + ); + }); + }); + describe('#doesBelongToStringEnum', () => { + enum TestEnums { + Test1 = 'Test1', + Test2 = 'Test2', + } + it('should not throw for valid input', () => { + const validInputs = [ + TestEnums.Test1, + TestEnums.Test2, + ]; + validInputs.forEach(input => + expect(assert.doesBelongToStringEnum.bind(assert, variableName, input, TestEnums)).to.not.throw(), + ); + }); + it('should throw for invalid input', () => { + const invalidInputs = [ + 42, + false, + { random: 'test' }, + undefined, + new BigNumber(45), + ]; + invalidInputs.forEach(input => + expect(assert.doesBelongToStringEnum.bind(assert, variableName, input, TestEnums)).to.throw(), + ); + }); + }); + describe('#hasAtMostOneUniqueValue', () => { + const errorMsg = 'more than one unique value'; + it('should not throw for valid input', () => { + const validInputs = [ + ['hello'], + ['goodbye', 'goodbye', 'goodbye'], + ]; + validInputs.forEach(input => + expect(assert.hasAtMostOneUniqueValue.bind(assert, input, errorMsg)).to.not.throw(), + ); + }); + it('should throw for invalid input', () => { + const invalidInputs = [ + ['hello', 'goodbye'], + ['goodbye', 42, false, false], + ]; + invalidInputs.forEach(input => + expect(assert.hasAtMostOneUniqueValue.bind(assert, input, errorMsg)).to.throw(), + ); + }); + }); + describe('#isNumber', () => { + it('should not throw for valid input', () => { + const validInputs = [ + 42, + 0.00, + 21e+42, + ]; + validInputs.forEach(input => expect(assert.isNumber.bind(assert, variableName, input)).to.not.throw()); + }); + it('should throw for invalid input', () => { + const invalidInputs = [ + false, + { random: 'test' }, + undefined, + new BigNumber(45), + ]; + invalidInputs.forEach(input => expect(assert.isNumber.bind(assert, variableName, input)).to.throw()); + }); + }); + describe('#isBoolean', () => { + it('should not throw for valid input', () => { + const validInputs = [ + true, + false, + ]; + validInputs.forEach(input => expect(assert.isBoolean.bind(assert, variableName, input)).to.not.throw()); + }); + it('should throw for invalid input', () => { + const invalidInputs = [ + 42, + { random: 'test' }, + undefined, + new BigNumber(45), + ]; + invalidInputs.forEach(input => expect(assert.isBoolean.bind(assert, variableName, input)).to.throw()); + }); + }); + describe('#isWeb3Provider', () => { + it('should not throw for valid input', () => { + const validInputs = [ + { send: () => 45 }, + { sendAsync: () => 45 }, + ]; + validInputs.forEach(input => + expect(assert.isWeb3Provider.bind(assert, variableName, input)).to.not.throw(), + ); + }); + it('should throw for invalid input', () => { + const invalidInputs = [ + 42, + { random: 'test' }, + undefined, + new BigNumber(45), + ]; + invalidInputs.forEach(input => + expect(assert.isWeb3Provider.bind(assert, variableName, input)).to.throw(), + ); + }); + }); + describe('#doesConformToSchema', () => { + const schema = schemas.addressSchema; + it('should not throw for valid input', () => { + const validInputs = [ + '0x6fffd0ae3f7d88c9b4925323f54c6e4b2918c5fd', + '0x12459c951127e0c374ff9105dda097662a027093', + ]; + validInputs.forEach(input => + expect(assert.doesConformToSchema.bind(assert, variableName, input, schema)).to.not.throw(), + ); + }); + it('should throw for invalid input', () => { + const invalidInputs = [ + 42, + { random: 'test' }, + undefined, + new BigNumber(45), + ]; + invalidInputs.forEach(input => + expect(assert.doesConformToSchema.bind(assert, variableName, input, schema)).to.throw(), + ); + }); + }); + describe('#isHttpUrl', () => { + it('should not throw for valid input', () => { + const validInputs = [ + 'http://www.google.com', + 'https://api.example-relayer.net', + 'https://api.radarrelay.com/0x/v0/', + 'https://zeroex.beta.radarrelay.com:8000/0x/v0/', + ]; + validInputs.forEach(input => + expect(assert.isHttpUrl.bind(assert, variableName, input)).to.not.throw(), + ); + }); + it('should throw for invalid input', () => { + const invalidInputs = [ + 42, + { random: 'test' }, + undefined, + new BigNumber(45), + 'ws://www.api.example-relayer.net', + 'www.google.com', + 'api.example-relayer.net', + 'user:password@api.example-relayer.net', + '//api.example-relayer.net', + ]; + invalidInputs.forEach(input => + expect(assert.isHttpUrl.bind(assert, variableName, input)).to.throw(), + ); + }); + }); + describe('#isUri', () => { + it('should not throw for valid input', () => { + const validInputs = [ + 'http://www.google.com', + 'https://api.example-relayer.net', + 'https://api.radarrelay.com/0x/v0/', + 'https://zeroex.beta.radarrelay.com:8000/0x/v0/', + 'ws://www.api.example-relayer.net', + 'wss://www.api.example-relayer.net', + 'user:password@api.example-relayer.net', + ]; + validInputs.forEach(input => + expect(assert.isUri.bind(assert, variableName, input)).to.not.throw(), + ); + }); + it('should throw for invalid input', () => { + const invalidInputs = [ + 42, + { random: 'test' }, + undefined, + new BigNumber(45), + 'www.google.com', + 'api.example-relayer.net', + '//api.example-relayer.net', + ]; + invalidInputs.forEach(input => + expect(assert.isUri.bind(assert, variableName, input)).to.throw(), + ); + }); + }); + describe('#assert', () => { + const assertMessage = 'assert not satisfied'; + it('should not throw for valid input', () => { + expect(assert.assert.bind(assert, true, assertMessage)).to.not.throw(); + }); + it('should throw for invalid input', () => { + expect(assert.assert.bind(assert, false, assertMessage)).to.throw(); + }); + }); + describe('#typeAssertionMessage', () => { + it('should render correct message', () => { + expect(assert.typeAssertionMessage('variable', 'string', 'number')) + .to.equal(`Expected variable to be of type string, encountered: number`); + }); + }); +}); diff --git a/packages/assert/tsconfig.json b/packages/assert/tsconfig.json new file mode 100644 index 000000000..709e20154 --- /dev/null +++ b/packages/assert/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "lib": [ "es2017", "dom"], + "outDir": "lib", + "sourceMap": true, + "declaration": true, + "noImplicitAny": true, + "strictNullChecks": true + }, + "include": [ + "./src/**/*", + "./test/**/*", + "../../node_modules/chai-typescript-typings/index.d.ts", + "../../node_modules/web3-typescript-typings/index.d.ts" + ] +} diff --git a/packages/assert/tslint.json b/packages/assert/tslint.json new file mode 100644 index 000000000..a07795151 --- /dev/null +++ b/packages/assert/tslint.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "@0xproject/tslint-config" + ] +} diff --git a/packages/json-schemas/README.md b/packages/json-schemas/README.md new file mode 100644 index 000000000..e9f80e106 --- /dev/null +++ b/packages/json-schemas/README.md @@ -0,0 +1,15 @@ +Contains 0x-related json schemas + +## Usage: +``` +import {SchemaValidator, ValidatorResult, schemas} from '@0xproject/json-schemas'; + +const {orderSchema} = schemas; +const validator = new SchemaValidator(); + +const order = { + ... +}; +const validatorResult: ValidatorResult = validator.validate(order, orderSchema); // Contains all errors +const isValid: boolean = validator.isValid(order, orderSchema); // Only returns boolean +``` diff --git a/packages/json-schemas/package.json b/packages/json-schemas/package.json new file mode 100644 index 000000000..07ed20551 --- /dev/null +++ b/packages/json-schemas/package.json @@ -0,0 +1,46 @@ +{ + "name": "@0xproject/json-schemas", + "version": "0.6.7", + "description": "0x-related json schemas", + "main": "lib/src/index.js", + "types": "lib/src/index.d.ts", + "scripts": { + "lint": "tslint src/*.ts test/*.ts", + "test": "run-s clean build run_mocha", + "test:circleci": "yarn test", + "run_mocha": "mocha lib/test/**/*_test.js", + "clean": "shx rm -rf _bundles lib test_temp", + "build": "tsc" + }, + "repository": { + "type": "git", + "url": "https://github.com/0xProject/0x.js.git" + }, + "author": "", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/0xProject/0x.js/issues" + }, + "homepage": "https://github.com/0xProject/0x.js/packages/json-schemas/README.md", + "dependencies": { + "es6-promisify": "^5.0.0", + "jsonschema": "^1.2.0", + "lodash.values": "^4.3.0" + }, + "devDependencies": { + "@0xproject/tslint-config": "^0.1.0", + "@types/lodash.foreach": "^4.5.3", + "@types/lodash.values": "^4.3.3", + "@types/mocha": "^2.2.42", + "bignumber.js": "^4.0.2", + "chai": "^4.1.1", + "chai-typescript-typings": "^0.0.1", + "dirty-chai": "^2.0.1", + "lodash.foreach": "^4.5.0", + "mocha": "^4.0.1", + "npm-run-all": "^4.1.1", + "shx": "^0.2.2", + "tslint": "5.8.0", + "typescript": "~2.6.1" + } +} diff --git a/packages/json-schemas/schemas/basic_type_schemas.ts b/packages/json-schemas/schemas/basic_type_schemas.ts new file mode 100644 index 000000000..9d81ff333 --- /dev/null +++ b/packages/json-schemas/schemas/basic_type_schemas.ts @@ -0,0 +1,11 @@ +export const addressSchema = { + id: '/Address', + type: 'string', + pattern: '^0x[0-9a-f]{40}$', +}; + +export const numberSchema = { + id: '/Number', + type: 'string', + pattern: '^\\d+(\\.\\d+)?$', +}; diff --git a/packages/json-schemas/schemas/ec_signature_schema.ts b/packages/json-schemas/schemas/ec_signature_schema.ts new file mode 100644 index 000000000..2b769f3b6 --- /dev/null +++ b/packages/json-schemas/schemas/ec_signature_schema.ts @@ -0,0 +1,20 @@ +export const ecSignatureParameterSchema = { + id: '/ECSignatureParameter', + type: 'string', + pattern: '^0[xX][0-9A-Fa-f]{64}$', +}; + +export const ecSignatureSchema = { + id: '/ECSignature', + properties: { + v: { + type: 'number', + minimum: 27, + maximum: 28, + }, + r: {$ref: '/ECSignatureParameter'}, + s: {$ref: '/ECSignatureParameter'}, + }, + required: ['v', 'r', 's'], + type: 'object', +}; diff --git a/packages/json-schemas/schemas/index_filter_values_schema.ts b/packages/json-schemas/schemas/index_filter_values_schema.ts new file mode 100644 index 000000000..f7e323e45 --- /dev/null +++ b/packages/json-schemas/schemas/index_filter_values_schema.ts @@ -0,0 +1,11 @@ +export const indexFilterValuesSchema = { + id: '/IndexFilterValues', + additionalProperties: { + oneOf: [ + {$ref: '/Number'}, + {$ref: '/Address'}, + {$ref: '/OrderHashSchema'}, + ], + }, + type: 'object', +}; diff --git a/packages/json-schemas/schemas/order_cancel_schema.ts b/packages/json-schemas/schemas/order_cancel_schema.ts new file mode 100644 index 000000000..ac7d2ee20 --- /dev/null +++ b/packages/json-schemas/schemas/order_cancel_schema.ts @@ -0,0 +1,12 @@ +export const orderCancellationRequestsSchema = { + id: '/OrderCancellationRequests', + type: 'array', + items: { + properties: { + order: {$ref: '/Order'}, + takerTokenCancelAmount: {$ref: '/Number'}, + }, + required: ['order', 'takerTokenCancelAmount'], + type: 'object', + }, +}; diff --git a/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.ts b/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.ts new file mode 100644 index 000000000..4ef7b069a --- /dev/null +++ b/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.ts @@ -0,0 +1,12 @@ +export const orderFillOrKillRequestsSchema = { + id: '/OrderFillOrKillRequests', + type: 'array', + items: { + properties: { + signedOrder: {$ref: '/SignedOrder'}, + fillTakerAmount: {$ref: '/Number'}, + }, + required: ['signedOrder', 'fillTakerAmount'], + type: 'object', + }, +}; diff --git a/packages/json-schemas/schemas/order_fill_requests_schema.ts b/packages/json-schemas/schemas/order_fill_requests_schema.ts new file mode 100644 index 000000000..ec19dd9f8 --- /dev/null +++ b/packages/json-schemas/schemas/order_fill_requests_schema.ts @@ -0,0 +1,12 @@ +export const orderFillRequestsSchema = { + id: '/OrderFillRequests', + type: 'array', + items: { + properties: { + signedOrder: {$ref: '/SignedOrder'}, + takerTokenFillAmount: {$ref: '/Number'}, + }, + required: ['signedOrder', 'takerTokenFillAmount'], + type: 'object', + }, +}; diff --git a/packages/json-schemas/schemas/order_hash_schema.ts b/packages/json-schemas/schemas/order_hash_schema.ts new file mode 100644 index 000000000..6af06927f --- /dev/null +++ b/packages/json-schemas/schemas/order_hash_schema.ts @@ -0,0 +1,5 @@ +export const orderHashSchema = { + id: '/OrderHashSchema', + type: 'string', + pattern: '^0x[0-9a-fA-F]{64}$', +}; diff --git a/packages/json-schemas/schemas/order_schemas.ts b/packages/json-schemas/schemas/order_schemas.ts new file mode 100644 index 000000000..3cce49351 --- /dev/null +++ b/packages/json-schemas/schemas/order_schemas.ts @@ -0,0 +1,35 @@ +export const orderSchema = { + id: '/Order', + properties: { + maker: {$ref: '/Address'}, + taker: {$ref: '/Address'}, + makerFee: {$ref: '/Number'}, + takerFee: {$ref: '/Number'}, + makerTokenAmount: {$ref: '/Number'}, + takerTokenAmount: {$ref: '/Number'}, + makerTokenAddress: {$ref: '/Address'}, + takerTokenAddress: {$ref: '/Address'}, + salt: {$ref: '/Number'}, + feeRecipient: {$ref: '/Address'}, + expirationUnixTimestampSec: {$ref: '/Number'}, + exchangeContractAddress: {$ref: '/Address'}, + }, + required: [ + 'maker', 'taker', 'makerFee', 'takerFee', 'makerTokenAmount', 'takerTokenAmount', + 'salt', 'feeRecipient', 'expirationUnixTimestampSec', 'exchangeContractAddress', + ], + type: 'object', +}; + +export const signedOrderSchema = { + id: '/SignedOrder', + allOf: [ + { $ref: '/Order' }, + { + properties: { + ecSignature: {$ref: '/ECSignature'}, + }, + required: ['ecSignature'], + }, + ], +}; diff --git a/packages/json-schemas/schemas/relayer_api_error_response_schema.ts b/packages/json-schemas/schemas/relayer_api_error_response_schema.ts new file mode 100644 index 000000000..eacbb2bce --- /dev/null +++ b/packages/json-schemas/schemas/relayer_api_error_response_schema.ts @@ -0,0 +1,21 @@ +export const relayerApiErrorResponseSchema = { + id: '/RelayerApiErrorResponse', + type: 'object', + properties: { + code: {type: 'number'}, + reason: {type: 'string'}, + validationErrors: { + type: 'array', + items: { + type: 'object', + properties: { + field: {type: 'string'}, + code: {type: 'number'}, + reason: {type: 'string'}, + }, + required: ['field', 'code', 'reason'], + }, + }, + }, + required: ['code', 'reason'], +}; diff --git a/packages/json-schemas/schemas/relayer_api_fees_payload_schema.ts b/packages/json-schemas/schemas/relayer_api_fees_payload_schema.ts new file mode 100644 index 000000000..645660844 --- /dev/null +++ b/packages/json-schemas/schemas/relayer_api_fees_payload_schema.ts @@ -0,0 +1,19 @@ +export const relayerApiFeesPayloadSchema = { + id: '/RelayerApiFeesPayload', + type: 'object', + properties: { + exchangeContractAddress: {$ref: '/Address'}, + maker: {$ref: '/Address'}, + taker: {$ref: '/Address'}, + makerTokenAddress: {$ref: '/Address'}, + takerTokenAddress: {$ref: '/Address'}, + makerTokenAmount: {$ref: '/Number'}, + takerTokenAmount: {$ref: '/Number'}, + expirationUnixTimestampSec: {$ref: '/Number'}, + salt: {$ref: '/Number'}, + }, + required: [ + 'exchangeContractAddress', 'maker', 'taker', 'makerTokenAddress', 'takerTokenAddress', + 'expirationUnixTimestampSec', 'salt', + ], +}; diff --git a/packages/json-schemas/schemas/relayer_api_fees_response_schema.ts b/packages/json-schemas/schemas/relayer_api_fees_response_schema.ts new file mode 100644 index 000000000..86e51feb0 --- /dev/null +++ b/packages/json-schemas/schemas/relayer_api_fees_response_schema.ts @@ -0,0 +1,10 @@ +export const relayerApiFeesResponseSchema = { + id: '/RelayerApiFeesResponse', + type: 'object', + properties: { + makerFee: {$ref: '/Number'}, + takerFee: {$ref: '/Number'}, + feeRecipient: {$ref: '/Address'}, + }, + required: ['makerFee', 'takerFee', 'feeRecipient'], +}; diff --git a/packages/json-schemas/schemas/relayer_api_orberbook_channel_subscribe_schema.ts b/packages/json-schemas/schemas/relayer_api_orberbook_channel_subscribe_schema.ts new file mode 100644 index 000000000..8ded9adb0 --- /dev/null +++ b/packages/json-schemas/schemas/relayer_api_orberbook_channel_subscribe_schema.ts @@ -0,0 +1,22 @@ +export const relayerApiOrderbookChannelSubscribeSchema = { + id: '/RelayerApiOrderbookChannelSubscribe', + type: 'object', + properties: { + type: {enum: ['subscribe']}, + channel: {enum: ['orderbook']}, + payload: {$ref: '/RelayerApiOrderbookChannelSubscribePayload'}, + }, + required: ['type', 'channel', 'payload'], +}; + +export const relayerApiOrderbookChannelSubscribePayload = { + id: '/RelayerApiOrderbookChannelSubscribePayload', + type: 'object', + properties: { + baseTokenAddress: {$ref: '/Address'}, + quoteTokenAddress: {$ref: '/Address'}, + snapshot: {type: 'boolean'}, + limit: {type: 'number'}, + }, + required: ['baseTokenAddress', 'quoteTokenAddress'], +}; diff --git a/packages/json-schemas/schemas/relayer_api_orderbook_channel_snapshot_schema.ts b/packages/json-schemas/schemas/relayer_api_orderbook_channel_snapshot_schema.ts new file mode 100644 index 000000000..cfc0ddc8f --- /dev/null +++ b/packages/json-schemas/schemas/relayer_api_orderbook_channel_snapshot_schema.ts @@ -0,0 +1,21 @@ +export const relayerApiOrderbookChannelSnapshotSchema = { + id: '/RelayerApiOrderbookChannelSnapshot', + type: 'object', + properties: { + type: {enum: ['snapshot']}, + channel: {enum: ['orderbook']}, + channelId: {type: 'number'}, + payload: {$ref: '/RelayerApiOrderbookChannelSnapshotPayload'}, + }, + required: ['type', 'channel', 'channelId', 'payload'], +}; + +export const relayerApiOrderbookChannelSnapshotPayload = { + id: '/RelayerApiOrderbookChannelSnapshotPayload', + type: 'object', + properties: { + bids: {$ref: '/signedOrdersSchema'}, + asks: {$ref: '/signedOrdersSchema'}, + }, + required: ['bids', 'asks'], +}; diff --git a/packages/json-schemas/schemas/relayer_api_orderbook_channel_update_response_schema.ts b/packages/json-schemas/schemas/relayer_api_orderbook_channel_update_response_schema.ts new file mode 100644 index 000000000..51308ed49 --- /dev/null +++ b/packages/json-schemas/schemas/relayer_api_orderbook_channel_update_response_schema.ts @@ -0,0 +1,11 @@ +export const relayerApiOrderbookChannelUpdateSchema = { + id: '/RelayerApiOrderbookChannelUpdate', + type: 'object', + properties: { + type: {enum: ['update']}, + channel: {enum: ['orderbook']}, + channelId: {type: 'number'}, + payload: {$ref: '/SignedOrder'}, + }, + required: ['type', 'channel', 'channelId', 'payload'], +}; diff --git a/packages/json-schemas/schemas/relayer_api_orderbook_response_schema.ts b/packages/json-schemas/schemas/relayer_api_orderbook_response_schema.ts new file mode 100644 index 000000000..b592d4f8e --- /dev/null +++ b/packages/json-schemas/schemas/relayer_api_orderbook_response_schema.ts @@ -0,0 +1,9 @@ +export const relayerApiOrderBookResponseSchema = { + id: '/RelayerApiOrderBookResponse', + type: 'object', + properties: { + bids: {$ref: '/signedOrdersSchema'}, + asks: {$ref: '/signedOrdersSchema'}, + }, + required: ['bids', 'asks'], +}; diff --git a/packages/json-schemas/schemas/relayer_api_token_pairs_response_schema.ts b/packages/json-schemas/schemas/relayer_api_token_pairs_response_schema.ts new file mode 100644 index 000000000..8ecab1424 --- /dev/null +++ b/packages/json-schemas/schemas/relayer_api_token_pairs_response_schema.ts @@ -0,0 +1,24 @@ +export const relayerApiTokenPairsResponseSchema = { + id: '/RelayerApiTokenPairsResponse', + type: 'array', + items: { + properties: { + tokenA: {$ref: '/RelayerApiTokenTradeInfo'}, + tokenB: {$ref: '/RelayerApiTokenTradeInfo'}, + }, + required: ['tokenA', 'tokenB'], + type: 'object', + }, +}; + +export const relayerApiTokenTradeInfoSchema = { + id: '/RelayerApiTokenTradeInfo', + type: 'object', + properties: { + address: {$ref: '/Address'}, + minAmount: {$ref: '/Number'}, + maxAmount: {$ref: '/Number'}, + precision: {type: 'number'}, + }, + required: ['address'], +}; diff --git a/packages/json-schemas/schemas/signed_orders_schema.ts b/packages/json-schemas/schemas/signed_orders_schema.ts new file mode 100644 index 000000000..c4c4a68ac --- /dev/null +++ b/packages/json-schemas/schemas/signed_orders_schema.ts @@ -0,0 +1,5 @@ +export const signedOrdersSchema = { + id: '/signedOrdersSchema', + type: 'array', + items: {$ref: '/SignedOrder'}, +}; diff --git a/packages/json-schemas/schemas/subscription_opts_schema.ts b/packages/json-schemas/schemas/subscription_opts_schema.ts new file mode 100644 index 000000000..a476e6963 --- /dev/null +++ b/packages/json-schemas/schemas/subscription_opts_schema.ts @@ -0,0 +1,20 @@ +export const blockParamSchema = { + id: '/BlockParam', + oneOf: [ + { + type: 'number', + }, + { + enum: ['latest', 'earliest', 'pending'], + }, + ], +}; + +export const subscriptionOptsSchema = { + id: '/SubscriptionOpts', + properties: { + fromBlock: {$ref: '/BlockParam'}, + toBlock: {$ref: '/BlockParam'}, + }, + type: 'object', +}; diff --git a/packages/json-schemas/schemas/token_schema.ts b/packages/json-schemas/schemas/token_schema.ts new file mode 100644 index 000000000..aca4d4ad2 --- /dev/null +++ b/packages/json-schemas/schemas/token_schema.ts @@ -0,0 +1,11 @@ +export const tokenSchema = { + id: '/Token', + properties: { + name: {type: 'string'}, + symbol: {type: 'string'}, + decimals: {type: 'number'}, + address: {$ref: '/Address'}, + }, + required: ['name', 'symbol', 'decimals', 'address'], + type: 'object', +}; diff --git a/packages/json-schemas/schemas/tx_data_schema.ts b/packages/json-schemas/schemas/tx_data_schema.ts new file mode 100644 index 000000000..41eaadd3c --- /dev/null +++ b/packages/json-schemas/schemas/tx_data_schema.ts @@ -0,0 +1,42 @@ +export const jsNumber = { + id: '/JsNumber', + type: 'number', + minimum: 0, +}; + +export const txDataSchema = { + id: '/TxData', + properties: { + from: {$ref: '/Address'}, + to: {$ref: '/Address'}, + value: { + oneOf: [ + {$ref: '/Number'}, + {$ref: '/JsNumber'}, + ], + }, + gas: { + oneOf: [ + {$ref: '/Number'}, + {$ref: '/JsNumber'}, + ], + }, + gasPrice: { + oneOf: [ + {$ref: '/Number'}, + {$ref: '/JsNumber'}, + ], + }, + data: { + type: 'string', + pattern: '^0x[0-9a-f]*$', + }, + nonce: { + type: 'number', + minimum: 0, + }, + }, + required: ['from'], + type: 'object', + additionalProperties: false, +}; diff --git a/packages/json-schemas/src/globals.d.ts b/packages/json-schemas/src/globals.d.ts new file mode 100644 index 000000000..157705f57 --- /dev/null +++ b/packages/json-schemas/src/globals.d.ts @@ -0,0 +1,7 @@ +declare module 'dirty-chai'; + +// es6-promisify declarations +declare function promisify(original: any, settings?: any): ((...arg: any[]) => Promise<any>); +declare module 'es6-promisify' { + export = promisify; +} diff --git a/packages/json-schemas/src/index.ts b/packages/json-schemas/src/index.ts new file mode 100644 index 000000000..b7cae277e --- /dev/null +++ b/packages/json-schemas/src/index.ts @@ -0,0 +1,4 @@ +export {ValidatorResult, Schema} from 'jsonschema'; + +export {SchemaValidator} from './schema_validator'; +export {schemas} from './schemas'; diff --git a/packages/json-schemas/src/schema_validator.ts b/packages/json-schemas/src/schema_validator.ts new file mode 100644 index 000000000..0bc88cc45 --- /dev/null +++ b/packages/json-schemas/src/schema_validator.ts @@ -0,0 +1,28 @@ +import values = require('lodash.values'); +import {Validator, ValidatorResult, Schema} from 'jsonschema'; +import {schemas} from './schemas'; + +export class SchemaValidator { + private validator: Validator; + constructor() { + this.validator = new Validator(); + for (const schema of values(schemas)) { + this.validator.addSchema(schema, schema.id); + } + } + public addSchema(schema: Schema) { + this.validator.addSchema(schema, schema.id); + } + // In order to validate a complex JS object using jsonschema, we must replace any complex + // sub-types (e.g BigNumber) with a simpler string representation. Since BigNumber and other + // complex types implement the `toString` method, we can stringify the object and + // then parse it. The resultant object can then be checked using jsonschema. + public validate(instance: any, schema: Schema): ValidatorResult { + const jsonSchemaCompatibleObject = JSON.parse(JSON.stringify(instance)); + return this.validator.validate(jsonSchemaCompatibleObject, schema); + } + public isValid(instance: any, schema: Schema): boolean { + const isValid = this.validate(instance, schema).errors.length === 0; + return isValid; + } +} diff --git a/packages/json-schemas/src/schemas.ts b/packages/json-schemas/src/schemas.ts new file mode 100644 index 000000000..a8e5ecbcb --- /dev/null +++ b/packages/json-schemas/src/schemas.ts @@ -0,0 +1,99 @@ +import { + numberSchema, + addressSchema, +} from '../schemas/basic_type_schemas'; +import { + ecSignatureSchema, + ecSignatureParameterSchema, +} from '../schemas/ec_signature_schema'; +import { + indexFilterValuesSchema, +} from '../schemas/index_filter_values_schema'; +import { + orderCancellationRequestsSchema, +} from '../schemas/order_cancel_schema'; +import { + orderFillOrKillRequestsSchema, +} from '../schemas/order_fill_or_kill_requests_schema'; +import { + orderFillRequestsSchema, +} from '../schemas/order_fill_requests_schema'; +import { + orderHashSchema, +} from '../schemas/order_hash_schema'; +import { + orderSchema, + signedOrderSchema, +} from '../schemas/order_schemas'; +import { + blockParamSchema, + subscriptionOptsSchema, +} from '../schemas/subscription_opts_schema'; +import { + tokenSchema, +} from '../schemas/token_schema'; +import { + signedOrdersSchema, +} from '../schemas/signed_orders_schema'; +import { + relayerApiErrorResponseSchema, +} from '../schemas/relayer_api_error_response_schema'; +import { + relayerApiFeesResponseSchema, +} from '../schemas/relayer_api_fees_response_schema'; +import { + relayerApiFeesPayloadSchema, +} from '../schemas/relayer_api_fees_payload_schema'; +import { + relayerApiOrderBookResponseSchema, +} from '../schemas/relayer_api_orderbook_response_schema'; +import { + relayerApiTokenPairsResponseSchema, + relayerApiTokenTradeInfoSchema, +} from '../schemas/relayer_api_token_pairs_response_schema'; +import { + jsNumber, + txDataSchema, +} from '../schemas/tx_data_schema'; +import { + relayerApiOrderbookChannelSubscribeSchema, + relayerApiOrderbookChannelSubscribePayload, +} from '../schemas/relayer_api_orberbook_channel_subscribe_schema'; +import { + relayerApiOrderbookChannelUpdateSchema, +} from '../schemas/relayer_api_orderbook_channel_update_response_schema'; +import { + relayerApiOrderbookChannelSnapshotSchema, + relayerApiOrderbookChannelSnapshotPayload, +} from '../schemas/relayer_api_orderbook_channel_snapshot_schema'; + +export const schemas = { + numberSchema, + addressSchema, + ecSignatureSchema, + ecSignatureParameterSchema, + indexFilterValuesSchema, + orderCancellationRequestsSchema, + orderFillOrKillRequestsSchema, + orderFillRequestsSchema, + orderHashSchema, + orderSchema, + signedOrderSchema, + signedOrdersSchema, + blockParamSchema, + subscriptionOptsSchema, + tokenSchema, + jsNumber, + txDataSchema, + relayerApiErrorResponseSchema, + relayerApiFeesPayloadSchema, + relayerApiFeesResponseSchema, + relayerApiOrderBookResponseSchema, + relayerApiTokenPairsResponseSchema, + relayerApiTokenTradeInfoSchema, + relayerApiOrderbookChannelSubscribeSchema, + relayerApiOrderbookChannelSubscribePayload, + relayerApiOrderbookChannelUpdateSchema, + relayerApiOrderbookChannelSnapshotSchema, + relayerApiOrderbookChannelSnapshotPayload, +}; diff --git a/packages/json-schemas/test/schema_test.ts b/packages/json-schemas/test/schema_test.ts new file mode 100644 index 000000000..0ff456dec --- /dev/null +++ b/packages/json-schemas/test/schema_test.ts @@ -0,0 +1,972 @@ +import 'mocha'; +import forEach = require('lodash.foreach'); +import * as dirtyChai from 'dirty-chai'; +import * as chai from 'chai'; +import BigNumber from 'bignumber.js'; +import promisify = require('es6-promisify'); +import {SchemaValidator, schemas} from '../src/index'; + +chai.config.includeStack = true; +chai.use(dirtyChai); +const expect = chai.expect; +const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; +const { + numberSchema, + addressSchema, + ecSignatureSchema, + ecSignatureParameterSchema, + indexFilterValuesSchema, + orderCancellationRequestsSchema, + orderFillOrKillRequestsSchema, + orderFillRequestsSchema, + orderHashSchema, + orderSchema, + signedOrderSchema, + signedOrdersSchema, + blockParamSchema, + subscriptionOptsSchema, + tokenSchema, + jsNumber, + txDataSchema, + relayerApiErrorResponseSchema, + relayerApiOrderBookResponseSchema, + relayerApiTokenPairsResponseSchema, + relayerApiFeesPayloadSchema, + relayerApiFeesResponseSchema, + relayerApiOrderbookChannelSubscribeSchema, + relayerApiOrderbookChannelUpdateSchema, + relayerApiOrderbookChannelSnapshotSchema, +} = schemas; + +describe('Schema', () => { + const validator = new SchemaValidator(); + const validateAgainstSchema = (testCases: any[], schema: any, shouldFail = false) => { + forEach(testCases, (testCase: any) => { + const validationResult = validator.validate(testCase, schema); + const hasErrors = validationResult.errors.length !== 0; + if (shouldFail) { + if (!hasErrors) { + throw new Error( + `Expected testCase: ${JSON.stringify(testCase, null, '\t')} to fail and it didn't.`, + ); + } + } else { + if (hasErrors) { + throw new Error(JSON.stringify(validationResult.errors, null, '\t')); + } + } + }); + }; + describe('#numberSchema', () => { + it('should validate valid numbers', () => { + const testCases = ['42', '0', '1.3', '0.2', '00.00']; + validateAgainstSchema(testCases, numberSchema); + }); + it('should fail for invalid numbers', () => { + const testCases = ['.3', '1.', 'abacaba', 'и', '1..0']; + const shouldFail = true; + validateAgainstSchema(testCases, numberSchema, shouldFail); + }); + }); + describe('#addressSchema', () => { + it('should validate valid addresses', () => { + const testCases = ['0x8b0292b11a196601ed2ce54b665cafeca0347d42', NULL_ADDRESS]; + validateAgainstSchema(testCases, addressSchema); + }); + it('should fail for invalid addresses', () => { + const testCases = [ + '0x', + '0', + '0x00', + '0xzzzzzzB11a196601eD2ce54B665CaFEca0347D42', + '0x8b0292B11a196601eD2ce54B665CaFEca0347D42', + ]; + const shouldFail = true; + validateAgainstSchema(testCases, addressSchema, shouldFail); + }); + }); + describe('#ecSignatureParameterSchema', () => { + it('should validate valid parameters', () => { + const testCases = [ + '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33', + '0X40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', + ]; + validateAgainstSchema(testCases, ecSignatureParameterSchema); + }); + it('should fail for invalid parameters', () => { + const testCases = [ + '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3', // shorter + '0xzzzz9190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', // invalid characters + '40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', // no 0x + ]; + const shouldFail = true; + validateAgainstSchema(testCases, ecSignatureParameterSchema, shouldFail); + }); + }); + describe('#ecSignatureSchema', () => { + it('should validate valid signature', () => { + const signature = { + v: 27, + r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33', + s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', + }; + const testCases = [ + signature, + { + ...signature, + v: 28, + }, + ]; + validateAgainstSchema(testCases, ecSignatureSchema); + }); + it('should fail for invalid signature', () => { + const v = 27; + const r = '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33'; + const s = '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254'; + const testCases = [ + {}, + {v}, + {r, s, v: 31}, + ]; + const shouldFail = true; + validateAgainstSchema(testCases, ecSignatureSchema, shouldFail); + }); + }); + describe('#orderHashSchema', () => { + it('should validate valid order hash', () => { + const testCases = [ + '0x61a3ed31B43c8780e905a260a35faefEc527be7516aa11c0256729b5b351bc33', + '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', + ]; + validateAgainstSchema(testCases, orderHashSchema); + }); + it('should fail for invalid order hash', () => { + const testCases = [ + {}, + '0x', + '0x8b0292B11a196601eD2ce54B665CaFEca0347D42', + '61a3ed31B43c8780e905a260a35faefEc527be7516aa11c0256729b5b351bc33', + ]; + const shouldFail = true; + validateAgainstSchema(testCases, orderHashSchema, shouldFail); + }); + }); + describe('#blockParamSchema', () => { + it('should validate valid block param', () => { + const testCases = [ + 42, + 'latest', + 'pending', + 'earliest', + ]; + validateAgainstSchema(testCases, blockParamSchema); + }); + it('should fail for invalid block param', () => { + const testCases = [ + {}, + '42', + 'pemding', + ]; + const shouldFail = true; + validateAgainstSchema(testCases, blockParamSchema, shouldFail); + }); + }); + describe('#subscriptionOptsSchema', () => { + it('should validate valid subscription opts', () => { + const testCases = [ + {fromBlock: 42, toBlock: 'latest'}, + {fromBlock: 42}, + {}, + ]; + validateAgainstSchema(testCases, subscriptionOptsSchema); + }); + it('should fail for invalid subscription opts', () => { + const testCases = [ + {fromBlock: '42'}, + ]; + const shouldFail = true; + validateAgainstSchema(testCases, subscriptionOptsSchema, shouldFail); + }); + }); + describe('#tokenSchema', () => { + const token = { + name: 'Zero Ex', + symbol: 'ZRX', + decimals: 100500, + address: '0x8b0292b11a196601ed2ce54b665cafeca0347d42', + url: 'https://0xproject.com', + }; + it('should validate valid token', () => { + const testCases = [ + token, + ]; + validateAgainstSchema(testCases, tokenSchema); + }); + it('should fail for invalid token', () => { + const testCases = [ + { + ...token, + address: null, + }, + { + ...token, + decimals: undefined, + }, + [], + 4, + ]; + const shouldFail = true; + validateAgainstSchema(testCases, tokenSchema, shouldFail); + }); + }); + describe('order including schemas', () => { + const order = { + maker: NULL_ADDRESS, + taker: NULL_ADDRESS, + makerFee: '1', + takerFee: '2', + makerTokenAmount: '1', + takerTokenAmount: '2', + makerTokenAddress: NULL_ADDRESS, + takerTokenAddress: NULL_ADDRESS, + salt: '67006738228878699843088602623665307406148487219438534730168799356281242528500', + feeRecipient: NULL_ADDRESS, + exchangeContractAddress: NULL_ADDRESS, + expirationUnixTimestampSec: '42', + }; + describe('#orderSchema', () => { + it('should validate valid order', () => { + const testCases = [ + order, + ]; + validateAgainstSchema(testCases, orderSchema); + }); + it('should fail for invalid order', () => { + const testCases = [ + { + ...order, + salt: undefined, + }, + { + ...order, + salt: 'salt', + }, + 'order', + ]; + const shouldFail = true; + validateAgainstSchema(testCases, orderSchema, shouldFail); + }); + }); + describe('signed order including schemas', () => { + const signedOrder = { + ...order, + ecSignature: { + v: 27, + r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33', + s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', + }, + }; + describe('#signedOrdersSchema', () => { + it('should validate valid signed orders', () => { + const testCases = [ + [signedOrder], + [], + ]; + validateAgainstSchema(testCases, signedOrdersSchema); + }); + it('should fail for invalid signed orders', () => { + const testCases = [ + [ + signedOrder, + 1, + ], + ]; + const shouldFail = true; + validateAgainstSchema(testCases, signedOrdersSchema, shouldFail); + }); + }); + describe('#signedOrderSchema', () => { + it('should validate valid signed order', () => { + const testCases = [ + signedOrder, + ]; + validateAgainstSchema(testCases, signedOrderSchema); + }); + it('should fail for invalid signed order', () => { + const testCases = [ + { + ...signedOrder, + ecSignature: undefined, + }, + ]; + const shouldFail = true; + validateAgainstSchema(testCases, signedOrderSchema, shouldFail); + }); + }); + describe('#orderFillOrKillRequestsSchema', () => { + const orderFillOrKillRequests = [ + { + signedOrder, + fillTakerAmount: '5', + }, + ]; + it('should validate valid order fill or kill requests', () => { + const testCases = [ + orderFillOrKillRequests, + ]; + validateAgainstSchema(testCases, orderFillOrKillRequestsSchema); + }); + it('should fail for invalid order fill or kill requests', () => { + const testCases = [ + [ + { + ...orderFillOrKillRequests[0], + fillTakerAmount: undefined, + }, + ], + ]; + const shouldFail = true; + validateAgainstSchema(testCases, orderFillOrKillRequestsSchema, shouldFail); + }); + }); + describe('#orderCancellationRequestsSchema', () => { + const orderCancellationRequests = [ + { + order, + takerTokenCancelAmount: '5', + }, + ]; + it('should validate valid order cancellation requests', () => { + const testCases = [ + orderCancellationRequests, + ]; + validateAgainstSchema(testCases, orderCancellationRequestsSchema); + }); + it('should fail for invalid order cancellation requests', () => { + const testCases = [ + [ + { + ...orderCancellationRequests[0], + takerTokenCancelAmount: undefined, + }, + ], + ]; + const shouldFail = true; + validateAgainstSchema(testCases, orderCancellationRequestsSchema, shouldFail); + }); + }); + describe('#orderFillRequestsSchema', () => { + const orderFillRequests = [ + { + signedOrder, + takerTokenFillAmount: '5', + }, + ]; + it('should validate valid order fill requests', () => { + const testCases = [ + orderFillRequests, + ]; + validateAgainstSchema(testCases, orderFillRequestsSchema); + }); + it('should fail for invalid order fill requests', () => { + const testCases = [ + [ + { + ...orderFillRequests[0], + takerTokenFillAmount: undefined, + }, + ], + ]; + const shouldFail = true; + validateAgainstSchema(testCases, orderFillRequestsSchema, shouldFail); + }); + }); + describe('#relayerApiOrderBookResponseSchema', () => { + it('should validate valid order book responses', () => { + const testCases = [ + { + bids: [], + asks: [], + }, + { + bids: [signedOrder, signedOrder], + asks: [], + }, + { + bids: [], + asks: [signedOrder, signedOrder], + }, + { + bids: [signedOrder], + asks: [signedOrder, signedOrder], + }, + ]; + validateAgainstSchema(testCases, relayerApiOrderBookResponseSchema); + }); + it('should fail for invalid order fill requests', () => { + const testCases = [ + {}, + { + bids: [signedOrder, signedOrder], + }, + { + asks: [signedOrder, signedOrder], + }, + { + bids: signedOrder, + asks: [signedOrder, signedOrder], + }, + { + bids: [signedOrder], + asks: signedOrder, + }, + ]; + const shouldFail = true; + validateAgainstSchema(testCases, relayerApiOrderBookResponseSchema, shouldFail); + }); + }); + describe('#relayerApiOrderbookChannelSubscribeSchema', () => { + it('should validate valid orderbook channel websocket subscribe message', () => { + const testCases = [ + { + type: 'subscribe', + channel: 'orderbook', + payload: { + baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + quoteTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + snapshot: true, + limit: 100, + }, + }, + { + type: 'subscribe', + channel: 'orderbook', + payload: { + baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + quoteTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + }, + }, + ]; + validateAgainstSchema(testCases, relayerApiOrderbookChannelSubscribeSchema); + }); + it('should fail for invalid orderbook channel websocket subscribe message', () => { + const checksummedAddress = '0xA2b31daCf30a9C50ca473337c01d8A201ae33e32'; + const testCases = [ + { + type: 'foo', + channel: 'orderbook', + payload: { + baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + quoteTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + }, + }, + { + type: 'subscribe', + channel: 'bar', + payload: { + baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + quoteTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + }, + }, + { + type: 'subscribe', + channel: 'orderbook', + payload: { + baseTokenAddress: checksummedAddress, + quoteTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + }, + }, + { + type: 'subscribe', + channel: 'orderbook', + payload: { + baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + quoteTokenAddress: checksummedAddress, + }, + }, + { + type: 'subscribe', + channel: 'orderbook', + payload: { + quoteTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + }, + }, + { + type: 'subscribe', + channel: 'orderbook', + payload: { + baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + }, + }, + { + type: 'subscribe', + channel: 'orderbook', + payload: { + baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + quoteTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + snapshot: 'true', + limit: 100, + }, + }, + { + type: 'subscribe', + channel: 'orderbook', + payload: { + baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + quoteTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + snapshot: true, + limit: '100', + }, + }, + ]; + const shouldFail = true; + validateAgainstSchema(testCases, relayerApiOrderbookChannelSubscribeSchema, shouldFail); + }); + }); + describe('#relayerApiOrderbookChannelSnapshotSchema', () => { + it('should validate valid orderbook channel websocket snapshot message', () => { + const testCases = [ + { + type: 'snapshot', + channel: 'orderbook', + channelId: 2, + payload: { + bids: [], + asks: [], + }, + }, + { + type: 'snapshot', + channel: 'orderbook', + channelId: 2, + payload: { + bids: [ + signedOrder, + ], + asks: [ + signedOrder, + ], + }, + }, + ]; + validateAgainstSchema(testCases, relayerApiOrderbookChannelSnapshotSchema); + }); + it('should fail for invalid orderbook channel websocket snapshot message', () => { + const testCases = [ + { + type: 'foo', + channel: 'orderbook', + channelId: 2, + payload: { + bids: [ + signedOrder, + ], + asks: [ + signedOrder, + ], + }, + }, + { + type: 'snapshot', + channel: 'bar', + channelId: 2, + payload: { + bids: [ + signedOrder, + ], + asks: [ + signedOrder, + ], + }, + }, + { + type: 'snapshot', + channel: 'orderbook', + payload: { + bids: [ + signedOrder, + ], + asks: [ + signedOrder, + ], + }, + }, + { + type: 'snapshot', + channel: 'orderbook', + channelId: '2', + payload: { + bids: [ + signedOrder, + ], + asks: [ + signedOrder, + ], + }, + }, + { + type: 'snapshot', + channel: 'orderbook', + channelId: 2, + payload: { + bids: [ + signedOrder, + ], + }, + }, + { + type: 'snapshot', + channel: 'orderbook', + channelId: 2, + payload: { + asks: [ + signedOrder, + ], + }, + }, + { + type: 'snapshot', + channel: 'orderbook', + channelId: 2, + payload: { + bids: [ + signedOrder, + ], + asks: [ + {}, + ], + }, + }, + { + type: 'snapshot', + channel: 'orderbook', + channelId: 2, + payload: { + bids: [ + {}, + ], + asks: [ + signedOrder, + ], + }, + }, + ]; + const shouldFail = true; + validateAgainstSchema(testCases, relayerApiOrderbookChannelSnapshotSchema, shouldFail); + }); + }); + describe('#relayerApiOrderbookChannelUpdateSchema', () => { + it('should validate valid orderbook channel websocket update message', () => { + const testCases = [ + { + type: 'update', + channel: 'orderbook', + channelId: 2, + payload: signedOrder, + }, + ]; + validateAgainstSchema(testCases, relayerApiOrderbookChannelUpdateSchema); + }); + it('should fail for invalid orderbook channel websocket update message', () => { + const testCases = [ + { + type: 'foo', + channel: 'orderbook', + payload: signedOrder, + }, + { + type: 'update', + channel: 'bar', + payload: signedOrder, + }, + { + type: 'update', + channel: 'orderbook', + payload: {}, + }, + ]; + const shouldFail = true; + validateAgainstSchema(testCases, relayerApiOrderbookChannelUpdateSchema, shouldFail); + }); + }); + }); + }); + describe('BigNumber serialization', () => { + it('should correctly serialize BigNumbers', () => { + const testCases = { + '42': '42', + '0': '0', + '1.3': '1.3', + '0.2': '0.2', + '00.00': '0', + '.3': '0.3', + }; + forEach(testCases, (serialized: string, input: string) => { + expect(JSON.parse(JSON.stringify(new BigNumber(input)))).to.be.equal(serialized); + }); + }); + }); + describe('#relayerApiErrorResponseSchema', () => { + it('should validate valid errorResponse', () => { + const testCases = [ + { + code: 102, + reason: 'Order submission disabled', + }, + { + code: 101, + reason: 'Validation failed', + validationErrors: [ + { + field: 'maker', + code: 1002, + reason: 'Invalid address', + }, + ], + }, + ]; + validateAgainstSchema(testCases, relayerApiErrorResponseSchema); + }); + it('should fail for invalid error responses', () => { + const testCases = [ + {}, + { + code: 102, + }, + { + code: '102', + reason: 'Order submission disabled', + }, + { + reason: 'Order submission disabled', + }, + { + code: 101, + reason: 'Validation failed', + validationErrors: [ + { + field: 'maker', + reason: 'Invalid address', + }, + ], + }, + { + code: 101, + reason: 'Validation failed', + validationErrors: [ + { + field: 'maker', + code: '1002', + reason: 'Invalid address', + }, + ], + }, + ]; + const shouldFail = true; + validateAgainstSchema(testCases, relayerApiErrorResponseSchema, shouldFail); + }); + }); + describe('#relayerApiFeesPayloadSchema', () => { + it('should validate valid fees payloads', () => { + const testCases = [ + { + exchangeContractAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + maker: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + taker: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + makerTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990', + makerTokenAmount: '10000000000000000000', + takerTokenAmount: '30000000000000000000', + expirationUnixTimestampSec: '42', + salt: '67006738228878699843088602623665307406148487219438534730168799356281242528500', + }, + ]; + validateAgainstSchema(testCases, relayerApiFeesPayloadSchema); + }); + it('should fail for invalid fees payloads', () => { + const checksummedAddress = '0xA2b31daCf30a9C50ca473337c01d8A201ae33e32'; + const testCases = [ + {}, + { + takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990', + makerTokenAmount: '10000000000000000000', + takerTokenAmount: '30000000000000000000', + }, + { + taker: checksummedAddress, + makerTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990', + makerTokenAmount: '10000000000000000000', + takerTokenAmount: '30000000000000000000', + }, + { + makerTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990', + makerTokenAmount: 10000000000000000000, + takerTokenAmount: 30000000000000000000, + }, + ]; + const shouldFail = true; + validateAgainstSchema(testCases, relayerApiFeesPayloadSchema, shouldFail); + }); + }); + describe('#relayerApiFeesResponseSchema', () => { + it('should validate valid fees responses', () => { + const testCases = [ + { + makerFee: '10000000000000000', + takerFee: '30000000000000000', + feeRecipient: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + }, + ]; + validateAgainstSchema(testCases, relayerApiFeesResponseSchema); + }); + it('should fail for invalid fees responses', () => { + const checksummedAddress = '0xA2b31daCf30a9C50ca473337c01d8A201ae33e32'; + const testCases = [ + {}, + { + makerFee: 10000000000000000, + takerFee: 30000000000000000, + }, + { + feeRecipient: checksummedAddress, + takerToSpecify: checksummedAddress, + makerFee: '10000000000000000', + takerFee: '30000000000000000', + }, + ]; + const shouldFail = true; + validateAgainstSchema(testCases, relayerApiFeesResponseSchema, shouldFail); + }); + }); + describe('#relayerApiTokenPairsResponseSchema', () => { + it('should validate valid tokenPairs response', () => { + const testCases = [ + [], + [ + { + tokenA: { + address: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + minAmount: '0', + maxAmount: '10000000000000000000', + precision: 5, + }, + tokenB: { + address: '0xef7fff64389b814a946f3e92105513705ca6b990', + minAmount: '0', + maxAmount: '50000000000000000000', + precision: 5, + }, + }, + ], + [ + { + tokenA: { + address: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + }, + tokenB: { + address: '0xef7fff64389b814a946f3e92105513705ca6b990', + }, + }, + ], + ]; + validateAgainstSchema(testCases, relayerApiTokenPairsResponseSchema); + }); + it('should fail for invalid tokenPairs responses', () => { + const checksummedAddress = '0xA2b31daCf30a9C50ca473337c01d8A201ae33e32'; + const testCases = [ + [ + { + tokenA: { + address: checksummedAddress, + }, + tokenB: { + address: checksummedAddress, + }, + }, + ], + [ + { + tokenA: { + address: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + minAmount: 0, + maxAmount: 10000000000000000000, + }, + tokenB: { + address: '0xef7fff64389b814a946f3e92105513705ca6b990', + minAmount: 0, + maxAmount: 50000000000000000000, + }, + }, + ], + [ + { + tokenA: { + address: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + precision: '5', + }, + tokenB: { + address: '0xef7fff64389b814a946f3e92105513705ca6b990', + precision: '5', + }, + }, + ], + ]; + const shouldFail = true; + validateAgainstSchema(testCases, relayerApiTokenPairsResponseSchema, shouldFail); + }); + }); + describe('#jsNumberSchema', () => { + it('should validate valid js number', () => { + const testCases = [ + 1, + 42, + ]; + validateAgainstSchema(testCases, jsNumber); + }); + it('should fail for invalid js number', () => { + const testCases = [ + NaN, + -1, + new BigNumber(1), + ]; + const shouldFail = true; + validateAgainstSchema(testCases, jsNumber, shouldFail); + }); + }); + describe('#txDataSchema', () => { + it('should validate valid txData', () => { + const testCases = [ + { + from: NULL_ADDRESS, + }, + { + from: NULL_ADDRESS, + gas: new BigNumber(42), + }, + { + from: NULL_ADDRESS, + gas: 42, + }, + ]; + validateAgainstSchema(testCases, txDataSchema); + }); + it('should fail for invalid txData', () => { + const testCases = [ + { + gas: new BigNumber(42), + }, + { + from: NULL_ADDRESS, + unknownProp: 'here', + }, + {}, + [], + new BigNumber(1), + ]; + const shouldFail = true; + validateAgainstSchema(testCases, txDataSchema, shouldFail); + }); + }); +}); diff --git a/packages/json-schemas/tsconfig.json b/packages/json-schemas/tsconfig.json new file mode 100644 index 000000000..40c2f0c8c --- /dev/null +++ b/packages/json-schemas/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "lib": [ "es2017", "dom"], + "outDir": "lib", + "sourceMap": true, + "declaration": true, + "noImplicitAny": true, + "strictNullChecks": true + }, + "include": [ + "./src/**/*", + "./test/**/*", + "../../node_modules/chai-typescript-typings/index.d.ts" + ] +} diff --git a/packages/json-schemas/tslint.json b/packages/json-schemas/tslint.json new file mode 100644 index 000000000..a07795151 --- /dev/null +++ b/packages/json-schemas/tslint.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "@0xproject/tslint-config" + ] +} diff --git a/packages/tslint-config/README.md b/packages/tslint-config/README.md new file mode 100644 index 000000000..d8bb0a51e --- /dev/null +++ b/packages/tslint-config/README.md @@ -0,0 +1,4 @@ +tslint-config +----------------------- + +Lint rules related to 0xProject for TSLint. diff --git a/packages/tslint-config/package.json b/packages/tslint-config/package.json new file mode 100644 index 000000000..ca46d63fc --- /dev/null +++ b/packages/tslint-config/package.json @@ -0,0 +1,38 @@ +{ + "name": "@0xproject/tslint-config", + "version": "0.1.0", + "description": "Lint rules related to 0xProject for TSLint", + "main": "tslint.json", + "files": [ + "tslint.js", + "README.md", + "LICENSE" + ], + "repository": { + "type": "git", + "url": "git://github.com/0xProject/0x.js.git" + }, + "keywords": [ + "tslint", + "config", + "0xProject", + "typescript", + "ts" + ], + "author": { + "name": "Fabio Berger", + "email": "fabio@0xproject.com" + }, + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/0xProject/0x.js/issues" + }, + "homepage": "https://github.com/0xProject/0x.js/packages/tslint-config/README.md", + "devDependencies": { + "tslint": "5.8.0", + "typescript": "2.6.1" + }, + "dependencies": { + "tslint-react": "^3.2.0" + } +} diff --git a/packages/tslint-config/tslint.json b/packages/tslint-config/tslint.json new file mode 100644 index 000000000..8b839f25a --- /dev/null +++ b/packages/tslint-config/tslint.json @@ -0,0 +1,53 @@ +{ + "extends": [ + "tslint:latest", + "tslint-react" + ], + "rules": { + "arrow-parens": [true, "ban-single-arg-parens"], + "ordered-imports": false, + "quotemark": [true, "single", "avoid-escape", "jsx-double"], + "callable-types": true, + "interface-name": false, + "interface-over-type-literal": true, + "object-literal-sort-keys": false, + "max-classes-per-file": false, + "max-line-length": [true, 120], + "member-ordering": [true, + "public-before-private", + "static-before-instance", + "variables-before-functions" + ], + "no-angle-bracket-type-assertion": true, + "no-default-export": true, + "no-empty-interface": false, + "no-string-throw": true, + "no-submodule-imports": false, + "no-implicit-dependencies": [true, "dev"], + "prefer-const": true, + "variable-name": [true, + "ban-keywords", + "allow-pascal-case" + ], + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-rest-spread", + "check-type", + "check-typecast", + "check-preblock" + ], + "jsx-alignment": true, + "jsx-boolean-value": true, + "jsx-curly-spacing": [true, "never"], + "jsx-no-lambda": true, + "jsx-no-multiline-js": false, + "jsx-no-string-ref": true, + "jsx-self-close": true, + "jsx-wrap-multiline": false, + "jsx-no-bind": false + } +} @@ -2,13 +2,6 @@ # yarn lockfile v1 -"0x-json-schemas@^0.6.1": - version "0.6.6" - resolved "https://registry.yarnpkg.com/0x-json-schemas/-/0x-json-schemas-0.6.6.tgz#3852e639245474a14daa2f8c454ba83ca5df8a9c" - dependencies: - jsonschema "^1.2.0" - lodash.values "^4.3.0" - "@types/fs-extra@^4.0.0": version "4.0.4" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-4.0.4.tgz#72947e108f2cbeda5ab288a927399fdf6d02bd42" @@ -36,7 +29,19 @@ dependencies: jsonschema "*" -"@types/lodash@^4.14.37", "@types/lodash@^4.14.64": +"@types/lodash.foreach@^4.5.3": + version "4.5.3" + resolved "https://registry.yarnpkg.com/@types/lodash.foreach/-/lodash.foreach-4.5.3.tgz#87c01a0c5d9d17eec936ca3c28897af79440cdfc" + dependencies: + "@types/lodash" "*" + +"@types/lodash.values@^4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@types/lodash.values/-/lodash.values-4.3.3.tgz#910edc65b391782d65dc4b4d8804a0dabc5370e6" + dependencies: + "@types/lodash" "*" + +"@types/lodash@*", "@types/lodash@^4.14.37", "@types/lodash@^4.14.64", "@types/lodash@^4.14.78": version "4.14.85" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.85.tgz#a16fbf942422f6eca5622b6910492c496c35069b" @@ -48,7 +53,7 @@ version "2.0.29" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-2.0.29.tgz#5002e14f75e2d71e564281df0431c8c1b4a2a36a" -"@types/mocha@^2.2.41": +"@types/mocha@^2.2.41", "@types/mocha@^2.2.42": version "2.2.44" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.44.tgz#1d4a798e53f35212fd5ad4d04050620171cd5b5e" @@ -73,6 +78,10 @@ dependencies: "@types/node" "*" +"@types/valid-url@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/valid-url/-/valid-url-1.0.2.tgz#60fa435ce24bfd5ba107b8d2a80796aeaf3a8f45" + JSONStream@^1.0.4: version "1.3.1" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a" @@ -802,7 +811,7 @@ big.js@^3.1.3: version "3.2.0" resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" -bignumber.js@^4.0.2, bignumber.js@^4.1.0: +bignumber.js@^4.0.2, bignumber.js@~4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-4.1.0.tgz#db6f14067c140bd46624815a7916c92d9b6c24b1" @@ -864,7 +873,7 @@ bn.js@4.11.7: version "4.11.7" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.7.tgz#ddb048e50d9482790094c13eb3fcfc833ce7ab46" -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.10.0, bn.js@^4.11.3, bn.js@^4.11.7, bn.js@^4.4.0, bn.js@^4.8.0: +bn.js@4.11.8, bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.10.0, bn.js@^4.11.3, bn.js@^4.11.7, bn.js@^4.4.0, bn.js@^4.8.0: version "4.11.8" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" @@ -1019,7 +1028,7 @@ buffer@^5.0.6: base64-js "^1.0.2" ieee754 "^1.1.4" -builtin-modules@^1.0.0: +builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -1115,7 +1124,7 @@ chai-typescript-typings@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/chai-typescript-typings/-/chai-typescript-typings-0.0.1.tgz#433dee303b0b2978ad0dd03129df0a5afb791274" -chai@^4.0.1: +chai@^4.0.1, chai@^4.1.1: version "4.1.2" resolved "https://registry.yarnpkg.com/chai/-/chai-4.1.2.tgz#0f64584ba642f0f2ace2806279f4f06ca23ad73c" dependencies: @@ -1599,7 +1608,7 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" -crypto-js@^3.1.4: +crypto-js@^3.1.4, crypto-js@^3.1.6: version "3.1.8" resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.1.8.tgz#715f070bf6014f2ae992a98b3929258b713f08d5" @@ -2011,6 +2020,12 @@ eth-sig-util@^1.3.0: ethereumjs-abi "git+https://github.com/ethereumjs/ethereumjs-abi.git" ethereumjs-util "^5.1.1" +ethereum-address@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/ethereum-address/-/ethereum-address-0.0.4.tgz#91729b2bc8a0044bbee2c05ccf6d0417953e5f95" + dependencies: + crypto-js "^3.1.6" + ethereum-common@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.2.0.tgz#13bf966131cce1eeade62a1b434249bb4cb120ca" @@ -3537,6 +3552,10 @@ lodash.assign@^4.0.3, lodash.assign@^4.0.6: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" +lodash.foreach@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" + lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" @@ -3827,7 +3846,7 @@ mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: dependencies: minimist "0.0.8" -mocha@^4.0.0: +mocha@^4.0.0, mocha@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.0.1.tgz#0aee5a95cf69a4618820f5e51fa31717117daf1b" dependencies: @@ -3982,7 +4001,7 @@ normalize-path@^2.0.0, normalize-path@^2.0.1: dependencies: remove-trailing-separator "^1.0.1" -npm-run-all@^4.0.2: +npm-run-all@^4.0.2, npm-run-all@^4.1.1: version "4.1.2" resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.2.tgz#90d62d078792d20669139e718621186656cea056" dependencies: @@ -5602,24 +5621,19 @@ tslib@^1.7.1: version "1.8.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.8.0.tgz#dc604ebad64bcbf696d613da6c954aa0e7ea1eb6" -tslint-config-0xproject@^0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/tslint-config-0xproject/-/tslint-config-0xproject-0.0.2.tgz#39901e0c0b3e9388f00092a28b90c015395d5bba" - dependencies: - tslint-react "^3.0.0" - -tslint-react@^3.0.0: +tslint-react@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/tslint-react/-/tslint-react-3.2.0.tgz#851fb505201c63d0343c51726e6364f7e9ad2e99" dependencies: tsutils "^2.8.0" -tslint@~5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.5.0.tgz#10e8dab3e3061fa61e9442e8cee3982acf20a6aa" +tslint@5.8.0: + version "5.8.0" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.8.0.tgz#1f49ad5b2e77c76c3af4ddcae552ae4e3612eb13" dependencies: babel-code-frame "^6.22.0" - colors "^1.1.2" + builtin-modules "^1.1.1" + chalk "^2.1.0" commander "^2.9.0" diff "^3.2.0" glob "^7.1.1" @@ -5627,9 +5641,9 @@ tslint@~5.5.0: resolve "^1.3.2" semver "^5.3.0" tslib "^1.7.1" - tsutils "^2.5.1" + tsutils "^2.12.1" -tsutils@^2.5.1, tsutils@^2.8.0: +tsutils@^2.12.1, tsutils@^2.8.0: version "2.12.2" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.12.2.tgz#ad58a4865d17ec3ddb6631b6ca53be14a5656ff3" dependencies: @@ -5701,7 +5715,7 @@ typescript@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.1.tgz#c3ccb16ddaa0b2314de031e7e6fee89e5ba346bc" -typescript@^2.4.1: +typescript@2.6.1, typescript@^2.4.2, typescript@~2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.1.tgz#ef39cdea27abac0b500242d6726ab90e0c846631" @@ -5813,6 +5827,10 @@ uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" +valid-url@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" + validate-npm-package-license@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" |