diff options
Diffstat (limited to 'packages/contract-wrappers')
19 files changed, 622 insertions, 44 deletions
diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index 4c12cd592..0d0bc53f9 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -1,6 +1,6 @@ [ { - "version": "1.0.1-rc.3", + "version": "1.0.1-rc.4", "changes": [ { "note": "Export missing ExchangeSignatureValidatorApprovalEventArgs type", @@ -9,6 +9,24 @@ ] }, { + "version": "1.0.1-rc.3", + "changes": [ + { + "pr": 915, + "note": "Added strict encoding/decoding checks for sendTransaction and call" + }, + { + "note": "Add ForwarderWrapper", + "pr": 934 + }, + { + "note": "Optimize orders in ForwarderWrapper", + "pr": 936 + } + ], + "timestamp": 1534210131 + }, + { "version": "1.0.1-rc.2", "changes": [ { diff --git a/packages/contract-wrappers/CHANGELOG.md b/packages/contract-wrappers/CHANGELOG.md index 8a981d371..c2ad7218e 100644 --- a/packages/contract-wrappers/CHANGELOG.md +++ b/packages/contract-wrappers/CHANGELOG.md @@ -5,6 +5,12 @@ Edit the package's CHANGELOG.json file only. CHANGELOG +## v1.0.1-rc.3 - _August 13, 2018_ + + * Added strict encoding/decoding checks for sendTransaction and call (#915) + * Add ForwarderWrapper (#934) + * Optimize orders in ForwarderWrapper (#936) + ## v1.0.1-rc.2 - _July 26, 2018_ * Fixed bug caused by importing non-existent dep @@ -17,7 +23,7 @@ CHANGELOG * Dependencies updated -## v1.0.0-rc.1 - _July 20, 2018_ +## v1.0.0-rc.1 - _July 19, 2018_ * Update blockstream to v5.0 and propogate up caught errors to active subscriptions (#815) * Update to v2 of 0x rpotocol (#822) diff --git a/packages/contract-wrappers/package.json b/packages/contract-wrappers/package.json index cbb12534e..8316d4733 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.2", + "version": "1.0.1-rc.3", "description": "Smart TS wrappers for 0x smart contracts", "keywords": [ "0xproject", @@ -14,7 +14,7 @@ "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).json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/contract_wrappers/generated --backend ethers", + "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", "lint": "tslint --project . --exclude **/src/contract_wrappers/**/* --exclude **/lib/**/*", "test:circleci": "run-s test:coverage", "test": "yarn run_mocha", @@ -29,7 +29,7 @@ "docs:json": "typedoc --excludePrivate --excludeExternals --target ES5 --json $JSON_FILE_PATH $PROJECT_FILES" }, "config": { - "contracts_v2_beta": "Exchange ERC20Proxy ERC20Token ERC721Proxy ERC721Token WETH9 ZRXToken", + "contracts_v2_beta": "Exchange ERC20Proxy ERC20Token ERC721Proxy ERC721Token WETH9 ZRXToken Forwarder", "contracts_v2": "DummyERC20Token DummyERC721Token", "postpublish": { "assets": [], @@ -45,12 +45,12 @@ "node": ">=6.0.0" }, "devDependencies": { - "@0xproject/abi-gen": "^1.0.4", - "@0xproject/dev-utils": "^1.0.3", - "@0xproject/migrations": "^1.0.3", - "@0xproject/sol-compiler": "^1.0.4", - "@0xproject/subproviders": "^1.0.4", - "@0xproject/tslint-config": "^1.0.4", + "@0xproject/abi-gen": "^1.0.5", + "@0xproject/dev-utils": "^1.0.4", + "@0xproject/migrations": "^1.0.4", + "@0xproject/sol-compiler": "^1.0.5", + "@0xproject/subproviders": "^1.0.5", + "@0xproject/tslint-config": "^1.0.5", "@types/lodash": "4.14.104", "@types/mocha": "^2.2.42", "@types/node": "^8.0.53", @@ -63,7 +63,7 @@ "copyfiles": "^1.2.0", "dirty-chai": "^2.0.1", "make-promises-safe": "^1.1.0", - "mocha": "^4.0.1", + "mocha": "^4.1.0", "npm-run-all": "^4.1.2", "nyc": "^11.0.1", "opn-cli": "^3.1.0", @@ -72,25 +72,25 @@ "source-map-support": "^0.5.0", "tslint": "5.11.0", "typedoc": "0xProject/typedoc", - "typescript": "2.7.1", + "typescript": "2.9.2", "web3-provider-engine": "14.0.6" }, "dependencies": { - "@0xproject/assert": "^1.0.4", - "@0xproject/base-contract": "^1.0.4", - "@0xproject/fill-scenarios": "^1.0.1-rc.2", - "@0xproject/json-schemas": "^1.0.1-rc.3", - "@0xproject/order-utils": "^1.0.1-rc.2", - "@0xproject/types": "^1.0.1-rc.3", - "@0xproject/typescript-typings": "^1.0.3", - "@0xproject/utils": "^1.0.4", - "@0xproject/web3-wrapper": "^1.1.2", - "ethereum-types": "^1.0.3", + "@0xproject/assert": "^1.0.5", + "@0xproject/base-contract": "^2.0.0-rc.1", + "@0xproject/fill-scenarios": "^1.0.1-rc.3", + "@0xproject/json-schemas": "^1.0.1-rc.4", + "@0xproject/order-utils": "^1.0.1-rc.3", + "@0xproject/types": "^1.0.1-rc.4", + "@0xproject/typescript-typings": "^1.0.4", + "@0xproject/utils": "^1.0.5", + "@0xproject/web3-wrapper": "^1.2.0", + "ethereum-types": "^1.0.4", "ethereumjs-blockstream": "5.0.0", "ethereumjs-util": "^5.1.1", "ethers": "3.0.22", "js-sha3": "^0.7.0", - "lodash": "^4.17.4", + "lodash": "^4.17.5", "uuid": "^3.1.0" }, "publishConfig": { diff --git a/packages/contract-wrappers/src/artifacts.ts b/packages/contract-wrappers/src/artifacts.ts index 742d0e1b2..2481b311a 100644 --- a/packages/contract-wrappers/src/artifacts.ts +++ b/packages/contract-wrappers/src/artifacts.ts @@ -7,6 +7,7 @@ import * as ERC20Token from './artifacts/ERC20Token.json'; 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 EtherToken from './artifacts/WETH9.json'; import * as ZRXToken from './artifacts/ZRXToken.json'; @@ -20,4 +21,5 @@ export const artifacts = { EtherToken: (EtherToken as any) as ContractArtifact, ERC20Proxy: (ERC20Proxy as any) as ContractArtifact, ERC721Proxy: (ERC721Proxy as any) as ContractArtifact, + Forwarder: (Forwarder as any) as ContractArtifact, }; diff --git a/packages/contract-wrappers/src/contract_wrappers.ts b/packages/contract-wrappers/src/contract_wrappers.ts index 8010242c5..4277a0746 100644 --- a/packages/contract-wrappers/src/contract_wrappers.ts +++ b/packages/contract-wrappers/src/contract_wrappers.ts @@ -11,6 +11,7 @@ import { ERC721ProxyWrapper } from './contract_wrappers/erc721_proxy_wrapper'; 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 { 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'; @@ -47,6 +48,11 @@ export class ContractWrappers { * erc721Proxy smart contract. */ public erc721Proxy: ERC721ProxyWrapper; + /** + * An instance of the ForwarderWrapper class containing methods for interacting with any Forwarder smart contract. + */ + public forwarder: ForwarderWrapper; + private _web3Wrapper: Web3Wrapper; /** * Instantiates a new ContractWrappers instance. @@ -104,6 +110,12 @@ export class ContractWrappers { config.zrxContractAddress, blockPollingIntervalMs, ); + this.forwarder = new ForwarderWrapper( + this._web3Wrapper, + config.networkId, + config.forwarderContractAddress, + config.zrxContractAddress, + ); } /** * Sets a new web3 provider for 0x.js. Updating the provider will stop all diff --git a/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts index 5beb35a27..0febd154f 100644 --- a/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts +++ b/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts @@ -882,16 +882,36 @@ export class ExchangeWrapper extends ContractWrapper { */ @decorators.asyncZeroExErrorHandler public async getOrderInfoAsync(order: Order | SignedOrder, methodOpts: MethodOpts = {}): Promise<OrderInfo> { + assert.doesConformToSchema('order', order, schemas.orderSchema); if (!_.isUndefined(methodOpts)) { assert.doesConformToSchema('methodOpts', methodOpts, methodOptsSchema); } const exchangeInstance = await this._getExchangeContractAsync(); - const txData = {}; const orderInfo = await exchangeInstance.getOrderInfo.callAsync(order, txData, methodOpts.defaultBlock); return orderInfo; } /** + * Get order info for multiple orders + * @param orders Orders + * @param methodOpts Optional arguments this method accepts. + * @returns Array of Order infos + */ + @decorators.asyncZeroExErrorHandler + public async getOrdersInfoAsync( + orders: Array<Order | SignedOrder>, + methodOpts: MethodOpts = {}, + ): Promise<OrderInfo[]> { + assert.doesConformToSchema('orders', orders, schemas.ordersSchema); + if (!_.isUndefined(methodOpts)) { + assert.doesConformToSchema('methodOpts', methodOpts, methodOptsSchema); + } + const exchangeInstance = await this._getExchangeContractAsync(); + const txData = {}; + const ordersInfo = await exchangeInstance.getOrdersInfo.callAsync(orders, txData, methodOpts.defaultBlock); + return ordersInfo; + } + /** * Cancel a given order. * @param order An object that conforms to the Order or SignedOrder interface. The order you would like to cancel. * @param orderTransactionOpts Optional arguments this method accepts. diff --git a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts new file mode 100644 index 000000000..13ef0fe01 --- /dev/null +++ b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts @@ -0,0 +1,220 @@ +import { schemas } from '@0xproject/json-schemas'; +import { AssetProxyId, 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 { orderTxOptsSchema } from '../schemas/order_tx_opts_schema'; +import { txOptsSchema } from '../schemas/tx_opts_schema'; +import { TransactionOpts } from '../types'; +import { assert } from '../utils/assert'; +import { calldataOptimizationUtils } from '../utils/calldata_optimization_utils'; +import { constants } from '../utils/constants'; + +import { ContractWrapper } from './contract_wrapper'; +import { ForwarderContract } from './generated/forwarder'; + +/** + * This class includes the functionality related to interacting with the Forwarder contract. + */ +export class ForwarderWrapper extends ContractWrapper { + public abi: ContractAbi = artifacts.Forwarder.compilerOutput.abi; + private _forwarderContractIfExists?: ForwarderContract; + private _contractAddressIfExists?: string; + private _zrxContractAddressIfExists?: string; + constructor( + web3Wrapper: Web3Wrapper, + networkId: number, + contractAddressIfExists?: string, + zrxContractAddressIfExists?: string, + ) { + super(web3Wrapper, networkId); + this._contractAddressIfExists = contractAddressIfExists; + this._zrxContractAddressIfExists = zrxContractAddressIfExists; + } + /** + * Purchases as much of orders' makerAssets as possible by selling up to 95% of transaction's ETH value. + * Any ZRX required to pay fees for primary orders will automatically be purchased by this contract. + * 5% of ETH value is reserved for paying fees to order feeRecipients (in ZRX) and forwarding contract feeRecipient (in ETH). + * Any ETH not spent will be refunded to sender. + * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders must specify the same makerAsset. + * All orders must specify WETH as the takerAsset + * @param takerAddress The user Ethereum address who would like to fill this order. Must be available via the supplied + * Provider provided at instantiation. + * @param ethAmount The amount of eth to send with the transaction (in wei). + * @param signedFeeOrders An array of objects that conform to the SignedOrder interface. All orders must specify ZRX as makerAsset and WETH as takerAsset. + * Used to purchase ZRX for primary order fees. + * @param feePercentage The percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. + * Defaults to 0. + * @param feeRecipientAddress The address that will receive ETH when signedFeeOrders are filled. + * @param txOpts Transaction parameters. + * @return Transaction hash. + */ + public async marketSellOrdersWithEthAsync( + signedOrders: SignedOrder[], + takerAddress: string, + ethAmount: BigNumber, + signedFeeOrders: SignedOrder[] = [], + feePercentage: BigNumber = constants.ZERO_AMOUNT, + feeRecipientAddress: string = constants.NULL_ADDRESS, + txOpts: TransactionOpts = {}, + ): Promise<string> { + // type assertions + assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + assert.isBigNumber('ethAmount', ethAmount); + assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema); + assert.isBigNumber('feePercentage', feePercentage); + assert.isETHAddressHex('feeRecipientAddress', feeRecipientAddress); + assert.doesConformToSchema('txOpts', txOpts, txOptsSchema); + // other assertions + assert.ordersCanBeUsedForForwarderContract(signedOrders, this.getEtherTokenAddress()); + assert.feeOrdersCanBeUsedForForwarderContract( + signedFeeOrders, + this.getZRXTokenAddress(), + this.getEtherTokenAddress(), + ); + // lowercase input addresses + const normalizedTakerAddress = takerAddress.toLowerCase(); + const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase(); + // optimize orders + const optimizedMarketOrders = calldataOptimizationUtils.optimizeForwarderOrders(signedOrders); + const optimizedFeeOrders = calldataOptimizationUtils.optimizeForwarderFeeOrders(signedFeeOrders); + // send transaction + const forwarderContractInstance = await this._getForwarderContractAsync(); + const txHash = await forwarderContractInstance.marketSellOrdersWithEth.sendTransactionAsync( + optimizedMarketOrders, + _.map(optimizedMarketOrders, order => order.signature), + optimizedFeeOrders, + _.map(optimizedFeeOrders, order => order.signature), + feePercentage, + feeRecipientAddress, + { + value: ethAmount, + from: normalizedTakerAddress, + gas: txOpts.gasLimit, + gasPrice: txOpts.gasPrice, + }, + ); + return txHash; + } + /** + * Attempt to purchase makerAssetFillAmount of makerAsset by selling ethAmount provided with transaction. + * Any ZRX required to pay fees for primary orders will automatically be purchased by the contract. + * Any ETH not spent will be refunded to sender. + * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders must specify the same makerAsset. + * All orders must specify WETH as the takerAsset + * @param makerAssetFillAmount The amount of the order (in taker asset baseUnits) that you wish to fill. + * @param takerAddress The user Ethereum address who would like to fill this order. Must be available via the supplied + * Provider provided at instantiation. + * @param ethAmount The amount of eth to send with the transaction (in wei). + * @param signedFeeOrders An array of objects that conform to the SignedOrder interface. All orders must specify ZRX as makerAsset and WETH as takerAsset. + * Used to purchase ZRX for primary order fees. + * @param feePercentage The percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. + * Defaults to 0. + * @param feeRecipientAddress The address that will receive ETH when signedFeeOrders are filled. + * @param txOpts Transaction parameters. + * @return Transaction hash. + */ + public async marketBuyOrdersWithEthAsync( + signedOrders: SignedOrder[], + makerAssetFillAmount: BigNumber, + takerAddress: string, + ethAmount: BigNumber, + signedFeeOrders: SignedOrder[] = [], + feePercentage: BigNumber = constants.ZERO_AMOUNT, + feeRecipientAddress: string = constants.NULL_ADDRESS, + txOpts: TransactionOpts = {}, + ): Promise<string> { + // type assertions + assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); + assert.isBigNumber('makerAssetFillAmount', makerAssetFillAmount); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + assert.isBigNumber('ethAmount', ethAmount); + assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema); + assert.isBigNumber('feePercentage', feePercentage); + assert.isETHAddressHex('feeRecipientAddress', feeRecipientAddress); + assert.doesConformToSchema('txOpts', txOpts, txOptsSchema); + // other assertions + assert.ordersCanBeUsedForForwarderContract(signedOrders, this.getEtherTokenAddress()); + assert.feeOrdersCanBeUsedForForwarderContract( + signedFeeOrders, + this.getZRXTokenAddress(), + this.getEtherTokenAddress(), + ); + // lowercase input addresses + const normalizedTakerAddress = takerAddress.toLowerCase(); + const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase(); + // optimize orders + const optimizedMarketOrders = calldataOptimizationUtils.optimizeForwarderOrders(signedOrders); + const optimizedFeeOrders = calldataOptimizationUtils.optimizeForwarderFeeOrders(signedFeeOrders); + // send transaction + const forwarderContractInstance = await this._getForwarderContractAsync(); + const txHash = await forwarderContractInstance.marketBuyOrdersWithEth.sendTransactionAsync( + optimizedMarketOrders, + makerAssetFillAmount, + _.map(optimizedMarketOrders, order => order.signature), + optimizedFeeOrders, + _.map(optimizedFeeOrders, order => order.signature), + feePercentage, + feeRecipientAddress, + { + value: ethAmount, + from: normalizedTakerAddress, + gas: txOpts.gasLimit, + gasPrice: txOpts.gasPrice, + }, + ); + return txHash; + } + /** + * Retrieves the Ethereum address of the Forwarder contract deployed on the network + * that the user-passed web3 provider is connected to. + * @returns The Ethereum address of the Forwarder contract being used. + */ + public getContractAddress(): string { + const contractAddress = this._getContractAddress(artifacts.Forwarder, this._contractAddressIfExists); + return contractAddress; + } + /** + * Returns the ZRX token address used by the forwarder contract. + * @return Address of ZRX token + */ + public getZRXTokenAddress(): string { + const contractAddress = this._getContractAddress(artifacts.ZRXToken, this._zrxContractAddressIfExists); + return contractAddress; + } + /** + * Returns the Ether token address used by the forwarder contract. + * @return Address of Ether token + */ + public getEtherTokenAddress(): string { + const contractAddress = this._getContractAddress(artifacts.EtherToken); + return contractAddress; + } + // 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._forwarderContractIfExists; + } + private async _getForwarderContractAsync(): Promise<ForwarderContract> { + if (!_.isUndefined(this._forwarderContractIfExists)) { + return this._forwarderContractIfExists; + } + const [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync( + artifacts.Forwarder, + this._contractAddressIfExists, + ); + const contractInstance = new ForwarderContract( + abi, + address, + this._web3Wrapper.getProvider(), + this._web3Wrapper.getContractDefaults(), + ); + this._forwarderContractIfExists = contractInstance; + return this._forwarderContractIfExists; + } +} diff --git a/packages/contract-wrappers/src/index.ts b/packages/contract-wrappers/src/index.ts index da9453640..41d60f05a 100644 --- a/packages/contract-wrappers/src/index.ts +++ b/packages/contract-wrappers/src/index.ts @@ -5,6 +5,7 @@ export { EtherTokenWrapper } from './contract_wrappers/ether_token_wrapper'; 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 { ContractWrappersError, diff --git a/packages/contract-wrappers/src/schemas/contract_wrappers_private_network_config_schema.ts b/packages/contract-wrappers/src/schemas/contract_wrappers_private_network_config_schema.ts index 7e2eca61c..904690ae7 100644 --- a/packages/contract-wrappers/src/schemas/contract_wrappers_private_network_config_schema.ts +++ b/packages/contract-wrappers/src/schemas/contract_wrappers_private_network_config_schema.ts @@ -5,11 +5,11 @@ export const contractWrappersPrivateNetworkConfigSchema = { type: 'number', minimum: 1, }, - gasPrice: { $ref: '/Number' }, - zrxContractAddress: { $ref: '/Address' }, - exchangeContractAddress: { $ref: '/Address' }, - erc20ProxyContractAddress: { $ref: '/Address' }, - erc721ProxyContractAddress: { $ref: '/Address' }, + gasPrice: { $ref: '/numberSchema' }, + zrxContractAddress: { $ref: '/addressSchema' }, + exchangeContractAddress: { $ref: '/addressSchema' }, + erc20ProxyContractAddress: { $ref: '/addressSchema' }, + erc721ProxyContractAddress: { $ref: '/addressSchema' }, blockPollingIntervalMs: { type: 'number' }, orderWatcherConfig: { type: 'object', diff --git a/packages/contract-wrappers/src/schemas/contract_wrappers_public_network_config_schema.ts b/packages/contract-wrappers/src/schemas/contract_wrappers_public_network_config_schema.ts index b80e04310..5cd008ae0 100644 --- a/packages/contract-wrappers/src/schemas/contract_wrappers_public_network_config_schema.ts +++ b/packages/contract-wrappers/src/schemas/contract_wrappers_public_network_config_schema.ts @@ -19,11 +19,11 @@ export const contractWrappersPublicNetworkConfigSchema = { networkNameToId.ganache, ], }, - gasPrice: { $ref: '/Number' }, - zrxContractAddress: { $ref: '/Address' }, - exchangeContractAddress: { $ref: '/Address' }, - erc20ProxyContractAddress: { $ref: '/Address' }, - erc721ProxyContractAddress: { $ref: '/Address' }, + gasPrice: { $ref: '/numberSchema' }, + zrxContractAddress: { $ref: '/addressSchema' }, + exchangeContractAddress: { $ref: '/addressSchema' }, + erc20ProxyContractAddress: { $ref: '/addressSchema' }, + erc721ProxyContractAddress: { $ref: '/addressSchema' }, blockPollingIntervalMs: { type: 'number' }, orderWatcherConfig: { type: 'object', diff --git a/packages/contract-wrappers/src/schemas/method_opts_schema.ts b/packages/contract-wrappers/src/schemas/method_opts_schema.ts index ef434070a..83003f818 100644 --- a/packages/contract-wrappers/src/schemas/method_opts_schema.ts +++ b/packages/contract-wrappers/src/schemas/method_opts_schema.ts @@ -1,7 +1,7 @@ export const methodOptsSchema = { id: '/MethodOpts', properties: { - defaultBlock: { $ref: '/BlockParam' }, + defaultBlock: { $ref: '/blockParamSchema' }, }, type: 'object', }; diff --git a/packages/contract-wrappers/src/schemas/tx_opts_schema.ts b/packages/contract-wrappers/src/schemas/tx_opts_schema.ts index bddc33b6c..83c819be2 100644 --- a/packages/contract-wrappers/src/schemas/tx_opts_schema.ts +++ b/packages/contract-wrappers/src/schemas/tx_opts_schema.ts @@ -1,7 +1,7 @@ export const txOptsSchema = { id: '/TxOpts', properties: { - gasPrice: { $ref: '/Number' }, + gasPrice: { $ref: '/numberSchema' }, gasLimit: { type: 'number' }, }, type: 'object', diff --git a/packages/contract-wrappers/src/types.ts b/packages/contract-wrappers/src/types.ts index f9d7a6b9f..2b3cdc591 100644 --- a/packages/contract-wrappers/src/types.ts +++ b/packages/contract-wrappers/src/types.ts @@ -109,6 +109,7 @@ export type SyncMethod = (...args: any[]) => any; * zrxContractAddress: The address of the ZRX contract to use * erc20ProxyContractAddress: The address of the erc20 token transfer proxy contract to use * erc721ProxyContractAddress: The address of the erc721 token transfer proxy contract to use + * forwarderContractAddress: The address of the forwarder contract to use * orderWatcherConfig: All the configs related to the orderWatcher * blockPollingIntervalMs: The interval to use for block polling in event watching methods (defaults to 1000) */ @@ -119,6 +120,7 @@ export interface ContractWrappersConfig { zrxContractAddress?: string; erc20ProxyContractAddress?: string; erc721ProxyContractAddress?: string; + forwarderContractAddress?: string; blockPollingIntervalMs?: number; } @@ -172,13 +174,13 @@ export enum TransferType { export type OnOrderStateChangeCallback = (err: Error | null, orderState?: OrderState) => void; export interface OrderInfo { - orderStatus: number; + orderStatus: OrderStatus; orderHash: string; orderTakerAssetFilledAmount: BigNumber; } export enum OrderStatus { - INVALID, + INVALID = 0, INVALID_MAKER_ASSET_AMOUNT, INVALID_TAKER_ASSET_AMOUNT, FILLABLE, diff --git a/packages/contract-wrappers/src/utils/assert.ts b/packages/contract-wrappers/src/utils/assert.ts index 652e5bec3..bed833b8f 100644 --- a/packages/contract-wrappers/src/utils/assert.ts +++ b/packages/contract-wrappers/src/utils/assert.ts @@ -1,11 +1,14 @@ import { assert as sharedAssert } from '@0xproject/assert'; // HACK: We need those two unused imports because they're actually used by sharedAssert which gets injected here import { Schema } from '@0xproject/json-schemas'; // tslint:disable-line:no-unused-variable -import { signatureUtils } from '@0xproject/order-utils'; -import { ECSignature } from '@0xproject/types'; // tslint:disable-line:no-unused-variable +import { signatureUtils, assetDataUtils } from '@0xproject/order-utils'; +import { Order } from '@0xproject/types'; // tslint:disable-line:no-unused-variable import { BigNumber } from '@0xproject/utils'; // tslint:disable-line:no-unused-variable import { Web3Wrapper } from '@0xproject/web3-wrapper'; import { Provider } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { constants } from './constants'; export const assert = { ...sharedAssert, @@ -16,12 +19,12 @@ export const assert = { signerAddress: string, ): Promise<void> { const isValid = await signatureUtils.isValidSignatureAsync(provider, orderHash, signature, signerAddress); - this.assert(isValid, `Expected order with hash '${orderHash}' to have a valid signature`); + sharedAssert.assert(isValid, `Expected order with hash '${orderHash}' to have a valid signature`); }, isValidSubscriptionToken(variableName: string, subscriptionToken: string): void { const uuidRegex = new RegExp('^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$'); const isValid = uuidRegex.test(subscriptionToken); - this.assert(isValid, `Expected ${variableName} to be a valid subscription token`); + sharedAssert.assert(isValid, `Expected ${variableName} to be a valid subscription token`); }, async isSenderAddressAsync( variableName: string, @@ -35,4 +38,53 @@ export const assert = { `Specified ${variableName} ${senderAddressHex} isn't available through the supplied web3 provider`, ); }, + ordersCanBeUsedForForwarderContract(orders: Order[], etherTokenAddress: string): void { + sharedAssert.assert(!_.isEmpty(orders), 'Expected at least 1 signed order. Found no orders'); + assert.ordersHaveAtMostOneUniqueValueForProperty(orders, 'makerAssetData'); + assert.allTakerAssetDatasAreErc20Token(orders, etherTokenAddress); + assert.allTakerAddressesAreNull(orders); + }, + feeOrdersCanBeUsedForForwarderContract(orders: Order[], zrxTokenAddress: string, etherTokenAddress: string): void { + if (!_.isEmpty(orders)) { + assert.allMakerAssetDatasAreErc20Token(orders, zrxTokenAddress); + assert.allTakerAssetDatasAreErc20Token(orders, etherTokenAddress); + } + }, + allTakerAddressesAreNull(orders: Order[]): void { + assert.ordersHaveAtMostOneUniqueValueForProperty(orders, 'takerAddress', constants.NULL_ADDRESS); + }, + allMakerAssetDatasAreErc20Token(orders: Order[], tokenAddress: string): void { + assert.ordersHaveAtMostOneUniqueValueForProperty( + orders, + 'makerAssetData', + assetDataUtils.encodeERC20AssetData(tokenAddress), + ); + }, + allTakerAssetDatasAreErc20Token(orders: Order[], tokenAddress: string): void { + assert.ordersHaveAtMostOneUniqueValueForProperty( + orders, + 'takerAssetData', + assetDataUtils.encodeERC20AssetData(tokenAddress), + ); + }, + /* + * Asserts that all the orders have the same value for the provided propertyName + * If the value parameter is provided, this asserts that all orders have the prope + */ + ordersHaveAtMostOneUniqueValueForProperty(orders: Order[], propertyName: string, value?: any): void { + const allValues = _.map(orders, order => _.get(order, propertyName)); + sharedAssert.hasAtMostOneUniqueValue( + allValues, + `Expected all orders to have the same ${propertyName} field. Found the following ${propertyName} values: ${JSON.stringify( + allValues, + )}`, + ); + if (!_.isUndefined(value)) { + const firstValue = _.head(allValues); + sharedAssert.assert( + firstValue === value, + `Expected all orders to have a ${propertyName} field with value: ${value}. Found: ${firstValue}`, + ); + } + }, }; diff --git a/packages/contract-wrappers/src/utils/calldata_optimization_utils.ts b/packages/contract-wrappers/src/utils/calldata_optimization_utils.ts new file mode 100644 index 000000000..3172cf531 --- /dev/null +++ b/packages/contract-wrappers/src/utils/calldata_optimization_utils.ts @@ -0,0 +1,44 @@ +import { SignedOrder } from '@0xproject/types'; +import * as _ from 'lodash'; + +import { constants } from './constants'; + +export const calldataOptimizationUtils = { + /** + * Takes an array of orders and outputs an array of equivalent orders where all takerAssetData are '0x' and + * all makerAssetData are '0x' except for that of the first order, which retains its original value + * @param orders An array of SignedOrder objects + * @returns optimized orders + */ + optimizeForwarderOrders(orders: SignedOrder[]): SignedOrder[] { + const optimizedOrders = _.map(orders, (order, index) => + transformOrder(order, { + makerAssetData: index === 0 ? order.makerAssetData : constants.NULL_BYTES, + takerAssetData: constants.NULL_BYTES, + }), + ); + return optimizedOrders; + }, + /** + * Takes an array of orders and outputs an array of equivalent orders where all takerAssetData are '0x' and + * all makerAssetData are '0x' + * @param orders An array of SignedOrder objects + * @returns optimized orders + */ + optimizeForwarderFeeOrders(orders: SignedOrder[]): SignedOrder[] { + const optimizedOrders = _.map(orders, (order, index) => + transformOrder(order, { + makerAssetData: constants.NULL_BYTES, + takerAssetData: constants.NULL_BYTES, + }), + ); + return optimizedOrders; + }, +}; + +const transformOrder = (order: SignedOrder, partialOrder: Partial<SignedOrder>) => { + return { + ...order, + ...partialOrder, + }; +}; diff --git a/packages/contract-wrappers/src/utils/constants.ts b/packages/contract-wrappers/src/utils/constants.ts index 039475b7f..2df11538c 100644 --- a/packages/contract-wrappers/src/utils/constants.ts +++ b/packages/contract-wrappers/src/utils/constants.ts @@ -2,6 +2,7 @@ import { BigNumber } from '@0xproject/utils'; export const constants = { NULL_ADDRESS: '0x0000000000000000000000000000000000000000', + NULL_BYTES: '0x', TESTRPC_NETWORK_ID: 50, INVALID_JUMP_PATTERN: 'invalid JUMP at', REVERT: 'revert', @@ -10,4 +11,5 @@ export const constants = { // tslint:disable-next-line:custom-no-magic-numbers UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1), DEFAULT_BLOCK_POLLING_INTERVAL: 1000, + ZERO_AMOUNT: new BigNumber(0), }; diff --git a/packages/contract-wrappers/test/calldata_optimization_utils_test.ts b/packages/contract-wrappers/test/calldata_optimization_utils_test.ts new file mode 100644 index 000000000..a4cea772f --- /dev/null +++ b/packages/contract-wrappers/test/calldata_optimization_utils_test.ts @@ -0,0 +1,60 @@ +import { orderFactory } from '@0xproject/order-utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; +import 'mocha'; + +import { assert } from '../src/utils/assert'; +import { calldataOptimizationUtils } from '../src/utils/calldata_optimization_utils'; +import { constants } from '../src/utils/constants'; + +import { chaiSetup } from './utils/chai_setup'; + +chaiSetup.configure(); +const expect = chai.expect; + +// utility for generating a set of order objects with mostly NULL values +// except for a specified makerAssetData and takerAssetData +const FAKE_ORDERS_COUNT = 5; +const generateFakeOrders = (makerAssetData: string, takerAssetData: string) => + _.map(_.range(FAKE_ORDERS_COUNT), index => { + const order = orderFactory.createOrder( + constants.NULL_ADDRESS, + constants.ZERO_AMOUNT, + makerAssetData, + constants.ZERO_AMOUNT, + takerAssetData, + constants.NULL_ADDRESS, + ); + return { + ...order, + signature: 'dummy signature', + }; + }); + +describe('calldataOptimizationUtils', () => { + const fakeMakerAssetData = 'fakeMakerAssetData'; + const fakeTakerAssetData = 'fakeTakerAssetData'; + const orders = generateFakeOrders(fakeMakerAssetData, fakeTakerAssetData); + describe('#optimizeForwarderOrders', () => { + it('should make makerAssetData `0x` unless first order', () => { + const optimizedOrders = calldataOptimizationUtils.optimizeForwarderOrders(orders); + expect(optimizedOrders[0].makerAssetData).to.equal(fakeMakerAssetData); + const ordersWithoutHead = _.slice(optimizedOrders, 1); + _.forEach(ordersWithoutHead, order => expect(order.makerAssetData).to.equal(constants.NULL_BYTES)); + }); + it('should make all takerAssetData `0x`', () => { + const optimizedOrders = calldataOptimizationUtils.optimizeForwarderOrders(orders); + _.forEach(optimizedOrders, order => expect(order.takerAssetData).to.equal(constants.NULL_BYTES)); + }); + }); + describe('#optimizeForwarderFeeOrders', () => { + it('should make all makerAssetData `0x`', () => { + const optimizedOrders = calldataOptimizationUtils.optimizeForwarderFeeOrders(orders); + _.forEach(optimizedOrders, order => expect(order.makerAssetData).to.equal(constants.NULL_BYTES)); + }); + it('should make all takerAssetData `0x`', () => { + const optimizedOrders = calldataOptimizationUtils.optimizeForwarderFeeOrders(orders); + _.forEach(optimizedOrders, order => expect(order.takerAssetData).to.equal(constants.NULL_BYTES)); + }); + }); +}); diff --git a/packages/contract-wrappers/test/exchange_wrapper_test.ts b/packages/contract-wrappers/test/exchange_wrapper_test.ts index dca212f65..fa3b49eb9 100644 --- a/packages/contract-wrappers/test/exchange_wrapper_test.ts +++ b/packages/contract-wrappers/test/exchange_wrapper_test.ts @@ -277,6 +277,15 @@ describe('ExchangeWrapper', () => { expect(orderInfo.orderHash).to.be.equal(orderHash); }); }); + describe('#getOrdersInfoAsync', () => { + it('should get the orders info', async () => { + const ordersInfo = await contractWrappers.exchange.getOrdersInfoAsync([signedOrder, anotherSignedOrder]); + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + expect(ordersInfo[0].orderHash).to.be.equal(orderHash); + const anotherOrderHash = orderHashUtils.getOrderHashHex(anotherSignedOrder); + expect(ordersInfo[1].orderHash).to.be.equal(anotherOrderHash); + }); + }); describe('#isValidSignature', () => { it('should check if the signature is valid', async () => { const orderHash = orderHashUtils.getOrderHashHex(signedOrder); @@ -295,7 +304,7 @@ describe('ExchangeWrapper', () => { }); }); describe('#isAllowedValidatorAsync', () => { - it('should check if the validator is alllowed', async () => { + it('should check if the validator is allowed', async () => { const signerAddress = makerAddress; const validatorAddress = constants.NULL_ADDRESS; const isAllowed = await contractWrappers.exchange.isAllowedValidatorAsync(signerAddress, validatorAddress); diff --git a/packages/contract-wrappers/test/forwarder_wrapper_test.ts b/packages/contract-wrappers/test/forwarder_wrapper_test.ts new file mode 100644 index 000000000..3f3b40e0b --- /dev/null +++ b/packages/contract-wrappers/test/forwarder_wrapper_test.ts @@ -0,0 +1,130 @@ +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 'mocha'; + +import { + ContractWrappers, + DecodedLogEvent, + ExchangeCancelEventArgs, + ExchangeEvents, + ExchangeFillEventArgs, + OrderStatus, +} from '../src'; + +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('ForwarderWrapper', () => { + const contractWrappersConfig = { + networkId: constants.TESTRPC_NETWORK_ID, + 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[]; + 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); + forwarderContractAddress = contractWrappers.forwarder.getContractAddress(); + 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] = 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( + makerAssetData, + takerAssetData, + makerAddress, + constants.NULL_ADDRESS, + fillableAmount, + ); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('#marketBuyOrdersWithEthAsync', () => { + it('should market buy orders with eth', async () => { + const signedOrders = [signedOrder, anotherSignedOrder]; + const makerAssetFillAmount = signedOrder.makerAssetAmount.plus(anotherSignedOrder.makerAssetAmount); + const txHash = await contractWrappers.forwarder.marketBuyOrdersWithEthAsync( + signedOrders, + makerAssetFillAmount, + takerAddress, + makerAssetFillAmount, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); + const ordersInfo = await contractWrappers.exchange.getOrdersInfoAsync([signedOrder, anotherSignedOrder]); + expect(ordersInfo[0].orderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + expect(ordersInfo[1].orderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + }); + }); + describe('#marketSellOrdersWithEthAsync', () => { + it('should market sell orders with eth', async () => { + const signedOrders = [signedOrder, anotherSignedOrder]; + const makerAssetFillAmount = signedOrder.makerAssetAmount.plus(anotherSignedOrder.makerAssetAmount); + const txHash = await contractWrappers.forwarder.marketSellOrdersWithEthAsync( + signedOrders, + takerAddress, + makerAssetFillAmount, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); + const ordersInfo = await contractWrappers.exchange.getOrdersInfoAsync([signedOrder, anotherSignedOrder]); + expect(ordersInfo[0].orderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + expect(ordersInfo[1].orderStatus).to.be.equal(OrderStatus.FILLABLE); + expect(ordersInfo[1].orderTakerAssetFilledAmount).to.be.bignumber.equal(new BigNumber(4)); // only 95% of ETH is sold + }); + }); +}); |