diff options
Diffstat (limited to 'packages/contract-wrappers')
25 files changed, 880 insertions, 51 deletions
diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index b3c9b0fb3..8863f61f4 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -1,5 +1,81 @@ [ { + "version": "2.0.0", + "changes": [ + { + "note": + "Fixes dropped events in subscriptions by fetching logs by blockHash instead of blockNumber. Support for fetching by blockHash was added in Geth > v1.8.13 and Parity > v2.1.0. Infura works too.", + "pr": 1080 + }, + { + "note": + "Fix misunderstanding about blockstream interface callbacks and pass the raw JSON RPC responses to it", + "pr": 1080 + } + ], + "timestamp": 1537907159 + }, + { + "version": "1.0.5", + "changes": [ + { + "note": "Dependencies updated" + } + ], + "timestamp": 1537875740 + }, + { + "version": "1.0.4", + "changes": [ + { + "note": "Dependencies updated" + } + ], + "timestamp": 1537541580 + }, + { + "version": "1.0.3", + "changes": [ + { + "note": "Drastically reduce the bundle size by removing unused parts of included contract artifacts." + } + ], + "timestamp": 1537369748 + }, + { + "version": "1.0.2", + "changes": [ + { + "note": "Add ZRX & WETH mainnet contract addresses into the included artifacts" + } + ], + "timestamp": 1537265493 + }, + { + "version": "1.0.1", + "changes": [ + { + "note": "Add `OrderValidatorWrapper`" + }, + { + "note": + "Fix bug where contracts not deployed on a network showed an `EXCHANGE_CONTRACT_DOES_NOT_EXIST` error instead of `CONTRACT_NOT_DEPLOYED_ON_NETWORK`", + "pr": 1044 + }, + { + "note": + "Export `AssetBalanceAndProxyAllowanceFetcher` and `OrderFilledCancelledFetcher` implementations", + "pr": 1054 + }, + { + "note": + "Add `validateOrderFillableOrThrowAsync` and `validateFillOrderThrowIfInvalidAsync` to ExchangeWrapper", + "pr": 1054 + } + ], + "timestamp": 1536142250 + }, + { "version": "1.0.1-rc.5", "changes": [ { diff --git a/packages/contract-wrappers/CHANGELOG.md b/packages/contract-wrappers/CHANGELOG.md index fb92ea858..41e5645a3 100644 --- a/packages/contract-wrappers/CHANGELOG.md +++ b/packages/contract-wrappers/CHANGELOG.md @@ -5,6 +5,34 @@ Edit the package's CHANGELOG.json file only. CHANGELOG +## v2.0.0 - _September 25, 2018_ + + * Fixes dropped events in subscriptions by fetching logs by blockHash instead of blockNumber. Support for fetching by blockHash was added in Geth > v1.8.13 and Parity > v2.1.0. Infura works too. (#1080) + * Fix misunderstanding about blockstream interface callbacks and pass the raw JSON RPC responses to it (#1080) + +## v1.0.5 - _September 25, 2018_ + + * Dependencies updated + +## v1.0.4 - _September 21, 2018_ + + * Dependencies updated + +## v1.0.3 - _September 19, 2018_ + + * Drastically reduce the bundle size by removing unused parts of included contract artifacts. + +## v1.0.2 - _September 18, 2018_ + + * Add ZRX & WETH mainnet contract addresses into the included artifacts + +## v1.0.1 - _September 5, 2018_ + + * Add `OrderValidatorWrapper` + * Fix bug where contracts not deployed on a network showed an `EXCHANGE_CONTRACT_DOES_NOT_EXIST` error instead of `CONTRACT_NOT_DEPLOYED_ON_NETWORK` (#1044) + * Export `AssetBalanceAndProxyAllowanceFetcher` and `OrderFilledCancelledFetcher` implementations (#1054) + * Add `validateOrderFillableOrThrowAsync` and `validateFillOrderThrowIfInvalidAsync` to ExchangeWrapper (#1054) + ## v1.0.1-rc.5 - _August 27, 2018_ * Fix missing `BlockParamLiteral` type import issue diff --git a/packages/contract-wrappers/package.json b/packages/contract-wrappers/package.json index 7d9417d65..1deb6a425 100644 --- a/packages/contract-wrappers/package.json +++ b/packages/contract-wrappers/package.json @@ -1,6 +1,6 @@ { "name": "@0xproject/contract-wrappers", - "version": "1.0.1-rc.5", + "version": "2.0.0", "description": "Smart TS wrappers for 0x smart contracts", "keywords": [ "0xproject", @@ -11,26 +11,23 @@ "main": "lib/src/index.js", "types": "lib/src/index.d.ts", "scripts": { - "watch_without_deps": "yarn pre_build && tsc -w", - "build": "yarn pre_build && tsc", - "pre_build": "run-s update_artifacts_v2_beta update_artifacts_v2 generate_contract_wrappers copy_artifacts", - "generate_contract_wrappers": "abi-gen --abis 'src/artifacts/@(Exchange|DummyERC20Token|DummyERC721Token|ZRXToken|ERC20Token|ERC721Token|WETH9|ERC20Proxy|ERC721Proxy|Forwarder).json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/contract_wrappers/generated --backend ethers", + "build": "yarn pre_build && tsc -b", + "pre_build": "run-s update_artifacts generate_contract_wrappers copy_artifacts", + "generate_contract_wrappers": "abi-gen --abis 'src/artifacts/@(Exchange|DummyERC20Token|DummyERC721Token|ZRXToken|ERC20Token|ERC721Token|WETH9|ERC20Proxy|ERC721Proxy|Forwarder|OrderValidator).json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/contract_wrappers/generated --backend ethers", "lint": "tslint --project . --exclude **/src/contract_wrappers/**/* --exclude **/lib/**/*", "test:circleci": "run-s test:coverage", "test": "yarn run_mocha", "rebuild_and_test": "run-s build test", "test:coverage": "nyc npm run test --all && yarn coverage:report:lcov", "coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info", - "update_artifacts_v2_beta": "for i in ${npm_package_config_contracts_v2_beta}; do copyfiles -u 4 ../migrations/artifacts/2.0.0-beta-testnet/$i.json src/artifacts; done;", - "update_artifacts_v2": "for i in ${npm_package_config_contracts_v2}; do copyfiles -u 4 ../migrations/artifacts/2.0.0/$i.json src/artifacts; done;", + "update_artifacts": "for i in ${npm_package_config_contracts_v2}; do copyfiles -u 4 ../migrations/artifacts/2.0.0-trimmed/$i.json src/artifacts; done;", "copy_artifacts": "copyfiles -u 2 './src/artifacts/**/*.json' ./lib/src/artifacts", "clean": "shx rm -rf _bundles lib test_temp test/artifacts src/contract_wrappers/generated src/artifacts generated_docs", "run_mocha": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js lib/test/global_hooks.js --timeout 10000 --bail --exit", - "docs:json": "typedoc --excludePrivate --excludeExternals --target ES5 --json $JSON_FILE_PATH $PROJECT_FILES" + "docs:json": "typedoc --excludePrivate --excludeExternals --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES" }, "config": { - "contracts_v2_beta": "AssetProxyOwner Exchange ERC20Proxy ERC20Token ERC721Proxy ERC721Token WETH9 ZRXToken Forwarder OrderValidator", - "contracts_v2": "DummyERC20Token DummyERC721Token", + "contracts_v2": "AssetProxyOwner Exchange ERC20Proxy ERC20Token ERC721Proxy ERC721Token WETH9 ZRXToken Forwarder OrderValidator DummyERC20Token DummyERC721Token", "postpublish": { "assets": [] } @@ -44,16 +41,17 @@ "node": ">=6.0.0" }, "devDependencies": { - "@0xproject/abi-gen": "^1.0.7", - "@0xproject/dev-utils": "^1.0.6", - "@0xproject/migrations": "^1.0.6", - "@0xproject/subproviders": "^2.0.1", - "@0xproject/tslint-config": "^1.0.6", + "@0xproject/abi-gen": "^1.0.11", + "@0xproject/dev-utils": "^1.0.10", + "@0xproject/migrations": "^1.0.12", + "@0xproject/subproviders": "^2.0.5", + "@0xproject/tslint-config": "^1.0.7", "@types/lodash": "4.14.104", "@types/mocha": "^2.2.42", - "@types/node": "^8.0.53", + "@types/node": "*", "@types/sinon": "^2.2.2", "@types/uuid": "^3.4.2", + "@types/web3-provider-engine": "^14.0.0", "awesome-typescript-loader": "^3.1.3", "chai": "^4.0.1", "chai-as-promised": "^7.1.0", @@ -74,19 +72,19 @@ "web3-provider-engine": "14.0.6" }, "dependencies": { - "@0xproject/assert": "^1.0.7", - "@0xproject/base-contract": "^2.0.1", - "@0xproject/fill-scenarios": "^1.0.1-rc.5", - "@0xproject/json-schemas": "^1.0.1-rc.6", - "@0xproject/order-utils": "^1.0.1-rc.6", - "@0xproject/types": "^1.0.1-rc.6", - "@0xproject/typescript-typings": "^1.0.5", - "@0xproject/utils": "^1.0.7", - "@0xproject/web3-wrapper": "^2.0.1", - "ethereum-types": "^1.0.5", - "ethereumjs-blockstream": "5.0.0", + "@0xproject/assert": "^1.0.11", + "@0xproject/base-contract": "^2.0.5", + "@0xproject/fill-scenarios": "^1.0.5", + "@0xproject/json-schemas": "^1.0.4", + "@0xproject/order-utils": "^1.0.5", + "@0xproject/types": "^1.1.1", + "@0xproject/typescript-typings": "^2.0.2", + "@0xproject/utils": "^1.0.11", + "@0xproject/web3-wrapper": "^3.0.1", + "ethereum-types": "^1.0.8", + "ethereumjs-blockstream": "6.0.0", "ethereumjs-util": "^5.1.1", - "ethers": "3.0.22", + "ethers": "4.0.0-beta.14", "js-sha3": "^0.7.0", "lodash": "^4.17.5", "uuid": "^3.1.0" diff --git a/packages/contract-wrappers/src/artifacts.ts b/packages/contract-wrappers/src/artifacts.ts index 7e67544d2..925f34162 100644 --- a/packages/contract-wrappers/src/artifacts.ts +++ b/packages/contract-wrappers/src/artifacts.ts @@ -8,6 +8,7 @@ import * as ERC721Proxy from './artifacts/ERC721Proxy.json'; import * as ERC721Token from './artifacts/ERC721Token.json'; import * as Exchange from './artifacts/Exchange.json'; import * as Forwarder from './artifacts/Forwarder.json'; +import * as OrderValidator from './artifacts/OrderValidator.json'; import * as EtherToken from './artifacts/WETH9.json'; import * as ZRXToken from './artifacts/ZRXToken.json'; @@ -22,4 +23,5 @@ export const artifacts = { ERC20Proxy: (ERC20Proxy as any) as ContractArtifact, ERC721Proxy: (ERC721Proxy as any) as ContractArtifact, Forwarder: (Forwarder as any) as ContractArtifact, + OrderValidator: (OrderValidator as any) as ContractArtifact, }; diff --git a/packages/contract-wrappers/src/contract_wrappers.ts b/packages/contract-wrappers/src/contract_wrappers.ts index 4277a0746..4272cc943 100644 --- a/packages/contract-wrappers/src/contract_wrappers.ts +++ b/packages/contract-wrappers/src/contract_wrappers.ts @@ -12,6 +12,7 @@ import { ERC721TokenWrapper } from './contract_wrappers/erc721_token_wrapper'; import { EtherTokenWrapper } from './contract_wrappers/ether_token_wrapper'; import { ExchangeWrapper } from './contract_wrappers/exchange_wrapper'; import { ForwarderWrapper } from './contract_wrappers/forwarder_wrapper'; +import { OrderValidatorWrapper } from './contract_wrappers/order_validator_wrapper'; import { ContractWrappersConfigSchema } from './schemas/contract_wrappers_config_schema'; import { contractWrappersPrivateNetworkConfigSchema } from './schemas/contract_wrappers_private_network_config_schema'; import { contractWrappersPublicNetworkConfigSchema } from './schemas/contract_wrappers_public_network_config_schema'; @@ -52,6 +53,10 @@ export class ContractWrappers { * An instance of the ForwarderWrapper class containing methods for interacting with any Forwarder smart contract. */ public forwarder: ForwarderWrapper; + /** + * An instance of the OrderValidatorWrapper class containing methods for interacting with any OrderValidator smart contract. + */ + public orderValidator: OrderValidatorWrapper; private _web3Wrapper: Web3Wrapper; /** @@ -106,6 +111,8 @@ export class ContractWrappers { this.exchange = new ExchangeWrapper( this._web3Wrapper, config.networkId, + this.erc20Token, + this.erc721Token, config.exchangeContractAddress, config.zrxContractAddress, blockPollingIntervalMs, @@ -116,6 +123,7 @@ export class ContractWrappers { config.forwarderContractAddress, config.zrxContractAddress, ); + this.orderValidator = new OrderValidatorWrapper(this._web3Wrapper, config.networkId); } /** * Sets a new web3 provider for 0x.js. Updating the provider will stop all diff --git a/packages/contract-wrappers/src/contract_wrappers/contract_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/contract_wrapper.ts index ba36afea1..f7a89e3be 100644 --- a/packages/contract-wrappers/src/contract_wrappers/contract_wrapper.ts +++ b/packages/contract-wrappers/src/contract_wrappers/contract_wrapper.ts @@ -1,5 +1,5 @@ import { AbiDecoder, intervalUtils, logUtils } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { marshaller, Web3Wrapper } from '@0xproject/web3-wrapper'; import { BlockParamLiteral, ContractAbi, @@ -8,6 +8,7 @@ import { LogEntry, LogWithDecodedArgs, RawLog, + RawLogEntry, } from 'ethereum-types'; import { Block, BlockAndLogStreamer, Log } from 'ethereumjs-blockstream'; import * as _ from 'lodash'; @@ -145,16 +146,20 @@ export abstract class ContractWrapper { } protected _getContractAddress(artifact: ContractArtifact, addressIfExists?: string): string { if (_.isUndefined(addressIfExists)) { + if (_.isUndefined(artifact.networks[this._networkId])) { + throw new Error(ContractWrappersError.ContractNotDeployedOnNetwork); + } const contractAddress = artifact.networks[this._networkId].address; if (_.isUndefined(contractAddress)) { - throw new Error(ContractWrappersError.ExchangeContractDoesNotExist); + throw new Error(CONTRACT_NAME_TO_NOT_FOUND_ERROR[artifact.contractName]); } return contractAddress; } else { return addressIfExists; } } - private _onLogStateChanged<ArgsType extends ContractEventArgs>(isRemoved: boolean, log: LogEntry): void { + private _onLogStateChanged<ArgsType extends ContractEventArgs>(isRemoved: boolean, rawLog: RawLogEntry): void { + const log: LogEntry = marshaller.unmarshalLog(rawLog); _.forEach(this._filters, (filter: FilterObject, filterToken: string) => { if (filterUtils.matchesFilter(log, filter)) { const decodedLog = this._tryToDecodeLogOrNoop(log) as LogWithDecodedArgs<ArgsType>; @@ -171,8 +176,8 @@ export abstract class ContractWrapper { throw new Error(ContractWrappersError.SubscriptionAlreadyPresent); } this._blockAndLogStreamerIfExists = new BlockAndLogStreamer( - this._web3Wrapper.getBlockAsync.bind(this._web3Wrapper), - this._web3Wrapper.getLogsAsync.bind(this._web3Wrapper), + this._blockstreamGetBlockOrNullAsync.bind(this), + this._blockstreamGetLogsAsync.bind(this), ContractWrapper._onBlockAndLogStreamerError.bind(this, isVerbose), ); const catchAllLogFilter = {}; @@ -191,6 +196,32 @@ export abstract class ContractWrapper { this._onLogStateChanged.bind(this, isRemoved), ); } + // This method only exists in order to comply with the expected interface of Blockstream's constructor + private async _blockstreamGetBlockOrNullAsync(hash: string): Promise<Block | null> { + const shouldIncludeTransactionData = false; + const blockOrNull = await this._web3Wrapper.sendRawPayloadAsync<Block | null>({ + method: 'eth_getBlockByHash', + params: [hash, shouldIncludeTransactionData], + }); + return blockOrNull; + } + // This method only exists in order to comply with the expected interface of Blockstream's constructor + private async _blockstreamGetLatestBlockOrNullAsync(): Promise<Block | null> { + const shouldIncludeTransactionData = false; + const blockOrNull = await this._web3Wrapper.sendRawPayloadAsync<Block | null>({ + method: 'eth_getBlockByNumber', + params: [BlockParamLiteral.Latest, shouldIncludeTransactionData], + }); + return blockOrNull; + } + // This method only exists in order to comply with the expected interface of Blockstream's constructor + private async _blockstreamGetLogsAsync(filterOptions: FilterObject): Promise<RawLogEntry[]> { + const logs = await this._web3Wrapper.sendRawPayloadAsync<RawLogEntry[]>({ + method: 'eth_getLogs', + params: [filterOptions], + }); + return logs as RawLogEntry[]; + } // HACK: This should be a package-scoped method (which doesn't exist in TS) // We don't want this method available in the public interface for all classes // who inherit from ContractWrapper, and it is only used by the internal implementation @@ -209,11 +240,14 @@ export abstract class ContractWrapper { delete this._blockAndLogStreamerIfExists; } private async _reconcileBlockAsync(): Promise<void> { - const latestBlock = await this._web3Wrapper.getBlockAsync(BlockParamLiteral.Latest); + const latestBlockOrNull = await this._blockstreamGetLatestBlockOrNullAsync(); + if (_.isNull(latestBlockOrNull)) { + return; // noop + } // We need to coerce to Block type cause Web3.Block includes types for mempool blocks if (!_.isUndefined(this._blockAndLogStreamerIfExists)) { // If we clear the interval while fetching the block - this._blockAndLogStreamer will be undefined - await this._blockAndLogStreamerIfExists.reconcileNewBlock((latestBlock as any) as Block); + await this._blockAndLogStreamerIfExists.reconcileNewBlock(latestBlockOrNull); } } } diff --git a/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts index cfef0f107..dc9ffcbf7 100644 --- a/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts +++ b/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts @@ -1,12 +1,19 @@ import { schemas } from '@0xproject/json-schemas'; -import { assetDataUtils } from '@0xproject/order-utils'; +import { + assetDataUtils, + BalanceAndProxyAllowanceLazyStore, + ExchangeTransferSimulator, + OrderValidationUtils, +} from '@0xproject/order-utils'; import { AssetProxyId, Order, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import { ContractAbi, LogWithDecodedArgs } from 'ethereum-types'; +import { BlockParamLiteral, ContractAbi, LogWithDecodedArgs } from 'ethereum-types'; import * as _ from 'lodash'; import { artifacts } from '../artifacts'; +import { AssetBalanceAndProxyAllowanceFetcher } from '../fetchers/asset_balance_and_proxy_allowance_fetcher'; +import { OrderFilledCancelledFetcher } from '../fetchers/order_filled_cancelled_fetcher'; import { methodOptsSchema } from '../schemas/method_opts_schema'; import { orderTxOptsSchema } from '../schemas/order_tx_opts_schema'; import { txOptsSchema } from '../schemas/tx_opts_schema'; @@ -17,13 +24,17 @@ import { IndexedFilterValues, MethodOpts, OrderInfo, + OrderStatus, OrderTransactionOpts, + ValidateOrderFillableOpts, } from '../types'; import { assert } from '../utils/assert'; import { decorators } from '../utils/decorators'; import { TransactionEncoder } from '../utils/transaction_encoder'; import { ContractWrapper } from './contract_wrapper'; +import { ERC20TokenWrapper } from './erc20_token_wrapper'; +import { ERC721TokenWrapper } from './erc721_token_wrapper'; import { ExchangeContract, ExchangeEventArgs, ExchangeEvents } from './generated/exchange'; /** @@ -33,6 +44,8 @@ import { ExchangeContract, ExchangeEventArgs, ExchangeEvents } from './generated export class ExchangeWrapper extends ContractWrapper { public abi: ContractAbi = artifacts.Exchange.compilerOutput.abi; private _exchangeContractIfExists?: ExchangeContract; + private _erc721TokenWrapper: ERC721TokenWrapper; + private _erc20TokenWrapper: ERC20TokenWrapper; private _contractAddressIfExists?: string; private _zrxContractAddressIfExists?: string; /** @@ -48,11 +61,15 @@ export class ExchangeWrapper extends ContractWrapper { constructor( web3Wrapper: Web3Wrapper, networkId: number, + erc20TokenWrapper: ERC20TokenWrapper, + erc721TokenWrapper: ERC721TokenWrapper, contractAddressIfExists?: string, zrxContractAddressIfExists?: string, blockPollingIntervalMs?: number, ) { super(web3Wrapper, networkId, blockPollingIntervalMs); + this._erc20TokenWrapper = erc20TokenWrapper; + this._erc721TokenWrapper = erc721TokenWrapper; this._contractAddressIfExists = contractAddressIfExists; this._zrxContractAddressIfExists = zrxContractAddressIfExists; } @@ -1085,6 +1102,64 @@ export class ExchangeWrapper extends ContractWrapper { return logs; } /** + * Validate if the supplied order is fillable, and throw if it isn't + * @param signedOrder SignedOrder of interest + * @param opts ValidateOrderFillableOpts options (e.g expectedFillTakerTokenAmount. + * If it isn't supplied, we check if the order is fillable for a non-zero amount) + */ + public async validateOrderFillableOrThrowAsync( + signedOrder: SignedOrder, + opts: ValidateOrderFillableOpts = {}, + ): Promise<void> { + const balanceAllowanceFetcher = new AssetBalanceAndProxyAllowanceFetcher( + this._erc20TokenWrapper, + this._erc721TokenWrapper, + BlockParamLiteral.Latest, + ); + const balanceAllowanceStore = new BalanceAndProxyAllowanceLazyStore(balanceAllowanceFetcher); + const exchangeTradeSimulator = new ExchangeTransferSimulator(balanceAllowanceStore); + + const expectedFillTakerTokenAmountIfExists = opts.expectedFillTakerTokenAmount; + const filledCancelledFetcher = new OrderFilledCancelledFetcher(this, BlockParamLiteral.Latest); + const orderValidationUtils = new OrderValidationUtils(filledCancelledFetcher); + await orderValidationUtils.validateOrderFillableOrThrowAsync( + exchangeTradeSimulator, + signedOrder, + this.getZRXAssetData(), + expectedFillTakerTokenAmountIfExists, + ); + } + /** + * Validate a call to FillOrder and throw if it wouldn't succeed + * @param signedOrder SignedOrder of interest + * @param fillTakerAssetAmount Amount we'd like to fill the order for + * @param takerAddress The taker of the order + */ + public async validateFillOrderThrowIfInvalidAsync( + signedOrder: SignedOrder, + fillTakerAssetAmount: BigNumber, + takerAddress: string, + ): Promise<void> { + const balanceAllowanceFetcher = new AssetBalanceAndProxyAllowanceFetcher( + this._erc20TokenWrapper, + this._erc721TokenWrapper, + BlockParamLiteral.Latest, + ); + const balanceAllowanceStore = new BalanceAndProxyAllowanceLazyStore(balanceAllowanceFetcher); + const exchangeTradeSimulator = new ExchangeTransferSimulator(balanceAllowanceStore); + + const filledCancelledFetcher = new OrderFilledCancelledFetcher(this, BlockParamLiteral.Latest); + const orderValidationUtils = new OrderValidationUtils(filledCancelledFetcher); + await orderValidationUtils.validateFillOrderThrowIfInvalidAsync( + exchangeTradeSimulator, + this._web3Wrapper.getProvider(), + signedOrder, + fillTakerAssetAmount, + takerAddress, + this.getZRXAssetData(), + ); + } + /** * Retrieves the Ethereum address of the Exchange contract deployed on the network * that the user-passed web3 provider is connected to. * @returns The Ethereum address of the Exchange contract being used. diff --git a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts index 13ef0fe01..906222731 100644 --- a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts +++ b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts @@ -12,6 +12,7 @@ import { TransactionOpts } from '../types'; import { assert } from '../utils/assert'; import { calldataOptimizationUtils } from '../utils/calldata_optimization_utils'; import { constants } from '../utils/constants'; +import { utils } from '../utils/utils'; import { ContractWrapper } from './contract_wrapper'; import { ForwarderContract } from './generated/forwarder'; @@ -57,7 +58,7 @@ export class ForwarderWrapper extends ContractWrapper { takerAddress: string, ethAmount: BigNumber, signedFeeOrders: SignedOrder[] = [], - feePercentage: BigNumber = constants.ZERO_AMOUNT, + feePercentage: number = 0, feeRecipientAddress: string = constants.NULL_ADDRESS, txOpts: TransactionOpts = {}, ): Promise<string> { @@ -66,7 +67,7 @@ export class ForwarderWrapper extends ContractWrapper { await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); assert.isBigNumber('ethAmount', ethAmount); assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema); - assert.isBigNumber('feePercentage', feePercentage); + assert.isNumber('feePercentage', feePercentage); assert.isETHAddressHex('feeRecipientAddress', feeRecipientAddress); assert.doesConformToSchema('txOpts', txOpts, txOptsSchema); // other assertions @@ -76,6 +77,8 @@ export class ForwarderWrapper extends ContractWrapper { this.getZRXTokenAddress(), this.getEtherTokenAddress(), ); + // format feePercentage + const formattedFeePercentage = utils.numberPercentageToEtherTokenAmountPercentage(feePercentage); // lowercase input addresses const normalizedTakerAddress = takerAddress.toLowerCase(); const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase(); @@ -89,7 +92,7 @@ export class ForwarderWrapper extends ContractWrapper { _.map(optimizedMarketOrders, order => order.signature), optimizedFeeOrders, _.map(optimizedFeeOrders, order => order.signature), - feePercentage, + formattedFeePercentage, feeRecipientAddress, { value: ethAmount, @@ -124,7 +127,7 @@ export class ForwarderWrapper extends ContractWrapper { takerAddress: string, ethAmount: BigNumber, signedFeeOrders: SignedOrder[] = [], - feePercentage: BigNumber = constants.ZERO_AMOUNT, + feePercentage: number = 0, feeRecipientAddress: string = constants.NULL_ADDRESS, txOpts: TransactionOpts = {}, ): Promise<string> { @@ -134,7 +137,7 @@ export class ForwarderWrapper extends ContractWrapper { await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); assert.isBigNumber('ethAmount', ethAmount); assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema); - assert.isBigNumber('feePercentage', feePercentage); + assert.isNumber('feePercentage', feePercentage); assert.isETHAddressHex('feeRecipientAddress', feeRecipientAddress); assert.doesConformToSchema('txOpts', txOpts, txOptsSchema); // other assertions @@ -144,6 +147,8 @@ export class ForwarderWrapper extends ContractWrapper { this.getZRXTokenAddress(), this.getEtherTokenAddress(), ); + // format feePercentage + const formattedFeePercentage = utils.numberPercentageToEtherTokenAmountPercentage(feePercentage); // lowercase input addresses const normalizedTakerAddress = takerAddress.toLowerCase(); const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase(); @@ -158,7 +163,7 @@ export class ForwarderWrapper extends ContractWrapper { _.map(optimizedMarketOrders, order => order.signature), optimizedFeeOrders, _.map(optimizedFeeOrders, order => order.signature), - feePercentage, + formattedFeePercentage, feeRecipientAddress, { value: ethAmount, diff --git a/packages/contract-wrappers/src/contract_wrappers/order_validator_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/order_validator_wrapper.ts new file mode 100644 index 000000000..1da88f624 --- /dev/null +++ b/packages/contract-wrappers/src/contract_wrappers/order_validator_wrapper.ts @@ -0,0 +1,187 @@ +import { schemas } from '@0xproject/json-schemas'; +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { ContractAbi } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { artifacts } from '../artifacts'; +import { BalanceAndAllowance, OrderAndTraderInfo, TraderInfo } from '../types'; +import { assert } from '../utils/assert'; + +import { ContractWrapper } from './contract_wrapper'; +import { OrderValidatorContract } from './generated/order_validator'; + +/** + * This class includes the functionality related to interacting with the OrderValidator contract. + */ +export class OrderValidatorWrapper extends ContractWrapper { + public abi: ContractAbi = artifacts.OrderValidator.compilerOutput.abi; + private _orderValidatorContractIfExists?: OrderValidatorContract; + /** + * Instantiate OrderValidatorWrapper + * @param web3Wrapper Web3Wrapper instance to use + * @param networkId Desired networkId + */ + constructor(web3Wrapper: Web3Wrapper, networkId: number) { + super(web3Wrapper, networkId); + } + /** + * Get an object conforming to OrderAndTraderInfo containing on-chain information of the provided order and address + * @param order An object conforming to SignedOrder + * @param takerAddress An ethereum address + * @return OrderAndTraderInfo + */ + public async getOrderAndTraderInfoAsync(order: SignedOrder, takerAddress: string): Promise<OrderAndTraderInfo> { + assert.doesConformToSchema('order', order, schemas.signedOrderSchema); + assert.isETHAddressHex('takerAddress', takerAddress); + const OrderValidatorContractInstance = await this._getOrderValidatorContractAsync(); + const orderAndTraderInfo = await OrderValidatorContractInstance.getOrderAndTraderInfo.callAsync( + order, + takerAddress, + ); + const result = { + orderInfo: orderAndTraderInfo[0], + traderInfo: orderAndTraderInfo[1], + }; + return result; + } + /** + * Get an array of objects conforming to OrderAndTraderInfo containing on-chain information of the provided orders and addresses + * @param orders An array of objects conforming to SignedOrder + * @param takerAddresses An array of ethereum addresses + * @return array of OrderAndTraderInfo + */ + public async getOrdersAndTradersInfoAsync( + orders: SignedOrder[], + takerAddresses: string[], + ): Promise<OrderAndTraderInfo[]> { + assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); + _.forEach(takerAddresses, (takerAddress, index) => + assert.isETHAddressHex(`takerAddresses[${index}]`, takerAddress), + ); + assert.assert(orders.length === takerAddresses.length, 'Expected orders.length to equal takerAddresses.length'); + const OrderValidatorContractInstance = await this._getOrderValidatorContractAsync(); + const ordersAndTradersInfo = await OrderValidatorContractInstance.getOrdersAndTradersInfo.callAsync( + orders, + takerAddresses, + ); + const orderInfos = ordersAndTradersInfo[0]; + const traderInfos = ordersAndTradersInfo[1]; + const result = _.map(orderInfos, (orderInfo, index) => { + const traderInfo = traderInfos[index]; + return { + orderInfo, + traderInfo, + }; + }); + return result; + } + /** + * Get an object conforming to TraderInfo containing on-chain balance and allowances for maker and taker of order + * @param order An object conforming to SignedOrder + * @param takerAddress An ethereum address + * @return TraderInfo + */ + public async getTraderInfoAsync(order: SignedOrder, takerAddress: string): Promise<TraderInfo> { + assert.doesConformToSchema('order', order, schemas.signedOrderSchema); + assert.isETHAddressHex('takerAddress', takerAddress); + const OrderValidatorContractInstance = await this._getOrderValidatorContractAsync(); + const result = await OrderValidatorContractInstance.getTraderInfo.callAsync(order, takerAddress); + return result; + } + /** + * Get an array of objects conforming to TraderInfo containing on-chain balance and allowances for maker and taker of order + * @param orders An array of objects conforming to SignedOrder + * @param takerAddresses An array of ethereum addresses + * @return array of TraderInfo + */ + public async getTradersInfoAsync(orders: SignedOrder[], takerAddresses: string[]): Promise<TraderInfo[]> { + assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); + _.forEach(takerAddresses, (takerAddress, index) => + assert.isETHAddressHex(`takerAddresses[${index}]`, takerAddress), + ); + assert.assert(orders.length === takerAddresses.length, 'Expected orders.length to equal takerAddresses.length'); + const OrderValidatorContractInstance = await this._getOrderValidatorContractAsync(); + const result = await OrderValidatorContractInstance.getTradersInfo.callAsync(orders, takerAddresses); + return result; + } + /** + * Get an object conforming to BalanceAndAllowance containing on-chain balance and allowance for some address and assetData + * @param address An ethereum address + * @param assetData An encoded string that can be decoded by a specified proxy contract + * @return BalanceAndAllowance + */ + public async getBalanceAndAllowanceAsync(address: string, assetData: string): Promise<BalanceAndAllowance> { + assert.isETHAddressHex('address', address); + assert.isHexString('assetData', assetData); + const OrderValidatorContractInstance = await this._getOrderValidatorContractAsync(); + const balanceAndAllowance = await OrderValidatorContractInstance.getBalanceAndAllowance.callAsync( + address, + assetData, + ); + const result = { + balance: balanceAndAllowance[0], + allowance: balanceAndAllowance[1], + }; + return result; + } + /** + * Get an array of objects conforming to BalanceAndAllowance containing on-chain balance and allowance for some address and array of assetDatas + * @param address An ethereum address + * @param assetDatas An array of encoded strings that can be decoded by a specified proxy contract + * @return BalanceAndAllowance + */ + public async getBalancesAndAllowancesAsync(address: string, assetDatas: string[]): Promise<BalanceAndAllowance[]> { + assert.isETHAddressHex('address', address); + _.forEach(assetDatas, (assetData, index) => assert.isHexString(`assetDatas[${index}]`, assetData)); + const OrderValidatorContractInstance = await this._getOrderValidatorContractAsync(); + const balancesAndAllowances = await OrderValidatorContractInstance.getBalancesAndAllowances.callAsync( + address, + assetDatas, + ); + const balances = balancesAndAllowances[0]; + const allowances = balancesAndAllowances[1]; + const result = _.map(balances, (balance, index) => { + const allowance = allowances[index]; + return { + balance, + allowance, + }; + }); + return result; + } + /** + * Get owner address of tokenId by calling `token.ownerOf(tokenId)`, but returns a null owner instead of reverting on an unowned token. + * @param tokenAddress An ethereum address + * @param tokenId An ERC721 tokenId + * @return Owner of tokenId or null address if unowned + */ + public async getERC721TokenOwnerAsync(tokenAddress: string, tokenId: BigNumber): Promise<string | undefined> { + assert.isETHAddressHex('tokenAddress', tokenAddress); + assert.isBigNumber('tokenId', tokenId); + const OrderValidatorContractInstance = await this._getOrderValidatorContractAsync(); + const result = await OrderValidatorContractInstance.getERC721TokenOwner.callAsync(tokenAddress, tokenId); + return result; + } + // HACK: We don't want this method to be visible to the other units within that package but not to the end user. + // TS doesn't give that possibility and therefore we make it private and access it over an any cast. Because of that tslint sees it as unused. + // tslint:disable-next-line:no-unused-variable + private _invalidateContractInstance(): void { + delete this._orderValidatorContractIfExists; + } + private async _getOrderValidatorContractAsync(): Promise<OrderValidatorContract> { + if (!_.isUndefined(this._orderValidatorContractIfExists)) { + return this._orderValidatorContractIfExists; + } + const [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync(artifacts.OrderValidator); + const contractInstance = new OrderValidatorContract( + abi, + address, + this._web3Wrapper.getProvider(), + this._web3Wrapper.getContractDefaults(), + ); + this._orderValidatorContractIfExists = contractInstance; + return this._orderValidatorContractIfExists; + } +} diff --git a/packages/contract-wrappers/src/fetchers/asset_balance_and_proxy_allowance_fetcher.ts b/packages/contract-wrappers/src/fetchers/asset_balance_and_proxy_allowance_fetcher.ts new file mode 100644 index 000000000..023cd5ac3 --- /dev/null +++ b/packages/contract-wrappers/src/fetchers/asset_balance_and_proxy_allowance_fetcher.ts @@ -0,0 +1,77 @@ +// tslint:disable:no-unnecessary-type-assertion +import { AbstractBalanceAndProxyAllowanceFetcher, assetDataUtils } from '@0xproject/order-utils'; +import { AssetProxyId, ERC20AssetData, ERC721AssetData } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { BlockParamLiteral } from 'ethereum-types'; + +import { ERC20TokenWrapper } from '../contract_wrappers/erc20_token_wrapper'; +import { ERC721TokenWrapper } from '../contract_wrappers/erc721_token_wrapper'; + +export class AssetBalanceAndProxyAllowanceFetcher implements AbstractBalanceAndProxyAllowanceFetcher { + private readonly _erc20Token: ERC20TokenWrapper; + private readonly _erc721Token: ERC721TokenWrapper; + private readonly _stateLayer: BlockParamLiteral; + constructor(erc20Token: ERC20TokenWrapper, erc721Token: ERC721TokenWrapper, stateLayer: BlockParamLiteral) { + this._erc20Token = erc20Token; + this._erc721Token = erc721Token; + this._stateLayer = stateLayer; + } + public async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber> { + const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData); + if (decodedAssetData.assetProxyId === AssetProxyId.ERC20) { + const decodedERC20AssetData = decodedAssetData as ERC20AssetData; + const balance = await this._erc20Token.getBalanceAsync(decodedERC20AssetData.tokenAddress, userAddress, { + defaultBlock: this._stateLayer, + }); + return balance; + } else { + const decodedERC721AssetData = decodedAssetData as ERC721AssetData; + const tokenOwner = await this._erc721Token.getOwnerOfAsync( + decodedERC721AssetData.tokenAddress, + decodedERC721AssetData.tokenId, + { + defaultBlock: this._stateLayer, + }, + ); + const balance = tokenOwner === userAddress ? new BigNumber(1) : new BigNumber(0); + return balance; + } + } + public async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber> { + const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData); + if (decodedAssetData.assetProxyId === AssetProxyId.ERC20) { + const decodedERC20AssetData = decodedAssetData as ERC20AssetData; + const proxyAllowance = await this._erc20Token.getProxyAllowanceAsync( + decodedERC20AssetData.tokenAddress, + userAddress, + { + defaultBlock: this._stateLayer, + }, + ); + return proxyAllowance; + } else { + const decodedERC721AssetData = decodedAssetData as ERC721AssetData; + + const isApprovedForAll = await this._erc721Token.isProxyApprovedForAllAsync( + decodedERC721AssetData.tokenAddress, + userAddress, + { + defaultBlock: this._stateLayer, + }, + ); + if (isApprovedForAll) { + return new BigNumber(this._erc20Token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS); + } else { + const isApproved = await this._erc721Token.isProxyApprovedAsync( + decodedERC721AssetData.tokenAddress, + decodedERC721AssetData.tokenId, + { + defaultBlock: this._stateLayer, + }, + ); + const proxyAllowance = isApproved ? new BigNumber(1) : new BigNumber(0); + return proxyAllowance; + } + } + } +} diff --git a/packages/contract-wrappers/src/fetchers/order_filled_cancelled_fetcher.ts b/packages/contract-wrappers/src/fetchers/order_filled_cancelled_fetcher.ts new file mode 100644 index 000000000..ba6f5fb5e --- /dev/null +++ b/packages/contract-wrappers/src/fetchers/order_filled_cancelled_fetcher.ts @@ -0,0 +1,30 @@ +// tslint:disable:no-unnecessary-type-assertion +import { AbstractOrderFilledCancelledFetcher } from '@0xproject/order-utils'; +import { BigNumber } from '@0xproject/utils'; +import { BlockParamLiteral } from 'ethereum-types'; + +import { ERC20TokenWrapper } from '../contract_wrappers/erc20_token_wrapper'; +import { ExchangeWrapper } from '../contract_wrappers/exchange_wrapper'; + +export class OrderFilledCancelledFetcher implements AbstractOrderFilledCancelledFetcher { + private readonly _exchange: ExchangeWrapper; + private readonly _stateLayer: BlockParamLiteral; + constructor(exchange: ExchangeWrapper, stateLayer: BlockParamLiteral) { + this._exchange = exchange; + this._stateLayer = stateLayer; + } + public async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber> { + const filledTakerAmount = this._exchange.getFilledTakerAssetAmountAsync(orderHash, { + defaultBlock: this._stateLayer, + }); + return filledTakerAmount; + } + public async isOrderCancelledAsync(orderHash: string): Promise<boolean> { + const isCancelled = await this._exchange.isCancelledAsync(orderHash); + return isCancelled; + } + public getZRXAssetData(): string { + const zrxAssetData = this._exchange.getZRXAssetData(); + return zrxAssetData; + } +} diff --git a/packages/contract-wrappers/src/index.ts b/packages/contract-wrappers/src/index.ts index 5e691fc21..2fcdd2ddb 100644 --- a/packages/contract-wrappers/src/index.ts +++ b/packages/contract-wrappers/src/index.ts @@ -6,6 +6,7 @@ export { ExchangeWrapper } from './contract_wrappers/exchange_wrapper'; export { ERC20ProxyWrapper } from './contract_wrappers/erc20_proxy_wrapper'; export { ERC721ProxyWrapper } from './contract_wrappers/erc721_proxy_wrapper'; export { ForwarderWrapper } from './contract_wrappers/forwarder_wrapper'; +export { OrderValidatorWrapper } from './contract_wrappers/order_validator_wrapper'; export { TransactionEncoder } from './utils/transaction_encoder'; @@ -21,6 +22,10 @@ export { OrderInfo, EventCallback, DecodedLogEvent, + BalanceAndAllowance, + OrderAndTraderInfo, + TraderInfo, + ValidateOrderFillableOpts, } from './types'; export { Order, SignedOrder, AssetProxyId } from '@0xproject/types'; @@ -81,3 +86,8 @@ export { ExchangeEventArgs, ExchangeEvents, } from './contract_wrappers/generated/exchange'; + +export { AbstractBalanceAndProxyAllowanceFetcher, AbstractOrderFilledCancelledFetcher } from '@0xproject/order-utils'; + +export { AssetBalanceAndProxyAllowanceFetcher } from './fetchers/asset_balance_and_proxy_allowance_fetcher'; +export { OrderFilledCancelledFetcher } from './fetchers/order_filled_cancelled_fetcher'; diff --git a/packages/contract-wrappers/src/types.ts b/packages/contract-wrappers/src/types.ts index 2b3cdc591..e0b12b7c9 100644 --- a/packages/contract-wrappers/src/types.ts +++ b/packages/contract-wrappers/src/types.ts @@ -188,3 +188,24 @@ export enum OrderStatus { FULLY_FILLED, CANCELLED, } + +export interface TraderInfo { + makerBalance: BigNumber; + makerAllowance: BigNumber; + takerBalance: BigNumber; + takerAllowance: BigNumber; + makerZrxBalance: BigNumber; + makerZrxAllowance: BigNumber; + takerZrxBalance: BigNumber; + takerZrxAllowance: BigNumber; +} + +export interface OrderAndTraderInfo { + orderInfo: OrderInfo; + traderInfo: TraderInfo; +} + +export interface BalanceAndAllowance { + balance: BigNumber; + allowance: BigNumber; +} diff --git a/packages/contract-wrappers/src/utils/constants.ts b/packages/contract-wrappers/src/utils/constants.ts index 2df11538c..78441decf 100644 --- a/packages/contract-wrappers/src/utils/constants.ts +++ b/packages/contract-wrappers/src/utils/constants.ts @@ -12,4 +12,6 @@ export const constants = { UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1), DEFAULT_BLOCK_POLLING_INTERVAL: 1000, ZERO_AMOUNT: new BigNumber(0), + ONE_AMOUNT: new BigNumber(1), + ETHER_TOKEN_DECIMALS: 18, }; diff --git a/packages/contract-wrappers/src/utils/decorators.ts b/packages/contract-wrappers/src/utils/decorators.ts index 6e77450e8..d6bf6ec1e 100644 --- a/packages/contract-wrappers/src/utils/decorators.ts +++ b/packages/contract-wrappers/src/utils/decorators.ts @@ -24,7 +24,7 @@ const contractCallErrorTransformer = (error: Error) => { const schemaErrorTransformer = (error: Error) => { if (_.includes(error.message, constants.INVALID_TAKER_FORMAT)) { const errMsg = - 'Order taker must be of type string. If you want anyone to be able to fill an order - pass ZeroEx.NULL_ADDRESS'; + 'Order taker must be of type string. If you want anyone to be able to fill an order - pass NULL_ADDRESS'; return new Error(errMsg); } return error; diff --git a/packages/contract-wrappers/src/utils/utils.ts b/packages/contract-wrappers/src/utils/utils.ts index 689a7ee0a..f7949ec34 100644 --- a/packages/contract-wrappers/src/utils/utils.ts +++ b/packages/contract-wrappers/src/utils/utils.ts @@ -1,4 +1,7 @@ import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; + +import { constants } from './constants'; export const utils = { getCurrentUnixTimestampSec(): BigNumber { @@ -8,4 +11,7 @@ export const utils = { getCurrentUnixTimestampMs(): BigNumber { return new BigNumber(Date.now()); }, + numberPercentageToEtherTokenAmountPercentage(percentage: number): BigNumber { + return Web3Wrapper.toBaseUnitAmount(constants.ONE_AMOUNT, constants.ETHER_TOKEN_DECIMALS).mul(percentage); + }, }; diff --git a/packages/contract-wrappers/test/exchange_wrapper_test.ts b/packages/contract-wrappers/test/exchange_wrapper_test.ts index 6762c1d43..b3bd7c633 100644 --- a/packages/contract-wrappers/test/exchange_wrapper_test.ts +++ b/packages/contract-wrappers/test/exchange_wrapper_test.ts @@ -358,7 +358,7 @@ describe('ExchangeWrapper', () => { describe('#getVersionAsync', () => { it('should return version the hash', async () => { const version = await contractWrappers.exchange.getVersionAsync(); - const VERSION = '2.0.1-alpha'; + const VERSION = '2.0.0'; expect(version).to.be.equal(VERSION); }); }); diff --git a/packages/contract-wrappers/test/forwarder_wrapper_test.ts b/packages/contract-wrappers/test/forwarder_wrapper_test.ts index d0b21225c..a969807b2 100644 --- a/packages/contract-wrappers/test/forwarder_wrapper_test.ts +++ b/packages/contract-wrappers/test/forwarder_wrapper_test.ts @@ -25,10 +25,8 @@ describe('ForwarderWrapper', () => { blockPollingIntervalMs: 0, }; const fillableAmount = new BigNumber(5); - const takerTokenFillAmount = new BigNumber(5); let contractWrappers: ContractWrappers; let fillScenarios: FillScenarios; - let forwarderContractAddress: string; let exchangeContractAddress: string; let zrxTokenAddress: string; let userAddresses: string[]; @@ -46,7 +44,6 @@ describe('ForwarderWrapper', () => { before(async () => { await blockchainLifecycle.startAsync(); contractWrappers = new ContractWrappers(provider, contractWrappersConfig); - forwarderContractAddress = contractWrappers.forwarder.getContractAddress(); exchangeContractAddress = contractWrappers.exchange.getContractAddress(); userAddresses = await web3Wrapper.getAvailableAddressesAsync(); zrxTokenAddress = tokenUtils.getProtocolTokenAddress(); diff --git a/packages/contract-wrappers/test/order_validator_wrapper_test.ts b/packages/contract-wrappers/test/order_validator_wrapper_test.ts new file mode 100644 index 000000000..2fdb00a71 --- /dev/null +++ b/packages/contract-wrappers/test/order_validator_wrapper_test.ts @@ -0,0 +1,142 @@ +import { BlockchainLifecycle, callbackErrorReporter } from '@0xproject/dev-utils'; +import { FillScenarios } from '@0xproject/fill-scenarios'; +import { assetDataUtils, orderHashUtils } from '@0xproject/order-utils'; +import { DoneCallback, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import { BlockParamLiteral } from 'ethereum-types'; +import * as _ from 'lodash'; +import 'mocha'; + +import { ContractWrappers, ExchangeCancelEventArgs, ExchangeEvents, ExchangeFillEventArgs, OrderStatus } from '../src'; +import { DecodedLogEvent, OrderInfo, TraderInfo } from '../src/types'; + +import { chaiSetup } from './utils/chai_setup'; +import { constants } from './utils/constants'; +import { tokenUtils } from './utils/token_utils'; +import { provider, web3Wrapper } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('OrderValidator', () => { + const contractWrappersConfig = { + networkId: constants.TESTRPC_NETWORK_ID, + blockPollingIntervalMs: 0, + }; + const fillableAmount = new BigNumber(5); + const partialFillAmount = new BigNumber(2); + let contractWrappers: ContractWrappers; + let fillScenarios: FillScenarios; + let exchangeContractAddress: string; + let zrxTokenAddress: string; + let zrxTokenAssetData: string; + let userAddresses: string[]; + let coinbase: string; + let makerAddress: string; + let takerAddress: string; + let feeRecipient: string; + let anotherMakerAddress: string; + let makerTokenAddress: string; + let takerTokenAddress: string; + let makerAssetData: string; + let takerAssetData: string; + let signedOrder: SignedOrder; + let anotherSignedOrder: SignedOrder; + before(async () => { + await blockchainLifecycle.startAsync(); + contractWrappers = new ContractWrappers(provider, contractWrappersConfig); + exchangeContractAddress = contractWrappers.exchange.getContractAddress(); + userAddresses = await web3Wrapper.getAvailableAddressesAsync(); + zrxTokenAddress = tokenUtils.getProtocolTokenAddress(); + zrxTokenAssetData = assetDataUtils.encodeERC20AssetData(zrxTokenAddress); + fillScenarios = new FillScenarios( + provider, + userAddresses, + zrxTokenAddress, + exchangeContractAddress, + contractWrappers.erc20Proxy.getContractAddress(), + contractWrappers.erc721Proxy.getContractAddress(), + ); + [coinbase, makerAddress, takerAddress, feeRecipient, anotherMakerAddress] = userAddresses; + [makerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses(); + takerTokenAddress = tokenUtils.getWethTokenAddress(); + [makerAssetData, takerAssetData] = [ + assetDataUtils.encodeERC20AssetData(makerTokenAddress), + assetDataUtils.encodeERC20AssetData(takerTokenAddress), + ]; + + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerAssetData, + takerAssetData, + makerAddress, + constants.NULL_ADDRESS, + fillableAmount, + ); + anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync( + zrxTokenAssetData, + takerAssetData, + makerAddress, + constants.NULL_ADDRESS, + fillableAmount, + ); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('#getOrdersAndTradersInfoAsync', () => { + let signedOrders: SignedOrder[]; + let takerAddresses: string[]; + let ordersInfo: OrderInfo[]; + let tradersInfo: TraderInfo[]; + beforeEach(async () => { + signedOrders = [signedOrder, anotherSignedOrder]; + takerAddresses = [takerAddress, takerAddress]; + const ordersAndTradersInfo = await contractWrappers.orderValidator.getOrdersAndTradersInfoAsync( + signedOrders, + takerAddresses, + ); + ordersInfo = _.map(ordersAndTradersInfo, orderAndTraderInfo => orderAndTraderInfo.orderInfo); + tradersInfo = _.map(ordersAndTradersInfo, orderAndTraderInfo => orderAndTraderInfo.traderInfo); + }); + it('should return the same number of order infos and trader infos as input orders', async () => { + expect(ordersInfo.length).to.be.equal(signedOrders.length); + expect(tradersInfo.length).to.be.equal(takerAddresses.length); + }); + it('should return correct on-chain order info for input orders', async () => { + const firstOrderInfo = ordersInfo[0]; + const secondOrderInfo = ordersInfo[1]; + expect(firstOrderInfo.orderStatus).to.be.equal(OrderStatus.FILLABLE); + expect(firstOrderInfo.orderTakerAssetFilledAmount).to.bignumber.equal(constants.ZERO_AMOUNT); + expect(secondOrderInfo.orderStatus).to.be.equal(OrderStatus.FILLABLE); + expect(secondOrderInfo.orderTakerAssetFilledAmount).to.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should return correct on-chain trader info for input takers', async () => { + const firstTraderInfo = tradersInfo[0]; + const secondTraderInfo = tradersInfo[1]; + expect(firstTraderInfo.makerBalance).to.bignumber.equal(new BigNumber(5)); + expect(firstTraderInfo.makerAllowance).to.bignumber.equal(new BigNumber(5)); + expect(firstTraderInfo.takerBalance).to.bignumber.equal(new BigNumber(0)); + expect(firstTraderInfo.takerAllowance).to.bignumber.equal(new BigNumber(0)); + expect(firstTraderInfo.makerZrxBalance).to.bignumber.equal(new BigNumber(5)); + expect(firstTraderInfo.makerZrxAllowance).to.bignumber.equal(new BigNumber(5)); + expect(firstTraderInfo.takerZrxBalance).to.bignumber.equal(new BigNumber(0)); + expect(firstTraderInfo.takerZrxAllowance).to.bignumber.equal(new BigNumber(0)); + expect(secondTraderInfo.makerBalance).to.bignumber.equal(new BigNumber(5)); + expect(secondTraderInfo.makerAllowance).to.bignumber.equal(new BigNumber(5)); + expect(secondTraderInfo.takerBalance).to.bignumber.equal(new BigNumber(0)); + expect(secondTraderInfo.takerAllowance).to.bignumber.equal(new BigNumber(0)); + expect(secondTraderInfo.makerZrxBalance).to.bignumber.equal(new BigNumber(5)); + expect(secondTraderInfo.makerZrxAllowance).to.bignumber.equal(new BigNumber(5)); + expect(secondTraderInfo.takerZrxBalance).to.bignumber.equal(new BigNumber(0)); + expect(secondTraderInfo.takerZrxAllowance).to.bignumber.equal(new BigNumber(0)); + }); + }); +}); diff --git a/packages/contract-wrappers/test/revert_validation_test.ts b/packages/contract-wrappers/test/revert_validation_test.ts new file mode 100644 index 000000000..da011c1d7 --- /dev/null +++ b/packages/contract-wrappers/test/revert_validation_test.ts @@ -0,0 +1,122 @@ +import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; +import { FillScenarios } from '@0xproject/fill-scenarios'; +import { runV2MigrationsAsync } from '@0xproject/migrations'; +import { assetDataUtils } from '@0xproject/order-utils'; +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; +import 'mocha'; + +import { ContractWrappers } from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { constants } from './utils/constants'; +import { tokenUtils } from './utils/token_utils'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('Revert Validation ExchangeWrapper', () => { + let contractWrappers: ContractWrappers; + let userAddresses: string[]; + let zrxTokenAddress: string; + let fillScenarios: FillScenarios; + let exchangeContractAddress: string; + let makerTokenAddress: string; + let takerTokenAddress: string; + let coinbase: string; + let makerAddress: string; + let anotherMakerAddress: string; + let takerAddress: string; + let makerAssetData: string; + let takerAssetData: string; + let feeRecipient: string; + let txHash: string; + let blockchainLifecycle: BlockchainLifecycle; + let web3Wrapper: Web3Wrapper; + const fillableAmount = new BigNumber(5); + const takerTokenFillAmount = new BigNumber(5); + let signedOrder: SignedOrder; + const config = { + networkId: constants.TESTRPC_NETWORK_ID, + blockPollingIntervalMs: 0, + }; + before(async () => { + // vmErrorsOnRPCResponse is useful for quick feedback and testing during development + // but is not the default behaviour in production. Here we ensure our failure cases + // are handled in an environment which behaves similar to production + const provider = web3Factory.getRpcProvider({ + shouldUseInProcessGanache: true, + shouldThrowErrorsOnGanacheRPCResponse: false, + }); + web3Wrapper = new Web3Wrapper(provider); + blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + const txDefaults = { + gas: devConstants.GAS_LIMIT, + from: devConstants.TESTRPC_FIRST_ADDRESS, + }; + const artifactsDir = `src/artifacts`; + // Re-deploy the artifacts in this provider, rather than in the default provider exposed in + // the beforeAll hook. This is due to the fact that the default provider enabled vmErrorsOnRPCResponse + // and we are explicity testing with vmErrorsOnRPCResponse disabled. + await runV2MigrationsAsync(provider, artifactsDir, txDefaults); + await blockchainLifecycle.startAsync(); + contractWrappers = new ContractWrappers(provider, config); + exchangeContractAddress = contractWrappers.exchange.getContractAddress(); + userAddresses = await web3Wrapper.getAvailableAddressesAsync(); + zrxTokenAddress = tokenUtils.getProtocolTokenAddress(); + fillScenarios = new FillScenarios( + provider, + userAddresses, + zrxTokenAddress, + exchangeContractAddress, + contractWrappers.erc20Proxy.getContractAddress(), + contractWrappers.erc721Proxy.getContractAddress(), + ); + [coinbase, makerAddress, takerAddress, feeRecipient, anotherMakerAddress] = userAddresses; + [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses(); + [makerAssetData, takerAssetData] = [ + assetDataUtils.encodeERC20AssetData(makerTokenAddress), + assetDataUtils.encodeERC20AssetData(takerTokenAddress), + ]; + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerAssetData, + takerAssetData, + makerAddress, + takerAddress, + fillableAmount, + ); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('#fillOrderAsync', () => { + it('should throw the revert reason when shouldValidate is true and a fill would revert', async () => { + // Create a scenario where the fill will revert + const makerTokenBalance = await contractWrappers.erc20Token.getBalanceAsync( + makerTokenAddress, + makerAddress, + ); + // Transfer all of the tokens from maker to create a failure scenario + txHash = await contractWrappers.erc20Token.transferAsync( + makerTokenAddress, + makerAddress, + takerAddress, + makerTokenBalance, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); + expect( + contractWrappers.exchange.fillOrderAsync(signedOrder, takerTokenFillAmount, takerAddress, { + shouldValidate: true, + }), + ).to.be.rejectedWith('TRANSFER_FAILED'); + }); + }); +}); diff --git a/packages/contract-wrappers/test/subscription_test.ts b/packages/contract-wrappers/test/subscription_test.ts index 81b9012bd..68ef7225e 100644 --- a/packages/contract-wrappers/test/subscription_test.ts +++ b/packages/contract-wrappers/test/subscription_test.ts @@ -61,7 +61,7 @@ describe('SubscriptionTest', () => { callback, ); stubs = [ - Sinon.stub((contractWrappers as any)._web3Wrapper, 'getBlockAsync').throws( + Sinon.stub((contractWrappers as any)._web3Wrapper, 'getBlockIfExistsAsync').throws( new Error('JSON RPC error'), ), ]; diff --git a/packages/contract-wrappers/test/transaction_encoder_test.ts b/packages/contract-wrappers/test/transaction_encoder_test.ts index e76c5b12d..a397e43a8 100644 --- a/packages/contract-wrappers/test/transaction_encoder_test.ts +++ b/packages/contract-wrappers/test/transaction_encoder_test.ts @@ -1,6 +1,6 @@ import { BlockchainLifecycle } from '@0xproject/dev-utils'; import { FillScenarios } from '@0xproject/fill-scenarios'; -import { assetDataUtils, signatureUtils, generatePseudoRandomSalt, orderHashUtils } from '@0xproject/order-utils'; +import { assetDataUtils, generatePseudoRandomSalt, orderHashUtils, signatureUtils } from '@0xproject/order-utils'; import { SignedOrder, SignerType } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import 'mocha'; diff --git a/packages/contract-wrappers/test/utils/constants.ts b/packages/contract-wrappers/test/utils/constants.ts index 60c3370a2..f38728b77 100644 --- a/packages/contract-wrappers/test/utils/constants.ts +++ b/packages/contract-wrappers/test/utils/constants.ts @@ -15,4 +15,5 @@ export const constants = { DUMMY_TOKEN_TOTAL_SUPPLY: new BigNumber(10 ** 27), // tslint:disable-line:custom-no-magic-numbers NUM_DUMMY_ERC20_TO_DEPLOY: 3, NUM_DUMMY_ERC721_TO_DEPLOY: 1, + ZERO_AMOUNT: new BigNumber(0), }; diff --git a/packages/contract-wrappers/tsconfig.json b/packages/contract-wrappers/tsconfig.json index e35816553..2ee711adc 100644 --- a/packages/contract-wrappers/tsconfig.json +++ b/packages/contract-wrappers/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "../../tsconfig", "compilerOptions": { - "outDir": "lib" + "outDir": "lib", + "rootDir": "." }, "include": ["./src/**/*", "./test/**/*"] } diff --git a/packages/contract-wrappers/typedoc-tsconfig.json b/packages/contract-wrappers/typedoc-tsconfig.json new file mode 100644 index 000000000..c9b0af1ae --- /dev/null +++ b/packages/contract-wrappers/typedoc-tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../typedoc-tsconfig", + "compilerOptions": { + "outDir": "lib" + }, + "include": ["./src/**/*", "./test/**/*"] +} |