From a7238d0fdb302d7062f3f63c3119910286f992c5 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Wed, 18 Jul 2018 16:21:24 -0700 Subject: [PATCH 01/49] Implement initial forwarder wrapper --- packages/contract-wrappers/package.json | 4 +- packages/contract-wrappers/src/artifacts.ts | 2 + .../src/contract_wrappers.ts | 7 + .../contract_wrappers/forwarder_wrapper.ts | 170 ++++++++++++++++++ packages/contract-wrappers/src/index.ts | 1 + packages/contract-wrappers/src/types.ts | 2 + .../contract-wrappers/src/utils/constants.ts | 1 + .../test/forwarder_wrapper_test.ts | 57 ++++++ 8 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts create mode 100644 packages/contract-wrappers/test/forwarder_wrapper_test.ts diff --git a/packages/contract-wrappers/package.json b/packages/contract-wrappers/package.json index ed0278caae..f27afaba9e 100644 --- a/packages/contract-wrappers/package.json +++ b/packages/contract-wrappers/package.json @@ -14,7 +14,7 @@ "watch_without_deps": "yarn pre_build && tsc -w", "build": "yarn pre_build && tsc && copyfiles -u 3 './lib/src/monorepo_scripts/**/*' ./scripts", "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 @@ "manual:postpublish": "yarn build; node ./scripts/postpublish.js" }, "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" }, "repository": { diff --git a/packages/contract-wrappers/src/artifacts.ts b/packages/contract-wrappers/src/artifacts.ts index 742d0e1b26..2481b311ab 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 8010242c5b..76aefbdcf0 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,7 @@ export class ContractWrappers { config.zrxContractAddress, blockPollingIntervalMs, ); + this.forwarder = new ForwarderWrapper(this._web3Wrapper, config.networkId, config.forwarderContractAddress); } /** * Sets a new web3 provider for 0x.js. Updating the provider will stop all 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 0000000000..56533fb783 --- /dev/null +++ b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts @@ -0,0 +1,170 @@ +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 { 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; + constructor(web3Wrapper: Web3Wrapper, networkId: number, contractAddressIfExists?: string) { + super(web3Wrapper, networkId); + this._contractAddressIfExists = contractAddressIfExists; + } + /** + * 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 + * @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 { + 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); + const normalizedTakerAddress = takerAddress.toLowerCase(); + const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase(); + const ForwarderContractInstance = await this._getForwarderContractAsync(); + const txHash = await ForwarderContractInstance.marketSellOrdersWithEth.sendTransactionAsync( + signedOrders, + _.map(signedOrders, order => order.signature), + signedFeeOrders, + _.map(signedFeeOrders, 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 + * @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 { + 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); + const normalizedTakerAddress = takerAddress.toLowerCase(); + const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase(); + const ForwarderContractInstance = await this._getForwarderContractAsync(); + const txHash = await ForwarderContractInstance.marketBuyOrdersWithEth.sendTransactionAsync( + signedOrders, + makerAssetFillAmount, + _.map(signedOrders, order => order.signature), + signedFeeOrders, + _.map(signedFeeOrders, 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; + } + // 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 { + 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 e5485d7a6c..1986e00047 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/types.ts b/packages/contract-wrappers/src/types.ts index f9d7a6b9fd..887d09c804 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; } diff --git a/packages/contract-wrappers/src/utils/constants.ts b/packages/contract-wrappers/src/utils/constants.ts index 039475b7f9..d436efefc6 100644 --- a/packages/contract-wrappers/src/utils/constants.ts +++ b/packages/contract-wrappers/src/utils/constants.ts @@ -10,4 +10,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/forwarder_wrapper_test.ts b/packages/contract-wrappers/test/forwarder_wrapper_test.ts new file mode 100644 index 0000000000..61a21a0d76 --- /dev/null +++ b/packages/contract-wrappers/test/forwarder_wrapper_test.ts @@ -0,0 +1,57 @@ +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', () => { + let contractWrappers: ContractWrappers; + let forwarderContractAddress: string; + let userAddresses: string[]; + const config = { + networkId: constants.TESTRPC_NETWORK_ID, + blockPollingIntervalMs: 0, + }; + before(async () => { + await blockchainLifecycle.startAsync(); + contractWrappers = new ContractWrappers(provider, config); + forwarderContractAddress = contractWrappers.exchange.getContractAddress(); + userAddresses = await web3Wrapper.getAvailableAddressesAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + // describe('#fillOrderAsync', () => { + // it('should fill a valid order', async () => { + // // txHash = await contractWrappers.exchange.fillOrderAsync(signedOrder, takerTokenFillAmount, takerAddress); + // // await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); + // }); + // }); +}); From 045751a430c512d94bf7e515d7531bac68dc2179 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Mon, 30 Jul 2018 17:43:02 -0700 Subject: [PATCH 02/49] Add getOrdersInfo to exchange_wrapper --- .../src/contract_wrappers/exchange_wrapper.ts | 22 ++++++++++++++++++- packages/contract-wrappers/src/types.ts | 4 ++-- .../test/exchange_wrapper_test.ts | 9 ++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts index 3e76192284..48bd00f90e 100644 --- a/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts +++ b/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts @@ -869,15 +869,35 @@ export class ExchangeWrapper extends ContractWrapper { */ @decorators.asyncZeroExErrorHandler public async getOrderInfoAsync(order: Order | SignedOrder, methodOpts: MethodOpts = {}): Promise { + 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, + methodOpts: MethodOpts = {}, + ): Promise { + 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. diff --git a/packages/contract-wrappers/src/types.ts b/packages/contract-wrappers/src/types.ts index 887d09c804..2b3cdc591a 100644 --- a/packages/contract-wrappers/src/types.ts +++ b/packages/contract-wrappers/src/types.ts @@ -174,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/test/exchange_wrapper_test.ts b/packages/contract-wrappers/test/exchange_wrapper_test.ts index dca212f65b..e468187f25 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); From 44498f2263ccfec805a6815a1ab673e2dd423e8d Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Mon, 30 Jul 2018 17:51:02 -0700 Subject: [PATCH 03/49] Fix spelling error in exchange wrapper tests --- packages/contract-wrappers/test/exchange_wrapper_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contract-wrappers/test/exchange_wrapper_test.ts b/packages/contract-wrappers/test/exchange_wrapper_test.ts index e468187f25..fa3b49eb96 100644 --- a/packages/contract-wrappers/test/exchange_wrapper_test.ts +++ b/packages/contract-wrappers/test/exchange_wrapper_test.ts @@ -304,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); From bc93ff0cb5c846fcb534f90b93e54bba0747f562 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Mon, 30 Jul 2018 19:43:44 -0700 Subject: [PATCH 04/49] Write initial test for forwarder_wrapper --- .../contract_wrappers/forwarder_wrapper.ts | 8 +- .../test/forwarder_wrapper_test.ts | 81 ++++++++++++++++--- 2 files changed, 73 insertions(+), 16 deletions(-) diff --git a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts index 56533fb783..db1e5390a6 100644 --- a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts +++ b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts @@ -62,8 +62,8 @@ export class ForwarderWrapper extends ContractWrapper { assert.doesConformToSchema('txOpts', txOpts, txOptsSchema); const normalizedTakerAddress = takerAddress.toLowerCase(); const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase(); - const ForwarderContractInstance = await this._getForwarderContractAsync(); - const txHash = await ForwarderContractInstance.marketSellOrdersWithEth.sendTransactionAsync( + const forwarderContractInstance = await this._getForwarderContractAsync(); + const txHash = await forwarderContractInstance.marketSellOrdersWithEth.sendTransactionAsync( signedOrders, _.map(signedOrders, order => order.signature), signedFeeOrders, @@ -117,8 +117,8 @@ export class ForwarderWrapper extends ContractWrapper { assert.doesConformToSchema('txOpts', txOpts, txOptsSchema); const normalizedTakerAddress = takerAddress.toLowerCase(); const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase(); - const ForwarderContractInstance = await this._getForwarderContractAsync(); - const txHash = await ForwarderContractInstance.marketBuyOrdersWithEth.sendTransactionAsync( + const forwarderContractInstance = await this._getForwarderContractAsync(); + const txHash = await forwarderContractInstance.marketBuyOrdersWithEth.sendTransactionAsync( signedOrders, makerAssetFillAmount, _.map(signedOrders, order => order.signature), diff --git a/packages/contract-wrappers/test/forwarder_wrapper_test.ts b/packages/contract-wrappers/test/forwarder_wrapper_test.ts index 61a21a0d76..0fb695b9e4 100644 --- a/packages/contract-wrappers/test/forwarder_wrapper_test.ts +++ b/packages/contract-wrappers/test/forwarder_wrapper_test.ts @@ -26,18 +26,65 @@ const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); describe('ForwarderWrapper', () => { - let contractWrappers: ContractWrappers; - let forwarderContractAddress: string; - let userAddresses: string[]; - const config = { + 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, config); - forwarderContractAddress = contractWrappers.exchange.getContractAddress(); + 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(); @@ -48,10 +95,20 @@ describe('ForwarderWrapper', () => { afterEach(async () => { await blockchainLifecycle.revertAsync(); }); - // describe('#fillOrderAsync', () => { - // it('should fill a valid order', async () => { - // // txHash = await contractWrappers.exchange.fillOrderAsync(signedOrder, takerTokenFillAmount, takerAddress); - // // await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); - // }); - // }); + 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); + }); + }); }); From 8ed3d59f969c2f07e34739c5a08c69de583cef88 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Mon, 30 Jul 2018 22:14:07 -0700 Subject: [PATCH 05/49] Add more assertions --- .../src/contract_wrappers.ts | 7 ++- .../contract_wrappers/forwarder_wrapper.ts | 41 ++++++++++++- .../contract-wrappers/src/utils/assert.ts | 60 +++++++++++++++++-- 3 files changed, 102 insertions(+), 6 deletions(-) diff --git a/packages/contract-wrappers/src/contract_wrappers.ts b/packages/contract-wrappers/src/contract_wrappers.ts index 76aefbdcf0..4277a0746a 100644 --- a/packages/contract-wrappers/src/contract_wrappers.ts +++ b/packages/contract-wrappers/src/contract_wrappers.ts @@ -110,7 +110,12 @@ export class ContractWrappers { config.zrxContractAddress, blockPollingIntervalMs, ); - this.forwarder = new ForwarderWrapper(this._web3Wrapper, config.networkId, config.forwarderContractAddress); + 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/forwarder_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts index db1e5390a6..beb2d1c818 100644 --- a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts +++ b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts @@ -22,9 +22,16 @@ export class ForwarderWrapper extends ContractWrapper { public abi: ContractAbi = artifacts.Forwarder.compilerOutput.abi; private _forwarderContractIfExists?: ForwarderContract; private _contractAddressIfExists?: string; - constructor(web3Wrapper: Web3Wrapper, networkId: number, 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. @@ -53,6 +60,7 @@ export class ForwarderWrapper extends ContractWrapper { feeRecipientAddress: string = constants.NULL_ADDRESS, txOpts: TransactionOpts = {}, ): Promise { + // type assertions assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); assert.isBigNumber('ethAmount', ethAmount); @@ -60,6 +68,13 @@ export class ForwarderWrapper extends ContractWrapper { 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(), + ); const normalizedTakerAddress = takerAddress.toLowerCase(); const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase(); const forwarderContractInstance = await this._getForwarderContractAsync(); @@ -107,6 +122,7 @@ export class ForwarderWrapper extends ContractWrapper { feeRecipientAddress: string = constants.NULL_ADDRESS, txOpts: TransactionOpts = {}, ): Promise { + // type assertions assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); assert.isBigNumber('makerAssetFillAmount', makerAssetFillAmount); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); @@ -115,6 +131,13 @@ export class ForwarderWrapper extends ContractWrapper { 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(), + ); const normalizedTakerAddress = takerAddress.toLowerCase(); const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase(); const forwarderContractInstance = await this._getForwarderContractAsync(); @@ -144,6 +167,22 @@ export class ForwarderWrapper extends ContractWrapper { 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 diff --git a/packages/contract-wrappers/src/utils/assert.ts b/packages/contract-wrappers/src/utils/assert.ts index 842b16fa0c..1836421707 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 { isValidSignatureAsync } from '@0xproject/order-utils'; -import { ECSignature } from '@0xproject/types'; // tslint:disable-line:no-unused-variable +import { assetDataUtils, isValidSignatureAsync } from '@0xproject/order-utils'; +import { ECSignature, 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 { const isValid = await 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}`, + ); + } + }, }; From 5d44a67e62eb47ba4a8664e83ed46568df5eb78f Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Tue, 31 Jul 2018 00:10:58 -0700 Subject: [PATCH 06/49] Update forwarder_wrapper_test --- .../test/forwarder_wrapper_test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/contract-wrappers/test/forwarder_wrapper_test.ts b/packages/contract-wrappers/test/forwarder_wrapper_test.ts index 0fb695b9e4..3f3b40e0be 100644 --- a/packages/contract-wrappers/test/forwarder_wrapper_test.ts +++ b/packages/contract-wrappers/test/forwarder_wrapper_test.ts @@ -111,4 +111,20 @@ describe('ForwarderWrapper', () => { 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 + }); + }); }); From ca1f926d6d137f9523a9765c047430ec39d45d86 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Tue, 31 Jul 2018 00:26:53 -0700 Subject: [PATCH 07/49] Clarify ethAmount is in wei --- .../src/contract_wrappers/forwarder_wrapper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts index beb2d1c818..90cfbe2afd 100644 --- a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts +++ b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts @@ -42,7 +42,7 @@ export class ForwarderWrapper extends ContractWrapper { * 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 + * @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. @@ -103,7 +103,7 @@ export class ForwarderWrapper extends ContractWrapper { * @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 + * @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. From 9f7f61085c1a6989b79df575beb0b5d8f2b3652d Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Wed, 1 Aug 2018 15:29:47 -0700 Subject: [PATCH 08/49] Update contract-wrappers CHANGELOG.json --- packages/contract-wrappers/CHANGELOG.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index d9eef089f9..8fa31052cf 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "1.1.0-rc.2", + "changes": [ + { + "note": "Add ForwarderWrapper", + "pr": 934 + } + ] + }, { "version": "1.0.1-rc.2", "changes": [ From 4f006fdc5c3cc24bb5d008f4c363d5225fe6728a Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Wed, 1 Aug 2018 14:27:17 -0700 Subject: [PATCH 09/49] Create marketBuyOrdersOptimizations --- .../contract_wrappers/forwarder_wrapper.ts | 27 ++++++++---- .../contract-wrappers/src/utils/constants.ts | 1 + .../utils/market_orders_optimization_utils.ts | 43 +++++++++++++++++++ 3 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 packages/contract-wrappers/src/utils/market_orders_optimization_utils.ts diff --git a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts index 90cfbe2afd..7d51889cf4 100644 --- a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts +++ b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts @@ -11,6 +11,7 @@ import { txOptsSchema } from '../schemas/tx_opts_schema'; import { TransactionOpts } from '../types'; import { assert } from '../utils/assert'; import { constants } from '../utils/constants'; +import { marketOrdersOptimizationUtils } from '../utils/market_orders_optimization_utils'; import { ContractWrapper } from './contract_wrapper'; import { ForwarderContract } from './generated/forwarder'; @@ -75,14 +76,19 @@ export class ForwarderWrapper extends ContractWrapper { this.getZRXTokenAddress(), this.getEtherTokenAddress(), ); + // lowercase input addresses const normalizedTakerAddress = takerAddress.toLowerCase(); const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase(); + // optimize orders + const optimizedMarketOrders = marketOrdersOptimizationUtils.optimizeMarketOrders(signedOrders); + const optimizedFeeOrders = marketOrdersOptimizationUtils.optimizeFeeOrders(signedFeeOrders); + // send transaction const forwarderContractInstance = await this._getForwarderContractAsync(); const txHash = await forwarderContractInstance.marketSellOrdersWithEth.sendTransactionAsync( - signedOrders, - _.map(signedOrders, order => order.signature), - signedFeeOrders, - _.map(signedFeeOrders, order => order.signature), + optimizedMarketOrders, + _.map(optimizedMarketOrders, order => order.signature), + optimizedFeeOrders, + _.map(optimizedFeeOrders, order => order.signature), feePercentage, feeRecipientAddress, { @@ -138,15 +144,20 @@ export class ForwarderWrapper extends ContractWrapper { this.getZRXTokenAddress(), this.getEtherTokenAddress(), ); + // lowercase input addresses const normalizedTakerAddress = takerAddress.toLowerCase(); const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase(); + // optimize orders + const optimizedMarketOrders = marketOrdersOptimizationUtils.optimizeMarketOrders(signedOrders); + const optimizedFeeOrders = marketOrdersOptimizationUtils.optimizeFeeOrders(signedFeeOrders); + // send transaction const forwarderContractInstance = await this._getForwarderContractAsync(); const txHash = await forwarderContractInstance.marketBuyOrdersWithEth.sendTransactionAsync( - signedOrders, + optimizedMarketOrders, makerAssetFillAmount, - _.map(signedOrders, order => order.signature), - signedFeeOrders, - _.map(signedFeeOrders, order => order.signature), + _.map(optimizedMarketOrders, order => order.signature), + optimizedFeeOrders, + _.map(optimizedFeeOrders, order => order.signature), feePercentage, feeRecipientAddress, { diff --git a/packages/contract-wrappers/src/utils/constants.ts b/packages/contract-wrappers/src/utils/constants.ts index d436efefc6..2df11538cc 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', diff --git a/packages/contract-wrappers/src/utils/market_orders_optimization_utils.ts b/packages/contract-wrappers/src/utils/market_orders_optimization_utils.ts new file mode 100644 index 0000000000..0041f1a64a --- /dev/null +++ b/packages/contract-wrappers/src/utils/market_orders_optimization_utils.ts @@ -0,0 +1,43 @@ +import { SignedOrder } from '@0xproject/types'; +import * as _ from 'lodash'; + +import { constants } from './constants'; + +export const marketOrdersOptimizationUtils = { + /** + * 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 + */ + optimizeMarketOrders(orders: SignedOrder[]): SignedOrder[] { + const optimizedOrders = _.map(orders, (order, index) => { + const makerAssetData = index === 0 ? order.makerAssetData : constants.NULL_BYTES; + const takerAssetData = constants.NULL_BYTES; + return { + ...order, + makerAssetData, + takerAssetData, + }; + }); + 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 + */ + optimizeFeeOrders(orders: SignedOrder[]): SignedOrder[] { + const optimizedOrders = _.map(orders, order => { + const makerAssetData = constants.NULL_BYTES; + const takerAssetData = constants.NULL_BYTES; + return { + ...order, + makerAssetData, + takerAssetData, + }; + }); + return optimizedOrders; + }, +}; From 7c864b81e0d958560e098ebd8bd241385e4aadff Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Wed, 1 Aug 2018 15:08:17 -0700 Subject: [PATCH 10/49] Add createOrder with no signing to orderFactory --- packages/order-utils/CHANGELOG.json | 8 +++++ packages/order-utils/src/constants.ts | 1 + packages/order-utils/src/order_factory.ts | 42 +++++++++++++++++++---- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index a399f5ea1d..7d9ca0a537 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -1,4 +1,12 @@ [ + { + "version": "1.1.0-rc.2", + "changes": [ + { + "note": "Added a synchronous `createOrder` method in `orderFactory`" + } + ] + }, { "version": "1.0.1-rc.2", "changes": [ diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index bb74821843..92eb89d705 100644 --- a/packages/order-utils/src/constants.ts +++ b/packages/order-utils/src/constants.ts @@ -10,4 +10,5 @@ export const constants = { ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH: 53, SELECTOR_LENGTH: 4, BASE_16: 16, + INFINITE_TIMESTAMP_SEC: new BigNumber(2524604400), // Close to infinite }; diff --git a/packages/order-utils/src/order_factory.ts b/packages/order-utils/src/order_factory.ts index 803cb82b10..4be7a19137 100644 --- a/packages/order-utils/src/order_factory.ts +++ b/packages/order-utils/src/order_factory.ts @@ -1,17 +1,17 @@ -import { ECSignature, SignedOrder } from '@0xproject/types'; +import { ECSignature, Order, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { Provider } from 'ethereum-types'; import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; +import { constants } from './constants'; import { orderHashUtils } from './order_hash'; import { generatePseudoRandomSalt } from './salt'; import { ecSignOrderHashAsync } from './signature_utils'; import { MessagePrefixType } from './types'; export const orderFactory = { - async createSignedOrderAsync( - provider: Provider, + createOrder( makerAddress: string, takerAddress: string, senderAddress: string, @@ -24,10 +24,9 @@ export const orderFactory = { exchangeAddress: string, feeRecipientAddress: string, expirationTimeSecondsIfExists?: BigNumber, - ): Promise { - const defaultExpirationUnixTimestampSec = new BigNumber(2524604400); // Close to infinite + ): Order { const expirationTimeSeconds = _.isUndefined(expirationTimeSecondsIfExists) - ? defaultExpirationUnixTimestampSec + ? constants.INFINITE_TIMESTAMP_SEC : expirationTimeSecondsIfExists; const order = { makerAddress, @@ -44,6 +43,37 @@ export const orderFactory = { feeRecipientAddress, expirationTimeSeconds, }; + return order; + }, + async createSignedOrderAsync( + provider: Provider, + makerAddress: string, + takerAddress: string, + senderAddress: string, + makerFee: BigNumber, + takerFee: BigNumber, + makerAssetAmount: BigNumber, + makerAssetData: string, + takerAssetAmount: BigNumber, + takerAssetData: string, + exchangeAddress: string, + feeRecipientAddress: string, + expirationTimeSecondsIfExists?: BigNumber, + ): Promise { + const order = orderFactory.createOrder( + makerAddress, + takerAddress, + senderAddress, + makerFee, + takerFee, + makerAssetAmount, + makerAssetData, + takerAssetAmount, + takerAssetData, + exchangeAddress, + feeRecipientAddress, + expirationTimeSecondsIfExists, + ); const orderHash = orderHashUtils.getOrderHashHex(order); const messagePrefixOpts = { prefixType: MessagePrefixType.EthSign, From 30c6fe08ec5f67fa3679c0043a24ffc420974964 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Wed, 1 Aug 2018 15:08:34 -0700 Subject: [PATCH 11/49] Add tests --- .../utils/market_orders_optimization_utils.ts | 37 ++++++----- .../market_orders_optimization_utils_test.ts | 66 +++++++++++++++++++ 2 files changed, 85 insertions(+), 18 deletions(-) create mode 100644 packages/contract-wrappers/test/market_orders_optimization_utils_test.ts diff --git a/packages/contract-wrappers/src/utils/market_orders_optimization_utils.ts b/packages/contract-wrappers/src/utils/market_orders_optimization_utils.ts index 0041f1a64a..e35b2eadc9 100644 --- a/packages/contract-wrappers/src/utils/market_orders_optimization_utils.ts +++ b/packages/contract-wrappers/src/utils/market_orders_optimization_utils.ts @@ -11,15 +11,12 @@ export const marketOrdersOptimizationUtils = { * @returns optimized orders */ optimizeMarketOrders(orders: SignedOrder[]): SignedOrder[] { - const optimizedOrders = _.map(orders, (order, index) => { - const makerAssetData = index === 0 ? order.makerAssetData : constants.NULL_BYTES; - const takerAssetData = constants.NULL_BYTES; - return { - ...order, - makerAssetData, - takerAssetData, - }; - }); + const optimizedOrders = _.map(orders, (order, index) => + transformOrder(order, { + makerAssetData: index === 0 ? order.makerAssetData : constants.NULL_BYTES, + takerAssetData: constants.NULL_BYTES, + }), + ); return optimizedOrders; }, /** @@ -29,15 +26,19 @@ export const marketOrdersOptimizationUtils = { * @returns optimized orders */ optimizeFeeOrders(orders: SignedOrder[]): SignedOrder[] { - const optimizedOrders = _.map(orders, order => { - const makerAssetData = constants.NULL_BYTES; - const takerAssetData = constants.NULL_BYTES; - return { - ...order, - makerAssetData, - takerAssetData, - }; - }); + const optimizedOrders = _.map(orders, (order, index) => + transformOrder(order, { + makerAssetData: constants.NULL_BYTES, + takerAssetData: constants.NULL_BYTES, + }), + ); return optimizedOrders; }, }; + +const transformOrder = (order: SignedOrder, partialOrder: Partial) => { + return { + ...order, + ...partialOrder, + }; +}; diff --git a/packages/contract-wrappers/test/market_orders_optimization_utils_test.ts b/packages/contract-wrappers/test/market_orders_optimization_utils_test.ts new file mode 100644 index 0000000000..742294df28 --- /dev/null +++ b/packages/contract-wrappers/test/market_orders_optimization_utils_test.ts @@ -0,0 +1,66 @@ +import { orderFactory } from '@0xproject/order-utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; +import 'mocha'; + +import { constants } from '../src/utils/constants'; +import { marketOrdersOptimizationUtils } from '../src/utils/market_orders_optimization_utils'; + +import { chaiSetup } from './utils/chai_setup'; +import { assert } from '../src/utils/assert'; +import { NULL_BYTES } from '@0xproject/utils'; + +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.NULL_ADDRESS, + constants.NULL_ADDRESS, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + makerAssetData, + constants.ZERO_AMOUNT, + takerAssetData, + constants.NULL_ADDRESS, + constants.NULL_ADDRESS, + ); + return { + ...order, + signature: 'dummy signature', + }; + }); + +describe('marketOrdersOptimizationUtils', () => { + const fakeMakerAssetData = 'fakeMakerAssetData'; + const fakeTakerAssetData = 'fakeTakerAssetData'; + const orders = generateFakeOrders(fakeMakerAssetData, fakeTakerAssetData); + describe('#optimizeMarketOrders', () => { + it('should make makerAssetData `0x` unless first order', () => { + const optimizedOrders = marketOrdersOptimizationUtils.optimizeMarketOrders(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 = marketOrdersOptimizationUtils.optimizeMarketOrders(orders); + _.forEach(optimizedOrders, order => expect(order.takerAssetData).to.equal(constants.NULL_BYTES)); + }); + }); + describe('#optimizeFeeOrders', () => { + it('should make all makerAssetData `0x`', () => { + const optimizedOrders = marketOrdersOptimizationUtils.optimizeFeeOrders(orders); + _.forEach(optimizedOrders, order => expect(order.makerAssetData).to.equal(constants.NULL_BYTES)); + }); + it('should make all takerAssetData `0x`', () => { + const optimizedOrders = marketOrdersOptimizationUtils.optimizeFeeOrders(orders); + _.forEach(optimizedOrders, order => expect(order.takerAssetData).to.equal(constants.NULL_BYTES)); + }); + }); +}); From 6e74896620137b462b78a9493278a30efd016c44 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Wed, 1 Aug 2018 20:49:10 -0700 Subject: [PATCH 12/49] CHANGELOG --- packages/contract-wrappers/CHANGELOG.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index 8fa31052cf..cf10751d4e 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Add ForwarderWrapper", "pr": 934 + }, + { + "note": "Optimize orders in ForwarderWrapper", + "pr": 935 } ] }, From c3e6be7956adf4fabcc8dfffe081515562a1dde0 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Thu, 2 Aug 2018 15:53:02 -0700 Subject: [PATCH 13/49] Add missing PR numbers --- packages/contract-wrappers/CHANGELOG.json | 2 +- packages/order-utils/CHANGELOG.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index cf10751d4e..d95c99a258 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -8,7 +8,7 @@ }, { "note": "Optimize orders in ForwarderWrapper", - "pr": 935 + "pr": 936 } ] }, diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index 7d9ca0a537..ddfa690fed 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -3,7 +3,8 @@ "version": "1.1.0-rc.2", "changes": [ { - "note": "Added a synchronous `createOrder` method in `orderFactory`" + "note": "Added a synchronous `createOrder` method in `orderFactory`", + "pr": 935 } ] }, From 82092ab50a73e15e167f387380cd75ac52581e88 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Thu, 2 Aug 2018 16:02:58 -0700 Subject: [PATCH 14/49] Rename to calldata utils --- .../src/contract_wrappers/forwarder_wrapper.ts | 10 +++++----- ...n_utils.ts => calldata_optimization_utils.ts} | 6 +++--- ...st.ts => calldata_optimization_utils_test.ts} | 16 ++++++++-------- 3 files changed, 16 insertions(+), 16 deletions(-) rename packages/contract-wrappers/src/utils/{market_orders_optimization_utils.ts => calldata_optimization_utils.ts} (88%) rename packages/contract-wrappers/test/{market_orders_optimization_utils_test.ts => calldata_optimization_utils_test.ts} (78%) diff --git a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts index 7d51889cf4..13ef0fe01f 100644 --- a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts +++ b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts @@ -10,8 +10,8 @@ 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 { marketOrdersOptimizationUtils } from '../utils/market_orders_optimization_utils'; import { ContractWrapper } from './contract_wrapper'; import { ForwarderContract } from './generated/forwarder'; @@ -80,8 +80,8 @@ export class ForwarderWrapper extends ContractWrapper { const normalizedTakerAddress = takerAddress.toLowerCase(); const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase(); // optimize orders - const optimizedMarketOrders = marketOrdersOptimizationUtils.optimizeMarketOrders(signedOrders); - const optimizedFeeOrders = marketOrdersOptimizationUtils.optimizeFeeOrders(signedFeeOrders); + const optimizedMarketOrders = calldataOptimizationUtils.optimizeForwarderOrders(signedOrders); + const optimizedFeeOrders = calldataOptimizationUtils.optimizeForwarderFeeOrders(signedFeeOrders); // send transaction const forwarderContractInstance = await this._getForwarderContractAsync(); const txHash = await forwarderContractInstance.marketSellOrdersWithEth.sendTransactionAsync( @@ -148,8 +148,8 @@ export class ForwarderWrapper extends ContractWrapper { const normalizedTakerAddress = takerAddress.toLowerCase(); const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase(); // optimize orders - const optimizedMarketOrders = marketOrdersOptimizationUtils.optimizeMarketOrders(signedOrders); - const optimizedFeeOrders = marketOrdersOptimizationUtils.optimizeFeeOrders(signedFeeOrders); + const optimizedMarketOrders = calldataOptimizationUtils.optimizeForwarderOrders(signedOrders); + const optimizedFeeOrders = calldataOptimizationUtils.optimizeForwarderFeeOrders(signedFeeOrders); // send transaction const forwarderContractInstance = await this._getForwarderContractAsync(); const txHash = await forwarderContractInstance.marketBuyOrdersWithEth.sendTransactionAsync( diff --git a/packages/contract-wrappers/src/utils/market_orders_optimization_utils.ts b/packages/contract-wrappers/src/utils/calldata_optimization_utils.ts similarity index 88% rename from packages/contract-wrappers/src/utils/market_orders_optimization_utils.ts rename to packages/contract-wrappers/src/utils/calldata_optimization_utils.ts index e35b2eadc9..3172cf5315 100644 --- a/packages/contract-wrappers/src/utils/market_orders_optimization_utils.ts +++ b/packages/contract-wrappers/src/utils/calldata_optimization_utils.ts @@ -3,14 +3,14 @@ import * as _ from 'lodash'; import { constants } from './constants'; -export const marketOrdersOptimizationUtils = { +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 */ - optimizeMarketOrders(orders: SignedOrder[]): SignedOrder[] { + optimizeForwarderOrders(orders: SignedOrder[]): SignedOrder[] { const optimizedOrders = _.map(orders, (order, index) => transformOrder(order, { makerAssetData: index === 0 ? order.makerAssetData : constants.NULL_BYTES, @@ -25,7 +25,7 @@ export const marketOrdersOptimizationUtils = { * @param orders An array of SignedOrder objects * @returns optimized orders */ - optimizeFeeOrders(orders: SignedOrder[]): SignedOrder[] { + optimizeForwarderFeeOrders(orders: SignedOrder[]): SignedOrder[] { const optimizedOrders = _.map(orders, (order, index) => transformOrder(order, { makerAssetData: constants.NULL_BYTES, diff --git a/packages/contract-wrappers/test/market_orders_optimization_utils_test.ts b/packages/contract-wrappers/test/calldata_optimization_utils_test.ts similarity index 78% rename from packages/contract-wrappers/test/market_orders_optimization_utils_test.ts rename to packages/contract-wrappers/test/calldata_optimization_utils_test.ts index 742294df28..107d913ba7 100644 --- a/packages/contract-wrappers/test/market_orders_optimization_utils_test.ts +++ b/packages/contract-wrappers/test/calldata_optimization_utils_test.ts @@ -4,7 +4,7 @@ import * as _ from 'lodash'; import 'mocha'; import { constants } from '../src/utils/constants'; -import { marketOrdersOptimizationUtils } from '../src/utils/market_orders_optimization_utils'; +import { calldataOptimizationUtils } from '../src/utils/calldata_optimization_utils'; import { chaiSetup } from './utils/chai_setup'; import { assert } from '../src/utils/assert'; @@ -37,29 +37,29 @@ const generateFakeOrders = (makerAssetData: string, takerAssetData: string) => }; }); -describe('marketOrdersOptimizationUtils', () => { +describe('calldataOptimizationUtils', () => { const fakeMakerAssetData = 'fakeMakerAssetData'; const fakeTakerAssetData = 'fakeTakerAssetData'; const orders = generateFakeOrders(fakeMakerAssetData, fakeTakerAssetData); - describe('#optimizeMarketOrders', () => { + describe('#optimizeForwarderOrders', () => { it('should make makerAssetData `0x` unless first order', () => { - const optimizedOrders = marketOrdersOptimizationUtils.optimizeMarketOrders(orders); + 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 = marketOrdersOptimizationUtils.optimizeMarketOrders(orders); + const optimizedOrders = calldataOptimizationUtils.optimizeForwarderOrders(orders); _.forEach(optimizedOrders, order => expect(order.takerAssetData).to.equal(constants.NULL_BYTES)); }); }); - describe('#optimizeFeeOrders', () => { + describe('#optimizeForwarderFeeOrders', () => { it('should make all makerAssetData `0x`', () => { - const optimizedOrders = marketOrdersOptimizationUtils.optimizeFeeOrders(orders); + const optimizedOrders = calldataOptimizationUtils.optimizeForwarderFeeOrders(orders); _.forEach(optimizedOrders, order => expect(order.makerAssetData).to.equal(constants.NULL_BYTES)); }); it('should make all takerAssetData `0x`', () => { - const optimizedOrders = marketOrdersOptimizationUtils.optimizeFeeOrders(orders); + const optimizedOrders = calldataOptimizationUtils.optimizeForwarderFeeOrders(orders); _.forEach(optimizedOrders, order => expect(order.takerAssetData).to.equal(constants.NULL_BYTES)); }); }); From 4f381ca1d9b6f8ecc232d0481d86f8ba695f7601 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Fri, 3 Aug 2018 15:47:19 -0400 Subject: [PATCH 15/49] Update orderFactory interface --- .../test/calldata_optimization_utils_test.ts | 10 +---- packages/fill-scenarios/src/fill_scenarios.ts | 8 ++-- packages/order-utils/src/constants.ts | 1 + packages/order-utils/src/order_factory.ts | 42 +++++++++---------- 4 files changed, 28 insertions(+), 33 deletions(-) diff --git a/packages/contract-wrappers/test/calldata_optimization_utils_test.ts b/packages/contract-wrappers/test/calldata_optimization_utils_test.ts index 107d913ba7..a4cea772f6 100644 --- a/packages/contract-wrappers/test/calldata_optimization_utils_test.ts +++ b/packages/contract-wrappers/test/calldata_optimization_utils_test.ts @@ -3,12 +3,11 @@ import * as chai from 'chai'; import * as _ from 'lodash'; import 'mocha'; -import { constants } from '../src/utils/constants'; +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'; -import { assert } from '../src/utils/assert'; -import { NULL_BYTES } from '@0xproject/utils'; chaiSetup.configure(); const expect = chai.expect; @@ -20,16 +19,11 @@ const generateFakeOrders = (makerAssetData: string, takerAssetData: string) => _.map(_.range(FAKE_ORDERS_COUNT), index => { const order = orderFactory.createOrder( constants.NULL_ADDRESS, - constants.NULL_ADDRESS, - constants.NULL_ADDRESS, - constants.ZERO_AMOUNT, - constants.ZERO_AMOUNT, constants.ZERO_AMOUNT, makerAssetData, constants.ZERO_AMOUNT, takerAssetData, constants.NULL_ADDRESS, - constants.NULL_ADDRESS, ); return { ...order, diff --git a/packages/fill-scenarios/src/fill_scenarios.ts b/packages/fill-scenarios/src/fill_scenarios.ts index 8f2766e24e..f350945604 100644 --- a/packages/fill-scenarios/src/fill_scenarios.ts +++ b/packages/fill-scenarios/src/fill_scenarios.ts @@ -194,15 +194,15 @@ export class FillScenarios { const signedOrder = await orderFactory.createSignedOrderAsync( this._web3Wrapper.getProvider(), makerAddress, - takerAddress, - senderAddress, - makerFee, - takerFee, makerFillableAmount, makerAssetData, takerFillableAmount, takerAssetData, this._exchangeAddress, + takerAddress, + senderAddress, + makerFee, + takerFee, feeRecepientAddress, expirationTimeSeconds, ); diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index 92eb89d705..ea3f8b9322 100644 --- a/packages/order-utils/src/constants.ts +++ b/packages/order-utils/src/constants.ts @@ -11,4 +11,5 @@ export const constants = { SELECTOR_LENGTH: 4, BASE_16: 16, INFINITE_TIMESTAMP_SEC: new BigNumber(2524604400), // Close to infinite + ZERO_AMOUNT: new BigNumber(0), }; diff --git a/packages/order-utils/src/order_factory.ts b/packages/order-utils/src/order_factory.ts index 4be7a19137..444e5a0b29 100644 --- a/packages/order-utils/src/order_factory.ts +++ b/packages/order-utils/src/order_factory.ts @@ -13,21 +13,19 @@ import { MessagePrefixType } from './types'; export const orderFactory = { createOrder( makerAddress: string, - takerAddress: string, - senderAddress: string, - makerFee: BigNumber, - takerFee: BigNumber, makerAssetAmount: BigNumber, makerAssetData: string, takerAssetAmount: BigNumber, takerAssetData: string, exchangeAddress: string, - feeRecipientAddress: string, - expirationTimeSecondsIfExists?: BigNumber, + takerAddress: string = constants.NULL_ADDRESS, + senderAddress: string = constants.NULL_ADDRESS, + makerFee: BigNumber = constants.ZERO_AMOUNT, + takerFee: BigNumber = constants.ZERO_AMOUNT, + feeRecipientAddress: string = constants.NULL_ADDRESS, + salt: BigNumber = generatePseudoRandomSalt(), + expirationTimeSeconds: BigNumber = constants.INFINITE_TIMESTAMP_SEC, ): Order { - const expirationTimeSeconds = _.isUndefined(expirationTimeSecondsIfExists) - ? constants.INFINITE_TIMESTAMP_SEC - : expirationTimeSecondsIfExists; const order = { makerAddress, takerAddress, @@ -38,7 +36,7 @@ export const orderFactory = { takerAssetAmount, makerAssetData, takerAssetData, - salt: generatePseudoRandomSalt(), + salt, exchangeAddress, feeRecipientAddress, expirationTimeSeconds, @@ -48,31 +46,33 @@ export const orderFactory = { async createSignedOrderAsync( provider: Provider, makerAddress: string, - takerAddress: string, - senderAddress: string, - makerFee: BigNumber, - takerFee: BigNumber, makerAssetAmount: BigNumber, makerAssetData: string, takerAssetAmount: BigNumber, takerAssetData: string, exchangeAddress: string, - feeRecipientAddress: string, - expirationTimeSecondsIfExists?: BigNumber, + takerAddress?: string, + senderAddress?: string, + makerFee?: BigNumber, + takerFee?: BigNumber, + feeRecipientAddress?: string, + salt?: BigNumber, + expirationTimeSeconds?: BigNumber, ): Promise { const order = orderFactory.createOrder( makerAddress, - takerAddress, - senderAddress, - makerFee, - takerFee, makerAssetAmount, makerAssetData, takerAssetAmount, takerAssetData, exchangeAddress, + takerAddress, + senderAddress, + makerFee, + takerFee, feeRecipientAddress, - expirationTimeSecondsIfExists, + salt, + expirationTimeSeconds, ); const orderHash = orderHashUtils.getOrderHashHex(order); const messagePrefixOpts = { From d00ee5df0d861e15e258e6747bec4af3284205b2 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Fri, 3 Aug 2018 15:58:17 -0400 Subject: [PATCH 16/49] Fix CHANGELOGs --- packages/contract-wrappers/CHANGELOG.json | 2 +- packages/fill-scenarios/CHANGELOG.json | 9 +++++++++ packages/order-utils/CHANGELOG.json | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index d95c99a258..02e1acf914 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -1,6 +1,6 @@ [ { - "version": "1.1.0-rc.2", + "version": "1.0.1-rc.3", "changes": [ { "note": "Add ForwarderWrapper", diff --git a/packages/fill-scenarios/CHANGELOG.json b/packages/fill-scenarios/CHANGELOG.json index 9548034624..af1f79cf99 100644 --- a/packages/fill-scenarios/CHANGELOG.json +++ b/packages/fill-scenarios/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "1.0.1-rc.3", + "changes": [ + { + "note": "Updated to use latest orderFactory interface", + "pr": 936 + } + ] + }, { "version": "1.0.1-rc.2", "changes": [ diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index ddfa690fed..cac29bf6b5 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -1,10 +1,10 @@ [ { - "version": "1.1.0-rc.2", + "version": "1.0.1-rc.3", "changes": [ { "note": "Added a synchronous `createOrder` method in `orderFactory`", - "pr": 935 + "pr": 936 } ] }, From 3865a081a0f8ea48be736adebb8dcdb4b7d80b04 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Fri, 3 Aug 2018 16:46:55 -0400 Subject: [PATCH 17/49] Prettier --- packages/sol-resolver/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/sol-resolver/CHANGELOG.md b/packages/sol-resolver/CHANGELOG.md index 8ff6ce6ed4..5d2ee154a9 100644 --- a/packages/sol-resolver/CHANGELOG.md +++ b/packages/sol-resolver/CHANGELOG.md @@ -5,7 +5,6 @@ Edit the package's CHANGELOG.json file only. CHANGELOG - ## v1.0.4 - _July 26, 2018_ * Dependencies updated From 47673ba4bb2932051cb810bd0012c208665eb277 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Sun, 5 Aug 2018 16:51:53 -0400 Subject: [PATCH 18/49] Update createFactory to accept one createOrderOpts param to encompass all optional params --- packages/fill-scenarios/CHANGELOG.json | 3 +- packages/fill-scenarios/src/fill_scenarios.ts | 24 ++++--- packages/order-utils/CHANGELOG.json | 3 +- packages/order-utils/src/order_factory.ts | 70 +++++++++++-------- 4 files changed, 59 insertions(+), 41 deletions(-) diff --git a/packages/fill-scenarios/CHANGELOG.json b/packages/fill-scenarios/CHANGELOG.json index af1f79cf99..04e0762039 100644 --- a/packages/fill-scenarios/CHANGELOG.json +++ b/packages/fill-scenarios/CHANGELOG.json @@ -3,7 +3,8 @@ "version": "1.0.1-rc.3", "changes": [ { - "note": "Updated to use latest orderFactory interface", + "note": + "Updated to use latest orderFactory interface, fixed `feeRecipient` spelling error in public interface", "pr": 936 } ] diff --git a/packages/fill-scenarios/src/fill_scenarios.ts b/packages/fill-scenarios/src/fill_scenarios.ts index f350945604..1a1adb3266 100644 --- a/packages/fill-scenarios/src/fill_scenarios.ts +++ b/packages/fill-scenarios/src/fill_scenarios.ts @@ -61,7 +61,7 @@ export class FillScenarios { makerAddress: string, takerAddress: string, fillableAmount: BigNumber, - feeRecepientAddress: string, + feeRecipientAddress: string, expirationTimeSeconds?: BigNumber, ): Promise { return this._createAsymmetricFillableSignedOrderWithFeesAsync( @@ -73,7 +73,7 @@ export class FillScenarios { takerAddress, fillableAmount, fillableAmount, - feeRecepientAddress, + feeRecipientAddress, expirationTimeSeconds, ); } @@ -88,7 +88,7 @@ export class FillScenarios { ): Promise { const makerFee = new BigNumber(0); const takerFee = new BigNumber(0); - const feeRecepientAddress = constants.NULL_ADDRESS; + const feeRecipientAddress = constants.NULL_ADDRESS; return this._createAsymmetricFillableSignedOrderWithFeesAsync( makerAssetData, takerAssetData, @@ -98,7 +98,7 @@ export class FillScenarios { takerAddress, makerFillableAmount, takerFillableAmount, - feeRecepientAddress, + feeRecipientAddress, expirationTimeSeconds, ); } @@ -148,7 +148,7 @@ export class FillScenarios { takerAddress: string, makerFillableAmount: BigNumber, takerFillableAmount: BigNumber, - feeRecepientAddress: string, + feeRecipientAddress: string, expirationTimeSeconds?: BigNumber, ): Promise { const decodedMakerAssetData = assetDataUtils.decodeAssetDataOrThrow(makerAssetData); @@ -199,12 +199,14 @@ export class FillScenarios { takerFillableAmount, takerAssetData, this._exchangeAddress, - takerAddress, - senderAddress, - makerFee, - takerFee, - feeRecepientAddress, - expirationTimeSeconds, + { + takerAddress, + senderAddress, + makerFee, + takerFee, + feeRecipientAddress, + expirationTimeSeconds, + }, ); return signedOrder; } diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index cac29bf6b5..70a75854ae 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -3,7 +3,8 @@ "version": "1.0.1-rc.3", "changes": [ { - "note": "Added a synchronous `createOrder` method in `orderFactory`", + "note": + "Added a synchronous `createOrder` method in `orderFactory`, updated public interfaces to support some optional parameters", "pr": 936 } ] diff --git a/packages/order-utils/src/order_factory.ts b/packages/order-utils/src/order_factory.ts index 444e5a0b29..5901d38c3e 100644 --- a/packages/order-utils/src/order_factory.ts +++ b/packages/order-utils/src/order_factory.ts @@ -10,6 +10,16 @@ import { generatePseudoRandomSalt } from './salt'; import { ecSignOrderHashAsync } from './signature_utils'; import { MessagePrefixType } from './types'; +export interface CreateOrderOpts { + takerAddress?: string; + senderAddress?: string; + makerFee?: BigNumber; + takerFee?: BigNumber; + feeRecipientAddress?: string; + salt?: BigNumber; + expirationTimeSeconds?: BigNumber; +} + export const orderFactory = { createOrder( makerAddress: string, @@ -18,28 +28,24 @@ export const orderFactory = { takerAssetAmount: BigNumber, takerAssetData: string, exchangeAddress: string, - takerAddress: string = constants.NULL_ADDRESS, - senderAddress: string = constants.NULL_ADDRESS, - makerFee: BigNumber = constants.ZERO_AMOUNT, - takerFee: BigNumber = constants.ZERO_AMOUNT, - feeRecipientAddress: string = constants.NULL_ADDRESS, - salt: BigNumber = generatePseudoRandomSalt(), - expirationTimeSeconds: BigNumber = constants.INFINITE_TIMESTAMP_SEC, + createOrderOpts: CreateOrderOpts = generateDefaultCreateOrderOpts(), ): Order { + const defaultCreateOrderOpts = generateDefaultCreateOrderOpts(); const order = { makerAddress, - takerAddress, - senderAddress, - makerFee, - takerFee, makerAssetAmount, takerAssetAmount, makerAssetData, takerAssetData, - salt, exchangeAddress, - feeRecipientAddress, - expirationTimeSeconds, + takerAddress: createOrderOpts.takerAddress || defaultCreateOrderOpts.takerAddress, + senderAddress: createOrderOpts.senderAddress || defaultCreateOrderOpts.senderAddress, + makerFee: createOrderOpts.makerFee || defaultCreateOrderOpts.makerFee, + takerFee: createOrderOpts.takerFee || defaultCreateOrderOpts.takerFee, + feeRecipientAddress: createOrderOpts.feeRecipientAddress || defaultCreateOrderOpts.feeRecipientAddress, + salt: createOrderOpts.salt || defaultCreateOrderOpts.salt, + expirationTimeSeconds: + createOrderOpts.expirationTimeSeconds || defaultCreateOrderOpts.expirationTimeSeconds, }; return order; }, @@ -51,13 +57,7 @@ export const orderFactory = { takerAssetAmount: BigNumber, takerAssetData: string, exchangeAddress: string, - takerAddress?: string, - senderAddress?: string, - makerFee?: BigNumber, - takerFee?: BigNumber, - feeRecipientAddress?: string, - salt?: BigNumber, - expirationTimeSeconds?: BigNumber, + createOrderOpts?: CreateOrderOpts, ): Promise { const order = orderFactory.createOrder( makerAddress, @@ -66,13 +66,7 @@ export const orderFactory = { takerAssetAmount, takerAssetData, exchangeAddress, - takerAddress, - senderAddress, - makerFee, - takerFee, - feeRecipientAddress, - salt, - expirationTimeSeconds, + createOrderOpts, ); const orderHash = orderHashUtils.getOrderHashHex(order); const messagePrefixOpts = { @@ -86,6 +80,26 @@ export const orderFactory = { }, }; +function generateDefaultCreateOrderOpts(): { + takerAddress: string; + senderAddress: string; + makerFee: BigNumber; + takerFee: BigNumber; + feeRecipientAddress: string; + salt: BigNumber; + expirationTimeSeconds: BigNumber; +} { + return { + takerAddress: constants.NULL_ADDRESS, + senderAddress: constants.NULL_ADDRESS, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + feeRecipientAddress: constants.NULL_ADDRESS, + salt: generatePseudoRandomSalt(), + expirationTimeSeconds: constants.INFINITE_TIMESTAMP_SEC, + }; +} + function getVRSHexString(ecSignature: ECSignature): string { const ETH_SIGN_SIGNATURE_TYPE = '03'; const vrs = `${intToHex(ecSignature.v)}${ethUtil.stripHexPrefix(ecSignature.r)}${ethUtil.stripHexPrefix( From 3cb955c136bf47b5f40cdbc44bcc4d19ec6d6453 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Sun, 5 Aug 2018 17:15:58 -0400 Subject: [PATCH 19/49] Move CreateOrderOpts into shared types --- packages/order-utils/src/index.ts | 10 +++++++++- packages/order-utils/src/order_factory.ts | 12 +----------- packages/order-utils/src/types.ts | 12 ++++++++++++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index 76be63bb8a..129eb0a3d1 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -13,7 +13,15 @@ export { orderFactory } from './order_factory'; export { constants } from './constants'; export { crypto } from './crypto'; export { generatePseudoRandomSalt } from './salt'; -export { OrderError, MessagePrefixType, MessagePrefixOpts, EIP712Parameter, EIP712Schema, EIP712Types } from './types'; +export { + CreateOrderOpts, + OrderError, + MessagePrefixType, + MessagePrefixOpts, + EIP712Parameter, + EIP712Schema, + EIP712Types, +} from './types'; export { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher'; export { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher'; export { BalanceAndProxyAllowanceLazyStore } from './store/balance_and_proxy_allowance_lazy_store'; diff --git a/packages/order-utils/src/order_factory.ts b/packages/order-utils/src/order_factory.ts index 5901d38c3e..14727fd976 100644 --- a/packages/order-utils/src/order_factory.ts +++ b/packages/order-utils/src/order_factory.ts @@ -8,17 +8,7 @@ import { constants } from './constants'; import { orderHashUtils } from './order_hash'; import { generatePseudoRandomSalt } from './salt'; import { ecSignOrderHashAsync } from './signature_utils'; -import { MessagePrefixType } from './types'; - -export interface CreateOrderOpts { - takerAddress?: string; - senderAddress?: string; - makerFee?: BigNumber; - takerFee?: BigNumber; - feeRecipientAddress?: string; - salt?: BigNumber; - expirationTimeSeconds?: BigNumber; -} +import { CreateOrderOpts, MessagePrefixType } from './types'; export const orderFactory = { createOrder( diff --git a/packages/order-utils/src/types.ts b/packages/order-utils/src/types.ts index b08e74e718..f44e943499 100644 --- a/packages/order-utils/src/types.ts +++ b/packages/order-utils/src/types.ts @@ -1,3 +1,5 @@ +import { BigNumber } from '@0xproject/utils'; + export enum OrderError { InvalidSignature = 'INVALID_SIGNATURE', } @@ -51,3 +53,13 @@ export enum EIP712Types { String = 'string', Uint256 = 'uint256', } + +export interface CreateOrderOpts { + takerAddress?: string; + senderAddress?: string; + makerFee?: BigNumber; + takerFee?: BigNumber; + feeRecipientAddress?: string; + salt?: BigNumber; + expirationTimeSeconds?: BigNumber; +} From 1c06380ef50ae401670d5de74ef2e32f972177ca Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Wed, 1 Aug 2018 23:14:24 -0700 Subject: [PATCH 20/49] Add findOrdersThatCoverMakerAssetFillAmount static method on ForwarderWrapper --- .../contract_wrappers/forwarder_wrapper.ts | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts index 13ef0fe01f..5e879b3a88 100644 --- a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts +++ b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts @@ -1,5 +1,5 @@ import { schemas } from '@0xproject/json-schemas'; -import { AssetProxyId, SignedOrder } from '@0xproject/types'; +import { AssetProxyId, OrderRelevantState, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import { ContractAbi } from 'ethereum-types'; @@ -24,6 +24,49 @@ export class ForwarderWrapper extends ContractWrapper { private _forwarderContractIfExists?: ForwarderContract; private _contractAddressIfExists?: string; private _zrxContractAddressIfExists?: string; + /** + * Takes an array of orders and returns a subset of those orders that has enough makerAssetAmount (taking into account on-chain balances, + * allowances, and partial fills) in order to fill the input makerAssetFillAmount plus slippageBufferAmount. Iterates from first order to last. + * Sort the input by rate in order to get the subset of orders that will cost the least ETH. + * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify the same makerAsset. + * All orders should specify WETH as the takerAsset. + * @param orderStates An array of objects corresponding to the signedOrders parameter that each contain on-chain state + * relevant to that order. + * @param makerAssetFillAmount The amount of makerAsset desired to be filled. + * @param slippageBufferAmount An additional amount makerAsset to be covered by the result in case of trade collisions or partial fills. + * @return Resulting orders and remaining fill amount that could not be covered by the input. + */ + public static findOrdersThatCoverMakerAssetFillAmount( + signedOrders: SignedOrder[], + orderStates: OrderRelevantState[], + makerAssetFillAmount: BigNumber, + slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT, + ): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } { + // type assertions + assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); + assert.isBigNumber('makerAssetFillAmount', makerAssetFillAmount); + assert.isBigNumber('slippageBufferAmount', slippageBufferAmount); + // calculate total amount of makerAsset needed to fill + const totalFillAmount = makerAssetFillAmount.plus(slippageBufferAmount); + // iterate through the signedOrders input from left to right until we have enough makerAsset to fill totalFillAmount + const result = _.reduce( + signedOrders, + ({ resultOrders, remainingFillAmount }, order, index) => { + if (remainingFillAmount.lessThanOrEqualTo(constants.ZERO_AMOUNT)) { + return { resultOrders, remainingFillAmount }; + } else { + const orderState = orderStates[index]; + const orderRemainingFillableMakerAssetAmount = orderState.remainingFillableMakerAssetAmount; + return { + resultOrders: _.concat(resultOrders, order), + remainingFillAmount: remainingFillAmount.minus(orderState.remainingFillableMakerAssetAmount), + }; + } + }, + { resultOrders: [] as SignedOrder[], remainingFillAmount: totalFillAmount }, + ); + return result; + } constructor( web3Wrapper: Web3Wrapper, networkId: number, From a016747c36e0bc2c182e3d2b565ca63fb95e2b1b Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Thu, 2 Aug 2018 10:55:09 -0700 Subject: [PATCH 21/49] Add findFeeOrdersThatCoverFeesForTargetOrders to ForwarderWrapper --- .../contract_wrappers/forwarder_wrapper.ts | 65 +++++++++++++++++-- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts index 5e879b3a88..c0961b3a34 100644 --- a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts +++ b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts @@ -27,7 +27,7 @@ export class ForwarderWrapper extends ContractWrapper { /** * Takes an array of orders and returns a subset of those orders that has enough makerAssetAmount (taking into account on-chain balances, * allowances, and partial fills) in order to fill the input makerAssetFillAmount plus slippageBufferAmount. Iterates from first order to last. - * Sort the input by rate in order to get the subset of orders that will cost the least ETH. + * Sort the input by ascending rate in order to get the subset of orders that will cost the least ETH. * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify the same makerAsset. * All orders should specify WETH as the takerAsset. * @param orderStates An array of objects corresponding to the signedOrders parameter that each contain on-chain state @@ -46,20 +46,20 @@ export class ForwarderWrapper extends ContractWrapper { assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); assert.isBigNumber('makerAssetFillAmount', makerAssetFillAmount); assert.isBigNumber('slippageBufferAmount', slippageBufferAmount); - // calculate total amount of makerAsset needed to fill + // calculate total amount of makerAsset needed to be filled const totalFillAmount = makerAssetFillAmount.plus(slippageBufferAmount); // iterate through the signedOrders input from left to right until we have enough makerAsset to fill totalFillAmount const result = _.reduce( signedOrders, ({ resultOrders, remainingFillAmount }, order, index) => { if (remainingFillAmount.lessThanOrEqualTo(constants.ZERO_AMOUNT)) { - return { resultOrders, remainingFillAmount }; + return { resultOrders, remainingFillAmount: constants.ZERO_AMOUNT }; } else { const orderState = orderStates[index]; - const orderRemainingFillableMakerAssetAmount = orderState.remainingFillableMakerAssetAmount; + const makerAssetAmountAvailable = ForwarderWrapper._getMakerAssetAmountAvailable(orderState); return { resultOrders: _.concat(resultOrders, order), - remainingFillAmount: remainingFillAmount.minus(orderState.remainingFillableMakerAssetAmount), + remainingFillAmount: remainingFillAmount.minus(makerAssetAmountAvailable), }; } }, @@ -67,6 +67,61 @@ export class ForwarderWrapper extends ContractWrapper { ); return result; } + /** + * Takes an array of orders and an array of feeOrders. Returns a subset of the feeOrders that has enough ZRX (taking into account + * on-chain balances, allowances, and partial fills) in order to fill the takerFees required by signedOrders plus a + * slippageBufferAmount. Iterates from first feeOrder to last. Sort the feeOrders by ascending rate in order to get the subset of + * feeOrders that will cost the least ETH. + * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as + * the makerAsset and WETH as the takerAsset. + * @param orderStates An array of objects corresponding to the signedOrders parameter that each contain on-chain state + * relevant to that order. + * @param signedFeeOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as + * the makerAsset and WETH as the takerAsset. + * @param feeOrderStates An array of objects corresponding to the signedOrders parameter that each contain on-chain state + * relevant to that order. + * @param makerAssetFillAmount The amount of makerAsset desired to be filled. + * @param slippageBufferAmount An additional amount makerAsset to be covered by the result in case of trade collisions or partial fills. + * @return Resulting orders and remaining fill amount that could not be covered by the input. + */ + public static findFeeOrdersThatCoverFeesForTargetOrders( + signedOrders: SignedOrder[], + orderStates: OrderRelevantState[], + signedFeeOrders: SignedOrder[], + feeOrderStates: OrderRelevantState[], + slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT, + ): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } { + // type assertions + assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); + assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema); + assert.isBigNumber('slippageBufferAmount', slippageBufferAmount); + // calculate total amount of ZRX needed to fill signedOrders + const totalFeeAmount = _.reduce( + signedOrders, + (accFees, order, index) => { + const orderState = orderStates[index]; + const makerAssetAmountAvailable = ForwarderWrapper._getMakerAssetAmountAvailable(orderState); + const feeToFillMakerAssetAmountAvailable = makerAssetAmountAvailable + .div(order.makerAssetAmount) + .mul(order.takerFee); + return feeToFillMakerAssetAmountAvailable; + }, + constants.ZERO_AMOUNT, + ); + return ForwarderWrapper.findOrdersThatCoverMakerAssetFillAmount( + signedFeeOrders, + feeOrderStates, + totalFeeAmount, + slippageBufferAmount, + ); + } + private static _getMakerAssetAmountAvailable(orderState: OrderRelevantState): BigNumber { + return BigNumber.min( + orderState.makerBalance, + orderState.remainingFillableMakerAssetAmount, + orderState.makerProxyAllowance, + ); + } constructor( web3Wrapper: Web3Wrapper, networkId: number, From d9933237a0be069d84944e4d4f1b3dffe6bb7643 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Thu, 2 Aug 2018 11:21:05 -0700 Subject: [PATCH 22/49] Move helper functions into order-utils --- .../contract_wrappers/forwarder_wrapper.ts | 100 +--------------- packages/order-utils/src/constants.ts | 2 +- packages/order-utils/src/market_utils.ts | 109 ++++++++++++++++++ 3 files changed, 111 insertions(+), 100 deletions(-) create mode 100644 packages/order-utils/src/market_utils.ts diff --git a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts index c0961b3a34..13ef0fe01f 100644 --- a/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts +++ b/packages/contract-wrappers/src/contract_wrappers/forwarder_wrapper.ts @@ -1,5 +1,5 @@ import { schemas } from '@0xproject/json-schemas'; -import { AssetProxyId, OrderRelevantState, SignedOrder } from '@0xproject/types'; +import { AssetProxyId, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import { ContractAbi } from 'ethereum-types'; @@ -24,104 +24,6 @@ export class ForwarderWrapper extends ContractWrapper { private _forwarderContractIfExists?: ForwarderContract; private _contractAddressIfExists?: string; private _zrxContractAddressIfExists?: string; - /** - * Takes an array of orders and returns a subset of those orders that has enough makerAssetAmount (taking into account on-chain balances, - * allowances, and partial fills) in order to fill the input makerAssetFillAmount plus slippageBufferAmount. Iterates from first order to last. - * Sort the input by ascending rate in order to get the subset of orders that will cost the least ETH. - * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify the same makerAsset. - * All orders should specify WETH as the takerAsset. - * @param orderStates An array of objects corresponding to the signedOrders parameter that each contain on-chain state - * relevant to that order. - * @param makerAssetFillAmount The amount of makerAsset desired to be filled. - * @param slippageBufferAmount An additional amount makerAsset to be covered by the result in case of trade collisions or partial fills. - * @return Resulting orders and remaining fill amount that could not be covered by the input. - */ - public static findOrdersThatCoverMakerAssetFillAmount( - signedOrders: SignedOrder[], - orderStates: OrderRelevantState[], - makerAssetFillAmount: BigNumber, - slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT, - ): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } { - // type assertions - assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); - assert.isBigNumber('makerAssetFillAmount', makerAssetFillAmount); - assert.isBigNumber('slippageBufferAmount', slippageBufferAmount); - // calculate total amount of makerAsset needed to be filled - const totalFillAmount = makerAssetFillAmount.plus(slippageBufferAmount); - // iterate through the signedOrders input from left to right until we have enough makerAsset to fill totalFillAmount - const result = _.reduce( - signedOrders, - ({ resultOrders, remainingFillAmount }, order, index) => { - if (remainingFillAmount.lessThanOrEqualTo(constants.ZERO_AMOUNT)) { - return { resultOrders, remainingFillAmount: constants.ZERO_AMOUNT }; - } else { - const orderState = orderStates[index]; - const makerAssetAmountAvailable = ForwarderWrapper._getMakerAssetAmountAvailable(orderState); - return { - resultOrders: _.concat(resultOrders, order), - remainingFillAmount: remainingFillAmount.minus(makerAssetAmountAvailable), - }; - } - }, - { resultOrders: [] as SignedOrder[], remainingFillAmount: totalFillAmount }, - ); - return result; - } - /** - * Takes an array of orders and an array of feeOrders. Returns a subset of the feeOrders that has enough ZRX (taking into account - * on-chain balances, allowances, and partial fills) in order to fill the takerFees required by signedOrders plus a - * slippageBufferAmount. Iterates from first feeOrder to last. Sort the feeOrders by ascending rate in order to get the subset of - * feeOrders that will cost the least ETH. - * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as - * the makerAsset and WETH as the takerAsset. - * @param orderStates An array of objects corresponding to the signedOrders parameter that each contain on-chain state - * relevant to that order. - * @param signedFeeOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as - * the makerAsset and WETH as the takerAsset. - * @param feeOrderStates An array of objects corresponding to the signedOrders parameter that each contain on-chain state - * relevant to that order. - * @param makerAssetFillAmount The amount of makerAsset desired to be filled. - * @param slippageBufferAmount An additional amount makerAsset to be covered by the result in case of trade collisions or partial fills. - * @return Resulting orders and remaining fill amount that could not be covered by the input. - */ - public static findFeeOrdersThatCoverFeesForTargetOrders( - signedOrders: SignedOrder[], - orderStates: OrderRelevantState[], - signedFeeOrders: SignedOrder[], - feeOrderStates: OrderRelevantState[], - slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT, - ): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } { - // type assertions - assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); - assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema); - assert.isBigNumber('slippageBufferAmount', slippageBufferAmount); - // calculate total amount of ZRX needed to fill signedOrders - const totalFeeAmount = _.reduce( - signedOrders, - (accFees, order, index) => { - const orderState = orderStates[index]; - const makerAssetAmountAvailable = ForwarderWrapper._getMakerAssetAmountAvailable(orderState); - const feeToFillMakerAssetAmountAvailable = makerAssetAmountAvailable - .div(order.makerAssetAmount) - .mul(order.takerFee); - return feeToFillMakerAssetAmountAvailable; - }, - constants.ZERO_AMOUNT, - ); - return ForwarderWrapper.findOrdersThatCoverMakerAssetFillAmount( - signedFeeOrders, - feeOrderStates, - totalFeeAmount, - slippageBufferAmount, - ); - } - private static _getMakerAssetAmountAvailable(orderState: OrderRelevantState): BigNumber { - return BigNumber.min( - orderState.makerBalance, - orderState.remainingFillableMakerAssetAmount, - orderState.makerProxyAllowance, - ); - } constructor( web3Wrapper: Web3Wrapper, networkId: number, diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index ea3f8b9322..b18546a6cb 100644 --- a/packages/order-utils/src/constants.ts +++ b/packages/order-utils/src/constants.ts @@ -10,6 +10,6 @@ export const constants = { ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH: 53, SELECTOR_LENGTH: 4, BASE_16: 16, - INFINITE_TIMESTAMP_SEC: new BigNumber(2524604400), // Close to infinite + INFINITE_TIMESTAMP_SEC: new BigNumber(2524604400), // Close to infinite, ZERO_AMOUNT: new BigNumber(0), }; diff --git a/packages/order-utils/src/market_utils.ts b/packages/order-utils/src/market_utils.ts new file mode 100644 index 0000000000..4ddcc6ec8f --- /dev/null +++ b/packages/order-utils/src/market_utils.ts @@ -0,0 +1,109 @@ +import { schemas } from '@0xproject/json-schemas'; +import { OrderRelevantState, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { assert } from './assert'; +import { constants } from './constants'; + +export const marketUtils = { + /** + * Takes an array of orders and returns a subset of those orders that has enough makerAssetAmount (taking into account on-chain balances, + * allowances, and partial fills) in order to fill the input makerAssetFillAmount plus slippageBufferAmount. Iterates from first order to last. + * Sort the input by ascending rate in order to get the subset of orders that will cost the least ETH. + * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify the same makerAsset. + * All orders should specify WETH as the takerAsset. + * @param orderStates An array of objects corresponding to the signedOrders parameter that each contain on-chain state + * relevant to that order. + * @param makerAssetFillAmount The amount of makerAsset desired to be filled. + * @param slippageBufferAmount An additional amount makerAsset to be covered by the result in case of trade collisions or partial fills. + * @return Resulting orders and remaining fill amount that could not be covered by the input. + */ + findOrdersThatCoverMakerAssetFillAmount( + signedOrders: SignedOrder[], + orderStates: OrderRelevantState[], + makerAssetFillAmount: BigNumber, + slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT, + ): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } { + // type assertions + assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); + assert.isBigNumber('makerAssetFillAmount', makerAssetFillAmount); + assert.isBigNumber('slippageBufferAmount', slippageBufferAmount); + // calculate total amount of makerAsset needed to be filled + const totalFillAmount = makerAssetFillAmount.plus(slippageBufferAmount); + // iterate through the signedOrders input from left to right until we have enough makerAsset to fill totalFillAmount + const result = _.reduce( + signedOrders, + ({ resultOrders, remainingFillAmount }, order, index) => { + if (remainingFillAmount.lessThanOrEqualTo(constants.ZERO_AMOUNT)) { + return { resultOrders, remainingFillAmount: constants.ZERO_AMOUNT }; + } else { + const orderState = orderStates[index]; + const makerAssetAmountAvailable = getMakerAssetAmountAvailable(orderState); + return { + resultOrders: _.concat(resultOrders, order), + remainingFillAmount: remainingFillAmount.minus(makerAssetAmountAvailable), + }; + } + }, + { resultOrders: [] as SignedOrder[], remainingFillAmount: totalFillAmount }, + ); + return result; + }, + /** + * Takes an array of orders and an array of feeOrders. Returns a subset of the feeOrders that has enough ZRX (taking into account + * on-chain balances, allowances, and partial fills) in order to fill the takerFees required by signedOrders plus a + * slippageBufferAmount. Iterates from first feeOrder to last. Sort the feeOrders by ascending rate in order to get the subset of + * feeOrders that will cost the least ETH. + * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as + * the makerAsset and WETH as the takerAsset. + * @param orderStates An array of objects corresponding to the signedOrders parameter that each contain on-chain state + * relevant to that order. + * @param signedFeeOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as + * the makerAsset and WETH as the takerAsset. + * @param feeOrderStates An array of objects corresponding to the signedOrders parameter that each contain on-chain state + * relevant to that order. + * @param makerAssetFillAmount The amount of makerAsset desired to be filled. + * @param slippageBufferAmount An additional amount makerAsset to be covered by the result in case of trade collisions or partial fills. + * @return Resulting orders and remaining fill amount that could not be covered by the input. + */ + findFeeOrdersThatCoverFeesForTargetOrders( + signedOrders: SignedOrder[], + orderStates: OrderRelevantState[], + signedFeeOrders: SignedOrder[], + feeOrderStates: OrderRelevantState[], + slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT, + ): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } { + // type assertions + assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); + assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema); + assert.isBigNumber('slippageBufferAmount', slippageBufferAmount); + // calculate total amount of ZRX needed to fill signedOrders + const totalFeeAmount = _.reduce( + signedOrders, + (accFees, order, index) => { + const orderState = orderStates[index]; + const makerAssetAmountAvailable = getMakerAssetAmountAvailable(orderState); + const feeToFillMakerAssetAmountAvailable = makerAssetAmountAvailable + .div(order.makerAssetAmount) + .mul(order.takerFee); + return accFees.plus(feeToFillMakerAssetAmountAvailable); + }, + constants.ZERO_AMOUNT, + ); + return marketUtils.findOrdersThatCoverMakerAssetFillAmount( + signedFeeOrders, + feeOrderStates, + totalFeeAmount, + slippageBufferAmount, + ); + }, +}; + +const getMakerAssetAmountAvailable = (orderState: OrderRelevantState) => { + return BigNumber.min( + orderState.makerBalance, + orderState.remainingFillableMakerAssetAmount, + orderState.makerProxyAllowance, + ); +}; From e5d65b585a2b0a159f50320eaf5bfec05a869478 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Thu, 2 Aug 2018 12:51:12 -0700 Subject: [PATCH 23/49] Update hex schema to match 0x --- packages/json-schemas/schemas/basic_type_schemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/json-schemas/schemas/basic_type_schemas.ts b/packages/json-schemas/schemas/basic_type_schemas.ts index 7565df9e07..301f9ec73e 100644 --- a/packages/json-schemas/schemas/basic_type_schemas.ts +++ b/packages/json-schemas/schemas/basic_type_schemas.ts @@ -7,7 +7,7 @@ export const addressSchema = { export const hexSchema = { id: '/Hex', type: 'string', - pattern: '^0x([0-9a-f][0-9a-f])+$', + pattern: '^0x(([0-9a-f][0-9a-f])+)?$', }; export const numberSchema = { From 09c0fc94fc91134acfdee1017d7a50e2047b019b Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Thu, 2 Aug 2018 13:22:04 -0700 Subject: [PATCH 24/49] Implement first round of tests for findOrdersThatCoverMakerAssetFillAmount --- packages/order-utils/src/constants.ts | 1 + packages/order-utils/src/index.ts | 1 + packages/order-utils/src/market_utils.ts | 23 ++- .../order-utils/test/market_utils_test.ts | 146 ++++++++++++++++++ .../test/utils/test_order_factory.ts | 63 ++++++++ 5 files changed, 222 insertions(+), 12 deletions(-) create mode 100644 packages/order-utils/test/market_utils_test.ts create mode 100644 packages/order-utils/test/utils/test_order_factory.ts diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index b18546a6cb..5137ff4990 100644 --- a/packages/order-utils/src/constants.ts +++ b/packages/order-utils/src/constants.ts @@ -2,6 +2,7 @@ import { BigNumber } from '@0xproject/utils'; export const constants = { NULL_ADDRESS: '0x0000000000000000000000000000000000000000', + NULL_BYTES: '0x', // tslint:disable-next-line:custom-no-magic-numbers UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1), TESTRPC_NETWORK_ID: 50, diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index 129eb0a3d1..858f500c62 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -32,3 +32,4 @@ export { assetDataUtils } from './asset_data_utils'; export { EIP712Utils } from './eip712_utils'; export { OrderValidationUtils } from './order_validation_utils'; export { ExchangeTransferSimulator } from './exchange_transfer_simulator'; +export { marketUtils } from './market_utils'; diff --git a/packages/order-utils/src/market_utils.ts b/packages/order-utils/src/market_utils.ts index 4ddcc6ec8f..710eddf8a1 100644 --- a/packages/order-utils/src/market_utils.ts +++ b/packages/order-utils/src/market_utils.ts @@ -39,10 +39,17 @@ export const marketUtils = { return { resultOrders, remainingFillAmount: constants.ZERO_AMOUNT }; } else { const orderState = orderStates[index]; - const makerAssetAmountAvailable = getMakerAssetAmountAvailable(orderState); + const makerAssetAmountAvailable = orderState.remainingFillableMakerAssetAmount; + // if there is no makerAssetAmountAvailable do not append order to resultOrders + // if we have exceeded the total amount we want to fill set remainingFillAmount to 0 return { - resultOrders: _.concat(resultOrders, order), - remainingFillAmount: remainingFillAmount.minus(makerAssetAmountAvailable), + resultOrders: makerAssetAmountAvailable.gt(constants.ZERO_AMOUNT) + ? _.concat(resultOrders, order) + : resultOrders, + remainingFillAmount: BigNumber.max( + constants.ZERO_AMOUNT, + remainingFillAmount.minus(makerAssetAmountAvailable), + ), }; } }, @@ -83,7 +90,7 @@ export const marketUtils = { signedOrders, (accFees, order, index) => { const orderState = orderStates[index]; - const makerAssetAmountAvailable = getMakerAssetAmountAvailable(orderState); + const makerAssetAmountAvailable = orderState.remainingFillableMakerAssetAmount; const feeToFillMakerAssetAmountAvailable = makerAssetAmountAvailable .div(order.makerAssetAmount) .mul(order.takerFee); @@ -99,11 +106,3 @@ export const marketUtils = { ); }, }; - -const getMakerAssetAmountAvailable = (orderState: OrderRelevantState) => { - return BigNumber.min( - orderState.makerBalance, - orderState.remainingFillableMakerAssetAmount, - orderState.makerProxyAllowance, - ); -}; diff --git a/packages/order-utils/test/market_utils_test.ts b/packages/order-utils/test/market_utils_test.ts new file mode 100644 index 0000000000..93779d0351 --- /dev/null +++ b/packages/order-utils/test/market_utils_test.ts @@ -0,0 +1,146 @@ +import { OrderRelevantState, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; +import 'mocha'; + +import { constants, marketUtils, orderFactory } from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { testOrderFactory } from './utils/test_order_factory'; + +chaiSetup.configure(); +const expect = chai.expect; + +// tslint:disable: no-unused-expression +describe('marketUtils', () => { + describe.only('#findOrdersThatCoverMakerAssetFillAmount', () => { + describe('no orders', () => { + it('returns empty and unchanged remainingFillAmount', async () => { + const fillAmount = new BigNumber(10); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + [], + [], + fillAmount, + ); + expect(resultOrders).to.be.empty; + expect(remainingFillAmount).to.be.bignumber.equal(fillAmount); + }); + }); + describe('orders are all completely fillable', () => { + // generate three signed orders each with 10 units of makerAsset, 30 total + const testOrderCount = 3; + const makerAssetAmount = new BigNumber(10); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + }, + testOrderCount, + ); + // generate order states that cover the required fill amount + const inputOrderStates = testOrderFactory.generateTestOrderRelevantStates( + { + remainingFillableMakerAssetAmount: makerAssetAmount, + }, + testOrderCount, + ); + it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => { + // try to fill 30 units of makerAsset + const fillAmount = new BigNumber(30); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + inputOrders, + inputOrderStates, + fillAmount, + ); + expect(resultOrders).to.be.deep.equal(inputOrders); + expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => { + // try to fill 25 units of makerAsset + const fillAmount = new BigNumber(25); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + inputOrders, + inputOrderStates, + fillAmount, + ); + expect(resultOrders).to.be.deep.equal(inputOrders); + expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => { + // try to fill 35 units of makerAsset + const fillAmount = new BigNumber(35); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + inputOrders, + inputOrderStates, + fillAmount, + ); + expect(resultOrders).to.be.deep.equal(inputOrders); + expect(remainingFillAmount).to.be.bignumber.equal(new BigNumber(5)); + }); + it('returns first order and zero remainingFillAmount when requested fill amount is exactly covered by the first order', async () => { + // try to fill 10 units of makerAsset + const fillAmount = new BigNumber(10); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + inputOrders, + inputOrderStates, + fillAmount, + ); + expect(resultOrders).to.be.deep.equal([inputOrders[0]]); + expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('returns first two orders and zero remainingFillAmount when requested fill amount is over covered by the first two order', async () => { + // try to fill 15 units of makerAsset + const fillAmount = new BigNumber(15); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + inputOrders, + inputOrderStates, + fillAmount, + ); + expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1]]); + expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + }); + describe('orders are partially fillable', () => { + // generate three signed orders each with 10 units of makerAsset, 30 total + const testOrderCount = 3; + const makerAssetAmount = new BigNumber(10); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + }, + testOrderCount, + ); + // generate order states that cover different scenarios + // 1. order is completely filled already + // 2. order is partially fillable + // 3. order is completely fillable + const partialOrderStates: Array> = [ + { + remainingFillableMakerAssetAmount: constants.ZERO_AMOUNT, + }, + { + remainingFillableMakerAssetAmount: new BigNumber(5), + }, + { + remainingFillableMakerAssetAmount: makerAssetAmount, + }, + ]; + const inputOrderStates: OrderRelevantState[] = _.map( + partialOrderStates, + testOrderFactory.generateTestOrderRelevantState, + ); + it('returns last 2 orders and non-zero remainingFillAmount when trying to fill original makerAssetAmounts', async () => { + // try to fill 30 units of makerAsset + const fillAmount = new BigNumber(30); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + inputOrders, + inputOrderStates, + fillAmount, + ); + expect(resultOrders).to.be.deep.equal([inputOrders[1], inputOrders[2]]); + expect(remainingFillAmount).to.be.bignumber.equal(new BigNumber(15)); + }); + }); + }); + describe('#findFeeOrdersThatCoverFeesForTargetOrders', () => {}); +}); diff --git a/packages/order-utils/test/utils/test_order_factory.ts b/packages/order-utils/test/utils/test_order_factory.ts new file mode 100644 index 0000000000..2c5d8cf614 --- /dev/null +++ b/packages/order-utils/test/utils/test_order_factory.ts @@ -0,0 +1,63 @@ +import { Order, OrderRelevantState, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { constants, orderFactory } from '../../src'; + +const BASE_TEST_ORDER: Order = orderFactory.createOrder( + constants.NULL_ADDRESS, + constants.NULL_ADDRESS, + constants.NULL_ADDRESS, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + constants.NULL_BYTES, + constants.ZERO_AMOUNT, + constants.NULL_BYTES, + constants.NULL_ADDRESS, + constants.NULL_ADDRESS, +); +const BASE_TEST_SIGNED_ORDER: SignedOrder = { + ...BASE_TEST_ORDER, + signature: constants.NULL_BYTES, +}; +const BASE_TEST_ORDER_RELEVANT_STATE: OrderRelevantState = { + makerBalance: constants.ZERO_AMOUNT, + makerProxyAllowance: constants.ZERO_AMOUNT, + makerFeeBalance: constants.ZERO_AMOUNT, + makerFeeProxyAllowance: constants.ZERO_AMOUNT, + filledTakerAssetAmount: constants.ZERO_AMOUNT, + remainingFillableMakerAssetAmount: constants.ZERO_AMOUNT, + remainingFillableTakerAssetAmount: constants.ZERO_AMOUNT, +}; + +export const testOrderFactory = { + generateTestSignedOrder(partialOrder: Partial): SignedOrder { + return transformObject(BASE_TEST_SIGNED_ORDER, partialOrder); + }, + generateTestSignedOrders(partialOrder: Partial, numOrders: number): SignedOrder[] { + const baseTestOrders = generateArrayOfInput(BASE_TEST_SIGNED_ORDER, numOrders); + return transformObjects(baseTestOrders, partialOrder); + }, + generateTestOrderRelevantState(partialOrderRelevantState: Partial): OrderRelevantState { + return transformObject(BASE_TEST_ORDER_RELEVANT_STATE, partialOrderRelevantState); + }, + generateTestOrderRelevantStates( + partialOrderRelevantState: Partial, + numOrderStates: number, + ): OrderRelevantState[] { + const baseTestOrderStates = generateArrayOfInput(BASE_TEST_ORDER_RELEVANT_STATE, numOrderStates); + return transformObjects(baseTestOrderStates, partialOrderRelevantState); + }, +}; + +function generateArrayOfInput(input: T, rangeLength: number): T[] { + return _.map(_.range(rangeLength), () => input); +} +function transformObject(input: T, transformation: Partial): T { + const copy = _.cloneDeep(input); + return _.assign(copy, transformation); +} +function transformObjects(inputs: T[], transformation: Partial): T[] { + return _.map(inputs, input => transformObject(input, transformation)); +} From bc5f8e52de9dfe920ce1d0e6b44c90a5a5826cbe Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Thu, 2 Aug 2018 15:47:29 -0700 Subject: [PATCH 25/49] Change orderStates param name to remaingFillableMakerAssetAmounts --- packages/order-utils/src/market_utils.ts | 85 ++++++++++++------- .../order-utils/test/market_utils_test.ts | 43 +++------- .../test/utils/test_order_factory.ts | 32 +------ 3 files changed, 69 insertions(+), 91 deletions(-) diff --git a/packages/order-utils/src/market_utils.ts b/packages/order-utils/src/market_utils.ts index 710eddf8a1..d66448a0bd 100644 --- a/packages/order-utils/src/market_utils.ts +++ b/packages/order-utils/src/market_utils.ts @@ -1,5 +1,5 @@ import { schemas } from '@0xproject/json-schemas'; -import { OrderRelevantState, SignedOrder } from '@0xproject/types'; +import { SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; @@ -11,24 +11,33 @@ export const marketUtils = { * Takes an array of orders and returns a subset of those orders that has enough makerAssetAmount (taking into account on-chain balances, * allowances, and partial fills) in order to fill the input makerAssetFillAmount plus slippageBufferAmount. Iterates from first order to last. * Sort the input by ascending rate in order to get the subset of orders that will cost the least ETH. - * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify the same makerAsset. - * All orders should specify WETH as the takerAsset. - * @param orderStates An array of objects corresponding to the signedOrders parameter that each contain on-chain state - * relevant to that order. - * @param makerAssetFillAmount The amount of makerAsset desired to be filled. - * @param slippageBufferAmount An additional amount makerAsset to be covered by the result in case of trade collisions or partial fills. + * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify the same makerAsset. + * All orders should specify WETH as the takerAsset. + * @param remainingFillableMakerAssetAmounts An array of BigNumbers corresponding to the signedOrders parameter. + * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups + * for these values. + * @param makerAssetFillAmount The amount of makerAsset desired to be filled. + * @param slippageBufferAmount An additional amount makerAsset to be covered by the result in case of trade collisions or partial fills. * @return Resulting orders and remaining fill amount that could not be covered by the input. */ findOrdersThatCoverMakerAssetFillAmount( signedOrders: SignedOrder[], - orderStates: OrderRelevantState[], + remainingFillableMakerAssetAmounts: BigNumber[], makerAssetFillAmount: BigNumber, slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT, ): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } { // type assertions assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); - assert.isBigNumber('makerAssetFillAmount', makerAssetFillAmount); - assert.isBigNumber('slippageBufferAmount', slippageBufferAmount); + _.forEach(remainingFillableMakerAssetAmounts, (amount, index) => + assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount), + ); + assert.isValidBaseUnitAmount('makerAssetFillAmount', makerAssetFillAmount); + assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount); + // other assertions + assert.assert( + signedOrders.length === remainingFillableMakerAssetAmounts.length, + 'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length', + ); // calculate total amount of makerAsset needed to be filled const totalFillAmount = makerAssetFillAmount.plus(slippageBufferAmount); // iterate through the signedOrders input from left to right until we have enough makerAsset to fill totalFillAmount @@ -38,8 +47,7 @@ export const marketUtils = { if (remainingFillAmount.lessThanOrEqualTo(constants.ZERO_AMOUNT)) { return { resultOrders, remainingFillAmount: constants.ZERO_AMOUNT }; } else { - const orderState = orderStates[index]; - const makerAssetAmountAvailable = orderState.remainingFillableMakerAssetAmount; + const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index]; // if there is no makerAssetAmountAvailable do not append order to resultOrders // if we have exceeded the total amount we want to fill set remainingFillAmount to 0 return { @@ -62,47 +70,64 @@ export const marketUtils = { * on-chain balances, allowances, and partial fills) in order to fill the takerFees required by signedOrders plus a * slippageBufferAmount. Iterates from first feeOrder to last. Sort the feeOrders by ascending rate in order to get the subset of * feeOrders that will cost the least ETH. - * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as - * the makerAsset and WETH as the takerAsset. - * @param orderStates An array of objects corresponding to the signedOrders parameter that each contain on-chain state - * relevant to that order. - * @param signedFeeOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as - * the makerAsset and WETH as the takerAsset. - * @param feeOrderStates An array of objects corresponding to the signedOrders parameter that each contain on-chain state - * relevant to that order. - * @param makerAssetFillAmount The amount of makerAsset desired to be filled. - * @param slippageBufferAmount An additional amount makerAsset to be covered by the result in case of trade collisions or partial fills. + * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as + * the makerAsset and WETH as the takerAsset. + * @param remainingFillableMakerAssetAmounts An array of BigNumbers corresponding to the signedOrders parameter. + * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups + * for these values. + * @param signedFeeOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as + * the makerAsset and WETH as the takerAsset. + * @param remainingFillableFeeAmounts An array of BigNumbers corresponding to the signedFeeOrders parameter. + * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups + * for these values. + * @param slippageBufferAmount An additional amount makerAsset to be covered by the result in case of trade collisions or partial fills. * @return Resulting orders and remaining fill amount that could not be covered by the input. */ findFeeOrdersThatCoverFeesForTargetOrders( signedOrders: SignedOrder[], - orderStates: OrderRelevantState[], + remainingFillableMakerAssetAmounts: BigNumber[], signedFeeOrders: SignedOrder[], - feeOrderStates: OrderRelevantState[], + remainingFillableFeeAmounts: BigNumber[], slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT, ): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } { // type assertions assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); + _.forEach(remainingFillableMakerAssetAmounts, (amount, index) => + assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount), + ); assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema); - assert.isBigNumber('slippageBufferAmount', slippageBufferAmount); + _.forEach(remainingFillableFeeAmounts, (amount, index) => + assert.isValidBaseUnitAmount(`remainingFillableFeeAmounts[${index}]`, amount), + ); + assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount); + // other assertions + assert.assert( + signedOrders.length === remainingFillableMakerAssetAmounts.length, + 'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length', + ); + assert.assert( + signedOrders.length === remainingFillableMakerAssetAmounts.length, + 'Expected signedFeeOrders.length to equal remainingFillableFeeAmounts.length', + ); // calculate total amount of ZRX needed to fill signedOrders const totalFeeAmount = _.reduce( signedOrders, (accFees, order, index) => { - const orderState = orderStates[index]; - const makerAssetAmountAvailable = orderState.remainingFillableMakerAssetAmount; + const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index]; const feeToFillMakerAssetAmountAvailable = makerAssetAmountAvailable - .div(order.makerAssetAmount) - .mul(order.takerFee); + .mul(order.takerFee) + .div(order.makerAssetAmount); return accFees.plus(feeToFillMakerAssetAmountAvailable); }, constants.ZERO_AMOUNT, ); return marketUtils.findOrdersThatCoverMakerAssetFillAmount( signedFeeOrders, - feeOrderStates, + remainingFillableFeeAmounts, totalFeeAmount, slippageBufferAmount, ); + // TODO: add more orders here to cover rounding + // https://github.com/0xProject/0x-protocol-specification/blob/master/v2/forwarding-contract-specification.md#over-buying-zrx }, }; diff --git a/packages/order-utils/test/market_utils_test.ts b/packages/order-utils/test/market_utils_test.ts index 93779d0351..ac3fb9b939 100644 --- a/packages/order-utils/test/market_utils_test.ts +++ b/packages/order-utils/test/market_utils_test.ts @@ -1,10 +1,8 @@ -import { OrderRelevantState, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import * as chai from 'chai'; -import * as _ from 'lodash'; import 'mocha'; -import { constants, marketUtils, orderFactory } from '../src'; +import { constants, marketUtils } from '../src'; import { chaiSetup } from './utils/chai_setup'; import { testOrderFactory } from './utils/test_order_factory'; @@ -37,19 +35,14 @@ describe('marketUtils', () => { }, testOrderCount, ); - // generate order states that cover the required fill amount - const inputOrderStates = testOrderFactory.generateTestOrderRelevantStates( - { - remainingFillableMakerAssetAmount: makerAssetAmount, - }, - testOrderCount, - ); + // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount + const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount]; it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => { // try to fill 30 units of makerAsset const fillAmount = new BigNumber(30); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( inputOrders, - inputOrderStates, + remainingFillableMakerAssetAmounts, fillAmount, ); expect(resultOrders).to.be.deep.equal(inputOrders); @@ -60,7 +53,7 @@ describe('marketUtils', () => { const fillAmount = new BigNumber(25); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( inputOrders, - inputOrderStates, + remainingFillableMakerAssetAmounts, fillAmount, ); expect(resultOrders).to.be.deep.equal(inputOrders); @@ -71,7 +64,7 @@ describe('marketUtils', () => { const fillAmount = new BigNumber(35); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( inputOrders, - inputOrderStates, + remainingFillableMakerAssetAmounts, fillAmount, ); expect(resultOrders).to.be.deep.equal(inputOrders); @@ -82,7 +75,7 @@ describe('marketUtils', () => { const fillAmount = new BigNumber(10); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( inputOrders, - inputOrderStates, + remainingFillableMakerAssetAmounts, fillAmount, ); expect(resultOrders).to.be.deep.equal([inputOrders[0]]); @@ -93,7 +86,7 @@ describe('marketUtils', () => { const fillAmount = new BigNumber(15); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( inputOrders, - inputOrderStates, + remainingFillableMakerAssetAmounts, fillAmount, ); expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1]]); @@ -110,31 +103,17 @@ describe('marketUtils', () => { }, testOrderCount, ); - // generate order states that cover different scenarios + // generate remainingFillableMakerAssetAmounts that cover different partial fill scenarios // 1. order is completely filled already // 2. order is partially fillable // 3. order is completely fillable - const partialOrderStates: Array> = [ - { - remainingFillableMakerAssetAmount: constants.ZERO_AMOUNT, - }, - { - remainingFillableMakerAssetAmount: new BigNumber(5), - }, - { - remainingFillableMakerAssetAmount: makerAssetAmount, - }, - ]; - const inputOrderStates: OrderRelevantState[] = _.map( - partialOrderStates, - testOrderFactory.generateTestOrderRelevantState, - ); + const remainingFillableMakerAssetAmounts = [constants.ZERO_AMOUNT, new BigNumber(5), makerAssetAmount]; it('returns last 2 orders and non-zero remainingFillAmount when trying to fill original makerAssetAmounts', async () => { // try to fill 30 units of makerAsset const fillAmount = new BigNumber(30); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( inputOrders, - inputOrderStates, + remainingFillableMakerAssetAmounts, fillAmount, ); expect(resultOrders).to.be.deep.equal([inputOrders[1], inputOrders[2]]); diff --git a/packages/order-utils/test/utils/test_order_factory.ts b/packages/order-utils/test/utils/test_order_factory.ts index 2c5d8cf614..611e777eac 100644 --- a/packages/order-utils/test/utils/test_order_factory.ts +++ b/packages/order-utils/test/utils/test_order_factory.ts @@ -1,5 +1,4 @@ -import { Order, OrderRelevantState, SignedOrder } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; +import { Order, SignedOrder } from '@0xproject/types'; import * as _ from 'lodash'; import { constants, orderFactory } from '../../src'; @@ -21,43 +20,18 @@ const BASE_TEST_SIGNED_ORDER: SignedOrder = { ...BASE_TEST_ORDER, signature: constants.NULL_BYTES, }; -const BASE_TEST_ORDER_RELEVANT_STATE: OrderRelevantState = { - makerBalance: constants.ZERO_AMOUNT, - makerProxyAllowance: constants.ZERO_AMOUNT, - makerFeeBalance: constants.ZERO_AMOUNT, - makerFeeProxyAllowance: constants.ZERO_AMOUNT, - filledTakerAssetAmount: constants.ZERO_AMOUNT, - remainingFillableMakerAssetAmount: constants.ZERO_AMOUNT, - remainingFillableTakerAssetAmount: constants.ZERO_AMOUNT, -}; export const testOrderFactory = { generateTestSignedOrder(partialOrder: Partial): SignedOrder { return transformObject(BASE_TEST_SIGNED_ORDER, partialOrder); }, generateTestSignedOrders(partialOrder: Partial, numOrders: number): SignedOrder[] { - const baseTestOrders = generateArrayOfInput(BASE_TEST_SIGNED_ORDER, numOrders); - return transformObjects(baseTestOrders, partialOrder); - }, - generateTestOrderRelevantState(partialOrderRelevantState: Partial): OrderRelevantState { - return transformObject(BASE_TEST_ORDER_RELEVANT_STATE, partialOrderRelevantState); - }, - generateTestOrderRelevantStates( - partialOrderRelevantState: Partial, - numOrderStates: number, - ): OrderRelevantState[] { - const baseTestOrderStates = generateArrayOfInput(BASE_TEST_ORDER_RELEVANT_STATE, numOrderStates); - return transformObjects(baseTestOrderStates, partialOrderRelevantState); + const baseTestOrders = _.map(_.range(numOrders), () => BASE_TEST_SIGNED_ORDER); + return _.map(baseTestOrders, order => transformObject(order, partialOrder)); }, }; -function generateArrayOfInput(input: T, rangeLength: number): T[] { - return _.map(_.range(rangeLength), () => input); -} function transformObject(input: T, transformation: Partial): T { const copy = _.cloneDeep(input); return _.assign(copy, transformation); } -function transformObjects(inputs: T[], transformation: Partial): T[] { - return _.map(inputs, input => transformObject(input, transformation)); -} From 8382161f7553539ed6f436be88df8672b00bf35e Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Sun, 5 Aug 2018 20:48:56 -0400 Subject: [PATCH 26/49] Add tests for findFeeOrdersThatCoverFeesForTargetOrders --- packages/order-utils/src/constants.ts | 2 +- packages/order-utils/src/market_utils.ts | 14 +- .../order-utils/test/market_utils_test.ts | 162 +++++++++++++++++- .../test/utils/test_order_factory.ts | 7 +- 4 files changed, 165 insertions(+), 20 deletions(-) diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index 5137ff4990..c23578c203 100644 --- a/packages/order-utils/src/constants.ts +++ b/packages/order-utils/src/constants.ts @@ -11,6 +11,6 @@ export const constants = { ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH: 53, SELECTOR_LENGTH: 4, BASE_16: 16, - INFINITE_TIMESTAMP_SEC: new BigNumber(2524604400), // Close to infinite, + INFINITE_TIMESTAMP_SEC: new BigNumber(2524604400), // Close to infinite ZERO_AMOUNT: new BigNumber(0), }; diff --git a/packages/order-utils/src/market_utils.ts b/packages/order-utils/src/market_utils.ts index d66448a0bd..94b5be4eb5 100644 --- a/packages/order-utils/src/market_utils.ts +++ b/packages/order-utils/src/market_utils.ts @@ -17,7 +17,7 @@ export const marketUtils = { * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups * for these values. * @param makerAssetFillAmount The amount of makerAsset desired to be filled. - * @param slippageBufferAmount An additional amount makerAsset to be covered by the result in case of trade collisions or partial fills. + * @param slippageBufferAmount An additional amount of makerAsset to be covered by the result in case of trade collisions or partial fills. * @return Resulting orders and remaining fill amount that could not be covered by the input. */ findOrdersThatCoverMakerAssetFillAmount( @@ -80,8 +80,8 @@ export const marketUtils = { * @param remainingFillableFeeAmounts An array of BigNumbers corresponding to the signedFeeOrders parameter. * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups * for these values. - * @param slippageBufferAmount An additional amount makerAsset to be covered by the result in case of trade collisions or partial fills. - * @return Resulting orders and remaining fill amount that could not be covered by the input. + * @param slippageBufferAmount An additional amount of fee to be covered by the result in case of trade collisions or partial fills. + * @return Resulting orders and remaining fee amount that could not be covered by the input. */ findFeeOrdersThatCoverFeesForTargetOrders( signedOrders: SignedOrder[], @@ -89,7 +89,7 @@ export const marketUtils = { signedFeeOrders: SignedOrder[], remainingFillableFeeAmounts: BigNumber[], slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT, - ): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } { + ): { resultOrders: SignedOrder[]; remainingFeeAmount: BigNumber } { // type assertions assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); _.forEach(remainingFillableMakerAssetAmounts, (amount, index) => @@ -121,12 +121,16 @@ export const marketUtils = { }, constants.ZERO_AMOUNT, ); - return marketUtils.findOrdersThatCoverMakerAssetFillAmount( + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( signedFeeOrders, remainingFillableFeeAmounts, totalFeeAmount, slippageBufferAmount, ); + return { + resultOrders, + remainingFeeAmount: remainingFillAmount, + }; // TODO: add more orders here to cover rounding // https://github.com/0xProject/0x-protocol-specification/blob/master/v2/forwarding-contract-specification.md#over-buying-zrx }, diff --git a/packages/order-utils/test/market_utils_test.ts b/packages/order-utils/test/market_utils_test.ts index ac3fb9b939..03f86c5818 100644 --- a/packages/order-utils/test/market_utils_test.ts +++ b/packages/order-utils/test/market_utils_test.ts @@ -12,7 +12,7 @@ const expect = chai.expect; // tslint:disable: no-unused-expression describe('marketUtils', () => { - describe.only('#findOrdersThatCoverMakerAssetFillAmount', () => { + describe('#findOrdersThatCoverMakerAssetFillAmount', () => { describe('no orders', () => { it('returns empty and unchanged remainingFillAmount', async () => { const fillAmount = new BigNumber(10); @@ -25,15 +25,14 @@ describe('marketUtils', () => { expect(remainingFillAmount).to.be.bignumber.equal(fillAmount); }); }); - describe('orders are all completely fillable', () => { + describe('orders are completely fillable', () => { // generate three signed orders each with 10 units of makerAsset, 30 total - const testOrderCount = 3; const makerAssetAmount = new BigNumber(10); const inputOrders = testOrderFactory.generateTestSignedOrders( { makerAssetAmount, }, - testOrderCount, + 3, ); // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount]; @@ -95,20 +94,19 @@ describe('marketUtils', () => { }); describe('orders are partially fillable', () => { // generate three signed orders each with 10 units of makerAsset, 30 total - const testOrderCount = 3; const makerAssetAmount = new BigNumber(10); const inputOrders = testOrderFactory.generateTestSignedOrders( { makerAssetAmount, }, - testOrderCount, + 3, ); // generate remainingFillableMakerAssetAmounts that cover different partial fill scenarios // 1. order is completely filled already // 2. order is partially fillable // 3. order is completely fillable const remainingFillableMakerAssetAmounts = [constants.ZERO_AMOUNT, new BigNumber(5), makerAssetAmount]; - it('returns last 2 orders and non-zero remainingFillAmount when trying to fill original makerAssetAmounts', async () => { + it('returns last two orders and non-zero remainingFillAmount when trying to fill original makerAssetAmounts', async () => { // try to fill 30 units of makerAsset const fillAmount = new BigNumber(30); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( @@ -121,5 +119,153 @@ describe('marketUtils', () => { }); }); }); - describe('#findFeeOrdersThatCoverFeesForTargetOrders', () => {}); + describe('#findFeeOrdersThatCoverFeesForTargetOrders', () => { + // generate three signed fee orders each with 10 units of ZRX, 30 total + const zrxAmount = new BigNumber(10); + const inputFeeOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount: zrxAmount, + }, + 3, + ); + // generate remainingFillableFeeAmounts that equal the zrxAmount + const remainingFillableFeeAmounts = [zrxAmount, zrxAmount, zrxAmount]; + describe('no target orders', () => { + it('returns empty and zero remainingFeeAmount', async () => { + const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + [], + [], + inputFeeOrders, + remainingFillableFeeAmounts, + ); + expect(resultOrders).to.be.empty; + expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + }); + describe('no fee orders', () => { + // generate three signed orders each with 10 units of makerAsset, 30 total + // each signed order requires 10 units of takerFee + const makerAssetAmount = new BigNumber(10); + const takerFee = new BigNumber(10); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + takerFee, + }, + 3, + ); + // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount + const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount]; + it('returns empty and non-zero remainingFeeAmount', async () => { + const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + inputOrders, + remainingFillableMakerAssetAmounts, + [], + [], + ); + expect(resultOrders).to.be.empty; + expect(remainingFeeAmount).to.be.bignumber.equal(new BigNumber(30)); + }); + }); + describe('target orders have no fees', () => { + // generate three signed orders each with 10 units of makerAsset, 30 total + const makerAssetAmount = new BigNumber(10); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + }, + 3, + ); + // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount + const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount]; + it('returns empty and zero remainingFeeAmount', async () => { + const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + inputOrders, + remainingFillableMakerAssetAmounts, + inputFeeOrders, + remainingFillableFeeAmounts, + ); + expect(resultOrders).to.be.empty; + expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + }); + describe('target orders require fees and are completely fillable', () => { + // generate three signed orders each with 10 units of makerAsset, 30 total + // each signed order requires 10 units of takerFee + const makerAssetAmount = new BigNumber(10); + const takerFee = new BigNumber(10); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + takerFee, + }, + 3, + ); + // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount + const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount]; + it('returns input fee orders and zero remainingFeeAmount', async () => { + const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + inputOrders, + remainingFillableMakerAssetAmounts, + inputFeeOrders, + remainingFillableFeeAmounts, + ); + expect(resultOrders).to.be.deep.equal(inputFeeOrders); + expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + }); + describe('target orders require fees and are partially fillable', () => { + // generate three signed orders each with 10 units of makerAsset, 30 total + // each signed order requires 10 units of takerFee + const makerAssetAmount = new BigNumber(10); + const takerFee = new BigNumber(10); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + takerFee, + }, + 3, + ); + // generate remainingFillableMakerAssetAmounts that cover different partial fill scenarios + // 1. order is completely filled already + // 2. order is partially fillable + // 3. order is completely fillable + const remainingFillableMakerAssetAmounts = [constants.ZERO_AMOUNT, new BigNumber(5), makerAssetAmount]; + it('returns first two input fee orders and zero remainingFeeAmount', async () => { + const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + inputOrders, + remainingFillableMakerAssetAmounts, + inputFeeOrders, + remainingFillableFeeAmounts, + ); + expect(resultOrders).to.be.deep.equal([inputFeeOrders[0], inputFeeOrders[1]]); + expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + }); + describe('target orders require more fees than available', () => { + // generate three signed orders each with 10 units of makerAsset, 30 total + // each signed order requires 20 units of takerFee + const makerAssetAmount = new BigNumber(10); + const takerFee = new BigNumber(20); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + takerFee, + }, + 3, + ); + // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount + const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount]; + it('returns input fee orders and non-zero remainingFeeAmount', async () => { + const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + inputOrders, + remainingFillableMakerAssetAmounts, + inputFeeOrders, + remainingFillableFeeAmounts, + ); + expect(resultOrders).to.be.deep.equal(inputFeeOrders); + expect(remainingFeeAmount).to.be.bignumber.equal(new BigNumber(30)); + }); + }); + }); }); diff --git a/packages/order-utils/test/utils/test_order_factory.ts b/packages/order-utils/test/utils/test_order_factory.ts index 611e777eac..75dc6f1f2d 100644 --- a/packages/order-utils/test/utils/test_order_factory.ts +++ b/packages/order-utils/test/utils/test_order_factory.ts @@ -5,14 +5,9 @@ import { constants, orderFactory } from '../../src'; const BASE_TEST_ORDER: Order = orderFactory.createOrder( constants.NULL_ADDRESS, - constants.NULL_ADDRESS, + constants.ZERO_AMOUNT, constants.NULL_ADDRESS, constants.ZERO_AMOUNT, - constants.ZERO_AMOUNT, - constants.ZERO_AMOUNT, - constants.NULL_BYTES, - constants.ZERO_AMOUNT, - constants.NULL_BYTES, constants.NULL_ADDRESS, constants.NULL_ADDRESS, ); From 7d0bec9b2a9960390b0b3b19e5ed4d84a679669b Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Sun, 5 Aug 2018 20:54:29 -0400 Subject: [PATCH 27/49] Add some test cases that stress slippageBufferAmount param --- .../order-utils/test/market_utils_test.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/order-utils/test/market_utils_test.ts b/packages/order-utils/test/market_utils_test.ts index 03f86c5818..21c0a4802b 100644 --- a/packages/order-utils/test/market_utils_test.ts +++ b/packages/order-utils/test/market_utils_test.ts @@ -37,34 +37,43 @@ describe('marketUtils', () => { // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount]; it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => { - // try to fill 30 units of makerAsset - const fillAmount = new BigNumber(30); + // try to fill 20 units of makerAsset + // include 10 units of slippageBufferAmount + const fillAmount = new BigNumber(20); + const slippageBufferAmount = new BigNumber(10); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( inputOrders, remainingFillableMakerAssetAmounts, fillAmount, + slippageBufferAmount, ); expect(resultOrders).to.be.deep.equal(inputOrders); expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => { - // try to fill 25 units of makerAsset - const fillAmount = new BigNumber(25); + // try to fill 15 units of makerAsset + // include 10 units of slippageBufferAmount + const fillAmount = new BigNumber(15); + const slippageBufferAmount = new BigNumber(10); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( inputOrders, remainingFillableMakerAssetAmounts, fillAmount, + slippageBufferAmount, ); expect(resultOrders).to.be.deep.equal(inputOrders); expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => { - // try to fill 35 units of makerAsset - const fillAmount = new BigNumber(35); + // try to fill 30 units of makerAsset + // include 5 units of slippageBufferAmount + const fillAmount = new BigNumber(30); + const slippageBufferAmount = new BigNumber(5); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( inputOrders, remainingFillableMakerAssetAmounts, fillAmount, + slippageBufferAmount, ); expect(resultOrders).to.be.deep.equal(inputOrders); expect(remainingFillAmount).to.be.bignumber.equal(new BigNumber(5)); From 2273798df9c5b625a678fefaa2f49e7e1cb99d0f Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Sun, 5 Aug 2018 20:58:57 -0400 Subject: [PATCH 28/49] Update CHANGELOGs --- packages/json-schemas/CHANGELOG.json | 9 +++++++++ packages/order-utils/CHANGELOG.json | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/packages/json-schemas/CHANGELOG.json b/packages/json-schemas/CHANGELOG.json index 31da6a7f74..33cf126e37 100644 --- a/packages/json-schemas/CHANGELOG.json +++ b/packages/json-schemas/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "1.0.1-rc.4", + "changes": [ + { + "note": "Change hexSchema to match `0x`", + "pr": 937 + } + ] + }, { "version": "1.0.1-rc.3", "changes": [ diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index 70a75854ae..776bd67ec9 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -6,6 +6,10 @@ "note": "Added a synchronous `createOrder` method in `orderFactory`, updated public interfaces to support some optional parameters", "pr": 936 + }, + { + "note": "Added marketUtils", + "pr": 937 } ] }, From 0bc775cdb8617fea73c87eaff015bf3d2dfadb42 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Sun, 5 Aug 2018 21:02:10 -0400 Subject: [PATCH 29/49] Remove 0x test case from hexSchema test --- packages/json-schemas/test/schema_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/json-schemas/test/schema_test.ts b/packages/json-schemas/test/schema_test.ts index d202b56434..f84553df1b 100644 --- a/packages/json-schemas/test/schema_test.ts +++ b/packages/json-schemas/test/schema_test.ts @@ -89,7 +89,7 @@ describe('Schema', () => { validateAgainstSchema(testCases, hexSchema); }); it('should fail for invalid hex string', () => { - const testCases = ['0x', '0', '0xzzzzzzB11a196601eD2ce54B665CaFEca0347D42']; + const testCases = ['0', '0xzzzzzzB11a196601eD2ce54B665CaFEca0347D42']; const shouldFail = true; validateAgainstSchema(testCases, hexSchema, shouldFail); }); From 35201af4b1aa64a0961de0d13ce9c5bac65ddbf8 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Mon, 6 Aug 2018 16:35:49 -0400 Subject: [PATCH 30/49] Remove assertion comments --- packages/order-utils/src/market_utils.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/order-utils/src/market_utils.ts b/packages/order-utils/src/market_utils.ts index 94b5be4eb5..681059ddf4 100644 --- a/packages/order-utils/src/market_utils.ts +++ b/packages/order-utils/src/market_utils.ts @@ -26,14 +26,12 @@ export const marketUtils = { makerAssetFillAmount: BigNumber, slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT, ): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } { - // type assertions assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); _.forEach(remainingFillableMakerAssetAmounts, (amount, index) => assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount), ); assert.isValidBaseUnitAmount('makerAssetFillAmount', makerAssetFillAmount); assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount); - // other assertions assert.assert( signedOrders.length === remainingFillableMakerAssetAmounts.length, 'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length', @@ -90,7 +88,6 @@ export const marketUtils = { remainingFillableFeeAmounts: BigNumber[], slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT, ): { resultOrders: SignedOrder[]; remainingFeeAmount: BigNumber } { - // type assertions assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); _.forEach(remainingFillableMakerAssetAmounts, (amount, index) => assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount), @@ -100,7 +97,6 @@ export const marketUtils = { assert.isValidBaseUnitAmount(`remainingFillableFeeAmounts[${index}]`, amount), ); assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount); - // other assertions assert.assert( signedOrders.length === remainingFillableMakerAssetAmounts.length, 'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length', From 3d6cf503645386734bb552e09df2c6709e2ed45c Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sun, 29 Jul 2018 21:47:21 -0700 Subject: [PATCH 31/49] Fix comments, styling, and optimize hashOrder --- .../protocol/Exchange/MixinTransactions.sol | 4 +- .../protocol/Exchange/libs/LibEIP712.sol | 15 ++++--- .../2.0.0/protocol/Exchange/libs/LibOrder.sol | 39 ++++++++++--------- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/MixinTransactions.sol b/packages/contracts/src/2.0.0/protocol/Exchange/MixinTransactions.sol index 88d2da7d70..0814638db7 100644 --- a/packages/contracts/src/2.0.0/protocol/Exchange/MixinTransactions.sol +++ b/packages/contracts/src/2.0.0/protocol/Exchange/MixinTransactions.sol @@ -123,10 +123,10 @@ contract MixinTransactions is bytes32 dataHash = keccak256(data); // Assembly for more efficiently computing: - // keccak256(abi.encode( + // keccak256(abi.encodePacked( // EIP712_ZEROEX_TRANSACTION_SCHEMA_HASH, // salt, - // signerAddress, + // bytes32(signerAddress), // keccak256(data) // )); diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibEIP712.sol b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibEIP712.sol index 1fc41dafdd..c9e45189dc 100644 --- a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibEIP712.sol +++ b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibEIP712.sol @@ -30,7 +30,7 @@ contract LibEIP712 { string constant internal EIP712_DOMAIN_VERSION = "2"; // Hash of the EIP712 Domain Separator Schema - bytes32 public constant EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH = keccak256(abi.encodePacked( + bytes32 constant internal EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH = keccak256(abi.encodePacked( "EIP712Domain(", "string name,", "string version,", @@ -45,11 +45,11 @@ contract LibEIP712 { constructor () public { - EIP712_DOMAIN_HASH = keccak256(abi.encode( + EIP712_DOMAIN_HASH = keccak256(abi.encodePacked( EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH, keccak256(bytes(EIP712_DOMAIN_NAME)), keccak256(bytes(EIP712_DOMAIN_VERSION)), - address(this) + bytes32(address(this)) )); } @@ -59,8 +59,13 @@ contract LibEIP712 { function hashEIP712Message(bytes32 hashStruct) internal view - returns (bytes32) + returns (bytes32 result) { - return keccak256(abi.encodePacked(EIP191_HEADER, EIP712_DOMAIN_HASH, hashStruct)); + result = keccak256(abi.encodePacked( + EIP191_HEADER, + EIP712_DOMAIN_HASH, + hashStruct + )); + return result; } } diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibOrder.sol b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibOrder.sol index 4031ff26b5..68f4f5f1b6 100644 --- a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibOrder.sol +++ b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibOrder.sol @@ -103,11 +103,12 @@ contract LibOrder is bytes32 takerAssetDataHash = keccak256(order.takerAssetData); // Assembly for more efficiently computing: - // keccak256(abi.encode( - // order.makerAddress, - // order.takerAddress, - // order.feeRecipientAddress, - // order.senderAddress, + // keccak256(abi.encodePacked( + // EIP712_ORDER_SCHEMA_HASH, + // bytes32(order.makerAddress), + // bytes32(order.takerAddress), + // bytes32(order.feeRecipientAddress), + // bytes32(order.senderAddress), // order.makerAssetAmount, // order.takerAssetAmount, // order.makerFee, @@ -119,24 +120,26 @@ contract LibOrder is // )); assembly { + // Calculate memory addresses that will be swapped out before hashing + let pos1 := sub(order, 32) + let pos2 := add(order, 320) + let pos3 := add(order, 352) + // Backup - // solhint-disable-next-line space-after-comma - let temp1 := mload(sub(order, 32)) - let temp2 := mload(add(order, 320)) - let temp3 := mload(add(order, 352)) + let temp1 := mload(pos1) + let temp2 := mload(pos2) + let temp3 := mload(pos3) // Hash in place - // solhint-disable-next-line space-after-comma - mstore(sub(order, 32), schemaHash) - mstore(add(order, 320), makerAssetDataHash) - mstore(add(order, 352), takerAssetDataHash) - result := keccak256(sub(order, 32), 416) + mstore(pos1, schemaHash) + mstore(pos2, makerAssetDataHash) + mstore(pos3, takerAssetDataHash) + result := keccak256(pos1, 416) // Restore - // solhint-disable-next-line space-after-comma - mstore(sub(order, 32), temp1) - mstore(add(order, 320), temp2) - mstore(add(order, 352), temp3) + mstore(pos1, temp1) + mstore(pos2, temp2) + mstore(pos3, temp3) } return result; } From 149c07dfd2ef2e2102d66ebbdaf1268a1938f4af Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Tue, 7 Aug 2018 16:27:02 -0700 Subject: [PATCH 32/49] Use asm for hashEIP712Message, increment free memory pointer after asm hashing functions --- .../protocol/Exchange/MixinTransactions.sol | 12 ++++++--- .../protocol/Exchange/libs/LibEIP712.sol | 25 +++++++++++++++---- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/MixinTransactions.sol b/packages/contracts/src/2.0.0/protocol/Exchange/MixinTransactions.sol index 0814638db7..b5de1a5deb 100644 --- a/packages/contracts/src/2.0.0/protocol/Exchange/MixinTransactions.sol +++ b/packages/contracts/src/2.0.0/protocol/Exchange/MixinTransactions.sol @@ -131,11 +131,15 @@ contract MixinTransactions is // )); assembly { + // Load free memory pointer let memPtr := mload(64) - mstore(memPtr, schemaHash) - mstore(add(memPtr, 32), salt) - mstore(add(memPtr, 64), and(signerAddress, 0xffffffffffffffffffffffffffffffffffffffff)) - mstore(add(memPtr, 96), dataHash) + + mstore(memPtr, schemaHash) // hash of schema + mstore(add(memPtr, 32), salt) // salt + mstore(add(memPtr, 64), and(signerAddress, 0xffffffffffffffffffffffffffffffffffffffff)) // signerAddress + mstore(add(memPtr, 96), dataHash) // hash of data + + // Compute hash result := keccak256(memPtr, 128) } diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibEIP712.sol b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibEIP712.sol index c9e45189dc..b02f7632ea 100644 --- a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibEIP712.sol +++ b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibEIP712.sol @@ -61,11 +61,26 @@ contract LibEIP712 { view returns (bytes32 result) { - result = keccak256(abi.encodePacked( - EIP191_HEADER, - EIP712_DOMAIN_HASH, - hashStruct - )); + bytes32 eip712DomainHash = EIP712_DOMAIN_HASH; + + // Assembly for more efficient computing: + // keccak256(abi.encodePacked( + // EIP191_HEADER, + // EIP712_DOMAIN_HASH, + // hashStruct + // )); + + assembly { + // Load free memory pointer + let memPtr := mload(64) + + mstore(memPtr, 0x1901000000000000000000000000000000000000000000000000000000000000) // EIP191 header + mstore(add(memPtr, 2), eip712DomainHash) // EIP712 domain hash + mstore(add(memPtr, 34), hashStruct) // Hash of struct + + // Compute hash + result := keccak256(memPtr, 66) + } return result; } } From 6e2e658162a5a128b722ba105f92fa5267c4bd62 Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Wed, 8 Aug 2018 11:27:38 -0700 Subject: [PATCH 33/49] Update TypeScript to version 2.9.2 --- packages/0x.js/package.json | 2 +- packages/abi-gen/package.json | 2 +- packages/assert/package.json | 2 +- packages/base-contract/package.json | 2 +- packages/connect/package.json | 2 +- packages/contract-wrappers/package.json | 2 +- packages/contracts/package.json | 2 +- .../contracts/test/exchange/match_orders.ts | 23 ++++-- packages/dev-utils/package.json | 2 +- packages/ethereum-types/package.json | 2 +- packages/fill-scenarios/package.json | 2 +- packages/json-schemas/package.json | 2 +- packages/metacoin/package.json | 2 +- packages/migrations/package.json | 2 +- packages/monorepo-scripts/package.json | 2 +- packages/order-utils/package.json | 2 +- packages/order-watcher/package.json | 2 +- packages/react-docs-example/package.json | 2 +- packages/react-docs/package.json | 2 +- packages/react-shared/package.json | 2 +- packages/sol-compiler/package.json | 2 +- packages/sol-cov/package.json | 2 +- packages/sol-resolver/package.json | 2 +- packages/sra-report/package.json | 2 +- packages/subproviders/package.json | 2 +- packages/testnet-faucets/package.json | 2 +- packages/tslint-config/package.json | 2 +- packages/types/package.json | 2 +- packages/utils/package.json | 2 +- packages/web3-wrapper/package.json | 2 +- packages/website/package.json | 2 +- yarn.lock | 71 ++----------------- 32 files changed, 53 insertions(+), 101 deletions(-) diff --git a/packages/0x.js/package.json b/packages/0x.js/package.json index 3b92752e1a..7d3fa92c36 100644 --- a/packages/0x.js/package.json +++ b/packages/0x.js/package.json @@ -94,7 +94,7 @@ "source-map-support": "^0.5.0", "tslint": "5.11.0", "typedoc": "0xProject/typedoc", - "typescript": "2.7.1", + "typescript": "2.9.2", "webpack": "^3.1.0" }, "dependencies": { diff --git a/packages/abi-gen/package.json b/packages/abi-gen/package.json index 2732cdb640..3934287719 100644 --- a/packages/abi-gen/package.json +++ b/packages/abi-gen/package.json @@ -63,7 +63,7 @@ "npm-run-all": "^4.1.2", "shx": "^0.2.2", "tslint": "5.11.0", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "publishConfig": { "access": "public" diff --git a/packages/assert/package.json b/packages/assert/package.json index 27fd51923f..f95190ad62 100644 --- a/packages/assert/package.json +++ b/packages/assert/package.json @@ -44,7 +44,7 @@ "nyc": "^11.0.1", "shx": "^0.2.2", "tslint": "5.11.0", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "dependencies": { "@0xproject/json-schemas": "^1.0.1-rc.3", diff --git a/packages/base-contract/package.json b/packages/base-contract/package.json index 7ac629bbfb..851b812624 100644 --- a/packages/base-contract/package.json +++ b/packages/base-contract/package.json @@ -40,7 +40,7 @@ "npm-run-all": "^4.1.2", "shx": "^0.2.2", "tslint": "5.11.0", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "dependencies": { "@0xproject/typescript-typings": "^1.0.3", diff --git a/packages/connect/package.json b/packages/connect/package.json index 57bd27c5e2..d2f18c4108 100644 --- a/packages/connect/package.json +++ b/packages/connect/package.json @@ -83,7 +83,7 @@ "shx": "^0.2.2", "tslint": "5.11.0", "typedoc": "~0.8.0", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "publishConfig": { "access": "public" diff --git a/packages/contract-wrappers/package.json b/packages/contract-wrappers/package.json index f27afaba9e..64de5b0c29 100644 --- a/packages/contract-wrappers/package.json +++ b/packages/contract-wrappers/package.json @@ -68,7 +68,7 @@ "sinon": "^4.0.0", "source-map-support": "^0.5.0", "tslint": "5.11.0", - "typescript": "2.7.1", + "typescript": "2.9.2", "web3-provider-engine": "14.0.6" }, "dependencies": { diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 014210d330..1f5c156740 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -68,7 +68,7 @@ "solc": "^0.4.24", "solhint": "^1.2.1", "tslint": "5.11.0", - "typescript": "2.7.1", + "typescript": "2.9.2", "yargs": "^10.0.3" }, "dependencies": { diff --git a/packages/contracts/test/exchange/match_orders.ts b/packages/contracts/test/exchange/match_orders.ts index 4400975629..46b3569bd6 100644 --- a/packages/contracts/test/exchange/match_orders.ts +++ b/packages/contracts/test/exchange/match_orders.ts @@ -69,13 +69,22 @@ describe('matchOrders', () => { before(async () => { // Create accounts const accounts = await web3Wrapper.getAvailableAddressesAsync(); + // Hack(albrow): Both Prettier and TSLint insert a trailing comma below + // but that is invalid syntax as of TypeScript version >= 2.8. We don't + // have the right fine-grained configuration options in TSLint, + // Prettier, or TypeScript, to reconcile this, so we will just have to + // wait for them to sort it out. We disable TSLint and Prettier for + // this part of the code for now. This occurs several times in this + // file. See https://github.com/prettier/prettier/issues/4624. + // prettier-ignore const usedAddresses = ([ owner, makerAddressLeft, makerAddressRight, takerAddress, feeRecipientAddressLeft, - feeRecipientAddressRight, + // tslint:disable-next-line:trailing-comma + feeRecipientAddressRight ] = _.slice(accounts, 0, 6)); // Create wrappers erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); @@ -201,9 +210,11 @@ describe('matchOrders', () => { // Match signedOrderLeft with signedOrderRight let newERC20BalancesByOwner: ERC20BalancesByOwner; let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + // prettier-ignore [ newERC20BalancesByOwner, - newERC721TokenIdsByOwner, + // tslint:disable-next-line:trailing-comma + newERC721TokenIdsByOwner ] = await matchOrderTester.matchOrdersAndVerifyBalancesAsync( signedOrderLeft, signedOrderRight, @@ -306,9 +317,11 @@ describe('matchOrders', () => { // Match orders let newERC20BalancesByOwner: ERC20BalancesByOwner; let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + // prettier-ignore [ newERC20BalancesByOwner, - newERC721TokenIdsByOwner, + // tslint:disable-next-line:trailing-comma + newERC721TokenIdsByOwner ] = await matchOrderTester.matchOrdersAndVerifyBalancesAsync( signedOrderLeft, signedOrderRight, @@ -374,9 +387,11 @@ describe('matchOrders', () => { // Match orders let newERC20BalancesByOwner: ERC20BalancesByOwner; let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + // prettier-ignore [ newERC20BalancesByOwner, - newERC721TokenIdsByOwner, + // tslint:disable-next-line:trailing-comma + newERC721TokenIdsByOwner ] = await matchOrderTester.matchOrdersAndVerifyBalancesAsync( signedOrderLeft, signedOrderRight, diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 2c7c531940..66f6472f16 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -42,7 +42,7 @@ "nyc": "^11.0.1", "shx": "^0.2.2", "tslint": "5.11.0", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "dependencies": { "@0xproject/subproviders": "^1.0.4", diff --git a/packages/ethereum-types/package.json b/packages/ethereum-types/package.json index 7ed99d419e..03e70a778e 100644 --- a/packages/ethereum-types/package.json +++ b/packages/ethereum-types/package.json @@ -41,7 +41,7 @@ "make-promises-safe": "^1.1.0", "shx": "^0.2.2", "tslint": "5.11.0", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "dependencies": { "@types/node": "^8.0.53", diff --git a/packages/fill-scenarios/package.json b/packages/fill-scenarios/package.json index 281575107e..f7e1e1ec49 100644 --- a/packages/fill-scenarios/package.json +++ b/packages/fill-scenarios/package.json @@ -38,7 +38,7 @@ "npm-run-all": "^4.1.2", "shx": "^0.2.2", "tslint": "5.11.0", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "dependencies": { "@0xproject/base-contract": "^1.0.4", diff --git a/packages/json-schemas/package.json b/packages/json-schemas/package.json index 4793fc0d54..6f0376d1be 100644 --- a/packages/json-schemas/package.json +++ b/packages/json-schemas/package.json @@ -70,7 +70,7 @@ "shx": "^0.2.2", "tslint": "5.11.0", "typedoc": "0xProject/typedoc", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "publishConfig": { "access": "public" diff --git a/packages/metacoin/package.json b/packages/metacoin/package.json index bab14bd5b9..890d14fbaf 100644 --- a/packages/metacoin/package.json +++ b/packages/metacoin/package.json @@ -56,6 +56,6 @@ "npm-run-all": "^4.1.2", "shx": "^0.2.2", "tslint": "5.11.0", - "typescript": "2.7.1" + "typescript": "2.9.2" } } diff --git a/packages/migrations/package.json b/packages/migrations/package.json index 0a1186f6a5..c4d14eaeed 100644 --- a/packages/migrations/package.json +++ b/packages/migrations/package.json @@ -49,7 +49,7 @@ "npm-run-all": "^4.1.2", "shx": "^0.2.2", "tslint": "5.11.0", - "typescript": "2.7.1", + "typescript": "2.9.2", "yargs": "^10.0.3" }, "dependencies": { diff --git a/packages/monorepo-scripts/package.json b/packages/monorepo-scripts/package.json index 128bdcff5e..c849c01baf 100644 --- a/packages/monorepo-scripts/package.json +++ b/packages/monorepo-scripts/package.json @@ -39,7 +39,7 @@ "npm-run-all": "^4.1.2", "shx": "^0.2.2", "tslint": "5.11.0", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "dependencies": { "@lerna/batch-packages": "^3.0.0-beta.18", diff --git a/packages/order-utils/package.json b/packages/order-utils/package.json index cab917a82f..7880b9352b 100644 --- a/packages/order-utils/package.json +++ b/packages/order-utils/package.json @@ -70,7 +70,7 @@ "sinon": "^4.0.0", "tslint": "5.11.0", "typedoc": "0xProject/typedoc", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "dependencies": { "@0xproject/assert": "^1.0.4", diff --git a/packages/order-watcher/package.json b/packages/order-watcher/package.json index e4226f017f..c000b4fece 100644 --- a/packages/order-watcher/package.json +++ b/packages/order-watcher/package.json @@ -67,7 +67,7 @@ "sinon": "^4.0.0", "source-map-support": "^0.5.0", "tslint": "5.11.0", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "dependencies": { "@0xproject/assert": "^1.0.4", diff --git a/packages/react-docs-example/package.json b/packages/react-docs-example/package.json index ca7a85b766..4eb109b3ea 100644 --- a/packages/react-docs-example/package.json +++ b/packages/react-docs-example/package.json @@ -45,7 +45,7 @@ "source-map-loader": "^0.2.3", "style-loader": "^0.20.2", "tslint": "^5.9.1", - "typescript": "2.7.1", + "typescript": "2.9.2", "webpack": "^3.11.0", "webpack-dev-server": "^2.11.1" }, diff --git a/packages/react-docs/package.json b/packages/react-docs/package.json index 1028f7fd6e..44044e54d9 100644 --- a/packages/react-docs/package.json +++ b/packages/react-docs/package.json @@ -33,7 +33,7 @@ "make-promises-safe": "^1.1.0", "shx": "^0.2.2", "tslint": "^5.9.1", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "dependencies": { "@0xproject/react-shared": "^1.0.5", diff --git a/packages/react-shared/package.json b/packages/react-shared/package.json index bb92117522..839bfccc5a 100644 --- a/packages/react-shared/package.json +++ b/packages/react-shared/package.json @@ -32,7 +32,7 @@ "make-promises-safe": "^1.1.0", "shx": "^0.2.2", "tslint": "^5.9.1", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "dependencies": { "@types/is-mobile": "0.3.0", diff --git a/packages/sol-compiler/package.json b/packages/sol-compiler/package.json index c034775449..c31d180c2d 100644 --- a/packages/sol-compiler/package.json +++ b/packages/sol-compiler/package.json @@ -71,7 +71,7 @@ "tslint": "5.11.0", "typedoc": "0xProject/typedoc", "types-bn": "^0.0.1", - "typescript": "2.7.1", + "typescript": "2.9.2", "web3-typescript-typings": "^0.10.2", "zeppelin-solidity": "1.8.0" }, diff --git a/packages/sol-cov/package.json b/packages/sol-cov/package.json index ee87543dbb..41758b30d6 100644 --- a/packages/sol-cov/package.json +++ b/packages/sol-cov/package.json @@ -89,7 +89,7 @@ "sinon": "^4.0.0", "tslint": "5.11.0", "typedoc": "0xProject/typedoc", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "publishConfig": { "access": "public" diff --git a/packages/sol-resolver/package.json b/packages/sol-resolver/package.json index dd5915237b..618f78eec0 100644 --- a/packages/sol-resolver/package.json +++ b/packages/sol-resolver/package.json @@ -30,7 +30,7 @@ "make-promises-safe": "^1.1.0", "shx": "^0.2.2", "tslint": "5.11.0", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "dependencies": { "@0xproject/types": "^1.0.1-rc.3", diff --git a/packages/sra-report/package.json b/packages/sra-report/package.json index 57e91a93ca..55063a7f0e 100644 --- a/packages/sra-report/package.json +++ b/packages/sra-report/package.json @@ -66,7 +66,7 @@ "nyc": "^11.0.1", "shx": "^0.2.2", "tslint": "5.11.0", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "publishConfig": { "access": "public" diff --git a/packages/subproviders/package.json b/packages/subproviders/package.json index 5e0153765d..d59326b6fc 100644 --- a/packages/subproviders/package.json +++ b/packages/subproviders/package.json @@ -84,7 +84,7 @@ "sinon": "^4.0.0", "tslint": "5.11.0", "typedoc": "0xProject/typedoc", - "typescript": "2.7.1", + "typescript": "2.9.2", "webpack": "^3.1.0" }, "optionalDependencies": { diff --git a/packages/testnet-faucets/package.json b/packages/testnet-faucets/package.json index 18306311e9..3b68c06ee1 100644 --- a/packages/testnet-faucets/package.json +++ b/packages/testnet-faucets/package.json @@ -43,7 +43,7 @@ "shx": "^0.2.2", "source-map-loader": "^0.1.6", "tslint": "5.11.0", - "typescript": "2.7.1", + "typescript": "2.9.2", "webpack": "^3.1.0", "webpack-node-externals": "^1.6.0" } diff --git a/packages/tslint-config/package.json b/packages/tslint-config/package.json index f6fdb3649c..040db472a9 100644 --- a/packages/tslint-config/package.json +++ b/packages/tslint-config/package.json @@ -39,7 +39,7 @@ "copyfiles": "^1.2.0", "make-promises-safe": "^1.1.0", "shx": "^0.2.2", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "dependencies": { "lodash": "^4.17.4", diff --git a/packages/types/package.json b/packages/types/package.json index d2fefa136f..e42c446307 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -30,7 +30,7 @@ "make-promises-safe": "^1.1.0", "shx": "^0.2.2", "tslint": "5.11.0", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "dependencies": { "@types/node": "^8.0.53", diff --git a/packages/utils/package.json b/packages/utils/package.json index b1a0d8eb93..ee150cb0e1 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -32,7 +32,7 @@ "npm-run-all": "^4.1.2", "shx": "^0.2.2", "tslint": "5.11.0", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "dependencies": { "@0xproject/types": "^1.0.1-rc.3", diff --git a/packages/web3-wrapper/package.json b/packages/web3-wrapper/package.json index d0f55c9052..300382c7f2 100644 --- a/packages/web3-wrapper/package.json +++ b/packages/web3-wrapper/package.json @@ -61,7 +61,7 @@ "shx": "^0.2.2", "tslint": "5.11.0", "typedoc": "0xProject/typedoc", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "dependencies": { "@0xproject/assert": "^1.0.4", diff --git a/packages/website/package.json b/packages/website/package.json index 13f1f53723..4a19fed6d8 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -98,7 +98,7 @@ "style-loader": "0.13.x", "tslint": "5.11.0", "tslint-config-0xproject": "^0.0.2", - "typescript": "2.7.1", + "typescript": "2.9.2", "uglifyjs-webpack-plugin": "^1.2.5", "webpack": "^3.1.0", "webpack-dev-middleware": "^1.10.0", diff --git a/yarn.lock b/yarn.lock index 10db1da422..90a2543542 100644 --- a/yarn.lock +++ b/yarn.lock @@ -565,37 +565,6 @@ lodash "4.17.10" uuid "3.2.1" -"@0xproject/contract-wrappers@^1.0.1-rc.2": - version "1.0.1-rc.1" - dependencies: - "@0xproject/assert" "^1.0.3" - "@0xproject/base-contract" "^1.0.3" - "@0xproject/fill-scenarios" "^1.0.1-rc.1" - "@0xproject/json-schemas" "^1.0.1-rc.2" - "@0xproject/order-utils" "^1.0.1-rc.1" - "@0xproject/types" "^1.0.1-rc.2" - "@0xproject/typescript-typings" "^1.0.3" - "@0xproject/utils" "^1.0.3" - "@0xproject/web3-wrapper" "^1.1.1" - ethereum-types "^1.0.3" - ethereumjs-blockstream "5.0.0" - ethereumjs-util "^5.1.1" - ethers "3.0.22" - js-sha3 "^0.7.0" - lodash "^4.17.4" - uuid "^3.1.0" - -"@0xproject/dev-utils@^1.0.3": - version "1.0.2" - dependencies: - "@0xproject/subproviders" "^1.0.3" - "@0xproject/types" "^1.0.1-rc.2" - "@0xproject/typescript-typings" "^1.0.3" - "@0xproject/utils" "^1.0.3" - "@0xproject/web3-wrapper" "^1.1.1" - ethereum-types "^1.0.3" - lodash "^4.17.4" - "@0xproject/fill-scenarios@^0.0.4": version "0.0.4" resolved "https://registry.yarnpkg.com/@0xproject/fill-scenarios/-/fill-scenarios-0.0.4.tgz#4d23c75abda7e9f117b698c0b8b142af07e0c69e" @@ -653,23 +622,6 @@ jsonschema "1.2.2" lodash.values "4.3.0" -"@0xproject/migrations@^1.0.3": - version "1.0.2" - dependencies: - "@0xproject/base-contract" "^1.0.3" - "@0xproject/order-utils" "^1.0.1-rc.1" - "@0xproject/sol-compiler" "^1.0.3" - "@0xproject/subproviders" "^1.0.3" - "@0xproject/typescript-typings" "^1.0.3" - "@0xproject/utils" "^1.0.3" - "@0xproject/web3-wrapper" "^1.1.1" - "@ledgerhq/hw-app-eth" "^4.3.0" - ethereum-types "^1.0.3" - ethers "3.0.22" - lodash "^4.17.4" - optionalDependencies: - "@ledgerhq/hw-transport-node-hid" "^4.3.0" - "@0xproject/order-utils@^0.0.7": version "0.0.7" resolved "https://registry.yarnpkg.com/@0xproject/order-utils/-/order-utils-0.0.7.tgz#eaa465782ea5745bdad54e1a851533172d993b7c" @@ -718,25 +670,6 @@ ethereumjs-util "5.1.5" lodash "4.17.10" -"@0xproject/order-utils@^1.0.1-rc.2": - version "1.0.1-rc.1" - dependencies: - "@0xproject/assert" "^1.0.3" - "@0xproject/base-contract" "^1.0.3" - "@0xproject/json-schemas" "^1.0.1-rc.2" - "@0xproject/sol-compiler" "^1.0.3" - "@0xproject/types" "^1.0.1-rc.2" - "@0xproject/typescript-typings" "^1.0.3" - "@0xproject/utils" "^1.0.3" - "@0xproject/web3-wrapper" "^1.1.1" - "@types/node" "^8.0.53" - bn.js "^4.11.8" - ethereum-types "^1.0.3" - ethereumjs-abi "0.6.5" - ethereumjs-util "^5.1.1" - ethers "3.0.22" - lodash "^4.17.4" - "@0xproject/order-watcher@^0.0.7": version "0.0.7" resolved "https://registry.yarnpkg.com/@0xproject/order-watcher/-/order-watcher-0.0.7.tgz#fbe019aa33447781096b5d562e7a3a4ec91a1da2" @@ -13146,6 +13079,10 @@ typescript@2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.7.1.tgz#bb3682c2c791ac90e7c6210b26478a8da085c359" +typescript@2.9.2: + version "2.9.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" + typewise-core@^1.2, typewise-core@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/typewise-core/-/typewise-core-1.2.0.tgz#97eb91805c7f55d2f941748fa50d315d991ef195" From 68fb1bf37622e244f7dbce276a7c3355e4ec88eb Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Wed, 8 Aug 2018 13:58:29 -0700 Subject: [PATCH 34/49] fix comments and styling for MixinSignatureValidator --- .../protocol/Exchange/MixinSignatureValidator.sol | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/MixinSignatureValidator.sol b/packages/contracts/src/2.0.0/protocol/Exchange/MixinSignatureValidator.sol index ac73827153..44de548177 100644 --- a/packages/contracts/src/2.0.0/protocol/Exchange/MixinSignatureValidator.sol +++ b/packages/contracts/src/2.0.0/protocol/Exchange/MixinSignatureValidator.sol @@ -96,14 +96,15 @@ contract MixinSignatureValidator is "LENGTH_GREATER_THAN_0_REQUIRED" ); - // Ensure signature is supported + // Pop last byte off of signature byte array. uint8 signatureTypeRaw = uint8(signature.popLastByte()); + + // Ensure signature is supported require( signatureTypeRaw < uint8(SignatureType.NSignatureTypes), "SIGNATURE_UNSUPPORTED" ); - // Pop last byte off of signature byte array. SignatureType signatureType = SignatureType(signatureTypeRaw); // Variables are not scoped in Solidity. @@ -141,7 +142,12 @@ contract MixinSignatureValidator is v = uint8(signature[0]); r = signature.readBytes32(1); s = signature.readBytes32(33); - recovered = ecrecover(hash, v, r, s); + recovered = ecrecover( + hash, + v, + r, + s + ); isValid = signerAddress == recovered; return isValid; @@ -197,7 +203,6 @@ contract MixinSignatureValidator is // | 0x14 + x | 1 | Signature type is always "\x06" | } else if (signatureType == SignatureType.Validator) { // Pop last 20 bytes off of signature byte array. - address validatorAddress = signature.popLast20Bytes(); // Ensure signer has approved validator. From 797fd38e00e48abddc03be214984f81bf7b1c29c Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Wed, 8 Aug 2018 14:01:12 -0700 Subject: [PATCH 35/49] feat(monorepo-scripts): Add confirmation prompt before publishing --- packages/monorepo-scripts/src/publish.ts | 22 +++++--- yarn.lock | 67 ------------------------ 2 files changed, 14 insertions(+), 75 deletions(-) diff --git a/packages/monorepo-scripts/src/publish.ts b/packages/monorepo-scripts/src/publish.ts index 5992131db4..6ff0c9bef8 100644 --- a/packages/monorepo-scripts/src/publish.ts +++ b/packages/monorepo-scripts/src/publish.ts @@ -31,12 +31,25 @@ const packageNameToWebsitePath: { [name: string]: string } = { 'ethereum-types': 'ethereum-types', }; +async function confirmAsync(message: string): Promise { + prompt.start(); + const result = await promisify(prompt.get)([message]); + const didConfirm = result[message] === 'y'; + if (!didConfirm) { + utils.log('Publish process aborted.'); + process.exit(0); + } +} + (async () => { // Fetch public, updated Lerna packages const shouldIncludePrivate = true; const allUpdatedPackages = await utils.getUpdatedPackagesAsync(shouldIncludePrivate); if (!configs.IS_LOCAL_PUBLISH) { + await confirmAsync( + 'THIS IS NOT A TEST PUBLISH! You are about to publish one or more packages to npm. Are you sure you want to continue? (y/n)', + ); await confirmDocPagesRenderAsync(allUpdatedPackages); } @@ -107,14 +120,7 @@ package.ts. Please add an entry for it and try again.`, opn(link); }); - prompt.start(); - const message = 'Do all the doc pages render properly? (yn)'; - const result = await promisify(prompt.get)([message]); - const didConfirm = result[message] === 'y'; - if (!didConfirm) { - utils.log('Publish process aborted.'); - process.exit(0); - } + await confirmAsync('Do all the doc pages render properly? (y/n)'); } async function pushChangelogsToGithubAsync(): Promise { diff --git a/yarn.lock b/yarn.lock index 10db1da422..84f6900113 100644 --- a/yarn.lock +++ b/yarn.lock @@ -565,37 +565,6 @@ lodash "4.17.10" uuid "3.2.1" -"@0xproject/contract-wrappers@^1.0.1-rc.2": - version "1.0.1-rc.1" - dependencies: - "@0xproject/assert" "^1.0.3" - "@0xproject/base-contract" "^1.0.3" - "@0xproject/fill-scenarios" "^1.0.1-rc.1" - "@0xproject/json-schemas" "^1.0.1-rc.2" - "@0xproject/order-utils" "^1.0.1-rc.1" - "@0xproject/types" "^1.0.1-rc.2" - "@0xproject/typescript-typings" "^1.0.3" - "@0xproject/utils" "^1.0.3" - "@0xproject/web3-wrapper" "^1.1.1" - ethereum-types "^1.0.3" - ethereumjs-blockstream "5.0.0" - ethereumjs-util "^5.1.1" - ethers "3.0.22" - js-sha3 "^0.7.0" - lodash "^4.17.4" - uuid "^3.1.0" - -"@0xproject/dev-utils@^1.0.3": - version "1.0.2" - dependencies: - "@0xproject/subproviders" "^1.0.3" - "@0xproject/types" "^1.0.1-rc.2" - "@0xproject/typescript-typings" "^1.0.3" - "@0xproject/utils" "^1.0.3" - "@0xproject/web3-wrapper" "^1.1.1" - ethereum-types "^1.0.3" - lodash "^4.17.4" - "@0xproject/fill-scenarios@^0.0.4": version "0.0.4" resolved "https://registry.yarnpkg.com/@0xproject/fill-scenarios/-/fill-scenarios-0.0.4.tgz#4d23c75abda7e9f117b698c0b8b142af07e0c69e" @@ -653,23 +622,6 @@ jsonschema "1.2.2" lodash.values "4.3.0" -"@0xproject/migrations@^1.0.3": - version "1.0.2" - dependencies: - "@0xproject/base-contract" "^1.0.3" - "@0xproject/order-utils" "^1.0.1-rc.1" - "@0xproject/sol-compiler" "^1.0.3" - "@0xproject/subproviders" "^1.0.3" - "@0xproject/typescript-typings" "^1.0.3" - "@0xproject/utils" "^1.0.3" - "@0xproject/web3-wrapper" "^1.1.1" - "@ledgerhq/hw-app-eth" "^4.3.0" - ethereum-types "^1.0.3" - ethers "3.0.22" - lodash "^4.17.4" - optionalDependencies: - "@ledgerhq/hw-transport-node-hid" "^4.3.0" - "@0xproject/order-utils@^0.0.7": version "0.0.7" resolved "https://registry.yarnpkg.com/@0xproject/order-utils/-/order-utils-0.0.7.tgz#eaa465782ea5745bdad54e1a851533172d993b7c" @@ -718,25 +670,6 @@ ethereumjs-util "5.1.5" lodash "4.17.10" -"@0xproject/order-utils@^1.0.1-rc.2": - version "1.0.1-rc.1" - dependencies: - "@0xproject/assert" "^1.0.3" - "@0xproject/base-contract" "^1.0.3" - "@0xproject/json-schemas" "^1.0.1-rc.2" - "@0xproject/sol-compiler" "^1.0.3" - "@0xproject/types" "^1.0.1-rc.2" - "@0xproject/typescript-typings" "^1.0.3" - "@0xproject/utils" "^1.0.3" - "@0xproject/web3-wrapper" "^1.1.1" - "@types/node" "^8.0.53" - bn.js "^4.11.8" - ethereum-types "^1.0.3" - ethereumjs-abi "0.6.5" - ethereumjs-util "^5.1.1" - ethers "3.0.22" - lodash "^4.17.4" - "@0xproject/order-watcher@^0.0.7": version "0.0.7" resolved "https://registry.yarnpkg.com/@0xproject/order-watcher/-/order-watcher-0.0.7.tgz#fbe019aa33447781096b5d562e7a3a4ec91a1da2" From 5ccf41c56693aa45988001260b68fdad2124b12c Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Wed, 8 Aug 2018 14:01:57 -0700 Subject: [PATCH 36/49] fix(monorepo-scripts): Fix typo in git tag command --- packages/monorepo-scripts/src/utils/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/monorepo-scripts/src/utils/utils.ts b/packages/monorepo-scripts/src/utils/utils.ts index d9bae3ea93..26ac801bdd 100644 --- a/packages/monorepo-scripts/src/utils/utils.ts +++ b/packages/monorepo-scripts/src/utils/utils.ts @@ -117,7 +117,7 @@ export const utils = { return tags; }, async getLocalGitTagsAsync(): Promise { - const result = await execAsync(`git tags`, { + const result = await execAsync(`git tag`, { cwd: constants.monorepoRootPath, }); const tagsString = result.stdout; From 6a5965d73bb542634631d7af76c150795d899744 Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Thu, 26 Jul 2018 14:11:03 -0700 Subject: [PATCH 37/49] Add strictArgumentEncodingCheck to BaseContract and use it in contract templates --- packages/0x.js/test/0x.js_test.ts | 3 +- packages/base-contract/src/index.ts | 17 ++ .../base-contract/test/base_contract_test.ts | 110 +++++++++++++ .../partials/callAsync.handlebars | 1 + .../contract_templates/partials/tx.handlebars | 1 + .../test/exchange/signature_validator.ts | 6 +- .../order-utils/test/signature_utils_test.ts | 3 +- packages/utils/package.json | 10 +- packages/utils/src/abi_utils.ts | 153 ++++++++++++++++++ packages/utils/test/abi_utils_test.ts | 19 +++ packages/utils/tsconfig.json | 5 +- 11 files changed, 319 insertions(+), 9 deletions(-) create mode 100644 packages/base-contract/test/base_contract_test.ts create mode 100644 packages/utils/test/abi_utils_test.ts diff --git a/packages/0x.js/test/0x.js_test.ts b/packages/0x.js/test/0x.js_test.ts index b4baecb1ec..2c1cb2c74a 100644 --- a/packages/0x.js/test/0x.js_test.ts +++ b/packages/0x.js/test/0x.js_test.ts @@ -48,8 +48,9 @@ describe('ZeroEx library', () => { const ethSignSignature = '0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace225403'; const address = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; + const bytes32Zeros = '0x0000000000000000000000000000000000000000000000000000000000000000'; it("should return false if the data doesn't pertain to the signature & address", async () => { - return expect((zeroEx.exchange as any).isValidSignatureAsync('0x0', address, ethSignSignature)).to.become( + return expect((zeroEx.exchange as any).isValidSignatureAsync(bytes32Zeros, address, ethSignSignature)).to.become( false, ); }); diff --git a/packages/base-contract/src/index.ts b/packages/base-contract/src/index.ts index a240fb8b6c..e354f109ec 100644 --- a/packages/base-contract/src/index.ts +++ b/packages/base-contract/src/index.ts @@ -82,6 +82,23 @@ export class BaseContract { } return txDataWithDefaults; } + // Throws if the given arguments cannot be safely/correctly encoded based on + // the given inputAbi. An argument may not be considered safely encodeable + // if it overflows the corresponding Solidity type, there is a bug in the + // encoder, or the encoder performs unsafe type coercion. + public static strictArgumentEncodingCheck(inputAbi: DataItem[], args: any[]): void { + const coder = (ethers as any).utils.AbiCoder.defaultCoder; + const params = abiUtils.parseEthersParams(inputAbi); + const rawEncoded = coder.encode(params.names, params.types, args); + const rawDecoded = coder.decode(params.names, params.types, rawEncoded); + for (let i = 0; i < rawDecoded.length; i++) { + const original = args[i]; + const decoded = rawDecoded[i]; + if (!abiUtils.isAbiDataEqual(params.names[i], params.types[i], original, decoded)) { + throw new Error(`Cannot safely encode argument: ${params.names[i]} (${original}) of type ${params.types[i]}. (Possible type overflow or other encoding error)`); + } + } + } protected _lookupEthersInterface(functionSignature: string): ethers.Interface { const ethersInterface = this._ethersInterfacesByFunctionSignature[functionSignature]; if (_.isUndefined(ethersInterface)) { diff --git a/packages/base-contract/test/base_contract_test.ts b/packages/base-contract/test/base_contract_test.ts new file mode 100644 index 0000000000..57aacdc8a2 --- /dev/null +++ b/packages/base-contract/test/base_contract_test.ts @@ -0,0 +1,110 @@ +import * as chai from 'chai'; +import 'mocha'; + +import { BaseContract } from '../src'; + +const { expect } = chai; + +describe('BaseContract', () => { + describe('strictArgumentEncodingCheck', () => { + it('works for simple types', () => { + BaseContract.strictArgumentEncodingCheck([{ name: 'to', type: 'address' }], ['0xe834ec434daba538cd1b9fe1582052b880bd7e63']); + }); + it('works for array types', () => { + const inputAbi = [{ + name: 'takerAssetFillAmounts', + type: 'uint256[]', + }]; + const args = [ + [ + '9000000000000000000', + '79000000000000000000', + '979000000000000000000', + '7979000000000000000000', + ], + ]; + BaseContract.strictArgumentEncodingCheck(inputAbi, args); + }); + it('works for tuple/struct types', () => { + const inputAbi = [ + { + components: [ + { + name: 'makerAddress', + type: 'address', + }, + { + name: 'takerAddress', + type: 'address', + }, + { + name: 'feeRecipientAddress', + type: 'address', + }, + { + name: 'senderAddress', + type: 'address', + }, + { + name: 'makerAssetAmount', + type: 'uint256', + }, + { + name: 'takerAssetAmount', + type: 'uint256', + }, + { + name: 'makerFee', + type: 'uint256', + }, + { + name: 'takerFee', + type: 'uint256', + }, + { + name: 'expirationTimeSeconds', + type: 'uint256', + }, + { + name: 'salt', + type: 'uint256', + }, + { + name: 'makerAssetData', + type: 'bytes', + }, + { + name: 'takerAssetData', + type: 'bytes', + }, + ], + name: 'order', + type: 'tuple', + }, + ]; + const args = [ + { + makerAddress: '0x6ecbe1db9ef729cbe972c83fb886247691fb6beb', + takerAddress: '0x0000000000000000000000000000000000000000', + feeRecipientAddress: '0xe834ec434daba538cd1b9fe1582052b880bd7e63', + senderAddress: '0x0000000000000000000000000000000000000000', + makerAssetAmount: '0', + takerAssetAmount: '200000000000000000000', + makerFee: '1000000000000000000', + takerFee: '1000000000000000000', + expirationTimeSeconds: '1532563026', + salt: '59342956082154660870994022243365949771115859664887449740907298019908621891376', + makerAssetData: '0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48', + takerAssetData: '0xf47261b00000000000000000000000001d7022f5b17d2f8b695918fb48fa1089c9f85401', + }, + ]; + BaseContract.strictArgumentEncodingCheck(inputAbi, args); + }); + it('throws for integer overflows', () => { + expect(() => BaseContract.strictArgumentEncodingCheck([{ name: 'amount', type: 'uint8' }], ['256'])).to.throw(); + }); + it('throws for fixed byte array overflows', () => { + expect(() => BaseContract.strictArgumentEncodingCheck([{ name: 'hash', type: 'bytes8' }], ['0x001122334455667788'])).to.throw(); + }); + }); +}); diff --git a/packages/contract_templates/partials/callAsync.handlebars b/packages/contract_templates/partials/callAsync.handlebars index fcaae57c66..94752691d9 100644 --- a/packages/contract_templates/partials/callAsync.handlebars +++ b/packages/contract_templates/partials/callAsync.handlebars @@ -7,6 +7,7 @@ async callAsync( const functionSignature = '{{this.functionSignature}}'; const inputAbi = self._lookupAbi(functionSignature).inputs; [{{> params inputs=inputs}}] = BaseContract._formatABIDataItemList(inputAbi, [{{> params inputs=inputs}}], BaseContract._bigNumberToString.bind(self)); + BaseContract.strictArgumentEncodingCheck(inputAbi, [{{> params inputs=inputs}}]); const ethersFunction = self._lookupEthersInterface(functionSignature).functions.{{this.name}}( {{> params inputs=inputs}} ) as ethers.CallDescription; diff --git a/packages/contract_templates/partials/tx.handlebars b/packages/contract_templates/partials/tx.handlebars index e297d05e60..4340d662e3 100644 --- a/packages/contract_templates/partials/tx.handlebars +++ b/packages/contract_templates/partials/tx.handlebars @@ -11,6 +11,7 @@ public {{this.tsName}} = { const self = this as any as {{contractName}}Contract; const inputAbi = self._lookupAbi('{{this.functionSignature}}').inputs; [{{> params inputs=inputs}}] = BaseContract._formatABIDataItemList(inputAbi, [{{> params inputs=inputs}}], BaseContract._bigNumberToString.bind(self)); + BaseContract.strictArgumentEncodingCheck(inputAbi, [{{> params inputs=inputs}}]); const encodedData = self._lookupEthersInterface('{{this.functionSignature}}').functions.{{this.name}}( {{> params inputs=inputs}} ).data; diff --git a/packages/contracts/test/exchange/signature_validator.ts b/packages/contracts/test/exchange/signature_validator.ts index f2bb42c753..bef0547bd3 100644 --- a/packages/contracts/test/exchange/signature_validator.ts +++ b/packages/contracts/test/exchange/signature_validator.ts @@ -113,7 +113,7 @@ describe('MixinSignatureValidator', () => { it('should revert when signature type is unsupported', async () => { const unsupportedSignatureType = SignatureType.NSignatureTypes; - const unsupportedSignatureHex = `0x${unsupportedSignatureType}`; + const unsupportedSignatureHex = '0x' + Buffer.from([unsupportedSignatureType]).toString('hex'); const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); return expectContractCallFailed( signatureValidator.publicIsValidSignature.callAsync( @@ -126,7 +126,7 @@ describe('MixinSignatureValidator', () => { }); it('should revert when SignatureType=Illegal', async () => { - const unsupportedSignatureHex = `0x${SignatureType.Illegal}`; + const unsupportedSignatureHex = '0x' + Buffer.from([SignatureType.Illegal]).toString('hex'); const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); return expectContractCallFailed( signatureValidator.publicIsValidSignature.callAsync( @@ -139,7 +139,7 @@ describe('MixinSignatureValidator', () => { }); it('should return false when SignatureType=Invalid and signature has a length of zero', async () => { - const signatureHex = `0x${SignatureType.Invalid}`; + const signatureHex = '0x' + Buffer.from([SignatureType.Invalid]).toString('hex'); const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( orderHashHex, diff --git a/packages/order-utils/test/signature_utils_test.ts b/packages/order-utils/test/signature_utils_test.ts index 5714f96711..baae2b4143 100644 --- a/packages/order-utils/test/signature_utils_test.ts +++ b/packages/order-utils/test/signature_utils_test.ts @@ -22,7 +22,8 @@ describe('Signature utils', () => { let address = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; it("should return false if the data doesn't pertain to the signature & address", async () => { - expect(await isValidSignatureAsync(provider, '0x0', ethSignSignature, address)).to.be.false(); + const bytes32Zeros = '0x0000000000000000000000000000000000000000000000000000000000000000'; + expect(await isValidSignatureAsync(provider, bytes32Zeros, ethSignSignature, address)).to.be.false(); }); it("should return false if the address doesn't pertain to the signature & data", async () => { const validUnrelatedAddress = '0x8b0292b11a196601ed2ce54b665cafeca0347d42'; diff --git a/packages/utils/package.json b/packages/utils/package.json index ee150cb0e1..46c1d05d00 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -5,12 +5,13 @@ "node": ">=6.12" }, "description": "0x TS utils", - "main": "lib/index.js", - "types": "lib/index.d.ts", + "main": "lib/src/index.js", + "types": "lib/src/index.d.ts", "scripts": { "watch_without_deps": "tsc -w", "build": "tsc && copyfiles -u 2 './lib/monorepo_scripts/**/*' ./scripts", "clean": "shx rm -rf lib scripts", + "test": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --bail --exit", "lint": "tslint --project .", "manual:postpublish": "yarn build; node ./scripts/postpublish.js" }, @@ -27,12 +28,15 @@ "@0xproject/monorepo-scripts": "^1.0.4", "@0xproject/tslint-config": "^1.0.4", "@types/lodash": "4.14.104", + "@types/mocha": "^2.2.42", "copyfiles": "^1.2.0", "make-promises-safe": "^1.1.0", "npm-run-all": "^4.1.2", "shx": "^0.2.2", "tslint": "5.11.0", - "typescript": "2.9.2" + "typescript": "2.9.2", + "chai": "^4.0.1", + "mocha": "^4.0.1" }, "dependencies": { "@0xproject/types": "^1.0.1-rc.3", diff --git a/packages/utils/src/abi_utils.ts b/packages/utils/src/abi_utils.ts index 421dd405cb..16aa72afd1 100644 --- a/packages/utils/src/abi_utils.ts +++ b/packages/utils/src/abi_utils.ts @@ -1,7 +1,160 @@ import { AbiDefinition, AbiType, ContractAbi, DataItem, MethodAbi } from 'ethereum-types'; import * as _ from 'lodash'; +import { BigNumber } from './configured_bignumber'; + +export type EthersParamName = null | string | EthersNestedParamName; + +export interface EthersNestedParamName { + name: string | null; + names: EthersParamName[]; +} + +// Note(albrow): This function is unexported in ethers.js. Copying it here for +// now. +// Source: https://github.com/ethers-io/ethers.js/blob/884593ab76004a808bf8097e9753fb5f8dcc3067/contracts/interface.js#L30 +function parseEthersParams(params: DataItem[]): { names: EthersParamName[]; types: string[] } { + const names: EthersParamName[] = []; + const types: string[] = []; + + params.forEach((param: DataItem) => { + if (param.components != null) { + let suffix = ''; + const arrayBracket = param.type.indexOf('['); + if (arrayBracket >= 0) { suffix = param.type.substring(arrayBracket); } + + const result = parseEthersParams(param.components); + names.push({ name: (param.name || null), names: result.names }); + types.push('tuple(' + result.types.join(',') + ')' + suffix); + } else { + names.push(param.name || null); + types.push(param.type); + } + }); + + return { + names, + types, + }; +} + +// returns true if x is equal to y and false otherwise. Performs some minimal +// type conversion and data massaging for x and y, depending on type. name and +// type should typically be derived from parseEthersParams. +function isAbiDataEqual(name: EthersParamName, type: string, x: any, y: any): boolean { + if (_.isUndefined(x) && _.isUndefined(y)) { + return true; + } else if (_.isUndefined(x) && !_.isUndefined(y)) { + return false; + } else if (!_.isUndefined(x) && _.isUndefined(y)) { + return false; + } + if (_.endsWith(type, '[]')) { + // For array types, we iterate through the elements and check each one + // individually. Strangely, name does not need to be changed in this + // case. + if (x.length !== y.length) { + return false; + } + const newType = _.trimEnd(type, '[]'); + for (let i = 0; i < x.length; i++) { + if (!isAbiDataEqual(name, newType, x[i], y[i])) { + return false; + } + } + return true; + } + if (_.startsWith(type, 'tuple(')) { + if (_.isString(name)) { + throw new Error('Internal error: type was tuple but names was a string'); + } else if (_.isNull(name)) { + throw new Error('Internal error: type was tuple but names was a null'); + } + // For tuples, we iterate through the underlying values and check each + // one individually. + const types = splitTupleTypes(type); + if (types.length !== name.names.length) { + throw new Error(`Internal error: parameter types/names length mismatch (${types.length} != ${name.names.length})`); + } + for (let i = 0; i < types.length; i++) { + // For tuples, name is an object with a names property that is an + // array. As an example, for orders, name looks like: + // + // { + // name: 'orders', + // names: [ + // 'makerAddress', + // // ... + // 'takerAssetData' + // ] + // } + // + const nestedName = _.isString(name.names[i]) ? name.names[i] as string : (name.names[i] as EthersNestedParamName).name as string; + if (!isAbiDataEqual(name.names[i], types[i], x[nestedName], y[nestedName])) { + return false; + } + } + return true; + } else if (type === 'address' || type === 'bytes') { + // HACK(albrow): ethers.js sometimes changes the case of addresses/bytes + // when decoding/encoding. To account for that, we convert to lowercase + // before comparing. + return _.isEqual(_.toLower(x), _.toLower(y)); + } else if (_.startsWith(type, 'uint') || _.startsWith(type, 'int')) { + return new BigNumber(x).eq(new BigNumber(y)); + } + return _.isEqual(x, y); +} + +// splitTupleTypes splits a tuple type string (of the form `tuple(X)` where X is +// any other type or list of types) into its component types. It works with +// nested tuples, so, e.g., `tuple(tuple(uint256,address),bytes32)` will yield: +// `['tuple(uint256,address)', 'bytes32']`. It expects exactly one tuple type as +// an argument (not an array). +function splitTupleTypes(type: string): string[] { + if (_.endsWith(type, '[]')) { + throw new Error('Internal error: array types are not supported'); + } else if (!_.startsWith(type, 'tuple(')) { + throw new Error('Internal error: expected tuple type but got non-tuple type: ' + type); + } + // Trim the outtermost tuple(). + const trimmedType = type.substring('tuple('.length, type.length - 1); + const types: string[] = []; + let currToken = ''; + let parenCount = 0; + // Tokenize the type string while keeping track of parentheses. + for (const char of trimmedType) { + switch (char) { + case '(': + parenCount += 1; + currToken += char; + break; + case ')': + parenCount -= 1; + currToken += char; + break; + case ',': + if (parenCount === 0) { + types.push(currToken); + currToken = ''; + break; + } else { + currToken += char; + break; + } + default: + currToken += char; + break; + } + } + types.push(currToken); + return types; +} + export const abiUtils = { + parseEthersParams, + isAbiDataEqual, + splitTupleTypes, parseFunctionParam(param: DataItem): string { if (param.type === 'tuple') { // Parse out tuple types into {type_1, type_2, ..., type_N} diff --git a/packages/utils/test/abi_utils_test.ts b/packages/utils/test/abi_utils_test.ts new file mode 100644 index 0000000000..0ebee64c48 --- /dev/null +++ b/packages/utils/test/abi_utils_test.ts @@ -0,0 +1,19 @@ +import * as chai from 'chai'; +import 'mocha'; + +import { abiUtils } from '../src'; + +const expect = chai.expect; + +describe('abiUtils', () => { + describe('splitTupleTypes', () => { + it('handles basic types', () => { + const got = abiUtils.splitTupleTypes('tuple(bytes,uint256,address)'); + expect(got).to.deep.equal(['bytes', 'uint256', 'address']); + }); + it('handles nested tuple types', () => { + const got = abiUtils.splitTupleTypes('tuple(tuple(bytes,uint256),address)'); + expect(got).to.deep.equal(['tuple(bytes,uint256)', 'address']); + }); + }); +}); diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json index c56d255d54..852708eba0 100644 --- a/packages/utils/tsconfig.json +++ b/packages/utils/tsconfig.json @@ -3,5 +3,8 @@ "compilerOptions": { "outDir": "lib" }, - "include": ["./src/**/*"] + "include": [ + "src/**/*", + "test/**/*" + ] } From 6a6739ebbec291b61226c047fde7b3d0bb4a7250 Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Thu, 26 Jul 2018 14:32:52 -0700 Subject: [PATCH 38/49] Apply prettier --- packages/0x.js/test/0x.js_test.ts | 6 ++-- packages/base-contract/src/index.ts | 6 +++- .../base-contract/test/base_contract_test.ts | 30 +++++++++++-------- packages/utils/src/abi_utils.ts | 14 ++++++--- packages/utils/tsconfig.json | 5 +--- 5 files changed, 36 insertions(+), 25 deletions(-) diff --git a/packages/0x.js/test/0x.js_test.ts b/packages/0x.js/test/0x.js_test.ts index 2c1cb2c74a..be2a94482a 100644 --- a/packages/0x.js/test/0x.js_test.ts +++ b/packages/0x.js/test/0x.js_test.ts @@ -50,9 +50,9 @@ describe('ZeroEx library', () => { const address = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; const bytes32Zeros = '0x0000000000000000000000000000000000000000000000000000000000000000'; it("should return false if the data doesn't pertain to the signature & address", async () => { - return expect((zeroEx.exchange as any).isValidSignatureAsync(bytes32Zeros, address, ethSignSignature)).to.become( - false, - ); + return expect( + (zeroEx.exchange as any).isValidSignatureAsync(bytes32Zeros, address, ethSignSignature), + ).to.become(false); }); it("should return false if the address doesn't pertain to the signature & data", async () => { const validUnrelatedAddress = '0x8b0292b11a196601ed2ce54b665cafeca0347d42'; diff --git a/packages/base-contract/src/index.ts b/packages/base-contract/src/index.ts index e354f109ec..31d2e4019d 100644 --- a/packages/base-contract/src/index.ts +++ b/packages/base-contract/src/index.ts @@ -95,7 +95,11 @@ export class BaseContract { const original = args[i]; const decoded = rawDecoded[i]; if (!abiUtils.isAbiDataEqual(params.names[i], params.types[i], original, decoded)) { - throw new Error(`Cannot safely encode argument: ${params.names[i]} (${original}) of type ${params.types[i]}. (Possible type overflow or other encoding error)`); + throw new Error( + `Cannot safely encode argument: ${params.names[i]} (${original}) of type ${ + params.types[i] + }. (Possible type overflow or other encoding error)`, + ); } } } diff --git a/packages/base-contract/test/base_contract_test.ts b/packages/base-contract/test/base_contract_test.ts index 57aacdc8a2..2c31d1f119 100644 --- a/packages/base-contract/test/base_contract_test.ts +++ b/packages/base-contract/test/base_contract_test.ts @@ -8,20 +8,20 @@ const { expect } = chai; describe('BaseContract', () => { describe('strictArgumentEncodingCheck', () => { it('works for simple types', () => { - BaseContract.strictArgumentEncodingCheck([{ name: 'to', type: 'address' }], ['0xe834ec434daba538cd1b9fe1582052b880bd7e63']); + BaseContract.strictArgumentEncodingCheck( + [{ name: 'to', type: 'address' }], + ['0xe834ec434daba538cd1b9fe1582052b880bd7e63'], + ); }); it('works for array types', () => { - const inputAbi = [{ - name: 'takerAssetFillAmounts', - type: 'uint256[]', - }]; + const inputAbi = [ + { + name: 'takerAssetFillAmounts', + type: 'uint256[]', + }, + ]; const args = [ - [ - '9000000000000000000', - '79000000000000000000', - '979000000000000000000', - '7979000000000000000000', - ], + ['9000000000000000000', '79000000000000000000', '979000000000000000000', '7979000000000000000000'], ]; BaseContract.strictArgumentEncodingCheck(inputAbi, args); }); @@ -101,10 +101,14 @@ describe('BaseContract', () => { BaseContract.strictArgumentEncodingCheck(inputAbi, args); }); it('throws for integer overflows', () => { - expect(() => BaseContract.strictArgumentEncodingCheck([{ name: 'amount', type: 'uint8' }], ['256'])).to.throw(); + expect(() => + BaseContract.strictArgumentEncodingCheck([{ name: 'amount', type: 'uint8' }], ['256']), + ).to.throw(); }); it('throws for fixed byte array overflows', () => { - expect(() => BaseContract.strictArgumentEncodingCheck([{ name: 'hash', type: 'bytes8' }], ['0x001122334455667788'])).to.throw(); + expect(() => + BaseContract.strictArgumentEncodingCheck([{ name: 'hash', type: 'bytes8' }], ['0x001122334455667788']), + ).to.throw(); }); }); }); diff --git a/packages/utils/src/abi_utils.ts b/packages/utils/src/abi_utils.ts index 16aa72afd1..874e0b2da0 100644 --- a/packages/utils/src/abi_utils.ts +++ b/packages/utils/src/abi_utils.ts @@ -21,10 +21,12 @@ function parseEthersParams(params: DataItem[]): { names: EthersParamName[]; type if (param.components != null) { let suffix = ''; const arrayBracket = param.type.indexOf('['); - if (arrayBracket >= 0) { suffix = param.type.substring(arrayBracket); } + if (arrayBracket >= 0) { + suffix = param.type.substring(arrayBracket); + } const result = parseEthersParams(param.components); - names.push({ name: (param.name || null), names: result.names }); + names.push({ name: param.name || null, names: result.names }); types.push('tuple(' + result.types.join(',') + ')' + suffix); } else { names.push(param.name || null); @@ -74,7 +76,9 @@ function isAbiDataEqual(name: EthersParamName, type: string, x: any, y: any): bo // one individually. const types = splitTupleTypes(type); if (types.length !== name.names.length) { - throw new Error(`Internal error: parameter types/names length mismatch (${types.length} != ${name.names.length})`); + throw new Error( + `Internal error: parameter types/names length mismatch (${types.length} != ${name.names.length})`, + ); } for (let i = 0; i < types.length; i++) { // For tuples, name is an object with a names property that is an @@ -89,7 +93,9 @@ function isAbiDataEqual(name: EthersParamName, type: string, x: any, y: any): bo // ] // } // - const nestedName = _.isString(name.names[i]) ? name.names[i] as string : (name.names[i] as EthersNestedParamName).name as string; + const nestedName = _.isString(name.names[i]) + ? (name.names[i] as string) + : ((name.names[i] as EthersNestedParamName).name as string); if (!isAbiDataEqual(name.names[i], types[i], x[nestedName], y[nestedName])) { return false; } diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json index 852708eba0..8b4cd47a26 100644 --- a/packages/utils/tsconfig.json +++ b/packages/utils/tsconfig.json @@ -3,8 +3,5 @@ "compilerOptions": { "outDir": "lib" }, - "include": [ - "src/**/*", - "test/**/*" - ] + "include": ["src/**/*", "test/**/*"] } From 52e094addcecc6136c9582a51dd52b8f44f769f7 Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Mon, 6 Aug 2018 16:58:46 -0700 Subject: [PATCH 39/49] Move some ethers-related types to typescript-typings/ethers --- packages/base-contract/src/index.ts | 2 +- .../typescript-typings/types/ethers/index.d.ts | 18 ++++++++++++++++++ packages/utils/src/abi_utils.ts | 18 ++++++------------ 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/base-contract/src/index.ts b/packages/base-contract/src/index.ts index 31d2e4019d..12f9744452 100644 --- a/packages/base-contract/src/index.ts +++ b/packages/base-contract/src/index.ts @@ -87,7 +87,7 @@ export class BaseContract { // if it overflows the corresponding Solidity type, there is a bug in the // encoder, or the encoder performs unsafe type coercion. public static strictArgumentEncodingCheck(inputAbi: DataItem[], args: any[]): void { - const coder = (ethers as any).utils.AbiCoder.defaultCoder; + const coder = ethers.utils.AbiCoder.defaultCoder; const params = abiUtils.parseEthersParams(inputAbi); const rawEncoded = coder.encode(params.names, params.types, args); const rawDecoded = coder.decode(params.names, params.types, rawEncoded); diff --git a/packages/typescript-typings/types/ethers/index.d.ts b/packages/typescript-typings/types/ethers/index.d.ts index f869196e09..58bc1e8a91 100644 --- a/packages/typescript-typings/types/ethers/index.d.ts +++ b/packages/typescript-typings/types/ethers/index.d.ts @@ -34,4 +34,22 @@ declare module 'ethers' { const enum errors { INVALID_ARGUMENT = 'INVALID_ARGUMENT', } + + export type ParamName = null | string | NestedParamName; + + export interface NestedParamName { + name: string | null; + names: ParamName[]; + } + + export const utils: { + AbiCoder: { + defaultCoder: AbiCoder; + }; + }; + + export interface AbiCoder { + encode: (names?: ParamName[], types: string[], args: any[]) => string; + decode: (names?: ParamName[], types: string[], data: string) => any; + } } diff --git a/packages/utils/src/abi_utils.ts b/packages/utils/src/abi_utils.ts index 874e0b2da0..fc64a2a891 100644 --- a/packages/utils/src/abi_utils.ts +++ b/packages/utils/src/abi_utils.ts @@ -1,20 +1,14 @@ import { AbiDefinition, AbiType, ContractAbi, DataItem, MethodAbi } from 'ethereum-types'; +import * as ethers from 'ethers'; import * as _ from 'lodash'; import { BigNumber } from './configured_bignumber'; -export type EthersParamName = null | string | EthersNestedParamName; - -export interface EthersNestedParamName { - name: string | null; - names: EthersParamName[]; -} - // Note(albrow): This function is unexported in ethers.js. Copying it here for // now. // Source: https://github.com/ethers-io/ethers.js/blob/884593ab76004a808bf8097e9753fb5f8dcc3067/contracts/interface.js#L30 -function parseEthersParams(params: DataItem[]): { names: EthersParamName[]; types: string[] } { - const names: EthersParamName[] = []; +function parseEthersParams(params: DataItem[]): { names: ethers.ParamName[]; types: string[] } { + const names: ethers.ParamName[] = []; const types: string[] = []; params.forEach((param: DataItem) => { @@ -43,7 +37,7 @@ function parseEthersParams(params: DataItem[]): { names: EthersParamName[]; type // returns true if x is equal to y and false otherwise. Performs some minimal // type conversion and data massaging for x and y, depending on type. name and // type should typically be derived from parseEthersParams. -function isAbiDataEqual(name: EthersParamName, type: string, x: any, y: any): boolean { +function isAbiDataEqual(name: ethers.ParamName, type: string, x: any, y: any): boolean { if (_.isUndefined(x) && _.isUndefined(y)) { return true; } else if (_.isUndefined(x) && !_.isUndefined(y)) { @@ -70,7 +64,7 @@ function isAbiDataEqual(name: EthersParamName, type: string, x: any, y: any): bo if (_.isString(name)) { throw new Error('Internal error: type was tuple but names was a string'); } else if (_.isNull(name)) { - throw new Error('Internal error: type was tuple but names was a null'); + throw new Error('Internal error: type was tuple but names was null'); } // For tuples, we iterate through the underlying values and check each // one individually. @@ -95,7 +89,7 @@ function isAbiDataEqual(name: EthersParamName, type: string, x: any, y: any): bo // const nestedName = _.isString(name.names[i]) ? (name.names[i] as string) - : ((name.names[i] as EthersNestedParamName).name as string); + : ((name.names[i] as ethers.NestedParamName).name as string); if (!isAbiDataEqual(name.names[i], types[i], x[nestedName], y[nestedName])) { return false; } From 09af23f950b6143cc3aec3c5843963e70d2a5b5c Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Tue, 7 Aug 2018 11:01:41 -0700 Subject: [PATCH 40/49] Update CHANGELOG.json for base-contract --- packages/base-contract/CHANGELOG.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/base-contract/CHANGELOG.json b/packages/base-contract/CHANGELOG.json index 9dddde18ac..b041e13da0 100644 --- a/packages/base-contract/CHANGELOG.json +++ b/packages/base-contract/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "2.0.0-rc.1", + "changes": [ + { + "pr": 915, + "note": "Added strict encoding/decoding checks for sendTransaction and call" + } + ] + }, { "timestamp": 1532619515, "version": "1.0.4", From 44b01f2069c10e84fc7fd55dc24767dfe877321c Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Wed, 8 Aug 2018 14:52:05 -0700 Subject: [PATCH 41/49] Update ethers typings for TypeScript 2.9.2 --- packages/typescript-typings/types/ethers/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typescript-typings/types/ethers/index.d.ts b/packages/typescript-typings/types/ethers/index.d.ts index 58bc1e8a91..875563ba2d 100644 --- a/packages/typescript-typings/types/ethers/index.d.ts +++ b/packages/typescript-typings/types/ethers/index.d.ts @@ -49,7 +49,7 @@ declare module 'ethers' { }; export interface AbiCoder { - encode: (names?: ParamName[], types: string[], args: any[]) => string; - decode: (names?: ParamName[], types: string[], data: string) => any; + encode: (names: ParamName[] | string[], types: string[] | any[], args: any[] | undefined) => string; + decode: (names: ParamName[] | string[], types: string[] | string, data: string | undefined) => any; } } From 44d909c0c789a5b3fba2393746593d39c0ed1df7 Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Wed, 8 Aug 2018 14:54:35 -0700 Subject: [PATCH 42/49] Update CHANGELOG.json for contract-wrappers --- packages/contract-wrappers/CHANGELOG.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index 02e1acf914..fb077ce734 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -2,6 +2,10 @@ { "version": "1.0.1-rc.3", "changes": [ + { + "pr": 915, + "note": "Added strict encoding/decoding checks for sendTransaction and call" + }, { "note": "Add ForwarderWrapper", "pr": 934 From 2a85f79040117cbcf58a03fb66dbd67f83b257d3 Mon Sep 17 00:00:00 2001 From: fragosti Date: Wed, 8 Aug 2018 15:42:09 -0700 Subject: [PATCH 43/49] Change amir picture --- packages/website/public/images/team/amir.jpeg | Bin 21098 -> 0 bytes packages/website/public/images/team/amir.png | Bin 0 -> 116488 bytes packages/website/ts/pages/about/about.tsx | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 packages/website/public/images/team/amir.jpeg create mode 100644 packages/website/public/images/team/amir.png diff --git a/packages/website/public/images/team/amir.jpeg b/packages/website/public/images/team/amir.jpeg deleted file mode 100644 index 7ee16263a531246eec1c5be32dd5991880635cd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21098 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 GAOG1w=NEne diff --git a/packages/website/public/images/team/amir.png b/packages/website/public/images/team/amir.png new file mode 100644 index 0000000000000000000000000000000000000000..2bb795d502f811c0b887344218143231c695b73f GIT binary patch literal 116488 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

ILM*u3||;U{0<3Yrij?YGbjLUZY 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

uWT>L<4u{;}bb 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 literal 0 HcmV?d00001 diff --git a/packages/website/ts/pages/about/about.tsx b/packages/website/ts/pages/about/about.tsx index b9bc906bdd..1574f65e00 100644 --- a/packages/website/ts/pages/about/about.tsx +++ b/packages/website/ts/pages/about/about.tsx @@ -27,7 +27,7 @@ const teamRow1: ProfileInfo[] = [ title: 'Co-founder & CTO', description: `Smart contract R&D. Previously fixed income trader at DRW. \ Finance at University of Illinois, Urbana-Champaign.`, - image: '/images/team/amir.jpeg', + image: '/images/team/amir.png', linkedIn: 'https://www.linkedin.com/in/abandeali1/', github: 'https://github.com/abandeali1', medium: 'https://medium.com/@abandeali1', From 762bbe9bcd79ca5b832859cc20ffa4c83603cbcb Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Wed, 8 Aug 2018 16:44:52 -0700 Subject: [PATCH 44/49] Update remaining CHANGELOG.json files --- packages/0x.js/CHANGELOG.json | 8 ++++++++ packages/fill-scenarios/CHANGELOG.json | 3 +++ packages/order-utils/CHANGELOG.json | 3 +++ packages/order-watcher/CHANGELOG.json | 8 ++++++++ 4 files changed, 22 insertions(+) diff --git a/packages/0x.js/CHANGELOG.json b/packages/0x.js/CHANGELOG.json index 4f39a9b8c0..913b7a76e6 100644 --- a/packages/0x.js/CHANGELOG.json +++ b/packages/0x.js/CHANGELOG.json @@ -1,4 +1,12 @@ [ + { + "version": "1.0.1-rc.3", + "changes": [ + { + "note": "Dependencies updated" + } + ] + }, { "version": "1.0.1-rc.2", "changes": [ diff --git a/packages/fill-scenarios/CHANGELOG.json b/packages/fill-scenarios/CHANGELOG.json index 04e0762039..1c3864da26 100644 --- a/packages/fill-scenarios/CHANGELOG.json +++ b/packages/fill-scenarios/CHANGELOG.json @@ -6,6 +6,9 @@ "note": "Updated to use latest orderFactory interface, fixed `feeRecipient` spelling error in public interface", "pr": 936 + }, + { + "note": "Dependencies updated" } ] }, diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index 776bd67ec9..fa82976ad1 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -10,6 +10,9 @@ { "note": "Added marketUtils", "pr": 937 + }, + { + "note": "Dependencies updated" } ] }, diff --git a/packages/order-watcher/CHANGELOG.json b/packages/order-watcher/CHANGELOG.json index 08c1f7f580..0c9ef7a8aa 100644 --- a/packages/order-watcher/CHANGELOG.json +++ b/packages/order-watcher/CHANGELOG.json @@ -1,4 +1,12 @@ [ + { + "version": "1.0.1-rc.3", + "changes": [ + { + "note": "Dependencies updated" + } + ] + }, { "version": "1.0.1-rc.2", "changes": [ From ca7d8a8940df23e56fb034878939112bc359cb2f Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Wed, 8 Aug 2018 16:47:36 -0700 Subject: [PATCH 45/49] Update CI config and package.json to run @0xproject/utils tests on CI --- .circleci/config.yml | 1 + packages/utils/package.json | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b93f62fa74..43e542a86c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -80,6 +80,7 @@ jobs: - run: yarn wsrun test:circleci @0xproject/sra-report - run: yarn wsrun test:circleci @0xproject/subproviders - run: yarn wsrun test:circleci @0xproject/web3-wrapper + - run: yarn wsrun test:circleci @0xproject/utils - save_cache: key: coverage-0xjs-{{ .Environment.CIRCLE_SHA1 }} paths: diff --git a/packages/utils/package.json b/packages/utils/package.json index 46c1d05d00..ee5fc264f7 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -11,7 +11,11 @@ "watch_without_deps": "tsc -w", "build": "tsc && copyfiles -u 2 './lib/monorepo_scripts/**/*' ./scripts", "clean": "shx rm -rf lib scripts", - "test": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --bail --exit", + "test": "yarn run_mocha", + "test:circleci": "yarn test:coverage", + "run_mocha": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --bail --exit", + "test:coverage": "nyc npm run test --all && yarn coverage:report:lcov", + "coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info", "lint": "tslint --project .", "manual:postpublish": "yarn build; node ./scripts/postpublish.js" }, From 5b7774f9d00a0f80601e6ff4ed0920c6a150a350 Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Wed, 8 Aug 2018 17:33:20 -0700 Subject: [PATCH 46/49] Add packages/coverage/.gitkeep file --- packages/utils/coverage/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/utils/coverage/.gitkeep diff --git a/packages/utils/coverage/.gitkeep b/packages/utils/coverage/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 From c4c37cafa0d8a77bfdc01b1cc111ba0101e86c8b Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Wed, 8 Aug 2018 17:58:04 -0700 Subject: [PATCH 47/49] Update comment about ethers checksummed address behavior --- packages/utils/src/abi_utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/utils/src/abi_utils.ts b/packages/utils/src/abi_utils.ts index fc64a2a891..c9b70966c1 100644 --- a/packages/utils/src/abi_utils.ts +++ b/packages/utils/src/abi_utils.ts @@ -96,9 +96,9 @@ function isAbiDataEqual(name: ethers.ParamName, type: string, x: any, y: any): b } return true; } else if (type === 'address' || type === 'bytes') { - // HACK(albrow): ethers.js sometimes changes the case of addresses/bytes - // when decoding/encoding. To account for that, we convert to lowercase - // before comparing. + // HACK(albrow): ethers.js returns the checksummed address even when + // initially passed in a non-checksummed address. To account for that, + // we convert to lowercase before comparing. return _.isEqual(_.toLower(x), _.toLower(y)); } else if (_.startsWith(type, 'uint') || _.startsWith(type, 'int')) { return new BigNumber(x).eq(new BigNumber(y)); From 68605ca261330acaf83daaf355170fbda20ad02f Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 9 Aug 2018 17:00:05 +0200 Subject: [PATCH 48/49] Import marshaller directly --- packages/subproviders/src/subproviders/signer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/subproviders/src/subproviders/signer.ts b/packages/subproviders/src/subproviders/signer.ts index 8c4e89cafe..d5fd86897a 100644 --- a/packages/subproviders/src/subproviders/signer.ts +++ b/packages/subproviders/src/subproviders/signer.ts @@ -1,5 +1,4 @@ -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import { marshaller } from '@0xproject/web3-wrapper/lib/src/marshaller'; +import { marshaller, Web3Wrapper } from '@0xproject/web3-wrapper'; import { JSONRPCRequestPayload, Provider } from 'ethereum-types'; import { Callback, ErrorCallback } from '../types'; From d44ff6a91582ed2b4dc25059d52556c4f9c6e163 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 9 Aug 2018 17:02:13 +0200 Subject: [PATCH 49/49] Add @return comments --- packages/web3-wrapper/src/marshaller.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/web3-wrapper/src/marshaller.ts b/packages/web3-wrapper/src/marshaller.ts index 7d254b49e6..572a322d6e 100644 --- a/packages/web3-wrapper/src/marshaller.ts +++ b/packages/web3-wrapper/src/marshaller.ts @@ -32,6 +32,7 @@ export const marshaller = { /** * Unmarshall block without transaction data * @param blockWithHexValues block to unmarshall + * @return unmarshalled block without transaction data */ unmarshalIntoBlockWithoutTransactionData( blockWithHexValues: BlockWithoutTransactionDataRPC, @@ -51,6 +52,7 @@ export const marshaller = { /** * Unmarshall block with transaction data * @param blockWithHexValues block to unmarshall + * @return unmarshalled block with transaction data */ unmarshalIntoBlockWithTransactionData(blockWithHexValues: BlockWithTransactionDataRPC): BlockWithTransactionData { const block = { @@ -73,6 +75,7 @@ export const marshaller = { /** * Unmarshall transaction * @param txRpc transaction to unmarshall + * @return unmarshalled transaction */ unmarshalTransaction(txRpc: TransactionRPC): Transaction { const tx = { @@ -91,6 +94,7 @@ export const marshaller = { /** * Unmarshall transaction data * @param txDataRpc transaction data to unmarshall + * @return unmarshalled transaction data */ unmarshalTxData(txDataRpc: TxDataRPC): TxData { if (_.isUndefined(txDataRpc.from)) { @@ -108,6 +112,7 @@ export const marshaller = { /** * Marshall transaction data * @param txData transaction data to marshall + * @return marshalled transaction data */ marshalTxData(txData: Partial): Partial { if (_.isUndefined(txData.from)) { @@ -133,6 +138,7 @@ export const marshaller = { /** * Marshall call data * @param callData call data to marshall + * @return marshalled call data */ marshalCallData(callData: Partial): Partial { const callTxDataBase = { @@ -149,6 +155,7 @@ export const marshaller = { /** * Marshall address * @param address address to marshall + * @return marshalled address */ marshalAddress(address: string): string { if (addressUtils.isAddress(address)) { @@ -159,6 +166,7 @@ export const marshaller = { /** * Marshall block param * @param blockParam block param to marshall + * @return marshalled block param */ marshalBlockParam(blockParam: BlockParam | string | number | undefined): string | undefined { if (_.isUndefined(blockParam)) { @@ -170,6 +178,7 @@ export const marshaller = { /** * Unmarshall log * @param rawLog log to unmarshall + * @return unmarshalled log */ unmarshalLog(rawLog: RawLogEntry): LogEntry { const formattedLog = {