From 91702bbae214f56e7ff66156fa799fcfb83f6e42 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Fri, 14 Sep 2018 13:45:14 +0200 Subject: Move packages/forwarder-helper into packages/asset-buyer --- packages/asset-buyer/.npmignore | 8 + packages/asset-buyer/CHANGELOG.json | 30 +++ packages/asset-buyer/CHANGELOG.md | 18 ++ packages/asset-buyer/README.md | 83 +++++++ packages/asset-buyer/package.json | 75 ++++++ packages/asset-buyer/src/constants.ts | 6 + .../asset-buyer/src/forwarder_helper_factory.ts | 261 +++++++++++++++++++++ packages/asset-buyer/src/forwarder_helper_impl.ts | 64 +++++ packages/asset-buyer/src/globals.d.ts | 6 + packages/asset-buyer/src/index.ts | 2 + packages/asset-buyer/src/types.ts | 49 ++++ .../utils/forwarder_helper_impl_config_utils.ts | 92 ++++++++ packages/asset-buyer/src/utils/order_utils.ts | 21 ++ .../asset-buyer/test/forwarder_helper_impl_test.ts | 136 +++++++++++ packages/asset-buyer/test/utils/chai_setup.ts | 13 + packages/asset-buyer/tsconfig.json | 8 + packages/asset-buyer/tslint.json | 3 + packages/asset-buyer/typedoc-tsconfig.json | 7 + 18 files changed, 882 insertions(+) create mode 100644 packages/asset-buyer/.npmignore create mode 100644 packages/asset-buyer/CHANGELOG.json create mode 100644 packages/asset-buyer/CHANGELOG.md create mode 100644 packages/asset-buyer/README.md create mode 100644 packages/asset-buyer/package.json create mode 100644 packages/asset-buyer/src/constants.ts create mode 100644 packages/asset-buyer/src/forwarder_helper_factory.ts create mode 100644 packages/asset-buyer/src/forwarder_helper_impl.ts create mode 100644 packages/asset-buyer/src/globals.d.ts create mode 100644 packages/asset-buyer/src/index.ts create mode 100644 packages/asset-buyer/src/types.ts create mode 100644 packages/asset-buyer/src/utils/forwarder_helper_impl_config_utils.ts create mode 100644 packages/asset-buyer/src/utils/order_utils.ts create mode 100644 packages/asset-buyer/test/forwarder_helper_impl_test.ts create mode 100644 packages/asset-buyer/test/utils/chai_setup.ts create mode 100644 packages/asset-buyer/tsconfig.json create mode 100644 packages/asset-buyer/tslint.json create mode 100644 packages/asset-buyer/typedoc-tsconfig.json (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/.npmignore b/packages/asset-buyer/.npmignore new file mode 100644 index 000000000..5333847e7 --- /dev/null +++ b/packages/asset-buyer/.npmignore @@ -0,0 +1,8 @@ +.* +yarn-error.log +/src/ +/scripts/ +/schemas/ +test/ +tsconfig.json +/lib/src/monorepo_scripts/ diff --git a/packages/asset-buyer/CHANGELOG.json b/packages/asset-buyer/CHANGELOG.json new file mode 100644 index 000000000..f0e646700 --- /dev/null +++ b/packages/asset-buyer/CHANGELOG.json @@ -0,0 +1,30 @@ +[ + { + "timestamp": 1536142250, + "version": "1.0.1", + "changes": [ + { + "note": "Dependencies updated" + } + ] + }, + { + "version": "1.0.1-rc.2", + "changes": [ + { + "note": "Dependencies updated" + } + ], + "timestamp": 1535377027 + }, + { + "version": "1.0.1-rc.1", + "changes": [ + { + "note": "Add initial forwarderHelperFactory", + "pr": 997 + } + ], + "timestamp": 1535133899 + } +] diff --git a/packages/asset-buyer/CHANGELOG.md b/packages/asset-buyer/CHANGELOG.md new file mode 100644 index 000000000..d6b67aa1b --- /dev/null +++ b/packages/asset-buyer/CHANGELOG.md @@ -0,0 +1,18 @@ + + +CHANGELOG + +## v1.0.1 - _September 5, 2018_ + + * Dependencies updated + +## v1.0.1-rc.2 - _August 27, 2018_ + + * Dependencies updated + +## v1.0.1-rc.1 - _August 24, 2018_ + + * Add initial forwarderHelperFactory (#997) diff --git a/packages/asset-buyer/README.md b/packages/asset-buyer/README.md new file mode 100644 index 000000000..c74526910 --- /dev/null +++ b/packages/asset-buyer/README.md @@ -0,0 +1,83 @@ +## @0xproject/forwarder-helper + +Provides convenience objects to help work with the Forwarder Contract + +### Read the [Documentation](https://0xproject.com/docs/forwarder-helper). + +## Installation + +```bash +yarn add @0xproject/forwarder-helper +``` + +**Import** + +```typescript +import { forwarderHelperFactory } from '@0xproject/forwarder-helper'; +``` + +or + +```javascript +var forwarderHelperFactory = require('@0xproject/forwarder-helper').forwarderHelperFactory; +``` + +If your project is in [TypeScript](https://www.typescriptlang.org/), add the following to your `tsconfig.json`: + +```json +"compilerOptions": { + "typeRoots": ["node_modules/@0xproject/typescript-typings/types", "node_modules/@types"], +} +``` + +## Contributing + +We welcome improvements and fixes from the wider community! To report bugs within this package, please create an issue in this repository. + +Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting started. + +### Install dependencies + +If you don't have yarn workspaces enabled (Yarn < v1.0) - enable them: + +```bash +yarn config set workspaces-experimental true +``` + +Then install dependencies + +```bash +yarn install +``` + +### Build + +To build this package and all other monorepo packages that it depends on, run the following from the monorepo root directory: + +```bash +PKG=@0xproject/forwarder-helper yarn build +``` + +Or continuously rebuild on change: + +```bash +PKG=@0xproject/forwarder-helper yarn watch +``` + +### Clean + +```bash +yarn clean +``` + +### Lint + +```bash +yarn lint +``` + +### Run Tests + +```bash +yarn test +``` diff --git a/packages/asset-buyer/package.json b/packages/asset-buyer/package.json new file mode 100644 index 000000000..f849fb4ed --- /dev/null +++ b/packages/asset-buyer/package.json @@ -0,0 +1,75 @@ +{ + "name": "@0xproject/forwarder-helper", + "version": "1.0.1", + "engines": { + "node": ">=6.12" + }, + "description": "Convenience object for working with the forwarder contract", + "main": "lib/src/index.js", + "types": "lib/src/index.d.ts", + "scripts": { + "watch_without_deps": "tsc -w", + "lint": "tslint --project .", + "test": "yarn run_mocha", + "rebuild_and_test": "run-s clean build test", + "test:coverage": "nyc npm run test --all && yarn coverage:report:lcov", + "coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info", + "test:circleci": "yarn test:coverage", + "run_mocha": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --exit", + "clean": "shx rm -rf lib test_temp scripts", + "build": "tsc && copyfiles -u 3 './lib/src/monorepo_scripts/**/*' ./scripts", + "manual:postpublish": "yarn build; node ./scripts/postpublish.js", + "docs:stage": "node scripts/stage_docs.js", + "docs:json": "typedoc --excludePrivate --excludeExternals --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES", + "upload_docs_json": "aws s3 cp generated_docs/index.json $S3_URL --profile 0xproject --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type application/json" + }, + "config": { + "postpublish": { + "assets": [] + } + }, + "repository": { + "type": "git", + "url": "https://github.com/0xProject/0x-monorepo.git" + }, + "author": "", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/0xProject/0x-monorepo/issues" + }, + "homepage": "https://github.com/0xProject/0x-monorepo/packages/forwarder-helper/README.md", + "dependencies": { + "@0xproject/assert": "^1.0.8", + "@0xproject/connect": "^2.0.0", + "@0xproject/contract-wrappers": "^1.0.1", + "@0xproject/json-schemas": "^1.0.1", + "@0xproject/order-utils": "^1.0.1", + "@0xproject/subproviders": "^2.0.2", + "@0xproject/types": "^1.0.1", + "@0xproject/typescript-typings": "^2.0.0", + "@0xproject/utils": "^1.0.8", + "lodash": "^4.17.10" + }, + "devDependencies": { + "@0xproject/tslint-config": "^1.0.7", + "@types/lodash": "^4.14.116", + "@types/mocha": "^2.2.42", + "@types/node": "^8.0.53", + "chai": "^4.0.1", + "chai-as-promised": "^7.1.0", + "chai-bignumber": "^2.0.1", + "copyfiles": "^1.2.0", + "dirty-chai": "^2.0.1", + "make-promises-safe": "^1.1.0", + "mocha": "^4.1.0", + "npm-run-all": "^4.1.2", + "nyc": "^11.0.1", + "shx": "^0.2.2", + "tslint": "5.11.0", + "typedoc": "0.12.0", + "typescript": "3.0.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/asset-buyer/src/constants.ts b/packages/asset-buyer/src/constants.ts new file mode 100644 index 000000000..c0a1b090e --- /dev/null +++ b/packages/asset-buyer/src/constants.ts @@ -0,0 +1,6 @@ +import { BigNumber } from '@0xproject/utils'; + +export const constants = { + ZERO_AMOUNT: new BigNumber(0), + NULL_ADDRESS: '0x0000000000000000000000000000000000000000', +}; diff --git a/packages/asset-buyer/src/forwarder_helper_factory.ts b/packages/asset-buyer/src/forwarder_helper_factory.ts new file mode 100644 index 000000000..2b37ac98f --- /dev/null +++ b/packages/asset-buyer/src/forwarder_helper_factory.ts @@ -0,0 +1,261 @@ +import { assert } from '@0xproject/assert'; +import { APIOrder, HttpClient, OrderbookResponse } from '@0xproject/connect'; +import { ContractWrappers, OrderAndTraderInfo, OrderStatus } from '@0xproject/contract-wrappers'; +import { schemas } from '@0xproject/json-schemas'; +import { assetDataUtils } from '@0xproject/order-utils'; +import { RemainingFillableCalculator } from '@0xproject/order-utils/lib/src/remaining_fillable_calculator'; +import { RPCSubprovider, Web3ProviderEngine } from '@0xproject/subproviders'; +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { constants } from './constants'; +import { ForwarderHelperImpl, ForwarderHelperImplConfig } from './forwarder_helper_impl'; +import { ForwarderHelper, ForwarderHelperFactoryError } from './types'; +import { orderUtils } from './utils/order_utils'; + +export const forwarderHelperFactory = { + /** + * Given an array of orders and an array of feeOrders, get a ForwarderHelper + * @param orders An array of objects conforming to SignedOrder. Each order should specify the same makerAssetData and takerAssetData + * @param feeOrders An array of objects conforming to SignedOrder. Each order should specify ZRX as makerAssetData WETH as takerAssetData + * @return A ForwarderHelper, see type for definition + */ + getForwarderHelperForOrders(orders: SignedOrder[], feeOrders: SignedOrder[] = []): ForwarderHelper { + assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); + assert.doesConformToSchema('feeOrders', orders, schemas.signedOrdersSchema); + // TODO: Add assertion here for orders all having the same makerAsset and takerAsset + const config: ForwarderHelperImplConfig = { + orders, + feeOrders, + }; + const helper = new ForwarderHelperImpl(config); + return helper; + }, + /** + * Given a desired makerAsset and SRA url, get a ForwarderHelper + * @param makerAssetData An array of objects conforming to SignedOrder. Each order should specify the same makerAssetData and takerAssetData + * @param sraUrl A url pointing to an SRA v2 compliant endpoint. + * @param rpcUrl A url pointing to an ethereum node. + * @param networkId The ethereum networkId, defaults to 1 (mainnet). + * @return A ForwarderHelper, see type for definition + */ + async getForwarderHelperForMakerAssetDataAsync( + makerAssetData: string, + sraUrl: string, + rpcUrl?: string, + networkId: number = 1, + ): Promise { + assert.isHexString('makerAssetData', makerAssetData); + assert.isWebUri('sraUrl', sraUrl); + if (!_.isUndefined(rpcUrl)) { + assert.isWebUri('rpcUrl', rpcUrl); + } + assert.isNumber('networkId', networkId); + // create provider + const providerEngine = new Web3ProviderEngine(); + if (!_.isUndefined(rpcUrl)) { + providerEngine.addProvider(new RPCSubprovider(rpcUrl)); + } + providerEngine.start(); + // create contract wrappers given provider and networkId + const contractWrappers = new ContractWrappers(providerEngine, { networkId }); + // find ether token asset data + const etherTokenAddressIfExists = contractWrappers.etherToken.getContractAddressIfExists(); + if (_.isUndefined(etherTokenAddressIfExists)) { + throw new Error(ForwarderHelperFactoryError.NoEtherTokenContractFound); + } + const etherTokenAssetData = assetDataUtils.encodeERC20AssetData(etherTokenAddressIfExists); + // find zrx token asset data + let zrxTokenAssetData: string; + try { + zrxTokenAssetData = contractWrappers.exchange.getZRXAssetData(); + } catch (err) { + throw new Error(ForwarderHelperFactoryError.NoZrxTokenContractFound); + } + // get orderbooks for makerAsset/WETH and ZRX/WETH + const sraClient = new HttpClient(sraUrl); + const orderbookRequests = [ + { baseAssetData: makerAssetData, quoteAssetData: etherTokenAssetData }, + { baseAssetData: zrxTokenAssetData, quoteAssetData: etherTokenAssetData }, + ]; + const requestOpts = { networkId }; + let makerAssetOrderbook: OrderbookResponse; + let zrxOrderbook: OrderbookResponse; + try { + [makerAssetOrderbook, zrxOrderbook] = await Promise.all( + _.map(orderbookRequests, request => sraClient.getOrderbookAsync(request, requestOpts)), + ); + } catch (err) { + throw new Error(ForwarderHelperFactoryError.StandardRelayerApiError); + } + // validate orders and find remaining fillable from on chain state or sra api + let ordersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts; + let feeOrdersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts; + if (!_.isUndefined(rpcUrl)) { + // if we do have an rpc url, get on-chain orders and traders info via the OrderValidatorWrapper + const ordersFromSra = getOpenAsksFromOrderbook(makerAssetOrderbook); + const feeOrdersFromSra = getOpenAsksFromOrderbook(zrxOrderbook); + // TODO: try catch these requests and throw a more domain specific error + // TODO: optimization, reduce this to once RPC call buy combining orders into one array and then splitting up the response + const [makerAssetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all( + _.map([ordersFromSra, feeOrdersFromSra], ordersToBeValidated => { + const takerAddresses = _.map(ordersToBeValidated, () => constants.NULL_ADDRESS); + return contractWrappers.orderValidator.getOrdersAndTradersInfoAsync( + ordersToBeValidated, + takerAddresses, + ); + }), + ); + // take maker asset orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts + ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( + ordersFromSra, + makerAssetOrdersAndTradersInfo, + zrxTokenAssetData, + ); + // take fee orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts + feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( + feeOrdersFromSra, + feeOrdersAndTradersInfo, + zrxTokenAssetData, + ); + } else { + // if we don't have an rpc url, assume all orders are valid and fallback to optional fill amounts from SRA + // if fill amounts are not available from the SRA, assume all orders are completely fillable + const apiOrdersFromSra = makerAssetOrderbook.asks.records; + const feeApiOrdersFromSra = zrxOrderbook.asks.records; + // take maker asset orders from SRA and the valid orders and remaining fillable maker asset amounts + ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( + apiOrdersFromSra, + ); + // take fee orders from SRA and find the valid orders and remaining fillable maker asset amounts + feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( + feeApiOrdersFromSra, + ); + } + // compile final config + const config: ForwarderHelperImplConfig = { + orders: ordersAndRemainingFillableMakerAssetAmounts.orders, + feeOrders: feeOrdersAndRemainingFillableMakerAssetAmounts.orders, + remainingFillableMakerAssetAmounts: + ordersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, + remainingFillableFeeAmounts: + feeOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, + }; + const helper = new ForwarderHelperImpl(config); + return helper; + }, +}; + +interface OrdersAndRemainingFillableMakerAssetAmounts { + orders: SignedOrder[]; + remainingFillableMakerAssetAmounts: BigNumber[]; +} + +/** + * Given an array of APIOrder objects from a standard relayer api, return an array + * of fillable orders with their corresponding remainingFillableMakerAssetAmounts + */ +function getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( + apiOrders: APIOrder[], +): OrdersAndRemainingFillableMakerAssetAmounts { + const result = _.reduce( + apiOrders, + (acc, apiOrder) => { + // get current accumulations + const { orders, remainingFillableMakerAssetAmounts } = acc; + // get order and metadata + const { order, metaData } = apiOrder; + // if the order is expired or not open, move on + if (orderUtils.isOrderExpired(order) || !orderUtils.isOpenOrder(order)) { + return acc; + } + // calculate remainingFillableMakerAssetAmount from api metadata, else assume order is completely fillable + const remainingFillableTakerAssetAmount = _.get( + metaData, + 'remainingTakerAssetAmount', + order.takerAssetAmount, + ); + const remainingFillableMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( + order, + remainingFillableTakerAssetAmount, + ); + // if there is some amount of maker asset left to fill and add the order and remaining amount to the accumulations + // if there is not any maker asset left to fill, do not add + if (remainingFillableMakerAssetAmount.gt(constants.ZERO_AMOUNT)) { + return { + orders: _.concat(orders, order), + remainingFillableMakerAssetAmounts: _.concat( + remainingFillableMakerAssetAmounts, + remainingFillableMakerAssetAmount, + ), + }; + } else { + return acc; + } + }, + { orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] }, + ); + return result; +} + +/** + * Given an array of orders and corresponding on-chain infos, return a subset of the orders + * that are still fillable orders with their corresponding remainingFillableMakerAssetAmounts + */ +function getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( + inputOrders: SignedOrder[], + ordersAndTradersInfo: OrderAndTraderInfo[], + zrxAssetData: string, +): OrdersAndRemainingFillableMakerAssetAmounts { + // iterate through the input orders and find the ones that are still fillable + // for the orders that are still fillable, calculate the remaining fillable maker asset amount + const result = _.reduce( + inputOrders, + (acc, order, index) => { + // get current accumulations + const { orders, remainingFillableMakerAssetAmounts } = acc; + // get corresponding on-chain state for the order + const { orderInfo, traderInfo } = ordersAndTradersInfo[index]; + // if the order IS NOT fillable, do not add anything to the accumulations and continue iterating + if (orderInfo.orderStatus !== OrderStatus.FILLABLE) { + return acc; + } + // if the order IS fillable, add the order and calculate the remaining fillable amount + const transferrableAssetAmount = BigNumber.min([traderInfo.makerAllowance, traderInfo.makerBalance]); + const transferrableFeeAssetAmount = BigNumber.min([ + traderInfo.makerZrxAllowance, + traderInfo.makerZrxBalance, + ]); + const remainingTakerAssetAmount = order.takerAssetAmount.minus(orderInfo.orderTakerAssetFilledAmount); + const remainingMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( + order, + remainingTakerAssetAmount, + ); + const remainingFillableCalculator = new RemainingFillableCalculator( + order.makerFee, + order.makerAssetAmount, + order.makerAssetData === zrxAssetData, + transferrableAssetAmount, + transferrableFeeAssetAmount, + remainingMakerAssetAmount, + ); + const remainingFillableAmount = remainingFillableCalculator.computeRemainingFillable(); + return { + orders: _.concat(orders, order), + remainingFillableMakerAssetAmounts: _.concat( + remainingFillableMakerAssetAmounts, + remainingFillableAmount, + ), + }; + }, + { orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] }, + ); + return result; +} + +function getOpenAsksFromOrderbook(orderbookResponse: OrderbookResponse): SignedOrder[] { + const asks = _.map(orderbookResponse.asks.records, apiOrder => apiOrder.order); + const result = _.filter(asks, ask => orderUtils.isOpenOrder(ask)); + return result; +} diff --git a/packages/asset-buyer/src/forwarder_helper_impl.ts b/packages/asset-buyer/src/forwarder_helper_impl.ts new file mode 100644 index 000000000..a90edb0bb --- /dev/null +++ b/packages/asset-buyer/src/forwarder_helper_impl.ts @@ -0,0 +1,64 @@ +import { marketUtils } from '@0xproject/order-utils'; +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { constants } from './constants'; +import { ForwarderHelper, ForwarderHelperError, MarketBuyOrdersInfo, MarketBuyOrdersInfoRequest } from './types'; +import { forwarderHelperImplConfigUtils } from './utils/forwarder_helper_impl_config_utils'; + +const SLIPPAGE_PERCENTAGE = new BigNumber(0.2); // 20% slippage protection, possibly move this into request interface + +export interface ForwarderHelperImplConfig { + orders: SignedOrder[]; + feeOrders: SignedOrder[]; + remainingFillableMakerAssetAmounts?: BigNumber[]; + remainingFillableFeeAmounts?: BigNumber[]; +} + +export class ForwarderHelperImpl implements ForwarderHelper { + public readonly config: ForwarderHelperImplConfig; + constructor(config: ForwarderHelperImplConfig) { + this.config = forwarderHelperImplConfigUtils.sortedConfig(config); + } + public getMarketBuyOrdersInfo(request: MarketBuyOrdersInfoRequest): MarketBuyOrdersInfo { + const { makerAssetFillAmount, feePercentage } = request; + const { orders, feeOrders, remainingFillableMakerAssetAmounts, remainingFillableFeeAmounts } = this.config; + // TODO: make the slippage percentage customizable + const slippageBufferAmount = makerAssetFillAmount.mul(SLIPPAGE_PERCENTAGE).round(); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + orders, + makerAssetFillAmount, + { + remainingFillableMakerAssetAmounts, + slippageBufferAmount, + }, + ); + if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) { + throw new Error(ForwarderHelperError.InsufficientMakerAssetLiquidity); + } + // TODO: update this logic to find the minimum amount of feeOrders to cover the worst case as opposed to + // finding order that cover all fees, this will help with estimating ETH and minimizing gas usage + const { resultFeeOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + resultOrders, + feeOrders, + { + remainingFillableMakerAssetAmounts, + remainingFillableFeeAmounts, + }, + ); + if (remainingFeeAmount.gt(constants.ZERO_AMOUNT)) { + throw new Error(ForwarderHelperError.InsufficientZrxLiquidity); + } + // TODO: calculate min and max eth usage + // TODO: optimize orders call data + return { + makerAssetFillAmount, + orders: resultOrders, + feeOrders: resultFeeOrders, + minEthAmount: constants.ZERO_AMOUNT, + maxEthAmount: constants.ZERO_AMOUNT, + feePercentage, + }; + } +} diff --git a/packages/asset-buyer/src/globals.d.ts b/packages/asset-buyer/src/globals.d.ts new file mode 100644 index 000000000..94e63a32d --- /dev/null +++ b/packages/asset-buyer/src/globals.d.ts @@ -0,0 +1,6 @@ +declare module '*.json' { + const json: any; + /* tslint:disable */ + export default json; + /* tslint:enable */ +} diff --git a/packages/asset-buyer/src/index.ts b/packages/asset-buyer/src/index.ts new file mode 100644 index 000000000..eb3a34bd5 --- /dev/null +++ b/packages/asset-buyer/src/index.ts @@ -0,0 +1,2 @@ +export { forwarderHelperFactory } from './forwarder_helper_factory'; +export { ForwarderHelper, ForwarderHelperError, MarketBuyOrdersInfoRequest, MarketBuyOrdersInfo } from './types'; diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts new file mode 100644 index 000000000..a7f02ff8d --- /dev/null +++ b/packages/asset-buyer/src/types.ts @@ -0,0 +1,49 @@ +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; + +export enum ForwarderHelperFactoryError { + NoEtherTokenContractFound = 'NO_ETHER_TOKEN_CONTRACT_FOUND', + NoZrxTokenContractFound = 'NO_ZRX_TOKEN_CONTRACT_FOUND', + StandardRelayerApiError = 'STANDARD_RELAYER_API_ERROR', +} + +export interface ForwarderHelper { + /** + * Given a MarketBuyOrdersInfoRequest, returns a MarketBuyOrdersInfo containing all information relevant to fulfilling the request + * using the ForwarderContract marketBuyOrdersWithEth function. + * @param request An object that conforms to MarketBuyOrdersInfoRequest. See type definition for more information. + * @return An object that conforms to MarketBuyOrdersInfo that satisfies the request. See type definition for more information. + */ + getMarketBuyOrdersInfo: (request: MarketBuyOrdersInfoRequest) => MarketBuyOrdersInfo; +} + +export enum ForwarderHelperError { + InsufficientMakerAssetLiquidity = 'INSUFFICIENT_MAKER_ASSET_LIQUIDITY', + InsufficientZrxLiquidity = 'INSUFFICIENT_ZRX_LIQUIDITY', +} + +/** + * makerAssetFillAmount: The amount of makerAsset requesting to be filled + * feePercentage: Optional affiliate percentage amount factoring into eth amount calculations + */ +export interface MarketBuyOrdersInfoRequest { + makerAssetFillAmount: BigNumber; + feePercentage?: BigNumber; +} + +/** + * makerAssetFillAmount: The amount of makerAsset requesting to be filled + * orders: An array of objects conforming to SignedOrder. These orders can be used to cover the requested makerAssetFillAmount plus slippage + * feeOrders: An array of objects conforming to SignedOrder. These orders can be used to cover the fees for the orders param above + * minEthAmount: Amount of eth in wei to send with the tx for the most optimistic case + * maxEthAmount: Amount of eth in wei to send with the tx for the worst case + * feePercentage: Affiliate fee percentage used to calculate the eth amounts above. Passed thru directly from the request + */ +export interface MarketBuyOrdersInfo { + makerAssetFillAmount: BigNumber; + orders: SignedOrder[]; + feeOrders: SignedOrder[]; + minEthAmount: BigNumber; + maxEthAmount: BigNumber; + feePercentage?: BigNumber; +} diff --git a/packages/asset-buyer/src/utils/forwarder_helper_impl_config_utils.ts b/packages/asset-buyer/src/utils/forwarder_helper_impl_config_utils.ts new file mode 100644 index 000000000..253384f65 --- /dev/null +++ b/packages/asset-buyer/src/utils/forwarder_helper_impl_config_utils.ts @@ -0,0 +1,92 @@ +import { sortingUtils } from '@0xproject/order-utils'; +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { ForwarderHelperImplConfig } from '../forwarder_helper_impl'; + +interface SignedOrderWithAmount extends SignedOrder { + remainingFillAmount: BigNumber; +} + +export const forwarderHelperImplConfigUtils = { + sortedConfig(config: ForwarderHelperImplConfig): ForwarderHelperImplConfig { + const { orders, feeOrders, remainingFillableMakerAssetAmounts, remainingFillableFeeAmounts } = config; + // TODO: provide a feeRate to the sorting function to more accurately sort based on the current market for ZRX tokens + const orderSorter = (ordersToSort: SignedOrder[]) => { + return sortingUtils.sortOrdersByFeeAdjustedRate(ordersToSort); + }; + const sortOrdersResult = sortOrdersAndRemainingFillAmounts( + orderSorter, + orders, + remainingFillableMakerAssetAmounts, + ); + const feeOrderSorter = (ordersToSort: SignedOrder[]) => { + return sortingUtils.sortFeeOrdersByFeeAdjustedRate(ordersToSort); + }; + const sortFeeOrdersResult = sortOrdersAndRemainingFillAmounts( + feeOrderSorter, + feeOrders, + remainingFillableFeeAmounts, + ); + return { + orders: sortOrdersResult.orders, + feeOrders: sortFeeOrdersResult.orders, + remainingFillableMakerAssetAmounts: sortOrdersResult.remainingFillAmounts, + remainingFillableFeeAmounts: sortFeeOrdersResult.remainingFillAmounts, + }; + }, +}; + +type OrderSorter = (orders: SignedOrder[]) => SignedOrder[]; + +function sortOrdersAndRemainingFillAmounts( + orderSorter: OrderSorter, + orders: SignedOrder[], + remainingFillAmounts?: BigNumber[], +): { orders: SignedOrder[]; remainingFillAmounts?: BigNumber[] } { + if (!_.isUndefined(remainingFillAmounts)) { + // Bundle orders together with their remainingFillAmounts so that we can sort them together + const orderWithAmounts = bundleSignedOrderWithAmounts(orders, remainingFillAmounts); + // Sort + const sortedOrderWithAmounts = orderSorter(orderWithAmounts) as SignedOrderWithAmount[]; + // Unbundle after sorting + const unbundledSortedOrderWithAmounts = unbundleSignedOrderWithAmounts(sortedOrderWithAmounts); + return { + orders: unbundledSortedOrderWithAmounts.orders, + remainingFillAmounts: unbundledSortedOrderWithAmounts.amounts, + }; + } else { + const sortedOrders = orderSorter(orders); + return { + orders: sortedOrders, + }; + } +} + +function bundleSignedOrderWithAmounts(orders: SignedOrder[], amounts: BigNumber[]): SignedOrderWithAmount[] { + const ordersAndAmounts = _.map(orders, (order, index) => { + return { + ...order, + remainingFillAmount: amounts[index], + }; + }); + return ordersAndAmounts; +} + +function unbundleSignedOrderWithAmounts( + signedOrderWithAmounts: SignedOrderWithAmount[], +): { orders: SignedOrder[]; amounts: BigNumber[] } { + const orders = _.map(signedOrderWithAmounts, order => { + const { remainingFillAmount, ...rest } = order; + return rest; + }); + const amounts = _.map(signedOrderWithAmounts, order => { + const { remainingFillAmount } = order; + return remainingFillAmount; + }); + return { + orders, + amounts, + }; +} diff --git a/packages/asset-buyer/src/utils/order_utils.ts b/packages/asset-buyer/src/utils/order_utils.ts new file mode 100644 index 000000000..bb0bdb80f --- /dev/null +++ b/packages/asset-buyer/src/utils/order_utils.ts @@ -0,0 +1,21 @@ +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; + +import { constants } from '../constants'; + +export const orderUtils = { + isOrderExpired(order: SignedOrder): boolean { + const millisecondsInSecond = 1000; + const currentUnixTimestampSec = new BigNumber(Date.now() / millisecondsInSecond).round(); + return order.expirationTimeSeconds.lessThan(currentUnixTimestampSec); + }, + calculateRemainingMakerAssetAmount(order: SignedOrder, remainingTakerAssetAmount: BigNumber): BigNumber { + const result = remainingTakerAssetAmount.eq(0) + ? constants.ZERO_AMOUNT + : remainingTakerAssetAmount.times(order.makerAssetAmount).dividedToIntegerBy(order.takerAssetAmount); + return result; + }, + isOpenOrder(order: SignedOrder): boolean { + return order.takerAddress === constants.NULL_ADDRESS; + }, +}; diff --git a/packages/asset-buyer/test/forwarder_helper_impl_test.ts b/packages/asset-buyer/test/forwarder_helper_impl_test.ts new file mode 100644 index 000000000..3c3b6db92 --- /dev/null +++ b/packages/asset-buyer/test/forwarder_helper_impl_test.ts @@ -0,0 +1,136 @@ +import { testOrderFactory } from '@0xproject/order-utils/lib/test/utils/test_order_factory'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; +import 'mocha'; + +import { ForwarderHelperImpl, ForwarderHelperImplConfig } from '../src/forwarder_helper_impl'; +import { ForwarderHelperError } from '../src/types'; + +import { chaiSetup } from './utils/chai_setup'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('ForwarderHelperImpl', () => { + // rate: 2 takerAsset / makerAsset + const testOrder1 = testOrderFactory.generateTestSignedOrder({ + makerAssetAmount: new BigNumber(100), + takerAssetAmount: new BigNumber(200), + }); + // rate: 1 takerAsset / makerAsset + const testOrder2 = testOrderFactory.generateTestSignedOrder({ + makerAssetAmount: new BigNumber(100), + takerAssetAmount: new BigNumber(100), + }); + // rate: 3 takerAsset / makerAsset + const testOrder3 = testOrderFactory.generateTestSignedOrder({ + makerAssetAmount: new BigNumber(100), + takerAssetAmount: new BigNumber(300), + takerFee: new BigNumber(1), + }); + // rate: 3 WETH / ZRX + const testFeeOrder1 = testOrderFactory.generateTestSignedOrder({ + makerAssetAmount: new BigNumber(100), + takerAssetAmount: new BigNumber(300), + }); + // rate: 2 WETH / ZRX + const testFeeOrder2 = testOrderFactory.generateTestSignedOrder({ + makerAssetAmount: new BigNumber(100), + takerAssetAmount: new BigNumber(200), + }); + // rate: 1 WETH / ZRX + const testFeeOrder3 = testOrderFactory.generateTestSignedOrder({ + makerAssetAmount: new BigNumber(100), + takerAssetAmount: new BigNumber(100), + }); + const inputForwarderHelperConfig: ForwarderHelperImplConfig = { + orders: [testOrder1, testOrder2, testOrder3], + feeOrders: [testFeeOrder1, testFeeOrder2, testFeeOrder3], + remainingFillableMakerAssetAmounts: [new BigNumber(1), new BigNumber(2), new BigNumber(3)], + remainingFillableFeeAmounts: [new BigNumber(4), new BigNumber(5), new BigNumber(6)], + }; + describe('#constructor', () => { + const inputForwarderHelperConfigNoRemainingAmounts: ForwarderHelperImplConfig = { + orders: [testOrder1, testOrder2, testOrder3], + feeOrders: [testFeeOrder1, testFeeOrder2, testFeeOrder3], + }; + it('sorts orders', () => { + const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); + expect(forwarderHelper.config.orders).deep.equals([testOrder2, testOrder1, testOrder3]); + }); + it('sorts fee orders', () => { + const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); + expect(forwarderHelper.config.feeOrders).deep.equals([testFeeOrder3, testFeeOrder2, testFeeOrder1]); + }); + it('sorts remainingFillableMakerAssetAmounts', () => { + const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); + expect(forwarderHelper.config.remainingFillableMakerAssetAmounts).to.be.not.undefined(); + expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 0)).to.bignumber.equal( + new BigNumber(2), + ); + expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 1)).to.bignumber.equal( + new BigNumber(1), + ); + expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 2)).to.bignumber.equal( + new BigNumber(3), + ); + }); + it('sorts remainingFillableFeeAmounts', () => { + const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); + expect(forwarderHelper.config.remainingFillableFeeAmounts).to.be.not.undefined(); + expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 0)).to.bignumber.equal(new BigNumber(6)); + expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 1)).to.bignumber.equal(new BigNumber(5)); + expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 2)).to.bignumber.equal(new BigNumber(4)); + }); + it('remainingFillableMakerAssetAmounts is undefined if none provided', () => { + const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoRemainingAmounts); + expect(forwarderHelper.config.remainingFillableMakerAssetAmounts).to.be.undefined(); + }); + it('remainingFillableFeeAmounts is undefined if none provided', () => { + const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoRemainingAmounts); + expect(forwarderHelper.config.remainingFillableFeeAmounts).to.be.undefined(); + }); + }); + describe('#getMarketBuyOrdersInfo', () => { + it('throws if not enough makerAsset liquidity', () => { + const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); + expect(() => { + // request for 6 makerAsset units, because we have exactly 6 available we should throw because there is a built in slippage buffer + forwarderHelper.getMarketBuyOrdersInfo({ + makerAssetFillAmount: new BigNumber(6), + }); + }).to.throw(ForwarderHelperError.InsufficientMakerAssetLiquidity); + }); + it('throws if not enough ZRX liquidity', () => { + const inputForwarderHelperConfigNoFees: ForwarderHelperImplConfig = { + orders: [testOrder1, testOrder2, testOrder3], + feeOrders: [], + }; + const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoFees); + expect(() => { + // request for 4 makerAsset units, we need fees but no fee orders exist, show we should throw + forwarderHelper.getMarketBuyOrdersInfo({ + makerAssetFillAmount: new BigNumber(250), + }); + }).to.throw(ForwarderHelperError.InsufficientZrxLiquidity); + }); + it('passes the makerAssetFillAmount from the request to the info response', () => { + const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); + const makerAssetFillAmount = new BigNumber(4); + const info = forwarderHelper.getMarketBuyOrdersInfo({ + makerAssetFillAmount, + }); + expect(info.makerAssetFillAmount).to.bignumber.equal(makerAssetFillAmount); + }); + it('passes the feePercentage from the request to the info response', () => { + const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); + const feePercentage = new BigNumber(0.2); + const info = forwarderHelper.getMarketBuyOrdersInfo({ + makerAssetFillAmount: new BigNumber(4), + feePercentage, + }); + expect(info.feePercentage).to.bignumber.equal(feePercentage); + }); + }); +}); diff --git a/packages/asset-buyer/test/utils/chai_setup.ts b/packages/asset-buyer/test/utils/chai_setup.ts new file mode 100644 index 000000000..1a8733093 --- /dev/null +++ b/packages/asset-buyer/test/utils/chai_setup.ts @@ -0,0 +1,13 @@ +import * as chai from 'chai'; +import chaiAsPromised = require('chai-as-promised'); +import ChaiBigNumber = require('chai-bignumber'); +import * as dirtyChai from 'dirty-chai'; + +export const chaiSetup = { + configure(): void { + chai.config.includeStack = true; + chai.use(ChaiBigNumber()); + chai.use(dirtyChai); + chai.use(chaiAsPromised); + }, +}; diff --git a/packages/asset-buyer/tsconfig.json b/packages/asset-buyer/tsconfig.json new file mode 100644 index 000000000..2ee711adc --- /dev/null +++ b/packages/asset-buyer/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "outDir": "lib", + "rootDir": "." + }, + "include": ["./src/**/*", "./test/**/*"] +} diff --git a/packages/asset-buyer/tslint.json b/packages/asset-buyer/tslint.json new file mode 100644 index 000000000..ffaefe83a --- /dev/null +++ b/packages/asset-buyer/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": ["@0xproject/tslint-config"] +} diff --git a/packages/asset-buyer/typedoc-tsconfig.json b/packages/asset-buyer/typedoc-tsconfig.json new file mode 100644 index 000000000..c9b0af1ae --- /dev/null +++ b/packages/asset-buyer/typedoc-tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../typedoc-tsconfig", + "compilerOptions": { + "outDir": "lib" + }, + "include": ["./src/**/*", "./test/**/*"] +} -- cgit v1.2.3 From 8da7d399981472ff9cbfa28fb0957a530de72b2d Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Fri, 14 Sep 2018 13:54:06 +0200 Subject: Update readme, changelog, and package.json with rename --- packages/asset-buyer/CHANGELOG.json | 24 ++---------------------- packages/asset-buyer/CHANGELOG.md | 17 ----------------- packages/asset-buyer/README.md | 16 ++++++++-------- packages/asset-buyer/package.json | 8 ++++---- 4 files changed, 14 insertions(+), 51 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/CHANGELOG.json b/packages/asset-buyer/CHANGELOG.json index f0e646700..2e4db1f6e 100644 --- a/packages/asset-buyer/CHANGELOG.json +++ b/packages/asset-buyer/CHANGELOG.json @@ -1,30 +1,10 @@ [ { - "timestamp": 1536142250, - "version": "1.0.1", + "version": "1.0.0-rc.1", "changes": [ { - "note": "Dependencies updated" + "note": "Init" } ] - }, - { - "version": "1.0.1-rc.2", - "changes": [ - { - "note": "Dependencies updated" - } - ], - "timestamp": 1535377027 - }, - { - "version": "1.0.1-rc.1", - "changes": [ - { - "note": "Add initial forwarderHelperFactory", - "pr": 997 - } - ], - "timestamp": 1535133899 } ] diff --git a/packages/asset-buyer/CHANGELOG.md b/packages/asset-buyer/CHANGELOG.md index d6b67aa1b..8b1378917 100644 --- a/packages/asset-buyer/CHANGELOG.md +++ b/packages/asset-buyer/CHANGELOG.md @@ -1,18 +1 @@ - -CHANGELOG - -## v1.0.1 - _September 5, 2018_ - - * Dependencies updated - -## v1.0.1-rc.2 - _August 27, 2018_ - - * Dependencies updated - -## v1.0.1-rc.1 - _August 24, 2018_ - - * Add initial forwarderHelperFactory (#997) diff --git a/packages/asset-buyer/README.md b/packages/asset-buyer/README.md index c74526910..717849bf2 100644 --- a/packages/asset-buyer/README.md +++ b/packages/asset-buyer/README.md @@ -1,25 +1,25 @@ -## @0xproject/forwarder-helper +## @0xproject/asset-buyer -Provides convenience objects to help work with the Forwarder Contract +Convenience package for buying assets -### Read the [Documentation](https://0xproject.com/docs/forwarder-helper). +### Read the [Documentation](https://0xproject.com/docs/asset-buyer). ## Installation ```bash -yarn add @0xproject/forwarder-helper +yarn add @0xproject/asset-buyer ``` **Import** ```typescript -import { forwarderHelperFactory } from '@0xproject/forwarder-helper'; +import { AssetBuyer } from '@0xproject/asset-buyer'; ``` or ```javascript -var forwarderHelperFactory = require('@0xproject/forwarder-helper').forwarderHelperFactory; +var AssetBuyer = require('@0xproject/asset-buyer').AssetBuyer; ``` If your project is in [TypeScript](https://www.typescriptlang.org/), add the following to your `tsconfig.json`: @@ -55,13 +55,13 @@ yarn install To build this package and all other monorepo packages that it depends on, run the following from the monorepo root directory: ```bash -PKG=@0xproject/forwarder-helper yarn build +PKG=@0xproject/asset-buyer yarn build ``` Or continuously rebuild on change: ```bash -PKG=@0xproject/forwarder-helper yarn watch +PKG=@0xproject/asset-buyer yarn watch ``` ### Clean diff --git a/packages/asset-buyer/package.json b/packages/asset-buyer/package.json index f849fb4ed..e4d3d0701 100644 --- a/packages/asset-buyer/package.json +++ b/packages/asset-buyer/package.json @@ -1,10 +1,10 @@ { - "name": "@0xproject/forwarder-helper", - "version": "1.0.1", + "name": "@0xproject/asset-buyer", + "version": "1.0.0-rc.1", "engines": { "node": ">=6.12" }, - "description": "Convenience object for working with the forwarder contract", + "description": "Convenience package for buying assets", "main": "lib/src/index.js", "types": "lib/src/index.d.ts", "scripts": { @@ -37,7 +37,7 @@ "bugs": { "url": "https://github.com/0xProject/0x-monorepo/issues" }, - "homepage": "https://github.com/0xProject/0x-monorepo/packages/forwarder-helper/README.md", + "homepage": "https://github.com/0xProject/0x-monorepo/packages/asset-buyer/README.md", "dependencies": { "@0xproject/assert": "^1.0.8", "@0xproject/connect": "^2.0.0", -- cgit v1.2.3 From 7b46cef83dca0a743bd598a70076004983cbf294 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Fri, 14 Sep 2018 16:18:16 +0200 Subject: Create initial AssetBuyer class --- packages/asset-buyer/package.json | 2 + .../asset-buyer/src/asset_buyers/asset_buyer.ts | 129 ++++++ packages/asset-buyer/src/constants.ts | 1 + .../asset-buyer/src/forwarder_helper_factory.ts | 510 ++++++++++----------- packages/asset-buyer/src/forwarder_helper_impl.ts | 64 --- packages/asset-buyer/src/index.ts | 4 +- packages/asset-buyer/src/types.ts | 63 ++- .../utils/forwarder_helper_impl_config_utils.ts | 170 +++---- .../asset-buyer/test/forwarder_helper_impl_test.ts | 264 +++++------ 9 files changed, 634 insertions(+), 573 deletions(-) create mode 100644 packages/asset-buyer/src/asset_buyers/asset_buyer.ts delete mode 100644 packages/asset-buyer/src/forwarder_helper_impl.ts (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/package.json b/packages/asset-buyer/package.json index e4d3d0701..0d6d1f52b 100644 --- a/packages/asset-buyer/package.json +++ b/packages/asset-buyer/package.json @@ -48,6 +48,8 @@ "@0xproject/types": "^1.0.1", "@0xproject/typescript-typings": "^2.0.0", "@0xproject/utils": "^1.0.8", + "@0xproject/web3-wrapper": "^2.0.2", + "ethereum-types": "^1.0.6", "lodash": "^4.17.10" }, "devDependencies": { diff --git a/packages/asset-buyer/src/asset_buyers/asset_buyer.ts b/packages/asset-buyer/src/asset_buyers/asset_buyer.ts new file mode 100644 index 000000000..eb7f85e2b --- /dev/null +++ b/packages/asset-buyer/src/asset_buyers/asset_buyer.ts @@ -0,0 +1,129 @@ +import { ContractWrappers } from '@0xproject/contract-wrappers'; +import { marketUtils } from '@0xproject/order-utils'; +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as _ from 'lodash'; +import { Provider } from 'ethereum-types'; + +import { constants } from '../constants'; +import { AssetBuyerError, BuyQuote, BuyQuoteRequest } from '../types'; + +const SLIPPAGE_PERCENTAGE = new BigNumber(0.2); // 20% slippage protection, possibly move this into request interface + +export interface AssetBuyerConfig { + orders: SignedOrder[]; + feeOrders: SignedOrder[]; + remainingFillableMakerAssetAmounts?: BigNumber[]; + remainingFillableFeeAmounts?: BigNumber[]; + networkId?: number; +} + +export class AssetBuyer { + public readonly provider: Provider; + public readonly config: AssetBuyerConfig; + private _contractWrappers: ContractWrappers; + constructor(provider: Provider, config: AssetBuyerConfig) { + this.provider = provider; + this.config = config; + const networkId = this.config.networkId || constants.MAINNET_NETWORK_ID; + this._contractWrappers = new ContractWrappers(this.provider, { + networkId, + }); + } + /** + * Given a BuyQuoteRequest, returns a BuyQuote containing all information relevant to fulfilling the buy. Pass the BuyQuote + * to executeBuyQuoteAsync to execute the buy. + * @param buyQuoteRequest An object that conforms to BuyQuoteRequest. See type definition for more information. + * @return An object that conforms to BuyQuote that satisfies the request. See type definition for more information. + */ + public getBuyQuote(buyQuoteRequest: BuyQuoteRequest): BuyQuote { + const { assetBuyAmount, feePercentage } = buyQuoteRequest; + const { orders, feeOrders, remainingFillableMakerAssetAmounts, remainingFillableFeeAmounts } = this.config; + // TODO: optimization + // make the slippage percentage customizable + const slippageBufferAmount = assetBuyAmount.mul(SLIPPAGE_PERCENTAGE).round(); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + orders, + assetBuyAmount, + { + remainingFillableMakerAssetAmounts, + slippageBufferAmount, + }, + ); + if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) { + throw new Error(AssetBuyerError.InsufficientAssetLiquidity); + } + // TODO: optimization + // update this logic to find the minimum amount of feeOrders to cover the worst case as opposed to + // finding order that cover all fees, this will help with estimating ETH and minimizing gas usage + const { resultFeeOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + resultOrders, + feeOrders, + { + remainingFillableMakerAssetAmounts, + remainingFillableFeeAmounts, + }, + ); + if (remainingFeeAmount.gt(constants.ZERO_AMOUNT)) { + throw new Error(AssetBuyerError.InsufficientZrxLiquidity); + } + const assetData = orders[0].makerAssetData; + // TODO: critical + // calculate minRate and maxRate by calculating min and max eth usage and then dividing into + // assetBuyAmount to get assetData / WETH + return { + assetData, + orders: resultOrders, + feeOrders: resultFeeOrders, + minRate: constants.ZERO_AMOUNT, + maxRate: constants.ZERO_AMOUNT, + assetBuyAmount, + feePercentage, + }; + } + /** + * Given a BuyQuote and desired rate, attempt to execute the buy. + * @param buyQuote An object that conforms to BuyQuote. See type definition for more information. + * @param rate The desired rate to execute the buy at. Affects the amount of ETH sent with the transaction, defaults to buyQuote.maxRate. + * @param takerAddress The address to perform the buy. Defaults to the first available address from the provider. + * @param feeRecipient The address where affiliate fees are sent. Defaults to null address (0x000...000). + * @return A promise of the txHash. + */ + public async executeBuyQuoteAsync( + buyQuote: BuyQuote, + rate?: BigNumber, + takerAddress?: string, + feeRecipient: string = constants.NULL_ADDRESS, + ): Promise { + const { orders, feeOrders, feePercentage, assetBuyAmount, maxRate } = buyQuote; + // if no takerAddress is provided, try to get one from the provider + let finalTakerAddress; + if (!_.isUndefined(takerAddress)) { + finalTakerAddress = takerAddress; + } else { + const web3Wrapper = new Web3Wrapper(this.provider); + const availableAddresses = await web3Wrapper.getAvailableAddressesAsync(); + const firstAvailableAddress = _.head(availableAddresses); + if (!_.isUndefined(firstAvailableAddress)) { + finalTakerAddress = firstAvailableAddress; + } else { + throw new Error(AssetBuyerError.NoAddressAvailable); + } + } + // if no rate is provided, default to the maxRate from buyQuote + const desiredRate = rate || maxRate; + // calculate how much eth is required to buy assetBuyAmount at the desired rate + const ethAmount = assetBuyAmount.dividedToIntegerBy(desiredRate); + const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync( + orders, + assetBuyAmount, + finalTakerAddress, + ethAmount, + feeOrders, + feePercentage, + feeRecipient, + ); + return txHash; + } +} diff --git a/packages/asset-buyer/src/constants.ts b/packages/asset-buyer/src/constants.ts index c0a1b090e..5785e705b 100644 --- a/packages/asset-buyer/src/constants.ts +++ b/packages/asset-buyer/src/constants.ts @@ -3,4 +3,5 @@ import { BigNumber } from '@0xproject/utils'; export const constants = { ZERO_AMOUNT: new BigNumber(0), NULL_ADDRESS: '0x0000000000000000000000000000000000000000', + MAINNET_NETWORK_ID: 1, }; diff --git a/packages/asset-buyer/src/forwarder_helper_factory.ts b/packages/asset-buyer/src/forwarder_helper_factory.ts index 2b37ac98f..9a3832e81 100644 --- a/packages/asset-buyer/src/forwarder_helper_factory.ts +++ b/packages/asset-buyer/src/forwarder_helper_factory.ts @@ -1,261 +1,261 @@ -import { assert } from '@0xproject/assert'; -import { APIOrder, HttpClient, OrderbookResponse } from '@0xproject/connect'; -import { ContractWrappers, OrderAndTraderInfo, OrderStatus } from '@0xproject/contract-wrappers'; -import { schemas } from '@0xproject/json-schemas'; -import { assetDataUtils } from '@0xproject/order-utils'; -import { RemainingFillableCalculator } from '@0xproject/order-utils/lib/src/remaining_fillable_calculator'; -import { RPCSubprovider, Web3ProviderEngine } from '@0xproject/subproviders'; -import { SignedOrder } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; -import * as _ from 'lodash'; +// import { assert } from '@0xproject/assert'; +// import { APIOrder, HttpClient, OrderbookResponse } from '@0xproject/connect'; +// import { ContractWrappers, OrderAndTraderInfo, OrderStatus } from '@0xproject/contract-wrappers'; +// import { schemas } from '@0xproject/json-schemas'; +// import { assetDataUtils } from '@0xproject/order-utils'; +// import { RemainingFillableCalculator } from '@0xproject/order-utils/lib/src/remaining_fillable_calculator'; +// import { RPCSubprovider, Web3ProviderEngine } from '@0xproject/subproviders'; +// import { SignedOrder } from '@0xproject/types'; +// import { BigNumber } from '@0xproject/utils'; +// import * as _ from 'lodash'; -import { constants } from './constants'; -import { ForwarderHelperImpl, ForwarderHelperImplConfig } from './forwarder_helper_impl'; -import { ForwarderHelper, ForwarderHelperFactoryError } from './types'; -import { orderUtils } from './utils/order_utils'; +// import { constants } from './constants'; +// import { ForwarderHelperImpl, ForwarderHelperImplConfig } from '@0xproject/asset-buyer/src/asset_buyer'; +// import { ForwarderHelper, ForwarderHelperFactoryError } from './types'; +// import { orderUtils } from './utils/order_utils'; -export const forwarderHelperFactory = { - /** - * Given an array of orders and an array of feeOrders, get a ForwarderHelper - * @param orders An array of objects conforming to SignedOrder. Each order should specify the same makerAssetData and takerAssetData - * @param feeOrders An array of objects conforming to SignedOrder. Each order should specify ZRX as makerAssetData WETH as takerAssetData - * @return A ForwarderHelper, see type for definition - */ - getForwarderHelperForOrders(orders: SignedOrder[], feeOrders: SignedOrder[] = []): ForwarderHelper { - assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); - assert.doesConformToSchema('feeOrders', orders, schemas.signedOrdersSchema); - // TODO: Add assertion here for orders all having the same makerAsset and takerAsset - const config: ForwarderHelperImplConfig = { - orders, - feeOrders, - }; - const helper = new ForwarderHelperImpl(config); - return helper; - }, - /** - * Given a desired makerAsset and SRA url, get a ForwarderHelper - * @param makerAssetData An array of objects conforming to SignedOrder. Each order should specify the same makerAssetData and takerAssetData - * @param sraUrl A url pointing to an SRA v2 compliant endpoint. - * @param rpcUrl A url pointing to an ethereum node. - * @param networkId The ethereum networkId, defaults to 1 (mainnet). - * @return A ForwarderHelper, see type for definition - */ - async getForwarderHelperForMakerAssetDataAsync( - makerAssetData: string, - sraUrl: string, - rpcUrl?: string, - networkId: number = 1, - ): Promise { - assert.isHexString('makerAssetData', makerAssetData); - assert.isWebUri('sraUrl', sraUrl); - if (!_.isUndefined(rpcUrl)) { - assert.isWebUri('rpcUrl', rpcUrl); - } - assert.isNumber('networkId', networkId); - // create provider - const providerEngine = new Web3ProviderEngine(); - if (!_.isUndefined(rpcUrl)) { - providerEngine.addProvider(new RPCSubprovider(rpcUrl)); - } - providerEngine.start(); - // create contract wrappers given provider and networkId - const contractWrappers = new ContractWrappers(providerEngine, { networkId }); - // find ether token asset data - const etherTokenAddressIfExists = contractWrappers.etherToken.getContractAddressIfExists(); - if (_.isUndefined(etherTokenAddressIfExists)) { - throw new Error(ForwarderHelperFactoryError.NoEtherTokenContractFound); - } - const etherTokenAssetData = assetDataUtils.encodeERC20AssetData(etherTokenAddressIfExists); - // find zrx token asset data - let zrxTokenAssetData: string; - try { - zrxTokenAssetData = contractWrappers.exchange.getZRXAssetData(); - } catch (err) { - throw new Error(ForwarderHelperFactoryError.NoZrxTokenContractFound); - } - // get orderbooks for makerAsset/WETH and ZRX/WETH - const sraClient = new HttpClient(sraUrl); - const orderbookRequests = [ - { baseAssetData: makerAssetData, quoteAssetData: etherTokenAssetData }, - { baseAssetData: zrxTokenAssetData, quoteAssetData: etherTokenAssetData }, - ]; - const requestOpts = { networkId }; - let makerAssetOrderbook: OrderbookResponse; - let zrxOrderbook: OrderbookResponse; - try { - [makerAssetOrderbook, zrxOrderbook] = await Promise.all( - _.map(orderbookRequests, request => sraClient.getOrderbookAsync(request, requestOpts)), - ); - } catch (err) { - throw new Error(ForwarderHelperFactoryError.StandardRelayerApiError); - } - // validate orders and find remaining fillable from on chain state or sra api - let ordersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts; - let feeOrdersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts; - if (!_.isUndefined(rpcUrl)) { - // if we do have an rpc url, get on-chain orders and traders info via the OrderValidatorWrapper - const ordersFromSra = getOpenAsksFromOrderbook(makerAssetOrderbook); - const feeOrdersFromSra = getOpenAsksFromOrderbook(zrxOrderbook); - // TODO: try catch these requests and throw a more domain specific error - // TODO: optimization, reduce this to once RPC call buy combining orders into one array and then splitting up the response - const [makerAssetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all( - _.map([ordersFromSra, feeOrdersFromSra], ordersToBeValidated => { - const takerAddresses = _.map(ordersToBeValidated, () => constants.NULL_ADDRESS); - return contractWrappers.orderValidator.getOrdersAndTradersInfoAsync( - ordersToBeValidated, - takerAddresses, - ); - }), - ); - // take maker asset orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts - ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( - ordersFromSra, - makerAssetOrdersAndTradersInfo, - zrxTokenAssetData, - ); - // take fee orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts - feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( - feeOrdersFromSra, - feeOrdersAndTradersInfo, - zrxTokenAssetData, - ); - } else { - // if we don't have an rpc url, assume all orders are valid and fallback to optional fill amounts from SRA - // if fill amounts are not available from the SRA, assume all orders are completely fillable - const apiOrdersFromSra = makerAssetOrderbook.asks.records; - const feeApiOrdersFromSra = zrxOrderbook.asks.records; - // take maker asset orders from SRA and the valid orders and remaining fillable maker asset amounts - ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( - apiOrdersFromSra, - ); - // take fee orders from SRA and find the valid orders and remaining fillable maker asset amounts - feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( - feeApiOrdersFromSra, - ); - } - // compile final config - const config: ForwarderHelperImplConfig = { - orders: ordersAndRemainingFillableMakerAssetAmounts.orders, - feeOrders: feeOrdersAndRemainingFillableMakerAssetAmounts.orders, - remainingFillableMakerAssetAmounts: - ordersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, - remainingFillableFeeAmounts: - feeOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, - }; - const helper = new ForwarderHelperImpl(config); - return helper; - }, -}; +// export const forwarderHelperFactory = { +// /** +// * Given an array of orders and an array of feeOrders, get a ForwarderHelper +// * @param orders An array of objects conforming to SignedOrder. Each order should specify the same makerAssetData and takerAssetData +// * @param feeOrders An array of objects conforming to SignedOrder. Each order should specify ZRX as makerAssetData WETH as takerAssetData +// * @return A ForwarderHelper, see type for definition +// */ +// getForwarderHelperForOrders(orders: SignedOrder[], feeOrders: SignedOrder[] = []): ForwarderHelper { +// assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); +// assert.doesConformToSchema('feeOrders', orders, schemas.signedOrdersSchema); +// // TODO: Add assertion here for orders all having the same makerAsset and takerAsset +// const config: ForwarderHelperImplConfig = { +// orders, +// feeOrders, +// }; +// const helper = new ForwarderHelperImpl(config); +// return helper; +// }, +// /** +// * Given a desired makerAsset and SRA url, get a ForwarderHelper +// * @param makerAssetData An array of objects conforming to SignedOrder. Each order should specify the same makerAssetData and takerAssetData +// * @param sraUrl A url pointing to an SRA v2 compliant endpoint. +// * @param rpcUrl A url pointing to an ethereum node. +// * @param networkId The ethereum networkId, defaults to 1 (mainnet). +// * @return A ForwarderHelper, see type for definition +// */ +// async getForwarderHelperForMakerAssetDataAsync( +// makerAssetData: string, +// sraUrl: string, +// rpcUrl?: string, +// networkId: number = 1, +// ): Promise { +// assert.isHexString('makerAssetData', makerAssetData); +// assert.isWebUri('sraUrl', sraUrl); +// if (!_.isUndefined(rpcUrl)) { +// assert.isWebUri('rpcUrl', rpcUrl); +// } +// assert.isNumber('networkId', networkId); +// // create provider +// const providerEngine = new Web3ProviderEngine(); +// if (!_.isUndefined(rpcUrl)) { +// providerEngine.addProvider(new RPCSubprovider(rpcUrl)); +// } +// providerEngine.start(); +// // create contract wrappers given provider and networkId +// const contractWrappers = new ContractWrappers(providerEngine, { networkId }); +// // find ether token asset data +// const etherTokenAddressIfExists = contractWrappers.etherToken.getContractAddressIfExists(); +// if (_.isUndefined(etherTokenAddressIfExists)) { +// throw new Error(ForwarderHelperFactoryError.NoEtherTokenContractFound); +// } +// const etherTokenAssetData = assetDataUtils.encodeERC20AssetData(etherTokenAddressIfExists); +// // find zrx token asset data +// let zrxTokenAssetData: string; +// try { +// zrxTokenAssetData = contractWrappers.exchange.getZRXAssetData(); +// } catch (err) { +// throw new Error(ForwarderHelperFactoryError.NoZrxTokenContractFound); +// } +// // get orderbooks for makerAsset/WETH and ZRX/WETH +// const sraClient = new HttpClient(sraUrl); +// const orderbookRequests = [ +// { baseAssetData: makerAssetData, quoteAssetData: etherTokenAssetData }, +// { baseAssetData: zrxTokenAssetData, quoteAssetData: etherTokenAssetData }, +// ]; +// const requestOpts = { networkId }; +// let makerAssetOrderbook: OrderbookResponse; +// let zrxOrderbook: OrderbookResponse; +// try { +// [makerAssetOrderbook, zrxOrderbook] = await Promise.all( +// _.map(orderbookRequests, request => sraClient.getOrderbookAsync(request, requestOpts)), +// ); +// } catch (err) { +// throw new Error(ForwarderHelperFactoryError.StandardRelayerApiError); +// } +// // validate orders and find remaining fillable from on chain state or sra api +// let ordersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts; +// let feeOrdersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts; +// if (!_.isUndefined(rpcUrl)) { +// // if we do have an rpc url, get on-chain orders and traders info via the OrderValidatorWrapper +// const ordersFromSra = getOpenAsksFromOrderbook(makerAssetOrderbook); +// const feeOrdersFromSra = getOpenAsksFromOrderbook(zrxOrderbook); +// // TODO: try catch these requests and throw a more domain specific error +// // TODO: optimization, reduce this to once RPC call buy combining orders into one array and then splitting up the response +// const [makerAssetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all( +// _.map([ordersFromSra, feeOrdersFromSra], ordersToBeValidated => { +// const takerAddresses = _.map(ordersToBeValidated, () => constants.NULL_ADDRESS); +// return contractWrappers.orderValidator.getOrdersAndTradersInfoAsync( +// ordersToBeValidated, +// takerAddresses, +// ); +// }), +// ); +// // take maker asset orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts +// ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( +// ordersFromSra, +// makerAssetOrdersAndTradersInfo, +// zrxTokenAssetData, +// ); +// // take fee orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts +// feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( +// feeOrdersFromSra, +// feeOrdersAndTradersInfo, +// zrxTokenAssetData, +// ); +// } else { +// // if we don't have an rpc url, assume all orders are valid and fallback to optional fill amounts from SRA +// // if fill amounts are not available from the SRA, assume all orders are completely fillable +// const apiOrdersFromSra = makerAssetOrderbook.asks.records; +// const feeApiOrdersFromSra = zrxOrderbook.asks.records; +// // take maker asset orders from SRA and the valid orders and remaining fillable maker asset amounts +// ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( +// apiOrdersFromSra, +// ); +// // take fee orders from SRA and find the valid orders and remaining fillable maker asset amounts +// feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( +// feeApiOrdersFromSra, +// ); +// } +// // compile final config +// const config: ForwarderHelperImplConfig = { +// orders: ordersAndRemainingFillableMakerAssetAmounts.orders, +// feeOrders: feeOrdersAndRemainingFillableMakerAssetAmounts.orders, +// remainingFillableMakerAssetAmounts: +// ordersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, +// remainingFillableFeeAmounts: +// feeOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, +// }; +// const helper = new ForwarderHelperImpl(config); +// return helper; +// }, +// }; -interface OrdersAndRemainingFillableMakerAssetAmounts { - orders: SignedOrder[]; - remainingFillableMakerAssetAmounts: BigNumber[]; -} +// interface OrdersAndRemainingFillableMakerAssetAmounts { +// orders: SignedOrder[]; +// remainingFillableMakerAssetAmounts: BigNumber[]; +// } -/** - * Given an array of APIOrder objects from a standard relayer api, return an array - * of fillable orders with their corresponding remainingFillableMakerAssetAmounts - */ -function getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( - apiOrders: APIOrder[], -): OrdersAndRemainingFillableMakerAssetAmounts { - const result = _.reduce( - apiOrders, - (acc, apiOrder) => { - // get current accumulations - const { orders, remainingFillableMakerAssetAmounts } = acc; - // get order and metadata - const { order, metaData } = apiOrder; - // if the order is expired or not open, move on - if (orderUtils.isOrderExpired(order) || !orderUtils.isOpenOrder(order)) { - return acc; - } - // calculate remainingFillableMakerAssetAmount from api metadata, else assume order is completely fillable - const remainingFillableTakerAssetAmount = _.get( - metaData, - 'remainingTakerAssetAmount', - order.takerAssetAmount, - ); - const remainingFillableMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( - order, - remainingFillableTakerAssetAmount, - ); - // if there is some amount of maker asset left to fill and add the order and remaining amount to the accumulations - // if there is not any maker asset left to fill, do not add - if (remainingFillableMakerAssetAmount.gt(constants.ZERO_AMOUNT)) { - return { - orders: _.concat(orders, order), - remainingFillableMakerAssetAmounts: _.concat( - remainingFillableMakerAssetAmounts, - remainingFillableMakerAssetAmount, - ), - }; - } else { - return acc; - } - }, - { orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] }, - ); - return result; -} +// /** +// * Given an array of APIOrder objects from a standard relayer api, return an array +// * of fillable orders with their corresponding remainingFillableMakerAssetAmounts +// */ +// function getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( +// apiOrders: APIOrder[], +// ): OrdersAndRemainingFillableMakerAssetAmounts { +// const result = _.reduce( +// apiOrders, +// (acc, apiOrder) => { +// // get current accumulations +// const { orders, remainingFillableMakerAssetAmounts } = acc; +// // get order and metadata +// const { order, metaData } = apiOrder; +// // if the order is expired or not open, move on +// if (orderUtils.isOrderExpired(order) || !orderUtils.isOpenOrder(order)) { +// return acc; +// } +// // calculate remainingFillableMakerAssetAmount from api metadata, else assume order is completely fillable +// const remainingFillableTakerAssetAmount = _.get( +// metaData, +// 'remainingTakerAssetAmount', +// order.takerAssetAmount, +// ); +// const remainingFillableMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( +// order, +// remainingFillableTakerAssetAmount, +// ); +// // if there is some amount of maker asset left to fill and add the order and remaining amount to the accumulations +// // if there is not any maker asset left to fill, do not add +// if (remainingFillableMakerAssetAmount.gt(constants.ZERO_AMOUNT)) { +// return { +// orders: _.concat(orders, order), +// remainingFillableMakerAssetAmounts: _.concat( +// remainingFillableMakerAssetAmounts, +// remainingFillableMakerAssetAmount, +// ), +// }; +// } else { +// return acc; +// } +// }, +// { orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] }, +// ); +// return result; +// } -/** - * Given an array of orders and corresponding on-chain infos, return a subset of the orders - * that are still fillable orders with their corresponding remainingFillableMakerAssetAmounts - */ -function getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( - inputOrders: SignedOrder[], - ordersAndTradersInfo: OrderAndTraderInfo[], - zrxAssetData: string, -): OrdersAndRemainingFillableMakerAssetAmounts { - // iterate through the input orders and find the ones that are still fillable - // for the orders that are still fillable, calculate the remaining fillable maker asset amount - const result = _.reduce( - inputOrders, - (acc, order, index) => { - // get current accumulations - const { orders, remainingFillableMakerAssetAmounts } = acc; - // get corresponding on-chain state for the order - const { orderInfo, traderInfo } = ordersAndTradersInfo[index]; - // if the order IS NOT fillable, do not add anything to the accumulations and continue iterating - if (orderInfo.orderStatus !== OrderStatus.FILLABLE) { - return acc; - } - // if the order IS fillable, add the order and calculate the remaining fillable amount - const transferrableAssetAmount = BigNumber.min([traderInfo.makerAllowance, traderInfo.makerBalance]); - const transferrableFeeAssetAmount = BigNumber.min([ - traderInfo.makerZrxAllowance, - traderInfo.makerZrxBalance, - ]); - const remainingTakerAssetAmount = order.takerAssetAmount.minus(orderInfo.orderTakerAssetFilledAmount); - const remainingMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( - order, - remainingTakerAssetAmount, - ); - const remainingFillableCalculator = new RemainingFillableCalculator( - order.makerFee, - order.makerAssetAmount, - order.makerAssetData === zrxAssetData, - transferrableAssetAmount, - transferrableFeeAssetAmount, - remainingMakerAssetAmount, - ); - const remainingFillableAmount = remainingFillableCalculator.computeRemainingFillable(); - return { - orders: _.concat(orders, order), - remainingFillableMakerAssetAmounts: _.concat( - remainingFillableMakerAssetAmounts, - remainingFillableAmount, - ), - }; - }, - { orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] }, - ); - return result; -} +// /** +// * Given an array of orders and corresponding on-chain infos, return a subset of the orders +// * that are still fillable orders with their corresponding remainingFillableMakerAssetAmounts +// */ +// function getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( +// inputOrders: SignedOrder[], +// ordersAndTradersInfo: OrderAndTraderInfo[], +// zrxAssetData: string, +// ): OrdersAndRemainingFillableMakerAssetAmounts { +// // iterate through the input orders and find the ones that are still fillable +// // for the orders that are still fillable, calculate the remaining fillable maker asset amount +// const result = _.reduce( +// inputOrders, +// (acc, order, index) => { +// // get current accumulations +// const { orders, remainingFillableMakerAssetAmounts } = acc; +// // get corresponding on-chain state for the order +// const { orderInfo, traderInfo } = ordersAndTradersInfo[index]; +// // if the order IS NOT fillable, do not add anything to the accumulations and continue iterating +// if (orderInfo.orderStatus !== OrderStatus.FILLABLE) { +// return acc; +// } +// // if the order IS fillable, add the order and calculate the remaining fillable amount +// const transferrableAssetAmount = BigNumber.min([traderInfo.makerAllowance, traderInfo.makerBalance]); +// const transferrableFeeAssetAmount = BigNumber.min([ +// traderInfo.makerZrxAllowance, +// traderInfo.makerZrxBalance, +// ]); +// const remainingTakerAssetAmount = order.takerAssetAmount.minus(orderInfo.orderTakerAssetFilledAmount); +// const remainingMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( +// order, +// remainingTakerAssetAmount, +// ); +// const remainingFillableCalculator = new RemainingFillableCalculator( +// order.makerFee, +// order.makerAssetAmount, +// order.makerAssetData === zrxAssetData, +// transferrableAssetAmount, +// transferrableFeeAssetAmount, +// remainingMakerAssetAmount, +// ); +// const remainingFillableAmount = remainingFillableCalculator.computeRemainingFillable(); +// return { +// orders: _.concat(orders, order), +// remainingFillableMakerAssetAmounts: _.concat( +// remainingFillableMakerAssetAmounts, +// remainingFillableAmount, +// ), +// }; +// }, +// { orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] }, +// ); +// return result; +// } -function getOpenAsksFromOrderbook(orderbookResponse: OrderbookResponse): SignedOrder[] { - const asks = _.map(orderbookResponse.asks.records, apiOrder => apiOrder.order); - const result = _.filter(asks, ask => orderUtils.isOpenOrder(ask)); - return result; -} +// function getOpenAsksFromOrderbook(orderbookResponse: OrderbookResponse): SignedOrder[] { +// const asks = _.map(orderbookResponse.asks.records, apiOrder => apiOrder.order); +// const result = _.filter(asks, ask => orderUtils.isOpenOrder(ask)); +// return result; +// } diff --git a/packages/asset-buyer/src/forwarder_helper_impl.ts b/packages/asset-buyer/src/forwarder_helper_impl.ts deleted file mode 100644 index a90edb0bb..000000000 --- a/packages/asset-buyer/src/forwarder_helper_impl.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { marketUtils } from '@0xproject/order-utils'; -import { SignedOrder } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; -import * as _ from 'lodash'; - -import { constants } from './constants'; -import { ForwarderHelper, ForwarderHelperError, MarketBuyOrdersInfo, MarketBuyOrdersInfoRequest } from './types'; -import { forwarderHelperImplConfigUtils } from './utils/forwarder_helper_impl_config_utils'; - -const SLIPPAGE_PERCENTAGE = new BigNumber(0.2); // 20% slippage protection, possibly move this into request interface - -export interface ForwarderHelperImplConfig { - orders: SignedOrder[]; - feeOrders: SignedOrder[]; - remainingFillableMakerAssetAmounts?: BigNumber[]; - remainingFillableFeeAmounts?: BigNumber[]; -} - -export class ForwarderHelperImpl implements ForwarderHelper { - public readonly config: ForwarderHelperImplConfig; - constructor(config: ForwarderHelperImplConfig) { - this.config = forwarderHelperImplConfigUtils.sortedConfig(config); - } - public getMarketBuyOrdersInfo(request: MarketBuyOrdersInfoRequest): MarketBuyOrdersInfo { - const { makerAssetFillAmount, feePercentage } = request; - const { orders, feeOrders, remainingFillableMakerAssetAmounts, remainingFillableFeeAmounts } = this.config; - // TODO: make the slippage percentage customizable - const slippageBufferAmount = makerAssetFillAmount.mul(SLIPPAGE_PERCENTAGE).round(); - const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( - orders, - makerAssetFillAmount, - { - remainingFillableMakerAssetAmounts, - slippageBufferAmount, - }, - ); - if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) { - throw new Error(ForwarderHelperError.InsufficientMakerAssetLiquidity); - } - // TODO: update this logic to find the minimum amount of feeOrders to cover the worst case as opposed to - // finding order that cover all fees, this will help with estimating ETH and minimizing gas usage - const { resultFeeOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( - resultOrders, - feeOrders, - { - remainingFillableMakerAssetAmounts, - remainingFillableFeeAmounts, - }, - ); - if (remainingFeeAmount.gt(constants.ZERO_AMOUNT)) { - throw new Error(ForwarderHelperError.InsufficientZrxLiquidity); - } - // TODO: calculate min and max eth usage - // TODO: optimize orders call data - return { - makerAssetFillAmount, - orders: resultOrders, - feeOrders: resultFeeOrders, - minEthAmount: constants.ZERO_AMOUNT, - maxEthAmount: constants.ZERO_AMOUNT, - feePercentage, - }; - } -} diff --git a/packages/asset-buyer/src/index.ts b/packages/asset-buyer/src/index.ts index eb3a34bd5..299b32edd 100644 --- a/packages/asset-buyer/src/index.ts +++ b/packages/asset-buyer/src/index.ts @@ -1,2 +1,2 @@ -export { forwarderHelperFactory } from './forwarder_helper_factory'; -export { ForwarderHelper, ForwarderHelperError, MarketBuyOrdersInfoRequest, MarketBuyOrdersInfo } from './types'; +export { AssetBuyerError, BuyQuote, BuyQuoteRequest } from './types'; +export { AssetBuyer } from './asset_buyers/asset_buyer'; diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index a7f02ff8d..8a12d0cf8 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -1,49 +1,42 @@ import { SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; -export enum ForwarderHelperFactoryError { - NoEtherTokenContractFound = 'NO_ETHER_TOKEN_CONTRACT_FOUND', - NoZrxTokenContractFound = 'NO_ZRX_TOKEN_CONTRACT_FOUND', - StandardRelayerApiError = 'STANDARD_RELAYER_API_ERROR', -} - -export interface ForwarderHelper { - /** - * Given a MarketBuyOrdersInfoRequest, returns a MarketBuyOrdersInfo containing all information relevant to fulfilling the request - * using the ForwarderContract marketBuyOrdersWithEth function. - * @param request An object that conforms to MarketBuyOrdersInfoRequest. See type definition for more information. - * @return An object that conforms to MarketBuyOrdersInfo that satisfies the request. See type definition for more information. - */ - getMarketBuyOrdersInfo: (request: MarketBuyOrdersInfoRequest) => MarketBuyOrdersInfo; -} - -export enum ForwarderHelperError { - InsufficientMakerAssetLiquidity = 'INSUFFICIENT_MAKER_ASSET_LIQUIDITY', - InsufficientZrxLiquidity = 'INSUFFICIENT_ZRX_LIQUIDITY', -} - /** - * makerAssetFillAmount: The amount of makerAsset requesting to be filled - * feePercentage: Optional affiliate percentage amount factoring into eth amount calculations + * assetBuyAmount: The amount of asset to buy. + * feePercentage: Optional affiliate percentage amount factoring into eth amount calculations. */ -export interface MarketBuyOrdersInfoRequest { - makerAssetFillAmount: BigNumber; +export interface BuyQuoteRequest { + assetBuyAmount: BigNumber; feePercentage?: BigNumber; } /** - * makerAssetFillAmount: The amount of makerAsset requesting to be filled - * orders: An array of objects conforming to SignedOrder. These orders can be used to cover the requested makerAssetFillAmount plus slippage - * feeOrders: An array of objects conforming to SignedOrder. These orders can be used to cover the fees for the orders param above - * minEthAmount: Amount of eth in wei to send with the tx for the most optimistic case - * maxEthAmount: Amount of eth in wei to send with the tx for the worst case - * feePercentage: Affiliate fee percentage used to calculate the eth amounts above. Passed thru directly from the request + * assetData: The asset information. + * orders: An array of objects conforming to SignedOrder. These orders can be used to cover the requested assetBuyAmount plus slippage. + * feeOrders: An array of objects conforming to SignedOrder. These orders can be used to cover the fees for the orders param above. + * minRate: Min rate that needs to be paid in order to execute the buy. + * maxRate: Max rate that can be paid in order to execute the buy. + * assetBuyAmount: The amount of asset to buy. Passed through directly from the request. + * feePercentage: Affiliate fee percentage used to calculate the eth amounts above. Passed through directly from the request. */ -export interface MarketBuyOrdersInfo { - makerAssetFillAmount: BigNumber; +export interface BuyQuote { + assetData: string; orders: SignedOrder[]; feeOrders: SignedOrder[]; - minEthAmount: BigNumber; - maxEthAmount: BigNumber; + minRate: BigNumber; + maxRate: BigNumber; + assetBuyAmount: BigNumber; feePercentage?: BigNumber; } + +/** + * Possible errors thrown by an AssetBuyer instance or associated static methods + */ +export enum AssetBuyerError { + NoEtherTokenContractFound = 'NO_ETHER_TOKEN_CONTRACT_FOUND', + NoZrxTokenContractFound = 'NO_ZRX_TOKEN_CONTRACT_FOUND', + StandardRelayerApiError = 'STANDARD_RELAYER_API_ERROR', + InsufficientAssetLiquidity = 'INSUFFICIENT_ASSET_LIQUIDITY', + InsufficientZrxLiquidity = 'INSUFFICIENT_ZRX_LIQUIDITY', + NoAddressAvailable = 'NO_ADDRESS_AVAILABLE', +} diff --git a/packages/asset-buyer/src/utils/forwarder_helper_impl_config_utils.ts b/packages/asset-buyer/src/utils/forwarder_helper_impl_config_utils.ts index 253384f65..d3cbb651a 100644 --- a/packages/asset-buyer/src/utils/forwarder_helper_impl_config_utils.ts +++ b/packages/asset-buyer/src/utils/forwarder_helper_impl_config_utils.ts @@ -1,92 +1,92 @@ -import { sortingUtils } from '@0xproject/order-utils'; -import { SignedOrder } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; -import * as _ from 'lodash'; +// import { sortingUtils } from '@0xproject/order-utils'; +// import { SignedOrder } from '@0xproject/types'; +// import { BigNumber } from '@0xproject/utils'; +// import * as _ from 'lodash'; -import { ForwarderHelperImplConfig } from '../forwarder_helper_impl'; +// import { ForwarderHelperImplConfig } from '@0xproject/asset-buyer/src/asset_buyer'; -interface SignedOrderWithAmount extends SignedOrder { - remainingFillAmount: BigNumber; -} +// interface SignedOrderWithAmount extends SignedOrder { +// remainingFillAmount: BigNumber; +// } -export const forwarderHelperImplConfigUtils = { - sortedConfig(config: ForwarderHelperImplConfig): ForwarderHelperImplConfig { - const { orders, feeOrders, remainingFillableMakerAssetAmounts, remainingFillableFeeAmounts } = config; - // TODO: provide a feeRate to the sorting function to more accurately sort based on the current market for ZRX tokens - const orderSorter = (ordersToSort: SignedOrder[]) => { - return sortingUtils.sortOrdersByFeeAdjustedRate(ordersToSort); - }; - const sortOrdersResult = sortOrdersAndRemainingFillAmounts( - orderSorter, - orders, - remainingFillableMakerAssetAmounts, - ); - const feeOrderSorter = (ordersToSort: SignedOrder[]) => { - return sortingUtils.sortFeeOrdersByFeeAdjustedRate(ordersToSort); - }; - const sortFeeOrdersResult = sortOrdersAndRemainingFillAmounts( - feeOrderSorter, - feeOrders, - remainingFillableFeeAmounts, - ); - return { - orders: sortOrdersResult.orders, - feeOrders: sortFeeOrdersResult.orders, - remainingFillableMakerAssetAmounts: sortOrdersResult.remainingFillAmounts, - remainingFillableFeeAmounts: sortFeeOrdersResult.remainingFillAmounts, - }; - }, -}; +// export const forwarderHelperImplConfigUtils = { +// sortedConfig(config: ForwarderHelperImplConfig): ForwarderHelperImplConfig { +// const { orders, feeOrders, remainingFillableMakerAssetAmounts, remainingFillableFeeAmounts } = config; +// // TODO: provide a feeRate to the sorting function to more accurately sort based on the current market for ZRX tokens +// const orderSorter = (ordersToSort: SignedOrder[]) => { +// return sortingUtils.sortOrdersByFeeAdjustedRate(ordersToSort); +// }; +// const sortOrdersResult = sortOrdersAndRemainingFillAmounts( +// orderSorter, +// orders, +// remainingFillableMakerAssetAmounts, +// ); +// const feeOrderSorter = (ordersToSort: SignedOrder[]) => { +// return sortingUtils.sortFeeOrdersByFeeAdjustedRate(ordersToSort); +// }; +// const sortFeeOrdersResult = sortOrdersAndRemainingFillAmounts( +// feeOrderSorter, +// feeOrders, +// remainingFillableFeeAmounts, +// ); +// return { +// orders: sortOrdersResult.orders, +// feeOrders: sortFeeOrdersResult.orders, +// remainingFillableMakerAssetAmounts: sortOrdersResult.remainingFillAmounts, +// remainingFillableFeeAmounts: sortFeeOrdersResult.remainingFillAmounts, +// }; +// }, +// }; -type OrderSorter = (orders: SignedOrder[]) => SignedOrder[]; +// type OrderSorter = (orders: SignedOrder[]) => SignedOrder[]; -function sortOrdersAndRemainingFillAmounts( - orderSorter: OrderSorter, - orders: SignedOrder[], - remainingFillAmounts?: BigNumber[], -): { orders: SignedOrder[]; remainingFillAmounts?: BigNumber[] } { - if (!_.isUndefined(remainingFillAmounts)) { - // Bundle orders together with their remainingFillAmounts so that we can sort them together - const orderWithAmounts = bundleSignedOrderWithAmounts(orders, remainingFillAmounts); - // Sort - const sortedOrderWithAmounts = orderSorter(orderWithAmounts) as SignedOrderWithAmount[]; - // Unbundle after sorting - const unbundledSortedOrderWithAmounts = unbundleSignedOrderWithAmounts(sortedOrderWithAmounts); - return { - orders: unbundledSortedOrderWithAmounts.orders, - remainingFillAmounts: unbundledSortedOrderWithAmounts.amounts, - }; - } else { - const sortedOrders = orderSorter(orders); - return { - orders: sortedOrders, - }; - } -} +// function sortOrdersAndRemainingFillAmounts( +// orderSorter: OrderSorter, +// orders: SignedOrder[], +// remainingFillAmounts?: BigNumber[], +// ): { orders: SignedOrder[]; remainingFillAmounts?: BigNumber[] } { +// if (!_.isUndefined(remainingFillAmounts)) { +// // Bundle orders together with their remainingFillAmounts so that we can sort them together +// const orderWithAmounts = bundleSignedOrderWithAmounts(orders, remainingFillAmounts); +// // Sort +// const sortedOrderWithAmounts = orderSorter(orderWithAmounts) as SignedOrderWithAmount[]; +// // Unbundle after sorting +// const unbundledSortedOrderWithAmounts = unbundleSignedOrderWithAmounts(sortedOrderWithAmounts); +// return { +// orders: unbundledSortedOrderWithAmounts.orders, +// remainingFillAmounts: unbundledSortedOrderWithAmounts.amounts, +// }; +// } else { +// const sortedOrders = orderSorter(orders); +// return { +// orders: sortedOrders, +// }; +// } +// } -function bundleSignedOrderWithAmounts(orders: SignedOrder[], amounts: BigNumber[]): SignedOrderWithAmount[] { - const ordersAndAmounts = _.map(orders, (order, index) => { - return { - ...order, - remainingFillAmount: amounts[index], - }; - }); - return ordersAndAmounts; -} +// function bundleSignedOrderWithAmounts(orders: SignedOrder[], amounts: BigNumber[]): SignedOrderWithAmount[] { +// const ordersAndAmounts = _.map(orders, (order, index) => { +// return { +// ...order, +// remainingFillAmount: amounts[index], +// }; +// }); +// return ordersAndAmounts; +// } -function unbundleSignedOrderWithAmounts( - signedOrderWithAmounts: SignedOrderWithAmount[], -): { orders: SignedOrder[]; amounts: BigNumber[] } { - const orders = _.map(signedOrderWithAmounts, order => { - const { remainingFillAmount, ...rest } = order; - return rest; - }); - const amounts = _.map(signedOrderWithAmounts, order => { - const { remainingFillAmount } = order; - return remainingFillAmount; - }); - return { - orders, - amounts, - }; -} +// function unbundleSignedOrderWithAmounts( +// signedOrderWithAmounts: SignedOrderWithAmount[], +// ): { orders: SignedOrder[]; amounts: BigNumber[] } { +// const orders = _.map(signedOrderWithAmounts, order => { +// const { remainingFillAmount, ...rest } = order; +// return rest; +// }); +// const amounts = _.map(signedOrderWithAmounts, order => { +// const { remainingFillAmount } = order; +// return remainingFillAmount; +// }); +// return { +// orders, +// amounts, +// }; +// } diff --git a/packages/asset-buyer/test/forwarder_helper_impl_test.ts b/packages/asset-buyer/test/forwarder_helper_impl_test.ts index 3c3b6db92..b7c014402 100644 --- a/packages/asset-buyer/test/forwarder_helper_impl_test.ts +++ b/packages/asset-buyer/test/forwarder_helper_impl_test.ts @@ -1,136 +1,136 @@ -import { testOrderFactory } from '@0xproject/order-utils/lib/test/utils/test_order_factory'; -import { BigNumber } from '@0xproject/utils'; -import * as chai from 'chai'; -import * as _ from 'lodash'; -import 'mocha'; +// import { testOrderFactory } from '@0xproject/order-utils/lib/test/utils/test_order_factory'; +// import { BigNumber } from '@0xproject/utils'; +// import * as chai from 'chai'; +// import * as _ from 'lodash'; +// import 'mocha'; -import { ForwarderHelperImpl, ForwarderHelperImplConfig } from '../src/forwarder_helper_impl'; -import { ForwarderHelperError } from '../src/types'; +// import { ForwarderHelperImpl, ForwarderHelperImplConfig } from '@0xproject/asset-buyer/src/asset_buyer'; +// import { ForwarderHelperError } from '../src/types'; -import { chaiSetup } from './utils/chai_setup'; +// import { chaiSetup } from './utils/chai_setup'; -chaiSetup.configure(); -const expect = chai.expect; +// chaiSetup.configure(); +// const expect = chai.expect; -describe('ForwarderHelperImpl', () => { - // rate: 2 takerAsset / makerAsset - const testOrder1 = testOrderFactory.generateTestSignedOrder({ - makerAssetAmount: new BigNumber(100), - takerAssetAmount: new BigNumber(200), - }); - // rate: 1 takerAsset / makerAsset - const testOrder2 = testOrderFactory.generateTestSignedOrder({ - makerAssetAmount: new BigNumber(100), - takerAssetAmount: new BigNumber(100), - }); - // rate: 3 takerAsset / makerAsset - const testOrder3 = testOrderFactory.generateTestSignedOrder({ - makerAssetAmount: new BigNumber(100), - takerAssetAmount: new BigNumber(300), - takerFee: new BigNumber(1), - }); - // rate: 3 WETH / ZRX - const testFeeOrder1 = testOrderFactory.generateTestSignedOrder({ - makerAssetAmount: new BigNumber(100), - takerAssetAmount: new BigNumber(300), - }); - // rate: 2 WETH / ZRX - const testFeeOrder2 = testOrderFactory.generateTestSignedOrder({ - makerAssetAmount: new BigNumber(100), - takerAssetAmount: new BigNumber(200), - }); - // rate: 1 WETH / ZRX - const testFeeOrder3 = testOrderFactory.generateTestSignedOrder({ - makerAssetAmount: new BigNumber(100), - takerAssetAmount: new BigNumber(100), - }); - const inputForwarderHelperConfig: ForwarderHelperImplConfig = { - orders: [testOrder1, testOrder2, testOrder3], - feeOrders: [testFeeOrder1, testFeeOrder2, testFeeOrder3], - remainingFillableMakerAssetAmounts: [new BigNumber(1), new BigNumber(2), new BigNumber(3)], - remainingFillableFeeAmounts: [new BigNumber(4), new BigNumber(5), new BigNumber(6)], - }; - describe('#constructor', () => { - const inputForwarderHelperConfigNoRemainingAmounts: ForwarderHelperImplConfig = { - orders: [testOrder1, testOrder2, testOrder3], - feeOrders: [testFeeOrder1, testFeeOrder2, testFeeOrder3], - }; - it('sorts orders', () => { - const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); - expect(forwarderHelper.config.orders).deep.equals([testOrder2, testOrder1, testOrder3]); - }); - it('sorts fee orders', () => { - const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); - expect(forwarderHelper.config.feeOrders).deep.equals([testFeeOrder3, testFeeOrder2, testFeeOrder1]); - }); - it('sorts remainingFillableMakerAssetAmounts', () => { - const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); - expect(forwarderHelper.config.remainingFillableMakerAssetAmounts).to.be.not.undefined(); - expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 0)).to.bignumber.equal( - new BigNumber(2), - ); - expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 1)).to.bignumber.equal( - new BigNumber(1), - ); - expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 2)).to.bignumber.equal( - new BigNumber(3), - ); - }); - it('sorts remainingFillableFeeAmounts', () => { - const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); - expect(forwarderHelper.config.remainingFillableFeeAmounts).to.be.not.undefined(); - expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 0)).to.bignumber.equal(new BigNumber(6)); - expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 1)).to.bignumber.equal(new BigNumber(5)); - expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 2)).to.bignumber.equal(new BigNumber(4)); - }); - it('remainingFillableMakerAssetAmounts is undefined if none provided', () => { - const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoRemainingAmounts); - expect(forwarderHelper.config.remainingFillableMakerAssetAmounts).to.be.undefined(); - }); - it('remainingFillableFeeAmounts is undefined if none provided', () => { - const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoRemainingAmounts); - expect(forwarderHelper.config.remainingFillableFeeAmounts).to.be.undefined(); - }); - }); - describe('#getMarketBuyOrdersInfo', () => { - it('throws if not enough makerAsset liquidity', () => { - const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); - expect(() => { - // request for 6 makerAsset units, because we have exactly 6 available we should throw because there is a built in slippage buffer - forwarderHelper.getMarketBuyOrdersInfo({ - makerAssetFillAmount: new BigNumber(6), - }); - }).to.throw(ForwarderHelperError.InsufficientMakerAssetLiquidity); - }); - it('throws if not enough ZRX liquidity', () => { - const inputForwarderHelperConfigNoFees: ForwarderHelperImplConfig = { - orders: [testOrder1, testOrder2, testOrder3], - feeOrders: [], - }; - const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoFees); - expect(() => { - // request for 4 makerAsset units, we need fees but no fee orders exist, show we should throw - forwarderHelper.getMarketBuyOrdersInfo({ - makerAssetFillAmount: new BigNumber(250), - }); - }).to.throw(ForwarderHelperError.InsufficientZrxLiquidity); - }); - it('passes the makerAssetFillAmount from the request to the info response', () => { - const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); - const makerAssetFillAmount = new BigNumber(4); - const info = forwarderHelper.getMarketBuyOrdersInfo({ - makerAssetFillAmount, - }); - expect(info.makerAssetFillAmount).to.bignumber.equal(makerAssetFillAmount); - }); - it('passes the feePercentage from the request to the info response', () => { - const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); - const feePercentage = new BigNumber(0.2); - const info = forwarderHelper.getMarketBuyOrdersInfo({ - makerAssetFillAmount: new BigNumber(4), - feePercentage, - }); - expect(info.feePercentage).to.bignumber.equal(feePercentage); - }); - }); -}); +// describe('ForwarderHelperImpl', () => { +// // rate: 2 takerAsset / makerAsset +// const testOrder1 = testOrderFactory.generateTestSignedOrder({ +// makerAssetAmount: new BigNumber(100), +// takerAssetAmount: new BigNumber(200), +// }); +// // rate: 1 takerAsset / makerAsset +// const testOrder2 = testOrderFactory.generateTestSignedOrder({ +// makerAssetAmount: new BigNumber(100), +// takerAssetAmount: new BigNumber(100), +// }); +// // rate: 3 takerAsset / makerAsset +// const testOrder3 = testOrderFactory.generateTestSignedOrder({ +// makerAssetAmount: new BigNumber(100), +// takerAssetAmount: new BigNumber(300), +// takerFee: new BigNumber(1), +// }); +// // rate: 3 WETH / ZRX +// const testFeeOrder1 = testOrderFactory.generateTestSignedOrder({ +// makerAssetAmount: new BigNumber(100), +// takerAssetAmount: new BigNumber(300), +// }); +// // rate: 2 WETH / ZRX +// const testFeeOrder2 = testOrderFactory.generateTestSignedOrder({ +// makerAssetAmount: new BigNumber(100), +// takerAssetAmount: new BigNumber(200), +// }); +// // rate: 1 WETH / ZRX +// const testFeeOrder3 = testOrderFactory.generateTestSignedOrder({ +// makerAssetAmount: new BigNumber(100), +// takerAssetAmount: new BigNumber(100), +// }); +// const inputForwarderHelperConfig: ForwarderHelperImplConfig = { +// orders: [testOrder1, testOrder2, testOrder3], +// feeOrders: [testFeeOrder1, testFeeOrder2, testFeeOrder3], +// remainingFillableMakerAssetAmounts: [new BigNumber(1), new BigNumber(2), new BigNumber(3)], +// remainingFillableFeeAmounts: [new BigNumber(4), new BigNumber(5), new BigNumber(6)], +// }; +// describe('#constructor', () => { +// const inputForwarderHelperConfigNoRemainingAmounts: ForwarderHelperImplConfig = { +// orders: [testOrder1, testOrder2, testOrder3], +// feeOrders: [testFeeOrder1, testFeeOrder2, testFeeOrder3], +// }; +// it('sorts orders', () => { +// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); +// expect(forwarderHelper.config.orders).deep.equals([testOrder2, testOrder1, testOrder3]); +// }); +// it('sorts fee orders', () => { +// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); +// expect(forwarderHelper.config.feeOrders).deep.equals([testFeeOrder3, testFeeOrder2, testFeeOrder1]); +// }); +// it('sorts remainingFillableMakerAssetAmounts', () => { +// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); +// expect(forwarderHelper.config.remainingFillableMakerAssetAmounts).to.be.not.undefined(); +// expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 0)).to.bignumber.equal( +// new BigNumber(2), +// ); +// expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 1)).to.bignumber.equal( +// new BigNumber(1), +// ); +// expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 2)).to.bignumber.equal( +// new BigNumber(3), +// ); +// }); +// it('sorts remainingFillableFeeAmounts', () => { +// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); +// expect(forwarderHelper.config.remainingFillableFeeAmounts).to.be.not.undefined(); +// expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 0)).to.bignumber.equal(new BigNumber(6)); +// expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 1)).to.bignumber.equal(new BigNumber(5)); +// expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 2)).to.bignumber.equal(new BigNumber(4)); +// }); +// it('remainingFillableMakerAssetAmounts is undefined if none provided', () => { +// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoRemainingAmounts); +// expect(forwarderHelper.config.remainingFillableMakerAssetAmounts).to.be.undefined(); +// }); +// it('remainingFillableFeeAmounts is undefined if none provided', () => { +// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoRemainingAmounts); +// expect(forwarderHelper.config.remainingFillableFeeAmounts).to.be.undefined(); +// }); +// }); +// describe('#getMarketBuyOrdersInfo', () => { +// it('throws if not enough makerAsset liquidity', () => { +// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); +// expect(() => { +// // request for 6 makerAsset units, because we have exactly 6 available we should throw because there is a built in slippage buffer +// forwarderHelper.getMarketBuyOrdersInfo({ +// makerAssetFillAmount: new BigNumber(6), +// }); +// }).to.throw(ForwarderHelperError.InsufficientMakerAssetLiquidity); +// }); +// it('throws if not enough ZRX liquidity', () => { +// const inputForwarderHelperConfigNoFees: ForwarderHelperImplConfig = { +// orders: [testOrder1, testOrder2, testOrder3], +// feeOrders: [], +// }; +// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoFees); +// expect(() => { +// // request for 4 makerAsset units, we need fees but no fee orders exist, show we should throw +// forwarderHelper.getMarketBuyOrdersInfo({ +// makerAssetFillAmount: new BigNumber(250), +// }); +// }).to.throw(ForwarderHelperError.InsufficientZrxLiquidity); +// }); +// it('passes the makerAssetFillAmount from the request to the info response', () => { +// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); +// const makerAssetFillAmount = new BigNumber(4); +// const info = forwarderHelper.getMarketBuyOrdersInfo({ +// makerAssetFillAmount, +// }); +// expect(info.makerAssetFillAmount).to.bignumber.equal(makerAssetFillAmount); +// }); +// it('passes the feePercentage from the request to the info response', () => { +// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); +// const feePercentage = new BigNumber(0.2); +// const info = forwarderHelper.getMarketBuyOrdersInfo({ +// makerAssetFillAmount: new BigNumber(4), +// feePercentage, +// }); +// expect(info.feePercentage).to.bignumber.equal(feePercentage); +// }); +// }); +// }); -- cgit v1.2.3 From f1a22e9bd7943bc9cb8d8308daca0c60af6e0039 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Sat, 15 Sep 2018 12:59:23 +0200 Subject: Flesh out the AssetBuyer class --- packages/asset-buyer/src/asset_buyer.ts | 227 +++++++++++++++++++++ .../asset-buyer/src/asset_buyers/asset_buyer.ts | 129 ------------ .../asset-buyer/src/forwarder_helper_factory.ts | 1 + packages/asset-buyer/src/index.ts | 15 +- packages/asset-buyer/src/types.ts | 50 ++++- packages/asset-buyer/src/utils/assert.ts | 23 +++ .../asset-buyer/src/utils/buy_quote_calculator.ts | 60 ++++++ .../utils/forwarder_helper_impl_config_utils.ts | 92 --------- .../src/utils/order_fetcher_response_processor.ts | 180 ++++++++++++++++ 9 files changed, 544 insertions(+), 233 deletions(-) create mode 100644 packages/asset-buyer/src/asset_buyer.ts delete mode 100644 packages/asset-buyer/src/asset_buyers/asset_buyer.ts create mode 100644 packages/asset-buyer/src/utils/assert.ts create mode 100644 packages/asset-buyer/src/utils/buy_quote_calculator.ts delete mode 100644 packages/asset-buyer/src/utils/forwarder_helper_impl_config_utils.ts create mode 100644 packages/asset-buyer/src/utils/order_fetcher_response_processor.ts (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts new file mode 100644 index 000000000..9cbdd8df6 --- /dev/null +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -0,0 +1,227 @@ +import { ContractWrappers } from '@0xproject/contract-wrappers'; +import { assetDataUtils, marketUtils } from '@0xproject/order-utils'; +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { Provider } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { constants } from './constants'; +import { + AssetBuyerError, + AssetBuyerOrdersAndFillableAmounts, + BuyQuote, + OrderFetcher, + OrderFetcherResponse, +} from './types'; +import { assert } from './utils/assert'; +import { buyQuoteCalculator } from './utils/buy_quote_calculator'; +import { orderFetcherResponseProcessor } from './utils/order_fetcher_response_processor'; + +const SLIPPAGE_PERCENTAGE = 0.2; // 20% slippage protection, possibly move this into request interface +const DEFAULT_ORDER_REFRESH_INTERVAL_MS = 10000; // 10 seconds +const DEFAULT_FEE_PERCENTAGE = 0; +const ETHER_TOKEN_DECIMALS = 18; + +export class AssetBuyer { + public readonly provider: Provider; + public readonly assetData: string; + public readonly orderFetcher: OrderFetcher; + public readonly networkId: number; + public readonly orderRefreshIntervalMs: number; + private _contractWrappers: ContractWrappers; + private _lastRefreshTimeIfExists?: number; + private _currentOrdersAndFillableAmountsIfExists?: AssetBuyerOrdersAndFillableAmounts; + /** + * Instantiates a new AssetBuyer instance + * @param provider The Provider instance you would like to use for interacting with the Ethereum network. + * @param assetData The assetData of the desired asset to buy (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). + * @param orderFetcher An object that conforms to OrderFetcher, see type for definition. + * @param networkId The ethereum network id. Defaults to 1 (mainnet). + * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. + * Defaults to 10000ms (10s). + * @return An instance of AssetBuyer + */ + constructor( + provider: Provider, + assetData: string, + orderFetcher: OrderFetcher, + networkId: number = constants.MAINNET_NETWORK_ID, + orderRefreshIntervalMs: number = DEFAULT_ORDER_REFRESH_INTERVAL_MS, + ) { + assert.isWeb3Provider('provider', provider); + assert.isString('assetData', assetData); + assert.isValidOrderFetcher('orderFetcher', orderFetcher); + assert.isNumber('networkId', networkId); + assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs); + this.provider = provider; + this.assetData = assetData; + this.orderFetcher = orderFetcher; + this.networkId = networkId; + this.orderRefreshIntervalMs = orderRefreshIntervalMs; + this._contractWrappers = new ContractWrappers(this.provider, { + networkId, + }); + } + /** + * Get a BuyQuote containing all information relevant to fulfilling a buy. + * Pass the BuyQuote to executeBuyQuoteAsync to execute the buy. + * @param assetBuyAmount The amount of asset to buy. + * @param feePercentage The affiliate fee percentage. Defaults to 0. + * @param forceOrderRefresh If set to true, new orders and state will be fetched instead of waiting for + * the next orderRefreshIntervalMs. Defaults to false. + * @return An object that conforms to BuyQuote that satisfies the request. See type definition for more information. + */ + public async getBuyQuoteAsync( + assetBuyAmount: BigNumber, + feePercentage: number = DEFAULT_FEE_PERCENTAGE, + forceOrderRefresh: boolean = false, + ): Promise { + assert.isBigNumber('assetBuyAmount', assetBuyAmount); + assert.isNumber('feePercentage', feePercentage); + assert.isBoolean('forceOrderRefresh', forceOrderRefresh); + // we should refresh if: + // we do not have any orders OR + // we are forced to OR + // we have some last refresh time AND that time was sufficiently long ago + const shouldRefresh = + _.isUndefined(this._currentOrdersAndFillableAmountsIfExists) || + forceOrderRefresh || + (!_.isUndefined(this._lastRefreshTimeIfExists) && + this._lastRefreshTimeIfExists + this.orderRefreshIntervalMs < Date.now()); + let ordersAndFillableAmounts: AssetBuyerOrdersAndFillableAmounts; + if (shouldRefresh) { + ordersAndFillableAmounts = await this._getLatestOrdersAndFillableAmountsAsync(); + this._lastRefreshTimeIfExists = Date.now(); + this._currentOrdersAndFillableAmountsIfExists = ordersAndFillableAmounts; + } else { + // it is safe to cast to AssetBuyerOrdersAndFillableAmounts because shouldRefresh catches the undefined case above + ordersAndFillableAmounts = this + ._currentOrdersAndFillableAmountsIfExists as AssetBuyerOrdersAndFillableAmounts; + } + // TODO: optimization + // make the slippage percentage customizable by integrator + const buyQuote = buyQuoteCalculator.calculate( + ordersAndFillableAmounts, + assetBuyAmount, + feePercentage, + SLIPPAGE_PERCENTAGE, + ); + return buyQuote; + } + /** + * Given a BuyQuote and desired rate, attempt to execute the buy. + * @param buyQuote An object that conforms to BuyQuote. See type definition for more information. + * @param rate The desired rate to execute the buy at. Affects the amount of ETH sent with the transaction, defaults to buyQuote.maxRate. + * @param takerAddress The address to perform the buy. Defaults to the first available address from the provider. + * @param feeRecipient The address where affiliate fees are sent. Defaults to null address (0x000...000). + * @return A promise of the txHash. + */ + public async executeBuyQuoteAsync( + buyQuote: BuyQuote, + rate?: BigNumber, + takerAddress?: string, + feeRecipient: string = constants.NULL_ADDRESS, + ): Promise { + assert.isValidBuyQuote('buyQuote', buyQuote); + if (!_.isUndefined(rate)) { + assert.isBigNumber('rate', rate); + } + if (!_.isUndefined(takerAddress)) { + assert.isETHAddressHex('takerAddress', takerAddress); + } + assert.isETHAddressHex('feeRecipient', feeRecipient); + const { orders, feeOrders, feePercentage, assetBuyAmount, maxRate } = buyQuote; + // if no takerAddress is provided, try to get one from the provider + let finalTakerAddress; + if (!_.isUndefined(takerAddress)) { + finalTakerAddress = takerAddress; + } else { + const web3Wrapper = new Web3Wrapper(this.provider); + const availableAddresses = await web3Wrapper.getAvailableAddressesAsync(); + const firstAvailableAddress = _.head(availableAddresses); + if (!_.isUndefined(firstAvailableAddress)) { + finalTakerAddress = firstAvailableAddress; + } else { + throw new Error(AssetBuyerError.NoAddressAvailable); + } + } + // if no rate is provided, default to the maxRate from buyQuote + const desiredRate = rate || maxRate; + // calculate how much eth is required to buy assetBuyAmount at the desired rate + const ethAmount = assetBuyAmount.dividedToIntegerBy(desiredRate); + // TODO: critical + // update the forwarder wrapper to take in feePercentage as a number instead of a BigNumber, verify with Amir that this is being done correctly + const feePercentageBigNumber = !_.isUndefined(feePercentage) + ? Web3Wrapper.toBaseUnitAmount(new BigNumber(1), ETHER_TOKEN_DECIMALS).mul(feePercentage) + : constants.ZERO_AMOUNT; + const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync( + orders, + assetBuyAmount, + finalTakerAddress, + ethAmount, + feeOrders, + feePercentageBigNumber, + feeRecipient, + ); + return txHash; + } + /** + * Ask the order fetcher for orders and process them. + */ + private async _getLatestOrdersAndFillableAmountsAsync(): Promise { + // find ether token asset data + const etherTokenAssetData = this._getEtherTokenAssetData(); + // find zrx token asset data + const zrxTokenAssetData = this._getZrxTokenAssetData(); + // construct order fetcher requests + const targetOrderFetcherRequest = { + makerAssetData: this.assetData, + takerAssetData: etherTokenAssetData, + networkId: this.networkId, + }; + const feeOrderFetcherRequest = { + makerAssetData: zrxTokenAssetData, + takerAssetData: etherTokenAssetData, + networkId: this.networkId, + }; + const requests = [targetOrderFetcherRequest, feeOrderFetcherRequest]; + // fetch orders and possible fillable amounts + const [targetOrderFetcherResponse, feeOrderFetcherResponse] = await Promise.all( + _.map(requests, request => this.orderFetcher.fetchOrdersAsync(request)), + ); + // process the responses into one object + const ordersAndFillableAmounts = await orderFetcherResponseProcessor.processAsync( + targetOrderFetcherResponse, + feeOrderFetcherResponse, + zrxTokenAssetData, + this._contractWrappers.orderValidator, + ); + return ordersAndFillableAmounts; + } + /** + * Get the assetData that represents the WETH token. + * Will throw if WETH does not exist for the current network. + */ + private _getEtherTokenAssetData(): string { + const etherTokenAddressIfExists = this._contractWrappers.etherToken.getContractAddressIfExists(); + if (_.isUndefined(etherTokenAddressIfExists)) { + throw new Error(AssetBuyerError.NoEtherTokenContractFound); + } + const etherTokenAssetData = assetDataUtils.encodeERC20AssetData(etherTokenAddressIfExists); + return etherTokenAssetData; + } + /** + * Get the assetData that represents the ZRX token. + * Will throw if ZRX does not exist for the current network. + */ + private _getZrxTokenAssetData(): string { + let zrxTokenAssetData: string; + try { + zrxTokenAssetData = this._contractWrappers.exchange.getZRXAssetData(); + } catch (err) { + throw new Error(AssetBuyerError.NoZrxTokenContractFound); + } + return zrxTokenAssetData; + } +} diff --git a/packages/asset-buyer/src/asset_buyers/asset_buyer.ts b/packages/asset-buyer/src/asset_buyers/asset_buyer.ts deleted file mode 100644 index eb7f85e2b..000000000 --- a/packages/asset-buyer/src/asset_buyers/asset_buyer.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { ContractWrappers } from '@0xproject/contract-wrappers'; -import { marketUtils } from '@0xproject/order-utils'; -import { SignedOrder } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as _ from 'lodash'; -import { Provider } from 'ethereum-types'; - -import { constants } from '../constants'; -import { AssetBuyerError, BuyQuote, BuyQuoteRequest } from '../types'; - -const SLIPPAGE_PERCENTAGE = new BigNumber(0.2); // 20% slippage protection, possibly move this into request interface - -export interface AssetBuyerConfig { - orders: SignedOrder[]; - feeOrders: SignedOrder[]; - remainingFillableMakerAssetAmounts?: BigNumber[]; - remainingFillableFeeAmounts?: BigNumber[]; - networkId?: number; -} - -export class AssetBuyer { - public readonly provider: Provider; - public readonly config: AssetBuyerConfig; - private _contractWrappers: ContractWrappers; - constructor(provider: Provider, config: AssetBuyerConfig) { - this.provider = provider; - this.config = config; - const networkId = this.config.networkId || constants.MAINNET_NETWORK_ID; - this._contractWrappers = new ContractWrappers(this.provider, { - networkId, - }); - } - /** - * Given a BuyQuoteRequest, returns a BuyQuote containing all information relevant to fulfilling the buy. Pass the BuyQuote - * to executeBuyQuoteAsync to execute the buy. - * @param buyQuoteRequest An object that conforms to BuyQuoteRequest. See type definition for more information. - * @return An object that conforms to BuyQuote that satisfies the request. See type definition for more information. - */ - public getBuyQuote(buyQuoteRequest: BuyQuoteRequest): BuyQuote { - const { assetBuyAmount, feePercentage } = buyQuoteRequest; - const { orders, feeOrders, remainingFillableMakerAssetAmounts, remainingFillableFeeAmounts } = this.config; - // TODO: optimization - // make the slippage percentage customizable - const slippageBufferAmount = assetBuyAmount.mul(SLIPPAGE_PERCENTAGE).round(); - const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( - orders, - assetBuyAmount, - { - remainingFillableMakerAssetAmounts, - slippageBufferAmount, - }, - ); - if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) { - throw new Error(AssetBuyerError.InsufficientAssetLiquidity); - } - // TODO: optimization - // update this logic to find the minimum amount of feeOrders to cover the worst case as opposed to - // finding order that cover all fees, this will help with estimating ETH and minimizing gas usage - const { resultFeeOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( - resultOrders, - feeOrders, - { - remainingFillableMakerAssetAmounts, - remainingFillableFeeAmounts, - }, - ); - if (remainingFeeAmount.gt(constants.ZERO_AMOUNT)) { - throw new Error(AssetBuyerError.InsufficientZrxLiquidity); - } - const assetData = orders[0].makerAssetData; - // TODO: critical - // calculate minRate and maxRate by calculating min and max eth usage and then dividing into - // assetBuyAmount to get assetData / WETH - return { - assetData, - orders: resultOrders, - feeOrders: resultFeeOrders, - minRate: constants.ZERO_AMOUNT, - maxRate: constants.ZERO_AMOUNT, - assetBuyAmount, - feePercentage, - }; - } - /** - * Given a BuyQuote and desired rate, attempt to execute the buy. - * @param buyQuote An object that conforms to BuyQuote. See type definition for more information. - * @param rate The desired rate to execute the buy at. Affects the amount of ETH sent with the transaction, defaults to buyQuote.maxRate. - * @param takerAddress The address to perform the buy. Defaults to the first available address from the provider. - * @param feeRecipient The address where affiliate fees are sent. Defaults to null address (0x000...000). - * @return A promise of the txHash. - */ - public async executeBuyQuoteAsync( - buyQuote: BuyQuote, - rate?: BigNumber, - takerAddress?: string, - feeRecipient: string = constants.NULL_ADDRESS, - ): Promise { - const { orders, feeOrders, feePercentage, assetBuyAmount, maxRate } = buyQuote; - // if no takerAddress is provided, try to get one from the provider - let finalTakerAddress; - if (!_.isUndefined(takerAddress)) { - finalTakerAddress = takerAddress; - } else { - const web3Wrapper = new Web3Wrapper(this.provider); - const availableAddresses = await web3Wrapper.getAvailableAddressesAsync(); - const firstAvailableAddress = _.head(availableAddresses); - if (!_.isUndefined(firstAvailableAddress)) { - finalTakerAddress = firstAvailableAddress; - } else { - throw new Error(AssetBuyerError.NoAddressAvailable); - } - } - // if no rate is provided, default to the maxRate from buyQuote - const desiredRate = rate || maxRate; - // calculate how much eth is required to buy assetBuyAmount at the desired rate - const ethAmount = assetBuyAmount.dividedToIntegerBy(desiredRate); - const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync( - orders, - assetBuyAmount, - finalTakerAddress, - ethAmount, - feeOrders, - feePercentage, - feeRecipient, - ); - return txHash; - } -} diff --git a/packages/asset-buyer/src/forwarder_helper_factory.ts b/packages/asset-buyer/src/forwarder_helper_factory.ts index 9a3832e81..4c4adfda0 100644 --- a/packages/asset-buyer/src/forwarder_helper_factory.ts +++ b/packages/asset-buyer/src/forwarder_helper_factory.ts @@ -89,6 +89,7 @@ // } catch (err) { // throw new Error(ForwarderHelperFactoryError.StandardRelayerApiError); // } + // // validate orders and find remaining fillable from on chain state or sra api // let ordersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts; // let feeOrdersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts; diff --git a/packages/asset-buyer/src/index.ts b/packages/asset-buyer/src/index.ts index 299b32edd..67ad06084 100644 --- a/packages/asset-buyer/src/index.ts +++ b/packages/asset-buyer/src/index.ts @@ -1,2 +1,13 @@ -export { AssetBuyerError, BuyQuote, BuyQuoteRequest } from './types'; -export { AssetBuyer } from './asset_buyers/asset_buyer'; +export { Provider } from 'ethereum-types'; +export { SignedOrder } from '@0xproject/types'; +export { BigNumber } from '@0xproject/utils'; + +export { AssetBuyer } from './asset_buyer'; +export { + AssetBuyerError, + BuyQuote, + OrderFetcher, + OrderFetcherRequest, + OrderFetcherResponse, + SignedOrderWithRemainingFillableMakerAssetAmount, +} from './types'; diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index 8a12d0cf8..0da30f48d 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -2,22 +2,45 @@ import { SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; /** - * assetBuyAmount: The amount of asset to buy. - * feePercentage: Optional affiliate percentage amount factoring into eth amount calculations. + * makerAssetData: The assetData representing the desired makerAsset. + * takerAssetData: The assetData representing the desired takerAsset. + * networkId: The networkId that the desired orders should be for. */ -export interface BuyQuoteRequest { - assetBuyAmount: BigNumber; - feePercentage?: BigNumber; +export interface OrderFetcherRequest { + makerAssetData: string; + takerAssetData: string; + networkId: number; +} + +/** + * orders: An array of orders with optional remaining fillable makerAsset amounts. See type for more info. + */ +export interface OrderFetcherResponse { + orders: SignedOrderWithRemainingFillableMakerAssetAmount[]; +} + +/** + * A normal SignedOrder with one extra optional property `remainingFillableMakerAssetAmount` + * remainingFillableMakerAssetAmount: The amount of the makerAsset that is available to be filled + */ +export interface SignedOrderWithRemainingFillableMakerAssetAmount extends SignedOrder { + remainingFillableMakerAssetAmount?: BigNumber; +} +/** + * Given an OrderFetchRequest, get an OrderFetchResponse. + */ +export interface OrderFetcher { + fetchOrdersAsync: (orderFetchRequest: OrderFetcherRequest) => Promise; } /** - * assetData: The asset information. + * assetData: String that represents a specific asset (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). * orders: An array of objects conforming to SignedOrder. These orders can be used to cover the requested assetBuyAmount plus slippage. * feeOrders: An array of objects conforming to SignedOrder. These orders can be used to cover the fees for the orders param above. * minRate: Min rate that needs to be paid in order to execute the buy. * maxRate: Max rate that can be paid in order to execute the buy. - * assetBuyAmount: The amount of asset to buy. Passed through directly from the request. - * feePercentage: Affiliate fee percentage used to calculate the eth amounts above. Passed through directly from the request. + * assetBuyAmount: The amount of asset to buy. + * feePercentage: Optional affiliate fee percentage used to calculate the eth amounts above. */ export interface BuyQuote { assetData: string; @@ -26,11 +49,11 @@ export interface BuyQuote { minRate: BigNumber; maxRate: BigNumber; assetBuyAmount: BigNumber; - feePercentage?: BigNumber; + feePercentage?: number; } /** - * Possible errors thrown by an AssetBuyer instance or associated static methods + * Possible errors thrown by an AssetBuyer instance or associated static methods. */ export enum AssetBuyerError { NoEtherTokenContractFound = 'NO_ETHER_TOKEN_CONTRACT_FOUND', @@ -40,3 +63,10 @@ export enum AssetBuyerError { InsufficientZrxLiquidity = 'INSUFFICIENT_ZRX_LIQUIDITY', NoAddressAvailable = 'NO_ADDRESS_AVAILABLE', } + +export interface AssetBuyerOrdersAndFillableAmounts { + orders: SignedOrderWithRemainingFillableMakerAssetAmount[]; + feeOrders: SignedOrderWithRemainingFillableMakerAssetAmount[]; + remainingFillableMakerAssetAmounts: BigNumber[]; + remainingFillableFeeAmounts: BigNumber[]; +} diff --git a/packages/asset-buyer/src/utils/assert.ts b/packages/asset-buyer/src/utils/assert.ts new file mode 100644 index 000000000..c4d611477 --- /dev/null +++ b/packages/asset-buyer/src/utils/assert.ts @@ -0,0 +1,23 @@ +import { assert as sharedAssert } from '@0xproject/assert'; +import { schemas } from '@0xproject/json-schemas'; +import * as _ from 'lodash'; + +import { BuyQuote, OrderFetcher } from '../types'; + +export const assert = { + ...sharedAssert, + isValidBuyQuote(variableName: string, buyQuote: BuyQuote): void { + sharedAssert.isHexString(`${variableName}.assetData`, buyQuote.assetData); + sharedAssert.doesConformToSchema(`${variableName}.orders`, buyQuote.orders, schemas.signedOrdersSchema); + sharedAssert.doesConformToSchema(`${variableName}.feeOrders`, buyQuote.feeOrders, schemas.signedOrdersSchema); + sharedAssert.isBigNumber(`${variableName}.minRate`, buyQuote.minRate); + sharedAssert.isBigNumber(`${variableName}.maxRate`, buyQuote.maxRate); + sharedAssert.isBigNumber(`${variableName}.assetBuyAmount`, buyQuote.assetBuyAmount); + if (!_.isUndefined(buyQuote.feePercentage)) { + sharedAssert.isNumber(`${variableName}.feePercentage`, buyQuote.feePercentage); + } + }, + isValidOrderFetcher(variableName: string, orderFetcher: OrderFetcher): void { + sharedAssert.isFunction(`${variableName}.fetchOrdersAsync`, orderFetcher.fetchOrdersAsync); + }, +}; diff --git a/packages/asset-buyer/src/utils/buy_quote_calculator.ts b/packages/asset-buyer/src/utils/buy_quote_calculator.ts new file mode 100644 index 000000000..e05ab1e55 --- /dev/null +++ b/packages/asset-buyer/src/utils/buy_quote_calculator.ts @@ -0,0 +1,60 @@ +import { marketUtils } from '@0xproject/order-utils'; +import { BigNumber } from '@0xproject/utils'; + +import { constants } from '../constants'; +import { AssetBuyerError, AssetBuyerOrdersAndFillableAmounts, BuyQuote } from '../types'; + +export const buyQuoteCalculator = { + calculate( + ordersAndFillableAmounts: AssetBuyerOrdersAndFillableAmounts, + assetBuyAmount: BigNumber, + feePercentage: number, + slippagePercentage: number, + ): BuyQuote { + const { + orders, + feeOrders, + remainingFillableMakerAssetAmounts, + remainingFillableFeeAmounts, + } = ordersAndFillableAmounts; + const slippageBufferAmount = assetBuyAmount.mul(slippagePercentage).round(); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + orders, + assetBuyAmount, + { + remainingFillableMakerAssetAmounts, + slippageBufferAmount, + }, + ); + if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) { + throw new Error(AssetBuyerError.InsufficientAssetLiquidity); + } + // TODO: optimization + // update this logic to find the minimum amount of feeOrders to cover the worst case as opposed to + // finding order that cover all fees, this will help with estimating ETH and minimizing gas usage + const { resultFeeOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + resultOrders, + feeOrders, + { + remainingFillableMakerAssetAmounts, + remainingFillableFeeAmounts, + }, + ); + if (remainingFeeAmount.gt(constants.ZERO_AMOUNT)) { + throw new Error(AssetBuyerError.InsufficientZrxLiquidity); + } + const assetData = orders[0].makerAssetData; + // TODO: critical + // calculate minRate and maxRate by calculating min and max eth usage and then dividing into + // assetBuyAmount to get assetData / WETH, needs to take into account feePercentage as well + return { + assetData, + orders: resultOrders, + feeOrders: resultFeeOrders, + minRate: constants.ZERO_AMOUNT, + maxRate: constants.ZERO_AMOUNT, + assetBuyAmount, + feePercentage, + }; + }, +}; diff --git a/packages/asset-buyer/src/utils/forwarder_helper_impl_config_utils.ts b/packages/asset-buyer/src/utils/forwarder_helper_impl_config_utils.ts deleted file mode 100644 index d3cbb651a..000000000 --- a/packages/asset-buyer/src/utils/forwarder_helper_impl_config_utils.ts +++ /dev/null @@ -1,92 +0,0 @@ -// import { sortingUtils } from '@0xproject/order-utils'; -// import { SignedOrder } from '@0xproject/types'; -// import { BigNumber } from '@0xproject/utils'; -// import * as _ from 'lodash'; - -// import { ForwarderHelperImplConfig } from '@0xproject/asset-buyer/src/asset_buyer'; - -// interface SignedOrderWithAmount extends SignedOrder { -// remainingFillAmount: BigNumber; -// } - -// export const forwarderHelperImplConfigUtils = { -// sortedConfig(config: ForwarderHelperImplConfig): ForwarderHelperImplConfig { -// const { orders, feeOrders, remainingFillableMakerAssetAmounts, remainingFillableFeeAmounts } = config; -// // TODO: provide a feeRate to the sorting function to more accurately sort based on the current market for ZRX tokens -// const orderSorter = (ordersToSort: SignedOrder[]) => { -// return sortingUtils.sortOrdersByFeeAdjustedRate(ordersToSort); -// }; -// const sortOrdersResult = sortOrdersAndRemainingFillAmounts( -// orderSorter, -// orders, -// remainingFillableMakerAssetAmounts, -// ); -// const feeOrderSorter = (ordersToSort: SignedOrder[]) => { -// return sortingUtils.sortFeeOrdersByFeeAdjustedRate(ordersToSort); -// }; -// const sortFeeOrdersResult = sortOrdersAndRemainingFillAmounts( -// feeOrderSorter, -// feeOrders, -// remainingFillableFeeAmounts, -// ); -// return { -// orders: sortOrdersResult.orders, -// feeOrders: sortFeeOrdersResult.orders, -// remainingFillableMakerAssetAmounts: sortOrdersResult.remainingFillAmounts, -// remainingFillableFeeAmounts: sortFeeOrdersResult.remainingFillAmounts, -// }; -// }, -// }; - -// type OrderSorter = (orders: SignedOrder[]) => SignedOrder[]; - -// function sortOrdersAndRemainingFillAmounts( -// orderSorter: OrderSorter, -// orders: SignedOrder[], -// remainingFillAmounts?: BigNumber[], -// ): { orders: SignedOrder[]; remainingFillAmounts?: BigNumber[] } { -// if (!_.isUndefined(remainingFillAmounts)) { -// // Bundle orders together with their remainingFillAmounts so that we can sort them together -// const orderWithAmounts = bundleSignedOrderWithAmounts(orders, remainingFillAmounts); -// // Sort -// const sortedOrderWithAmounts = orderSorter(orderWithAmounts) as SignedOrderWithAmount[]; -// // Unbundle after sorting -// const unbundledSortedOrderWithAmounts = unbundleSignedOrderWithAmounts(sortedOrderWithAmounts); -// return { -// orders: unbundledSortedOrderWithAmounts.orders, -// remainingFillAmounts: unbundledSortedOrderWithAmounts.amounts, -// }; -// } else { -// const sortedOrders = orderSorter(orders); -// return { -// orders: sortedOrders, -// }; -// } -// } - -// function bundleSignedOrderWithAmounts(orders: SignedOrder[], amounts: BigNumber[]): SignedOrderWithAmount[] { -// const ordersAndAmounts = _.map(orders, (order, index) => { -// return { -// ...order, -// remainingFillAmount: amounts[index], -// }; -// }); -// return ordersAndAmounts; -// } - -// function unbundleSignedOrderWithAmounts( -// signedOrderWithAmounts: SignedOrderWithAmount[], -// ): { orders: SignedOrder[]; amounts: BigNumber[] } { -// const orders = _.map(signedOrderWithAmounts, order => { -// const { remainingFillAmount, ...rest } = order; -// return rest; -// }); -// const amounts = _.map(signedOrderWithAmounts, order => { -// const { remainingFillAmount } = order; -// return remainingFillAmount; -// }); -// return { -// orders, -// amounts, -// }; -// } diff --git a/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts b/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts new file mode 100644 index 000000000..04c5355eb --- /dev/null +++ b/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts @@ -0,0 +1,180 @@ +import { OrderAndTraderInfo, OrderStatus, OrderValidatorWrapper } from '@0xproject/contract-wrappers'; +import { sortingUtils } from '@0xproject/order-utils'; +import { RemainingFillableCalculator } from '@0xproject/order-utils/lib/src/remaining_fillable_calculator'; +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { constants } from '../constants'; +import { + AssetBuyerOrdersAndFillableAmounts, + OrderFetcherResponse, + SignedOrderWithRemainingFillableMakerAssetAmount, +} from '../types'; + +import { orderUtils } from './order_utils'; + +interface OrdersAndRemainingFillableMakerAssetAmounts { + orders: SignedOrder[]; + remainingFillableMakerAssetAmounts: BigNumber[]; +} + +export const orderFetcherResponseProcessor = { + /** + * Take the responses for the target orders to buy and fee orders and process them. + * Processing includes: + * - Drop orders that are expired or not open orders (null taker address) + * - If shouldValidateOnChain, attempt to grab fillable amounts from on-chain otherwise assume completely fillable + * - Sort by rate + */ + async processAsync( + targetOrderFetcherResponse: OrderFetcherResponse, + feeOrderFetcherResponse: OrderFetcherResponse, + zrxTokenAssetData: string, + orderValidator?: OrderValidatorWrapper, + ): Promise { + // drop orders that are expired or not open + const filteredTargetOrders = filterOutExpiredAndNonOpenOrders(targetOrderFetcherResponse.orders); + const filteredFeeOrders = filterOutExpiredAndNonOpenOrders(feeOrderFetcherResponse.orders); + // set the orders to be sorted equal to the filtered orders + let unsortedTargetOrders = filteredTargetOrders; + let unsortedFeeOrders = filteredFeeOrders; + // if an orderValidator is provided, use on chain information to calculate remaining fillable makerAsset amounts + if (!_.isUndefined(orderValidator)) { + // TODO: critical + // try catch these requests and throw a more domain specific error + // TODO: optimization + // reduce this to once RPC call buy combining orders into one array and then splitting up the response + const [targetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all( + _.map([filteredTargetOrders, filteredFeeOrders], ordersToBeValidated => { + const takerAddresses = _.map(ordersToBeValidated, () => constants.NULL_ADDRESS); + return orderValidator.getOrdersAndTradersInfoAsync(ordersToBeValidated, takerAddresses); + }), + ); + // take orders + on chain information and find the valid orders and remaining fillable maker asset amounts + unsortedTargetOrders = getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain( + filteredTargetOrders, + targetOrdersAndTradersInfo, + zrxTokenAssetData, + ); + // take orders + on chain information and find the valid orders and remaining fillable maker asset amounts + unsortedFeeOrders = getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain( + filteredFeeOrders, + feeOrdersAndTradersInfo, + zrxTokenAssetData, + ); + } + // sort orders by rate + // TODO: optimization + // provide a feeRate to the sorting function to more accurately sort based on the current market for ZRX tokens + const sortedTargetOrders = sortingUtils.sortOrdersByFeeAdjustedRate(unsortedTargetOrders); + const sortedFeeOrders = sortingUtils.sortFeeOrdersByFeeAdjustedRate(unsortedFeeOrders); + // unbundle orders and fillable amounts and compile final result + const targetOrdersAndRemainingFillableMakerAssetAmounts = unbundleOrdersWithAmounts(sortedTargetOrders); + const feeOrdersAndRemainingFillableMakerAssetAmounts = unbundleOrdersWithAmounts(sortedFeeOrders); + return { + orders: targetOrdersAndRemainingFillableMakerAssetAmounts.orders, + feeOrders: feeOrdersAndRemainingFillableMakerAssetAmounts.orders, + remainingFillableMakerAssetAmounts: + targetOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, + remainingFillableFeeAmounts: + feeOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, + }; + }, +}; + +/** + * Given an array of orders, return a new array with expired and non open orders filtered out. + */ +function filterOutExpiredAndNonOpenOrders( + orders: SignedOrderWithRemainingFillableMakerAssetAmount[], +): SignedOrderWithRemainingFillableMakerAssetAmount[] { + const result = _.filter(orders, order => { + return orderUtils.isOpenOrder(order) && orderUtils.isOrderExpired(order); + }); + return result; +} + +/** + * Given an array of orders and corresponding on-chain infos, return a subset of the orders + * that are still fillable orders with their corresponding remainingFillableMakerAssetAmounts. + */ +function getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain( + inputOrders: SignedOrder[], + ordersAndTradersInfo: OrderAndTraderInfo[], + zrxAssetData: string, +): SignedOrderWithRemainingFillableMakerAssetAmount[] { + // iterate through the input orders and find the ones that are still fillable + // for the orders that are still fillable, calculate the remaining fillable maker asset amount + const result = _.reduce( + inputOrders, + (accOrders, order, index) => { + // get corresponding on-chain state for the order + const { orderInfo, traderInfo } = ordersAndTradersInfo[index]; + // if the order IS NOT fillable, do not add anything to the accumulations and continue iterating + if (orderInfo.orderStatus !== OrderStatus.FILLABLE) { + return accOrders; + } + // if the order IS fillable, add the order and calculate the remaining fillable amount + const transferrableAssetAmount = BigNumber.min([traderInfo.makerAllowance, traderInfo.makerBalance]); + const transferrableFeeAssetAmount = BigNumber.min([ + traderInfo.makerZrxAllowance, + traderInfo.makerZrxBalance, + ]); + const remainingTakerAssetAmount = order.takerAssetAmount.minus(orderInfo.orderTakerAssetFilledAmount); + const remainingMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( + order, + remainingTakerAssetAmount, + ); + const remainingFillableCalculator = new RemainingFillableCalculator( + order.makerFee, + order.makerAssetAmount, + order.makerAssetData === zrxAssetData, + transferrableAssetAmount, + transferrableFeeAssetAmount, + remainingMakerAssetAmount, + ); + const remainingFillableAmount = remainingFillableCalculator.computeRemainingFillable(); + // if the order does not have any remaining fillable makerAsset, do not add anything to the accumulations and continue iterating + if (remainingFillableAmount.lte(constants.ZERO_AMOUNT)) { + return accOrders; + } + const orderWithRemainingFillableMakerAssetAmount = { + ...order, + remainingFillableMakerAssetAmount: remainingFillableAmount, + }; + const newAccOrders = _.concat(accOrders, orderWithRemainingFillableMakerAssetAmount); + return newAccOrders; + }, + [] as SignedOrderWithRemainingFillableMakerAssetAmount[], + ); + return result; +} + +/** + * Given an array of orders with remaining fillable maker asset amounts. Unbundle into an instance of OrdersAndRemainingFillableMakerAssetAmounts. + * If an order is missing a corresponding remainingFillableMakerAssetAmount, assume it is completely fillable. + */ +function unbundleOrdersWithAmounts( + ordersWithAmounts: SignedOrderWithRemainingFillableMakerAssetAmount[], +): OrdersAndRemainingFillableMakerAssetAmounts { + const result = _.reduce( + ordersWithAmounts, + (acc, orderWithAmount) => { + const { orders, remainingFillableMakerAssetAmounts } = acc; + const { remainingFillableMakerAssetAmount, ...order } = orderWithAmount; + // if we are still missing a remainingFillableMakerAssetAmount, assume the order is completely fillable + const newRemainingAmount = remainingFillableMakerAssetAmount || order.makerAssetAmount; + const newAcc = { + orders: _.concat(orders, order), + remainingFillableMakerAssetAmounts: _.concat(remainingFillableMakerAssetAmounts, newRemainingAmount), + }; + return newAcc; + }, + { + orders: [] as SignedOrder[], + remainingFillableMakerAssetAmounts: [] as BigNumber[], + }, + ); + return result; +} -- cgit v1.2.3 From 190bf2599c1327fffd03d8a9d50bc7568190cf33 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Sat, 15 Sep 2018 13:56:20 +0200 Subject: Implement StandardRelayerAPIOrderFetcher --- .../asset-buyer/src/forwarder_helper_factory.ts | 262 --------------------- .../standard_relayer_api_order_fetcher.ts | 80 +++++++ .../src/utils/order_fetcher_response_processor.ts | 4 + 3 files changed, 84 insertions(+), 262 deletions(-) delete mode 100644 packages/asset-buyer/src/forwarder_helper_factory.ts create mode 100644 packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/forwarder_helper_factory.ts b/packages/asset-buyer/src/forwarder_helper_factory.ts deleted file mode 100644 index 4c4adfda0..000000000 --- a/packages/asset-buyer/src/forwarder_helper_factory.ts +++ /dev/null @@ -1,262 +0,0 @@ -// import { assert } from '@0xproject/assert'; -// import { APIOrder, HttpClient, OrderbookResponse } from '@0xproject/connect'; -// import { ContractWrappers, OrderAndTraderInfo, OrderStatus } from '@0xproject/contract-wrappers'; -// import { schemas } from '@0xproject/json-schemas'; -// import { assetDataUtils } from '@0xproject/order-utils'; -// import { RemainingFillableCalculator } from '@0xproject/order-utils/lib/src/remaining_fillable_calculator'; -// import { RPCSubprovider, Web3ProviderEngine } from '@0xproject/subproviders'; -// import { SignedOrder } from '@0xproject/types'; -// import { BigNumber } from '@0xproject/utils'; -// import * as _ from 'lodash'; - -// import { constants } from './constants'; -// import { ForwarderHelperImpl, ForwarderHelperImplConfig } from '@0xproject/asset-buyer/src/asset_buyer'; -// import { ForwarderHelper, ForwarderHelperFactoryError } from './types'; -// import { orderUtils } from './utils/order_utils'; - -// export const forwarderHelperFactory = { -// /** -// * Given an array of orders and an array of feeOrders, get a ForwarderHelper -// * @param orders An array of objects conforming to SignedOrder. Each order should specify the same makerAssetData and takerAssetData -// * @param feeOrders An array of objects conforming to SignedOrder. Each order should specify ZRX as makerAssetData WETH as takerAssetData -// * @return A ForwarderHelper, see type for definition -// */ -// getForwarderHelperForOrders(orders: SignedOrder[], feeOrders: SignedOrder[] = []): ForwarderHelper { -// assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); -// assert.doesConformToSchema('feeOrders', orders, schemas.signedOrdersSchema); -// // TODO: Add assertion here for orders all having the same makerAsset and takerAsset -// const config: ForwarderHelperImplConfig = { -// orders, -// feeOrders, -// }; -// const helper = new ForwarderHelperImpl(config); -// return helper; -// }, -// /** -// * Given a desired makerAsset and SRA url, get a ForwarderHelper -// * @param makerAssetData An array of objects conforming to SignedOrder. Each order should specify the same makerAssetData and takerAssetData -// * @param sraUrl A url pointing to an SRA v2 compliant endpoint. -// * @param rpcUrl A url pointing to an ethereum node. -// * @param networkId The ethereum networkId, defaults to 1 (mainnet). -// * @return A ForwarderHelper, see type for definition -// */ -// async getForwarderHelperForMakerAssetDataAsync( -// makerAssetData: string, -// sraUrl: string, -// rpcUrl?: string, -// networkId: number = 1, -// ): Promise { -// assert.isHexString('makerAssetData', makerAssetData); -// assert.isWebUri('sraUrl', sraUrl); -// if (!_.isUndefined(rpcUrl)) { -// assert.isWebUri('rpcUrl', rpcUrl); -// } -// assert.isNumber('networkId', networkId); -// // create provider -// const providerEngine = new Web3ProviderEngine(); -// if (!_.isUndefined(rpcUrl)) { -// providerEngine.addProvider(new RPCSubprovider(rpcUrl)); -// } -// providerEngine.start(); -// // create contract wrappers given provider and networkId -// const contractWrappers = new ContractWrappers(providerEngine, { networkId }); -// // find ether token asset data -// const etherTokenAddressIfExists = contractWrappers.etherToken.getContractAddressIfExists(); -// if (_.isUndefined(etherTokenAddressIfExists)) { -// throw new Error(ForwarderHelperFactoryError.NoEtherTokenContractFound); -// } -// const etherTokenAssetData = assetDataUtils.encodeERC20AssetData(etherTokenAddressIfExists); -// // find zrx token asset data -// let zrxTokenAssetData: string; -// try { -// zrxTokenAssetData = contractWrappers.exchange.getZRXAssetData(); -// } catch (err) { -// throw new Error(ForwarderHelperFactoryError.NoZrxTokenContractFound); -// } -// // get orderbooks for makerAsset/WETH and ZRX/WETH -// const sraClient = new HttpClient(sraUrl); -// const orderbookRequests = [ -// { baseAssetData: makerAssetData, quoteAssetData: etherTokenAssetData }, -// { baseAssetData: zrxTokenAssetData, quoteAssetData: etherTokenAssetData }, -// ]; -// const requestOpts = { networkId }; -// let makerAssetOrderbook: OrderbookResponse; -// let zrxOrderbook: OrderbookResponse; -// try { -// [makerAssetOrderbook, zrxOrderbook] = await Promise.all( -// _.map(orderbookRequests, request => sraClient.getOrderbookAsync(request, requestOpts)), -// ); -// } catch (err) { -// throw new Error(ForwarderHelperFactoryError.StandardRelayerApiError); -// } - -// // validate orders and find remaining fillable from on chain state or sra api -// let ordersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts; -// let feeOrdersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts; -// if (!_.isUndefined(rpcUrl)) { -// // if we do have an rpc url, get on-chain orders and traders info via the OrderValidatorWrapper -// const ordersFromSra = getOpenAsksFromOrderbook(makerAssetOrderbook); -// const feeOrdersFromSra = getOpenAsksFromOrderbook(zrxOrderbook); -// // TODO: try catch these requests and throw a more domain specific error -// // TODO: optimization, reduce this to once RPC call buy combining orders into one array and then splitting up the response -// const [makerAssetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all( -// _.map([ordersFromSra, feeOrdersFromSra], ordersToBeValidated => { -// const takerAddresses = _.map(ordersToBeValidated, () => constants.NULL_ADDRESS); -// return contractWrappers.orderValidator.getOrdersAndTradersInfoAsync( -// ordersToBeValidated, -// takerAddresses, -// ); -// }), -// ); -// // take maker asset orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts -// ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( -// ordersFromSra, -// makerAssetOrdersAndTradersInfo, -// zrxTokenAssetData, -// ); -// // take fee orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts -// feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( -// feeOrdersFromSra, -// feeOrdersAndTradersInfo, -// zrxTokenAssetData, -// ); -// } else { -// // if we don't have an rpc url, assume all orders are valid and fallback to optional fill amounts from SRA -// // if fill amounts are not available from the SRA, assume all orders are completely fillable -// const apiOrdersFromSra = makerAssetOrderbook.asks.records; -// const feeApiOrdersFromSra = zrxOrderbook.asks.records; -// // take maker asset orders from SRA and the valid orders and remaining fillable maker asset amounts -// ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( -// apiOrdersFromSra, -// ); -// // take fee orders from SRA and find the valid orders and remaining fillable maker asset amounts -// feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( -// feeApiOrdersFromSra, -// ); -// } -// // compile final config -// const config: ForwarderHelperImplConfig = { -// orders: ordersAndRemainingFillableMakerAssetAmounts.orders, -// feeOrders: feeOrdersAndRemainingFillableMakerAssetAmounts.orders, -// remainingFillableMakerAssetAmounts: -// ordersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, -// remainingFillableFeeAmounts: -// feeOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, -// }; -// const helper = new ForwarderHelperImpl(config); -// return helper; -// }, -// }; - -// interface OrdersAndRemainingFillableMakerAssetAmounts { -// orders: SignedOrder[]; -// remainingFillableMakerAssetAmounts: BigNumber[]; -// } - -// /** -// * Given an array of APIOrder objects from a standard relayer api, return an array -// * of fillable orders with their corresponding remainingFillableMakerAssetAmounts -// */ -// function getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( -// apiOrders: APIOrder[], -// ): OrdersAndRemainingFillableMakerAssetAmounts { -// const result = _.reduce( -// apiOrders, -// (acc, apiOrder) => { -// // get current accumulations -// const { orders, remainingFillableMakerAssetAmounts } = acc; -// // get order and metadata -// const { order, metaData } = apiOrder; -// // if the order is expired or not open, move on -// if (orderUtils.isOrderExpired(order) || !orderUtils.isOpenOrder(order)) { -// return acc; -// } -// // calculate remainingFillableMakerAssetAmount from api metadata, else assume order is completely fillable -// const remainingFillableTakerAssetAmount = _.get( -// metaData, -// 'remainingTakerAssetAmount', -// order.takerAssetAmount, -// ); -// const remainingFillableMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( -// order, -// remainingFillableTakerAssetAmount, -// ); -// // if there is some amount of maker asset left to fill and add the order and remaining amount to the accumulations -// // if there is not any maker asset left to fill, do not add -// if (remainingFillableMakerAssetAmount.gt(constants.ZERO_AMOUNT)) { -// return { -// orders: _.concat(orders, order), -// remainingFillableMakerAssetAmounts: _.concat( -// remainingFillableMakerAssetAmounts, -// remainingFillableMakerAssetAmount, -// ), -// }; -// } else { -// return acc; -// } -// }, -// { orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] }, -// ); -// return result; -// } - -// /** -// * Given an array of orders and corresponding on-chain infos, return a subset of the orders -// * that are still fillable orders with their corresponding remainingFillableMakerAssetAmounts -// */ -// function getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( -// inputOrders: SignedOrder[], -// ordersAndTradersInfo: OrderAndTraderInfo[], -// zrxAssetData: string, -// ): OrdersAndRemainingFillableMakerAssetAmounts { -// // iterate through the input orders and find the ones that are still fillable -// // for the orders that are still fillable, calculate the remaining fillable maker asset amount -// const result = _.reduce( -// inputOrders, -// (acc, order, index) => { -// // get current accumulations -// const { orders, remainingFillableMakerAssetAmounts } = acc; -// // get corresponding on-chain state for the order -// const { orderInfo, traderInfo } = ordersAndTradersInfo[index]; -// // if the order IS NOT fillable, do not add anything to the accumulations and continue iterating -// if (orderInfo.orderStatus !== OrderStatus.FILLABLE) { -// return acc; -// } -// // if the order IS fillable, add the order and calculate the remaining fillable amount -// const transferrableAssetAmount = BigNumber.min([traderInfo.makerAllowance, traderInfo.makerBalance]); -// const transferrableFeeAssetAmount = BigNumber.min([ -// traderInfo.makerZrxAllowance, -// traderInfo.makerZrxBalance, -// ]); -// const remainingTakerAssetAmount = order.takerAssetAmount.minus(orderInfo.orderTakerAssetFilledAmount); -// const remainingMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( -// order, -// remainingTakerAssetAmount, -// ); -// const remainingFillableCalculator = new RemainingFillableCalculator( -// order.makerFee, -// order.makerAssetAmount, -// order.makerAssetData === zrxAssetData, -// transferrableAssetAmount, -// transferrableFeeAssetAmount, -// remainingMakerAssetAmount, -// ); -// const remainingFillableAmount = remainingFillableCalculator.computeRemainingFillable(); -// return { -// orders: _.concat(orders, order), -// remainingFillableMakerAssetAmounts: _.concat( -// remainingFillableMakerAssetAmounts, -// remainingFillableAmount, -// ), -// }; -// }, -// { orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] }, -// ); -// return result; -// } - -// function getOpenAsksFromOrderbook(orderbookResponse: OrderbookResponse): SignedOrder[] { -// const asks = _.map(orderbookResponse.asks.records, apiOrder => apiOrder.order); -// const result = _.filter(asks, ask => orderUtils.isOpenOrder(ask)); -// return result; -// } diff --git a/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts b/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts new file mode 100644 index 000000000..bf177b93b --- /dev/null +++ b/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts @@ -0,0 +1,80 @@ +import { APIOrder, HttpClient, OrderbookResponse } from '@0xproject/connect'; +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { constants } from '../constants'; +import { + AssetBuyerError, + OrderFetcher, + OrderFetcherRequest, + OrderFetcherResponse, + SignedOrderWithRemainingFillableMakerAssetAmount, +} from '../types'; +import { assert } from '../utils/assert'; +import { orderUtils } from '../utils/order_utils'; + +export class StandardRelayerAPIOrderFetcher implements OrderFetcher { + public readonly apiUrl: string; + private _sraClient: HttpClient; + /** + * Given an array of APIOrder objects from a standard relayer api, return an array + * of SignedOrderWithRemainingFillableMakerAssetAmounts + */ + private static _getSignedOrderWithRemainingFillableMakerAssetAmountFromApi( + apiOrders: APIOrder[], + ): SignedOrderWithRemainingFillableMakerAssetAmount[] { + const result = _.map(apiOrders, apiOrder => { + const { order, metaData } = apiOrder; + // calculate remainingFillableMakerAssetAmount from api metadata, else assume order is completely fillable + const remainingFillableTakerAssetAmount = _.get( + metaData, + 'remainingTakerAssetAmount', + order.takerAssetAmount, + ); + const remainingFillableMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( + order, + remainingFillableTakerAssetAmount, + ); + const newOrder = { + ...order, + remainingFillableMakerAssetAmount, + }; + return newOrder; + }); + return result; + } + /** + * Instantiates a new StandardRelayerAPIOrderFetcher instance + * @param apiUrl The relayer API base HTTP url you would like to interact with. + * @return An instance of StandardRelayerAPIOrderFetcher + */ + constructor(apiUrl: string) { + assert.isWebUri('apiUrl', apiUrl); + this.apiUrl = apiUrl; + this._sraClient = new HttpClient(apiUrl); + } + /** + * Given an object that conforms to OrderFetcherRequest, return the corresponding OrderFetcherResponse that satisfies the request. + * @param orderFetchRequest An instance of OrderFetcherRequest. See type for more information. + * @return An instance of OrderFetcherResponse. See type for more information. + */ + public async fetchOrdersAsync(orderFetchRequest: OrderFetcherRequest): Promise { + const { makerAssetData, takerAssetData, networkId } = orderFetchRequest; + const orderbookRequest = { baseAssetData: makerAssetData, quoteAssetData: takerAssetData }; + const requestOpts = { networkId }; + let orderbook: OrderbookResponse; + try { + orderbook = await this._sraClient.getOrderbookAsync(orderbookRequest, requestOpts); + } catch (err) { + throw new Error(AssetBuyerError.StandardRelayerApiError); + } + const apiOrders = orderbook.asks.records; + const orders = StandardRelayerAPIOrderFetcher._getSignedOrderWithRemainingFillableMakerAssetAmountFromApi( + apiOrders, + ); + return { + orders, + }; + } +} diff --git a/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts b/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts index 04c5355eb..f1116a80f 100644 --- a/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts +++ b/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts @@ -165,6 +165,10 @@ function unbundleOrdersWithAmounts( const { remainingFillableMakerAssetAmount, ...order } = orderWithAmount; // if we are still missing a remainingFillableMakerAssetAmount, assume the order is completely fillable const newRemainingAmount = remainingFillableMakerAssetAmount || order.makerAssetAmount; + // if remaining amount is less than or equal to zero, do not add it + if (newRemainingAmount.lte(constants.ZERO_AMOUNT)) { + return acc; + } const newAcc = { orders: _.concat(orders, order), remainingFillableMakerAssetAmounts: _.concat(remainingFillableMakerAssetAmounts, newRemainingAmount), -- cgit v1.2.3 From 4e59be9afce49fca8b396c305d5271c8299c85c1 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Sat, 15 Sep 2018 14:08:19 +0200 Subject: Implement ProvidedOrderFetcher --- .../src/order_fetchers/provided_order_fetcher.ts | 41 ++++++++++++++++++++++ .../standard_relayer_api_order_fetcher.ts | 1 + packages/asset-buyer/src/utils/assert.ts | 7 +++- 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 packages/asset-buyer/src/order_fetchers/provided_order_fetcher.ts (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/order_fetchers/provided_order_fetcher.ts b/packages/asset-buyer/src/order_fetchers/provided_order_fetcher.ts new file mode 100644 index 000000000..29b03f1c4 --- /dev/null +++ b/packages/asset-buyer/src/order_fetchers/provided_order_fetcher.ts @@ -0,0 +1,41 @@ +import { schemas } from '@0xproject/json-schemas'; +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { constants } from '../constants'; +import { + AssetBuyerError, + OrderFetcher, + OrderFetcherRequest, + OrderFetcherResponse, + SignedOrderWithRemainingFillableMakerAssetAmount, +} from '../types'; +import { assert } from '../utils/assert'; +import { orderUtils } from '../utils/order_utils'; + +export class ProvidedOrderFetcher implements OrderFetcher { + public readonly providedOrders: SignedOrder[]; + /** + * Instantiates a new ProvidedOrderFetcher instance + * @param providedOrders An array of objects that conform to SignedOrder to fetch from. + * @return An instance of ProvidedOrderFetcher + */ + constructor(providedOrders: SignedOrder[]) { + assert.doesConformToSchema('providedOrders', providedOrders, schemas.signedOrdersSchema); + this.providedOrders = providedOrders; + } + /** + * Given an object that conforms to OrderFetcherRequest, return the corresponding OrderFetcherResponse that satisfies the request. + * @param orderFetchRequest An instance of OrderFetcherRequest. See type for more information. + * @return An instance of OrderFetcherResponse. See type for more information. + */ + public async fetchOrdersAsync(orderFetchRequest: OrderFetcherRequest): Promise { + assert.isValidOrderFetcherRequest('orderFetchRequest', orderFetchRequest); + const { makerAssetData, takerAssetData } = orderFetchRequest; + const orders = _.filter(this.providedOrders, order => { + return order.makerAssetData === makerAssetData && order.takerAssetData === takerAssetData; + }); + return { orders }; + } +} diff --git a/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts b/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts index bf177b93b..5688daf69 100644 --- a/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts +++ b/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts @@ -60,6 +60,7 @@ export class StandardRelayerAPIOrderFetcher implements OrderFetcher { * @return An instance of OrderFetcherResponse. See type for more information. */ public async fetchOrdersAsync(orderFetchRequest: OrderFetcherRequest): Promise { + assert.isValidOrderFetcherRequest('orderFetchRequest', orderFetchRequest); const { makerAssetData, takerAssetData, networkId } = orderFetchRequest; const orderbookRequest = { baseAssetData: makerAssetData, quoteAssetData: takerAssetData }; const requestOpts = { networkId }; diff --git a/packages/asset-buyer/src/utils/assert.ts b/packages/asset-buyer/src/utils/assert.ts index c4d611477..0085ca41e 100644 --- a/packages/asset-buyer/src/utils/assert.ts +++ b/packages/asset-buyer/src/utils/assert.ts @@ -2,7 +2,7 @@ import { assert as sharedAssert } from '@0xproject/assert'; import { schemas } from '@0xproject/json-schemas'; import * as _ from 'lodash'; -import { BuyQuote, OrderFetcher } from '../types'; +import { BuyQuote, OrderFetcher, OrderFetcherRequest } from '../types'; export const assert = { ...sharedAssert, @@ -20,4 +20,9 @@ export const assert = { isValidOrderFetcher(variableName: string, orderFetcher: OrderFetcher): void { sharedAssert.isFunction(`${variableName}.fetchOrdersAsync`, orderFetcher.fetchOrdersAsync); }, + isValidOrderFetcherRequest(variableName: string, orderFetcherRequest: OrderFetcherRequest): void { + sharedAssert.isHexString(`${variableName}.makerAssetData`, orderFetcherRequest.makerAssetData); + sharedAssert.isHexString(`${variableName}.takerAssetData`, orderFetcherRequest.takerAssetData); + sharedAssert.isNumber(`${variableName}.networkId`, orderFetcherRequest.networkId); + }, }; -- cgit v1.2.3 From dfe58e4975e0ad3edf08fdc6d3441e83171096a9 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Sat, 15 Sep 2018 14:08:34 +0200 Subject: Delete old test --- .../asset-buyer/test/forwarder_helper_impl_test.ts | 136 --------------------- 1 file changed, 136 deletions(-) delete mode 100644 packages/asset-buyer/test/forwarder_helper_impl_test.ts (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/test/forwarder_helper_impl_test.ts b/packages/asset-buyer/test/forwarder_helper_impl_test.ts deleted file mode 100644 index b7c014402..000000000 --- a/packages/asset-buyer/test/forwarder_helper_impl_test.ts +++ /dev/null @@ -1,136 +0,0 @@ -// import { testOrderFactory } from '@0xproject/order-utils/lib/test/utils/test_order_factory'; -// import { BigNumber } from '@0xproject/utils'; -// import * as chai from 'chai'; -// import * as _ from 'lodash'; -// import 'mocha'; - -// import { ForwarderHelperImpl, ForwarderHelperImplConfig } from '@0xproject/asset-buyer/src/asset_buyer'; -// import { ForwarderHelperError } from '../src/types'; - -// import { chaiSetup } from './utils/chai_setup'; - -// chaiSetup.configure(); -// const expect = chai.expect; - -// describe('ForwarderHelperImpl', () => { -// // rate: 2 takerAsset / makerAsset -// const testOrder1 = testOrderFactory.generateTestSignedOrder({ -// makerAssetAmount: new BigNumber(100), -// takerAssetAmount: new BigNumber(200), -// }); -// // rate: 1 takerAsset / makerAsset -// const testOrder2 = testOrderFactory.generateTestSignedOrder({ -// makerAssetAmount: new BigNumber(100), -// takerAssetAmount: new BigNumber(100), -// }); -// // rate: 3 takerAsset / makerAsset -// const testOrder3 = testOrderFactory.generateTestSignedOrder({ -// makerAssetAmount: new BigNumber(100), -// takerAssetAmount: new BigNumber(300), -// takerFee: new BigNumber(1), -// }); -// // rate: 3 WETH / ZRX -// const testFeeOrder1 = testOrderFactory.generateTestSignedOrder({ -// makerAssetAmount: new BigNumber(100), -// takerAssetAmount: new BigNumber(300), -// }); -// // rate: 2 WETH / ZRX -// const testFeeOrder2 = testOrderFactory.generateTestSignedOrder({ -// makerAssetAmount: new BigNumber(100), -// takerAssetAmount: new BigNumber(200), -// }); -// // rate: 1 WETH / ZRX -// const testFeeOrder3 = testOrderFactory.generateTestSignedOrder({ -// makerAssetAmount: new BigNumber(100), -// takerAssetAmount: new BigNumber(100), -// }); -// const inputForwarderHelperConfig: ForwarderHelperImplConfig = { -// orders: [testOrder1, testOrder2, testOrder3], -// feeOrders: [testFeeOrder1, testFeeOrder2, testFeeOrder3], -// remainingFillableMakerAssetAmounts: [new BigNumber(1), new BigNumber(2), new BigNumber(3)], -// remainingFillableFeeAmounts: [new BigNumber(4), new BigNumber(5), new BigNumber(6)], -// }; -// describe('#constructor', () => { -// const inputForwarderHelperConfigNoRemainingAmounts: ForwarderHelperImplConfig = { -// orders: [testOrder1, testOrder2, testOrder3], -// feeOrders: [testFeeOrder1, testFeeOrder2, testFeeOrder3], -// }; -// it('sorts orders', () => { -// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); -// expect(forwarderHelper.config.orders).deep.equals([testOrder2, testOrder1, testOrder3]); -// }); -// it('sorts fee orders', () => { -// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); -// expect(forwarderHelper.config.feeOrders).deep.equals([testFeeOrder3, testFeeOrder2, testFeeOrder1]); -// }); -// it('sorts remainingFillableMakerAssetAmounts', () => { -// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); -// expect(forwarderHelper.config.remainingFillableMakerAssetAmounts).to.be.not.undefined(); -// expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 0)).to.bignumber.equal( -// new BigNumber(2), -// ); -// expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 1)).to.bignumber.equal( -// new BigNumber(1), -// ); -// expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 2)).to.bignumber.equal( -// new BigNumber(3), -// ); -// }); -// it('sorts remainingFillableFeeAmounts', () => { -// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); -// expect(forwarderHelper.config.remainingFillableFeeAmounts).to.be.not.undefined(); -// expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 0)).to.bignumber.equal(new BigNumber(6)); -// expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 1)).to.bignumber.equal(new BigNumber(5)); -// expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 2)).to.bignumber.equal(new BigNumber(4)); -// }); -// it('remainingFillableMakerAssetAmounts is undefined if none provided', () => { -// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoRemainingAmounts); -// expect(forwarderHelper.config.remainingFillableMakerAssetAmounts).to.be.undefined(); -// }); -// it('remainingFillableFeeAmounts is undefined if none provided', () => { -// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoRemainingAmounts); -// expect(forwarderHelper.config.remainingFillableFeeAmounts).to.be.undefined(); -// }); -// }); -// describe('#getMarketBuyOrdersInfo', () => { -// it('throws if not enough makerAsset liquidity', () => { -// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); -// expect(() => { -// // request for 6 makerAsset units, because we have exactly 6 available we should throw because there is a built in slippage buffer -// forwarderHelper.getMarketBuyOrdersInfo({ -// makerAssetFillAmount: new BigNumber(6), -// }); -// }).to.throw(ForwarderHelperError.InsufficientMakerAssetLiquidity); -// }); -// it('throws if not enough ZRX liquidity', () => { -// const inputForwarderHelperConfigNoFees: ForwarderHelperImplConfig = { -// orders: [testOrder1, testOrder2, testOrder3], -// feeOrders: [], -// }; -// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoFees); -// expect(() => { -// // request for 4 makerAsset units, we need fees but no fee orders exist, show we should throw -// forwarderHelper.getMarketBuyOrdersInfo({ -// makerAssetFillAmount: new BigNumber(250), -// }); -// }).to.throw(ForwarderHelperError.InsufficientZrxLiquidity); -// }); -// it('passes the makerAssetFillAmount from the request to the info response', () => { -// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); -// const makerAssetFillAmount = new BigNumber(4); -// const info = forwarderHelper.getMarketBuyOrdersInfo({ -// makerAssetFillAmount, -// }); -// expect(info.makerAssetFillAmount).to.bignumber.equal(makerAssetFillAmount); -// }); -// it('passes the feePercentage from the request to the info response', () => { -// const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); -// const feePercentage = new BigNumber(0.2); -// const info = forwarderHelper.getMarketBuyOrdersInfo({ -// makerAssetFillAmount: new BigNumber(4), -// feePercentage, -// }); -// expect(info.feePercentage).to.bignumber.equal(feePercentage); -// }); -// }); -// }); -- cgit v1.2.3 From c692115cdcbc761c0c4074db79f8c127b92da7e0 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Sat, 15 Sep 2018 14:14:19 +0200 Subject: Fix lint errors --- packages/asset-buyer/src/asset_buyer.ts | 7 +++---- .../asset-buyer/src/order_fetchers/provided_order_fetcher.ts | 11 +---------- .../src/order_fetchers/standard_relayer_api_order_fetcher.ts | 5 +---- 3 files changed, 5 insertions(+), 18 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 9cbdd8df6..3b603d929 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -1,6 +1,5 @@ import { ContractWrappers } from '@0xproject/contract-wrappers'; -import { assetDataUtils, marketUtils } from '@0xproject/order-utils'; -import { SignedOrder } from '@0xproject/types'; +import { assetDataUtils } from '@0xproject/order-utils'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import { Provider } from 'ethereum-types'; @@ -29,7 +28,7 @@ export class AssetBuyer { public readonly orderFetcher: OrderFetcher; public readonly networkId: number; public readonly orderRefreshIntervalMs: number; - private _contractWrappers: ContractWrappers; + private readonly _contractWrappers: ContractWrappers; private _lastRefreshTimeIfExists?: number; private _currentOrdersAndFillableAmountsIfExists?: AssetBuyerOrdersAndFillableAmounts; /** @@ -188,7 +187,7 @@ export class AssetBuyer { const requests = [targetOrderFetcherRequest, feeOrderFetcherRequest]; // fetch orders and possible fillable amounts const [targetOrderFetcherResponse, feeOrderFetcherResponse] = await Promise.all( - _.map(requests, request => this.orderFetcher.fetchOrdersAsync(request)), + _.map(requests, async request => this.orderFetcher.fetchOrdersAsync(request)), ); // process the responses into one object const ordersAndFillableAmounts = await orderFetcherResponseProcessor.processAsync( diff --git a/packages/asset-buyer/src/order_fetchers/provided_order_fetcher.ts b/packages/asset-buyer/src/order_fetchers/provided_order_fetcher.ts index 29b03f1c4..7a7ffcdfe 100644 --- a/packages/asset-buyer/src/order_fetchers/provided_order_fetcher.ts +++ b/packages/asset-buyer/src/order_fetchers/provided_order_fetcher.ts @@ -1,18 +1,9 @@ import { schemas } from '@0xproject/json-schemas'; import { SignedOrder } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; -import { constants } from '../constants'; -import { - AssetBuyerError, - OrderFetcher, - OrderFetcherRequest, - OrderFetcherResponse, - SignedOrderWithRemainingFillableMakerAssetAmount, -} from '../types'; +import { OrderFetcher, OrderFetcherRequest, OrderFetcherResponse } from '../types'; import { assert } from '../utils/assert'; -import { orderUtils } from '../utils/order_utils'; export class ProvidedOrderFetcher implements OrderFetcher { public readonly providedOrders: SignedOrder[]; diff --git a/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts b/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts index 5688daf69..9fcd16ca1 100644 --- a/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts +++ b/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts @@ -1,9 +1,6 @@ import { APIOrder, HttpClient, OrderbookResponse } from '@0xproject/connect'; -import { SignedOrder } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; -import { constants } from '../constants'; import { AssetBuyerError, OrderFetcher, @@ -16,7 +13,7 @@ import { orderUtils } from '../utils/order_utils'; export class StandardRelayerAPIOrderFetcher implements OrderFetcher { public readonly apiUrl: string; - private _sraClient: HttpClient; + private readonly _sraClient: HttpClient; /** * Given an array of APIOrder objects from a standard relayer api, return an array * of SignedOrderWithRemainingFillableMakerAssetAmounts -- cgit v1.2.3 From d57619b5db8d4b5a8d31f095d8dfef04faef44c9 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Sat, 15 Sep 2018 14:16:08 +0200 Subject: Export the order fetchers --- packages/asset-buyer/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/index.ts b/packages/asset-buyer/src/index.ts index 67ad06084..efd3523fd 100644 --- a/packages/asset-buyer/src/index.ts +++ b/packages/asset-buyer/src/index.ts @@ -3,6 +3,8 @@ export { SignedOrder } from '@0xproject/types'; export { BigNumber } from '@0xproject/utils'; export { AssetBuyer } from './asset_buyer'; +export { ProvidedOrderFetcher } from './order_fetchers/provided_order_fetcher'; +export { StandardRelayerAPIOrderFetcher } from './order_fetchers/standard_relayer_api_order_fetcher'; export { AssetBuyerError, BuyQuote, -- cgit v1.2.3 From af40989f5f6b606172370d8e878c51ce2e7382eb Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Sat, 15 Sep 2018 14:47:44 +0200 Subject: Add factory method on AssetBuyer for provided orders --- packages/asset-buyer/src/asset_buyer.ts | 24 +++++++++++++++++++++++- packages/asset-buyer/src/utils/assert.ts | 16 ++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 3b603d929..a68658d22 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -1,5 +1,6 @@ import { ContractWrappers } from '@0xproject/contract-wrappers'; -import { assetDataUtils } from '@0xproject/order-utils'; +import { schemas } from '@0xproject/json-schemas'; +import { assetDataUtils, SignedOrder } from '@0xproject/order-utils'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import { Provider } from 'ethereum-types'; @@ -13,6 +14,7 @@ import { OrderFetcher, OrderFetcherResponse, } from './types'; +import { ProvidedOrderFetcher } from './order_fetchers/provided_order_fetcher'; import { assert } from './utils/assert'; import { buyQuoteCalculator } from './utils/buy_quote_calculator'; import { orderFetcherResponseProcessor } from './utils/order_fetcher_response_processor'; @@ -31,6 +33,26 @@ export class AssetBuyer { private readonly _contractWrappers: ContractWrappers; private _lastRefreshTimeIfExists?: number; private _currentOrdersAndFillableAmountsIfExists?: AssetBuyerOrdersAndFillableAmounts; + public static getAssetBuyerForProvidedOrders( + provider: Provider, + orders: SignedOrder[], + feeOrders: SignedOrder[] = [], + networkId: number = constants.MAINNET_NETWORK_ID, + orderRefreshIntervalMs: number = DEFAULT_ORDER_REFRESH_INTERVAL_MS, + ): AssetBuyer { + assert.isWeb3Provider('provider', provider); + assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); + assert.doesConformToSchema('feeOrders', feeOrders, schemas.signedOrdersSchema); + assert.isNumber('networkId', networkId); + assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs); + assert.areValidProvidedOrders('orders', orders); + assert.areValidProvidedOrders('feeOrders', feeOrders); + assert.assert(orders.length !== 0, `Expected orders to contain at least one order`); + const assetData = orders[0].makerAssetData; + const orderFetcher = new ProvidedOrderFetcher(_.concat(orders, feeOrders)); + const assetBuyer = new AssetBuyer(provider, assetData, orderFetcher, networkId, orderRefreshIntervalMs); + return assetBuyer; + } /** * Instantiates a new AssetBuyer instance * @param provider The Provider instance you would like to use for interacting with the Ethereum network. diff --git a/packages/asset-buyer/src/utils/assert.ts b/packages/asset-buyer/src/utils/assert.ts index 0085ca41e..edc90608c 100644 --- a/packages/asset-buyer/src/utils/assert.ts +++ b/packages/asset-buyer/src/utils/assert.ts @@ -1,5 +1,6 @@ import { assert as sharedAssert } from '@0xproject/assert'; import { schemas } from '@0xproject/json-schemas'; +import { SignedOrder } from '@0xproject/types'; import * as _ from 'lodash'; import { BuyQuote, OrderFetcher, OrderFetcherRequest } from '../types'; @@ -25,4 +26,19 @@ export const assert = { sharedAssert.isHexString(`${variableName}.takerAssetData`, orderFetcherRequest.takerAssetData); sharedAssert.isNumber(`${variableName}.networkId`, orderFetcherRequest.networkId); }, + areValidProvidedOrders(variableName: string, orders: SignedOrder[]): void { + if (orders.length === 0) { + return; + } + const makerAssetData = orders[0].makerAssetData; + const takerAssetData = orders[0].takerAssetData; + const filteredOrders = _.filter( + orders, + order => order.makerAssetData === makerAssetData && order.takerAssetData === takerAssetData, + ); + sharedAssert.assert( + orders.length === filteredOrders.length, + `Expected all orders in ${variableName} to have the same makerAssetData and takerAssetData.`, + ); + }, }; -- cgit v1.2.3 From 9ec2b5a2d53bad2820b297f90bd16528f87537eb Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Sat, 15 Sep 2018 14:53:04 +0200 Subject: Add factory method on AssetBuyer for specific token address --- packages/asset-buyer/src/asset_buyer.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index a68658d22..bb3d6d77c 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -7,6 +7,8 @@ import { Provider } from 'ethereum-types'; import * as _ from 'lodash'; import { constants } from './constants'; +import { ProvidedOrderFetcher } from './order_fetchers/provided_order_fetcher'; +import { StandardRelayerAPIOrderFetcher } from './order_fetchers/standard_relayer_api_order_fetcher'; import { AssetBuyerError, AssetBuyerOrdersAndFillableAmounts, @@ -14,7 +16,7 @@ import { OrderFetcher, OrderFetcherResponse, } from './types'; -import { ProvidedOrderFetcher } from './order_fetchers/provided_order_fetcher'; + import { assert } from './utils/assert'; import { buyQuoteCalculator } from './utils/buy_quote_calculator'; import { orderFetcherResponseProcessor } from './utils/order_fetcher_response_processor'; @@ -53,6 +55,23 @@ export class AssetBuyer { const assetBuyer = new AssetBuyer(provider, assetData, orderFetcher, networkId, orderRefreshIntervalMs); return assetBuyer; } + public static getAssetBuyerForERC20TokenAddress( + provider: Provider, + tokenAddress: string, + sraApiUrl: string, + networkId: number = constants.MAINNET_NETWORK_ID, + orderRefreshIntervalMs: number = DEFAULT_ORDER_REFRESH_INTERVAL_MS, + ): AssetBuyer { + assert.isWeb3Provider('provider', provider); + assert.isETHAddressHex('tokenAddress', tokenAddress); + assert.isWebUri('sraApiUrl', sraApiUrl); + assert.isNumber('networkId', networkId); + assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs); + const assetData = assetDataUtils.encodeERC20AssetData(tokenAddress); + const orderFetcher = new StandardRelayerAPIOrderFetcher(sraApiUrl); + const assetBuyer = new AssetBuyer(provider, assetData, orderFetcher, networkId, orderRefreshIntervalMs); + return assetBuyer; + } /** * Instantiates a new AssetBuyer instance * @param provider The Provider instance you would like to use for interacting with the Ethereum network. -- cgit v1.2.3 From a22d2dc7ee3a1f354074009f3ff117b14050f5c6 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Sat, 15 Sep 2018 15:05:27 +0200 Subject: Add factory method for specific assetData to buy and add comments --- packages/asset-buyer/src/asset_buyer.ts | 55 +++++++++++++++++++++- .../standard_relayer_api_order_fetcher.ts | 2 +- 2 files changed, 54 insertions(+), 3 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index bb3d6d77c..fa1118e18 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -35,6 +35,16 @@ export class AssetBuyer { private readonly _contractWrappers: ContractWrappers; private _lastRefreshTimeIfExists?: number; private _currentOrdersAndFillableAmountsIfExists?: AssetBuyerOrdersAndFillableAmounts; + /** + * Instantiates a new AssetBuyer instance + * @param provider The Provider instance you would like to use for interacting with the Ethereum network. + * @param orders A non-empty array of objects that conform to SignedOrder. All orders must have the same makerAssetData and takerAssetData (WETH). + * @param feeOrders A array of objects that conform to SignedOrder. All orders must have the same makerAssetData (ZRX) and takerAssetData (WETH). Defaults to an empty array. + * @param networkId The ethereum network id. Defaults to 1 (mainnet). + * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. + * Defaults to 10000ms (10s). + * @return An instance of AssetBuyer + */ public static getAssetBuyerForProvidedOrders( provider: Provider, orders: SignedOrder[], @@ -55,6 +65,42 @@ export class AssetBuyer { const assetBuyer = new AssetBuyer(provider, assetData, orderFetcher, networkId, orderRefreshIntervalMs); return assetBuyer; } + /** + * Instantiates a new AssetBuyer instance + * @param provider The Provider instance you would like to use for interacting with the Ethereum network. + * @param assetData The assetData that identifies the desired asset to buy. + * @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from. + * @param networkId The ethereum network id. Defaults to 1 (mainnet). + * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. + * Defaults to 10000ms (10s). + * @return An instance of AssetBuyer + */ + public static getAssetBuyerForAssetData( + provider: Provider, + assetData: string, + sraApiUrl: string, + networkId: number = constants.MAINNET_NETWORK_ID, + orderRefreshIntervalMs: number = DEFAULT_ORDER_REFRESH_INTERVAL_MS, + ): AssetBuyer { + assert.isWeb3Provider('provider', provider); + assert.isHexString('assetData', assetData); + assert.isWebUri('sraApiUrl', sraApiUrl); + assert.isNumber('networkId', networkId); + assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs); + const orderFetcher = new StandardRelayerAPIOrderFetcher(sraApiUrl); + const assetBuyer = new AssetBuyer(provider, assetData, orderFetcher, networkId, orderRefreshIntervalMs); + return assetBuyer; + } + /** + * Instantiates a new AssetBuyer instance + * @param provider The Provider instance you would like to use for interacting with the Ethereum network. + * @param tokenAddress The ERC20 token address that identifies the desired asset to buy. + * @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from. + * @param networkId The ethereum network id. Defaults to 1 (mainnet). + * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. + * Defaults to 10000ms (10s). + * @return An instance of AssetBuyer + */ public static getAssetBuyerForERC20TokenAddress( provider: Provider, tokenAddress: string, @@ -68,8 +114,13 @@ export class AssetBuyer { assert.isNumber('networkId', networkId); assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs); const assetData = assetDataUtils.encodeERC20AssetData(tokenAddress); - const orderFetcher = new StandardRelayerAPIOrderFetcher(sraApiUrl); - const assetBuyer = new AssetBuyer(provider, assetData, orderFetcher, networkId, orderRefreshIntervalMs); + const assetBuyer = AssetBuyer.getAssetBuyerForAssetData( + provider, + assetData, + sraApiUrl, + networkId, + orderRefreshIntervalMs, + ); return assetBuyer; } /** diff --git a/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts b/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts index 9fcd16ca1..3b082b68d 100644 --- a/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts +++ b/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts @@ -43,7 +43,7 @@ export class StandardRelayerAPIOrderFetcher implements OrderFetcher { } /** * Instantiates a new StandardRelayerAPIOrderFetcher instance - * @param apiUrl The relayer API base HTTP url you would like to interact with. + * @param apiUrl The standard relayer API base HTTP url you would like to source orders from. * @return An instance of StandardRelayerAPIOrderFetcher */ constructor(apiUrl: string) { -- cgit v1.2.3 From 65f709d50ae559ec1b17f3c44f3d87e5d3cc4da7 Mon Sep 17 00:00:00 2001 From: fragosti Date: Tue, 18 Sep 2018 16:07:08 +0200 Subject: Move all constants to own file --- packages/asset-buyer/src/asset_buyer.ts | 19 +++++++------------ packages/asset-buyer/src/constants.ts | 4 ++++ 2 files changed, 11 insertions(+), 12 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index fa1118e18..76b96427e 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -21,11 +21,6 @@ import { assert } from './utils/assert'; import { buyQuoteCalculator } from './utils/buy_quote_calculator'; import { orderFetcherResponseProcessor } from './utils/order_fetcher_response_processor'; -const SLIPPAGE_PERCENTAGE = 0.2; // 20% slippage protection, possibly move this into request interface -const DEFAULT_ORDER_REFRESH_INTERVAL_MS = 10000; // 10 seconds -const DEFAULT_FEE_PERCENTAGE = 0; -const ETHER_TOKEN_DECIMALS = 18; - export class AssetBuyer { public readonly provider: Provider; public readonly assetData: string; @@ -50,7 +45,7 @@ export class AssetBuyer { orders: SignedOrder[], feeOrders: SignedOrder[] = [], networkId: number = constants.MAINNET_NETWORK_ID, - orderRefreshIntervalMs: number = DEFAULT_ORDER_REFRESH_INTERVAL_MS, + orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS, ): AssetBuyer { assert.isWeb3Provider('provider', provider); assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); @@ -80,7 +75,7 @@ export class AssetBuyer { assetData: string, sraApiUrl: string, networkId: number = constants.MAINNET_NETWORK_ID, - orderRefreshIntervalMs: number = DEFAULT_ORDER_REFRESH_INTERVAL_MS, + orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS, ): AssetBuyer { assert.isWeb3Provider('provider', provider); assert.isHexString('assetData', assetData); @@ -106,7 +101,7 @@ export class AssetBuyer { tokenAddress: string, sraApiUrl: string, networkId: number = constants.MAINNET_NETWORK_ID, - orderRefreshIntervalMs: number = DEFAULT_ORDER_REFRESH_INTERVAL_MS, + orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS, ): AssetBuyer { assert.isWeb3Provider('provider', provider); assert.isETHAddressHex('tokenAddress', tokenAddress); @@ -138,7 +133,7 @@ export class AssetBuyer { assetData: string, orderFetcher: OrderFetcher, networkId: number = constants.MAINNET_NETWORK_ID, - orderRefreshIntervalMs: number = DEFAULT_ORDER_REFRESH_INTERVAL_MS, + orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS, ) { assert.isWeb3Provider('provider', provider); assert.isString('assetData', assetData); @@ -165,7 +160,7 @@ export class AssetBuyer { */ public async getBuyQuoteAsync( assetBuyAmount: BigNumber, - feePercentage: number = DEFAULT_FEE_PERCENTAGE, + feePercentage: number = constants.DEFAULT_FEE_PERCENTAGE, forceOrderRefresh: boolean = false, ): Promise { assert.isBigNumber('assetBuyAmount', assetBuyAmount); @@ -196,7 +191,7 @@ export class AssetBuyer { ordersAndFillableAmounts, assetBuyAmount, feePercentage, - SLIPPAGE_PERCENTAGE, + constants.DEFAULT_SLIPPAGE_PERCENTAGE, ); return buyQuote; } @@ -244,7 +239,7 @@ export class AssetBuyer { // TODO: critical // update the forwarder wrapper to take in feePercentage as a number instead of a BigNumber, verify with Amir that this is being done correctly const feePercentageBigNumber = !_.isUndefined(feePercentage) - ? Web3Wrapper.toBaseUnitAmount(new BigNumber(1), ETHER_TOKEN_DECIMALS).mul(feePercentage) + ? Web3Wrapper.toBaseUnitAmount(new BigNumber(1), constants.ETHER_TOKEN_DECIMALS).mul(feePercentage) : constants.ZERO_AMOUNT; const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync( orders, diff --git a/packages/asset-buyer/src/constants.ts b/packages/asset-buyer/src/constants.ts index 5785e705b..e20bcc6ed 100644 --- a/packages/asset-buyer/src/constants.ts +++ b/packages/asset-buyer/src/constants.ts @@ -4,4 +4,8 @@ export const constants = { ZERO_AMOUNT: new BigNumber(0), NULL_ADDRESS: '0x0000000000000000000000000000000000000000', MAINNET_NETWORK_ID: 1, + DEFAULT_SLIPPAGE_PERCENTAGE: 0.2, // 20% slippage protection + DEFAULT_ORDER_REFRESH_INTERVAL_MS: 10000, // 10 seconds + DEFAULT_FEE_PERCENTAGE: 0, + ETHER_TOKEN_DECIMALS: 18, }; -- cgit v1.2.3 From 38afd108f8e1dbb56655260a415050f44754c630 Mon Sep 17 00:00:00 2001 From: fragosti Date: Tue, 18 Sep 2018 16:07:47 +0200 Subject: Fix order expired bug --- packages/asset-buyer/src/utils/order_fetcher_response_processor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts b/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts index f1116a80f..f700cadf9 100644 --- a/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts +++ b/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts @@ -90,7 +90,7 @@ function filterOutExpiredAndNonOpenOrders( orders: SignedOrderWithRemainingFillableMakerAssetAmount[], ): SignedOrderWithRemainingFillableMakerAssetAmount[] { const result = _.filter(orders, order => { - return orderUtils.isOpenOrder(order) && orderUtils.isOrderExpired(order); + return orderUtils.isOpenOrder(order) && !orderUtils.isOrderExpired(order); }); return result; } -- cgit v1.2.3 From b947609ff18f518ac36435083a97604b983a3081 Mon Sep 17 00:00:00 2001 From: fragosti Date: Tue, 18 Sep 2018 16:26:50 +0200 Subject: Restrict orders field of AssetBuyerOrdersAndFillableAmounts to SignedOrder for consistency --- packages/asset-buyer/src/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index 0da30f48d..75773f0e8 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -65,8 +65,8 @@ export enum AssetBuyerError { } export interface AssetBuyerOrdersAndFillableAmounts { - orders: SignedOrderWithRemainingFillableMakerAssetAmount[]; - feeOrders: SignedOrderWithRemainingFillableMakerAssetAmount[]; + orders: SignedOrder[]; + feeOrders: SignedOrder[]; remainingFillableMakerAssetAmounts: BigNumber[]; remainingFillableFeeAmounts: BigNumber[]; } -- cgit v1.2.3 From 0003666050c4991623cb36ec994d536e4703b4fe Mon Sep 17 00:00:00 2001 From: fragosti Date: Tue, 18 Sep 2018 16:42:52 +0200 Subject: make the slippage percentage customizable by integrator --- packages/asset-buyer/src/asset_buyer.ts | 10 +++++----- packages/asset-buyer/src/constants.ts | 11 +++++++++-- packages/asset-buyer/src/types.ts | 6 ++++++ 3 files changed, 20 insertions(+), 7 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 76b96427e..a2c0f37c9 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -13,6 +13,7 @@ import { AssetBuyerError, AssetBuyerOrdersAndFillableAmounts, BuyQuote, + BuyQuoteRequestOpts, OrderFetcher, OrderFetcherResponse, } from './types'; @@ -160,12 +161,13 @@ export class AssetBuyer { */ public async getBuyQuoteAsync( assetBuyAmount: BigNumber, - feePercentage: number = constants.DEFAULT_FEE_PERCENTAGE, - forceOrderRefresh: boolean = false, + options: Partial, ): Promise { + const { feePercentage, forceOrderRefresh, slippagePercentage } = { ...options, ...constants.DEFAULT_BUY_QUOTE_REQUEST_OPTS }; assert.isBigNumber('assetBuyAmount', assetBuyAmount); assert.isNumber('feePercentage', feePercentage); assert.isBoolean('forceOrderRefresh', forceOrderRefresh); + assert.isNumber('feePercentage', slippagePercentage); // we should refresh if: // we do not have any orders OR // we are forced to OR @@ -185,13 +187,11 @@ export class AssetBuyer { ordersAndFillableAmounts = this ._currentOrdersAndFillableAmountsIfExists as AssetBuyerOrdersAndFillableAmounts; } - // TODO: optimization - // make the slippage percentage customizable by integrator const buyQuote = buyQuoteCalculator.calculate( ordersAndFillableAmounts, assetBuyAmount, feePercentage, - constants.DEFAULT_SLIPPAGE_PERCENTAGE, + slippagePercentage, ); return buyQuote; } diff --git a/packages/asset-buyer/src/constants.ts b/packages/asset-buyer/src/constants.ts index e20bcc6ed..53fe89160 100644 --- a/packages/asset-buyer/src/constants.ts +++ b/packages/asset-buyer/src/constants.ts @@ -1,11 +1,18 @@ import { BigNumber } from '@0xproject/utils'; +import { BuyQuoteRequestOpts } from './types'; + +const DEFAULT_BUY_QUOTE_REQUEST_OPTS: BuyQuoteRequestOpts = { + feePercentage: 0, + forceOrderRefresh: false, + slippagePercentage: 0.2, // 20% slippage protection +}; + export const constants = { ZERO_AMOUNT: new BigNumber(0), NULL_ADDRESS: '0x0000000000000000000000000000000000000000', MAINNET_NETWORK_ID: 1, - DEFAULT_SLIPPAGE_PERCENTAGE: 0.2, // 20% slippage protection DEFAULT_ORDER_REFRESH_INTERVAL_MS: 10000, // 10 seconds - DEFAULT_FEE_PERCENTAGE: 0, ETHER_TOKEN_DECIMALS: 18, + DEFAULT_BUY_QUOTE_REQUEST_OPTS, }; diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index 75773f0e8..fb4aecd77 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -52,6 +52,12 @@ export interface BuyQuote { feePercentage?: number; } +export interface BuyQuoteRequestOpts { + feePercentage: number; + forceOrderRefresh: boolean; + slippagePercentage: number; +} + /** * Possible errors thrown by an AssetBuyer instance or associated static methods. */ -- cgit v1.2.3 From 93f7e33f6a1d0a056198a9f22bcb10ef3e4f4f25 Mon Sep 17 00:00:00 2001 From: fragosti Date: Wed, 19 Sep 2018 00:57:05 +0200 Subject: Add isValidPercentage assert --- packages/asset-buyer/src/asset_buyer.ts | 20 +++++++------------- packages/asset-buyer/src/utils/assert.ts | 7 +++++++ 2 files changed, 14 insertions(+), 13 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index a2c0f37c9..7becb8285 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -159,15 +159,14 @@ export class AssetBuyer { * the next orderRefreshIntervalMs. Defaults to false. * @return An object that conforms to BuyQuote that satisfies the request. See type definition for more information. */ - public async getBuyQuoteAsync( - assetBuyAmount: BigNumber, - options: Partial, - ): Promise { - const { feePercentage, forceOrderRefresh, slippagePercentage } = { ...options, ...constants.DEFAULT_BUY_QUOTE_REQUEST_OPTS }; + public async getBuyQuoteAsync(assetBuyAmount: BigNumber, options: Partial): Promise { + const { feePercentage, forceOrderRefresh, slippagePercentage } = { + ...options, + ...constants.DEFAULT_BUY_QUOTE_REQUEST_OPTS, + }; assert.isBigNumber('assetBuyAmount', assetBuyAmount); - assert.isNumber('feePercentage', feePercentage); + assert.isValidPercentage('feePercentage', feePercentage); assert.isBoolean('forceOrderRefresh', forceOrderRefresh); - assert.isNumber('feePercentage', slippagePercentage); // we should refresh if: // we do not have any orders OR // we are forced to OR @@ -236,18 +235,13 @@ export class AssetBuyer { const desiredRate = rate || maxRate; // calculate how much eth is required to buy assetBuyAmount at the desired rate const ethAmount = assetBuyAmount.dividedToIntegerBy(desiredRate); - // TODO: critical - // update the forwarder wrapper to take in feePercentage as a number instead of a BigNumber, verify with Amir that this is being done correctly - const feePercentageBigNumber = !_.isUndefined(feePercentage) - ? Web3Wrapper.toBaseUnitAmount(new BigNumber(1), constants.ETHER_TOKEN_DECIMALS).mul(feePercentage) - : constants.ZERO_AMOUNT; const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync( orders, assetBuyAmount, finalTakerAddress, ethAmount, feeOrders, - feePercentageBigNumber, + feePercentage, feeRecipient, ); return txHash; diff --git a/packages/asset-buyer/src/utils/assert.ts b/packages/asset-buyer/src/utils/assert.ts index edc90608c..745ba726f 100644 --- a/packages/asset-buyer/src/utils/assert.ts +++ b/packages/asset-buyer/src/utils/assert.ts @@ -41,4 +41,11 @@ export const assert = { `Expected all orders in ${variableName} to have the same makerAssetData and takerAssetData.`, ); }, + isValidPercentage(variableName: string, percentage: number): void { + assert.isNumber(variableName, percentage); + assert.assert( + percentage >= 0 && percentage <= 1, + `Expected ${variableName} to be between 0 and 1, but is ${percentage}`, + ); + }, }; -- cgit v1.2.3 From 60e2dfdbda1e089ee4d4419243167eaeb769ff6a Mon Sep 17 00:00:00 2001 From: fragosti Date: Wed, 19 Sep 2018 15:58:30 +0200 Subject: Calculate min and max rates in buy quote --- .../asset-buyer/src/utils/buy_quote_calculator.ts | 62 +++++++++++++++------- 1 file changed, 43 insertions(+), 19 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/utils/buy_quote_calculator.ts b/packages/asset-buyer/src/utils/buy_quote_calculator.ts index e05ab1e55..52cecf8ad 100644 --- a/packages/asset-buyer/src/utils/buy_quote_calculator.ts +++ b/packages/asset-buyer/src/utils/buy_quote_calculator.ts @@ -1,5 +1,6 @@ import { marketUtils } from '@0xproject/order-utils'; import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; import { constants } from '../constants'; import { AssetBuyerError, AssetBuyerOrdersAndFillableAmounts, BuyQuote } from '../types'; @@ -18,41 +19,64 @@ export const buyQuoteCalculator = { remainingFillableFeeAmounts, } = ordersAndFillableAmounts; const slippageBufferAmount = assetBuyAmount.mul(slippagePercentage).round(); - const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( - orders, - assetBuyAmount, - { - remainingFillableMakerAssetAmounts, - slippageBufferAmount, - }, - ); + const { + resultOrders, + remainingFillAmount, + ordersRemainingFillableMakerAssetAmounts, + } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(orders, assetBuyAmount, { + remainingFillableMakerAssetAmounts, + slippageBufferAmount, + }); if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) { throw new Error(AssetBuyerError.InsufficientAssetLiquidity); } // TODO: optimization // update this logic to find the minimum amount of feeOrders to cover the worst case as opposed to // finding order that cover all fees, this will help with estimating ETH and minimizing gas usage - const { resultFeeOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( - resultOrders, - feeOrders, - { - remainingFillableMakerAssetAmounts, - remainingFillableFeeAmounts, - }, - ); + const { + resultFeeOrders, + remainingFeeAmount, + feeOrdersRemainingFillableMakerAssetAmounts, + } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(resultOrders, feeOrders, { + remainingFillableMakerAssetAmounts, + remainingFillableFeeAmounts, + }); if (remainingFeeAmount.gt(constants.ZERO_AMOUNT)) { throw new Error(AssetBuyerError.InsufficientZrxLiquidity); } const assetData = orders[0].makerAssetData; - // TODO: critical + // calculate minRate and maxRate by calculating min and max eth usage and then dividing into // assetBuyAmount to get assetData / WETH, needs to take into account feePercentage as well + // minEthAmount = (sum(takerAssetAmount[i]) until sum(makerAssetAmount[i]) >= assetBuyAmount ) * (1 + feePercentage) + // maxEthAmount = (sum(takerAssetAmount[i]) until i == orders.length) * (1 + feePercentage) + const allOrders = _.concat(resultOrders, resultFeeOrders); + const allRemainingAmounts = _.concat( + ordersRemainingFillableMakerAssetAmounts, + feeOrdersRemainingFillableMakerAssetAmounts, + ); + let minEthAmount = constants.ZERO_AMOUNT; + let maxEthAmount = constants.ZERO_AMOUNT; + let cumulativeMakerAmount = constants.ZERO_AMOUNT; + _.forEach(allOrders, (order, index) => { + const remainingFillableMakerAssetAmount = allRemainingAmounts[index]; + const orderRate = order.takerAssetAmount.div(order.makerAssetAmount); + const claimableTakerAssetAmount = orderRate.mul(remainingFillableMakerAssetAmount); + // taker asset is always assumed to be WETH + maxEthAmount = maxEthAmount.plus(claimableTakerAssetAmount); + if (cumulativeMakerAmount.lessThan(assetBuyAmount)) { + minEthAmount = minEthAmount.plus(claimableTakerAssetAmount); + } + cumulativeMakerAmount = cumulativeMakerAmount.plus(remainingFillableMakerAssetAmount); + }); + const feeAdjustedMinRate = minEthAmount.mul(feePercentage + 1).div(assetBuyAmount); + const feeAdjustedMaxRate = minEthAmount.mul(feePercentage + 1).div(assetBuyAmount); return { assetData, orders: resultOrders, feeOrders: resultFeeOrders, - minRate: constants.ZERO_AMOUNT, - maxRate: constants.ZERO_AMOUNT, + minRate: feeAdjustedMinRate, + maxRate: feeAdjustedMaxRate, assetBuyAmount, feePercentage, }; -- cgit v1.2.3 From 14345ab24e0cd8db7bd2804b169acfaf18040ea4 Mon Sep 17 00:00:00 2001 From: fragosti Date: Wed, 19 Sep 2018 16:02:52 +0200 Subject: Rename forceOrderRefresh to shouldForceOrderRefresh --- packages/asset-buyer/src/asset_buyer.ts | 6 +++--- packages/asset-buyer/src/constants.ts | 2 +- packages/asset-buyer/src/types.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 7becb8285..81b46d89a 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -160,20 +160,20 @@ export class AssetBuyer { * @return An object that conforms to BuyQuote that satisfies the request. See type definition for more information. */ public async getBuyQuoteAsync(assetBuyAmount: BigNumber, options: Partial): Promise { - const { feePercentage, forceOrderRefresh, slippagePercentage } = { + const { feePercentage, shouldForceOrderRefresh, slippagePercentage } = { ...options, ...constants.DEFAULT_BUY_QUOTE_REQUEST_OPTS, }; assert.isBigNumber('assetBuyAmount', assetBuyAmount); assert.isValidPercentage('feePercentage', feePercentage); - assert.isBoolean('forceOrderRefresh', forceOrderRefresh); + assert.isBoolean('shouldForceOrderRefresh', shouldForceOrderRefresh); // we should refresh if: // we do not have any orders OR // we are forced to OR // we have some last refresh time AND that time was sufficiently long ago const shouldRefresh = _.isUndefined(this._currentOrdersAndFillableAmountsIfExists) || - forceOrderRefresh || + shouldForceOrderRefresh || (!_.isUndefined(this._lastRefreshTimeIfExists) && this._lastRefreshTimeIfExists + this.orderRefreshIntervalMs < Date.now()); let ordersAndFillableAmounts: AssetBuyerOrdersAndFillableAmounts; diff --git a/packages/asset-buyer/src/constants.ts b/packages/asset-buyer/src/constants.ts index 53fe89160..701832fd4 100644 --- a/packages/asset-buyer/src/constants.ts +++ b/packages/asset-buyer/src/constants.ts @@ -4,7 +4,7 @@ import { BuyQuoteRequestOpts } from './types'; const DEFAULT_BUY_QUOTE_REQUEST_OPTS: BuyQuoteRequestOpts = { feePercentage: 0, - forceOrderRefresh: false, + shouldForceOrderRefresh: false, slippagePercentage: 0.2, // 20% slippage protection }; diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index fb4aecd77..339bce52c 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -54,7 +54,7 @@ export interface BuyQuote { export interface BuyQuoteRequestOpts { feePercentage: number; - forceOrderRefresh: boolean; + shouldForceOrderRefresh: boolean; slippagePercentage: number; } -- cgit v1.2.3 From a44f77a83811146ad68ee6f56bcda94e56d6a634 Mon Sep 17 00:00:00 2001 From: fragosti Date: Wed, 19 Sep 2018 18:38:50 +0200 Subject: Implement StandardRelayerAPIAssetBuyerManager --- packages/asset-buyer/src/asset_buyer.ts | 18 +-- packages/asset-buyer/src/constants.ts | 1 + packages/asset-buyer/src/index.ts | 2 + .../standard_relayer_api_asset_buyer_manager.ts | 125 +++++++++++++++++++++ packages/asset-buyer/src/types.ts | 7 ++ packages/asset-buyer/src/utils/asset_data_utils.ts | 26 +++++ .../asset-buyer/src/utils/buy_quote_calculator.ts | 1 + 7 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts create mode 100644 packages/asset-buyer/src/utils/asset_data_utils.ts (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 81b46d89a..5f57e3d7f 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -1,6 +1,6 @@ import { ContractWrappers } from '@0xproject/contract-wrappers'; import { schemas } from '@0xproject/json-schemas'; -import { assetDataUtils, SignedOrder } from '@0xproject/order-utils'; +import { SignedOrder } from '@0xproject/order-utils'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import { Provider } from 'ethereum-types'; @@ -19,6 +19,7 @@ import { } from './types'; import { assert } from './utils/assert'; +import { assetDataUtils } from './utils/asset_data_utils'; import { buyQuoteCalculator } from './utils/buy_quote_calculator'; import { orderFetcherResponseProcessor } from './utils/order_fetcher_response_processor'; @@ -284,24 +285,13 @@ export class AssetBuyer { * Will throw if WETH does not exist for the current network. */ private _getEtherTokenAssetData(): string { - const etherTokenAddressIfExists = this._contractWrappers.etherToken.getContractAddressIfExists(); - if (_.isUndefined(etherTokenAddressIfExists)) { - throw new Error(AssetBuyerError.NoEtherTokenContractFound); - } - const etherTokenAssetData = assetDataUtils.encodeERC20AssetData(etherTokenAddressIfExists); - return etherTokenAssetData; + return assetDataUtils.getEtherTokenAssetData(this._contractWrappers); } /** * Get the assetData that represents the ZRX token. * Will throw if ZRX does not exist for the current network. */ private _getZrxTokenAssetData(): string { - let zrxTokenAssetData: string; - try { - zrxTokenAssetData = this._contractWrappers.exchange.getZRXAssetData(); - } catch (err) { - throw new Error(AssetBuyerError.NoZrxTokenContractFound); - } - return zrxTokenAssetData; + return assetDataUtils.getZrxTokenAssetData(this._contractWrappers); } } diff --git a/packages/asset-buyer/src/constants.ts b/packages/asset-buyer/src/constants.ts index 701832fd4..0ebe0f8e2 100644 --- a/packages/asset-buyer/src/constants.ts +++ b/packages/asset-buyer/src/constants.ts @@ -15,4 +15,5 @@ export const constants = { DEFAULT_ORDER_REFRESH_INTERVAL_MS: 10000, // 10 seconds ETHER_TOKEN_DECIMALS: 18, DEFAULT_BUY_QUOTE_REQUEST_OPTS, + MAX_PER_PAGE: 10000, }; diff --git a/packages/asset-buyer/src/index.ts b/packages/asset-buyer/src/index.ts index efd3523fd..2156b7e96 100644 --- a/packages/asset-buyer/src/index.ts +++ b/packages/asset-buyer/src/index.ts @@ -5,6 +5,7 @@ export { BigNumber } from '@0xproject/utils'; export { AssetBuyer } from './asset_buyer'; export { ProvidedOrderFetcher } from './order_fetchers/provided_order_fetcher'; export { StandardRelayerAPIOrderFetcher } from './order_fetchers/standard_relayer_api_order_fetcher'; +export { StandardRelayerAPIAssetBuyerManager } from './standard_relayer_api_asset_buyer_manager'; export { AssetBuyerError, BuyQuote, @@ -12,4 +13,5 @@ export { OrderFetcherRequest, OrderFetcherResponse, SignedOrderWithRemainingFillableMakerAssetAmount, + StandardRelayerApiAssetBuyerManagerError, } from './types'; diff --git a/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts b/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts new file mode 100644 index 000000000..6cd96fab2 --- /dev/null +++ b/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts @@ -0,0 +1,125 @@ +import { HttpClient } from '@0xproject/connect'; +import { ContractWrappers } from '@0xproject/contract-wrappers'; +import { ObjectMap } from '@0xproject/types'; +import { Provider } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { AssetBuyer } from './asset_buyer'; +import { constants } from './constants'; +import { assert } from './utils/assert'; +import { assetDataUtils } from './utils/asset_data_utils'; + +import { OrderFetcher, StandardRelayerApiAssetBuyerManagerError } from './types'; + +export class StandardRelayerAPIAssetBuyerManager { + // Map of assetData to AssetBuyer for that assetData + public readonly assetBuyerMap: ObjectMap; + /** + * Returns an array of all assetDatas available at the provided sraApiUrl + * @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from. + * @param pairedWithAssetData Optional filter argument to return assetDatas that only pair with this assetData value. + * + * @return An array of all assetDatas available at the provider sraApiUrl + */ + public static async getAllAvailableAssetDatasAsync( + sraApiUrl: string, + pairedWithAssetData?: string, + ): Promise { + const client = new HttpClient(sraApiUrl); + const params = { + assetDataA: pairedWithAssetData, + perPage: constants.MAX_PER_PAGE, + }; + const assetPairsResponse = await client.getAssetPairsAsync(params); + return _.uniq(_.map(assetPairsResponse.records, pairsItem => pairsItem.assetDataB.assetData)); + } + /** + * Instantiates a new StandardRelayerAPIAssetBuyerManager instance with all available assetDatas at the provided sraApiUrl + * @param provider The Provider instance you would like to use for interacting with the Ethereum network. + * @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from. + * @param orderFetcher An object that conforms to OrderFetcher, see type for definition. + * @param networkId The ethereum network id. Defaults to 1 (mainnet). + * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. + * Defaults to 10000ms (10s). + * @return An promise of an instance of StandardRelayerAPIAssetBuyerManager + */ + public static async getAssetBuyerManagerWithAllAvailableAssetDatasAsync( + provider: Provider, + sraApiUrl: string, + orderFetcher: OrderFetcher, + networkId: number = constants.MAINNET_NETWORK_ID, + orderRefreshIntervalMs?: number, + ): Promise { + const contractWrappers = new ContractWrappers(provider, { networkId }); + const etherTokenAssetData = assetDataUtils.getEtherTokenAssetData(contractWrappers); + const assetDatas = await StandardRelayerAPIAssetBuyerManager.getAllAvailableAssetDatasAsync( + sraApiUrl, + etherTokenAssetData, + ); + return new StandardRelayerAPIAssetBuyerManager( + provider, + assetDatas, + orderFetcher, + networkId, + orderRefreshIntervalMs, + ); + } + /** + * Instantiates a new StandardRelayerAPIAssetBuyerManager instance + * @param provider The Provider instance you would like to use for interacting with the Ethereum network. + * @param assetDatas The assetDatas of the desired assets to buy (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). + * @param orderFetcher An object that conforms to OrderFetcher, see type for definition. + * @param networkId The ethereum network id. Defaults to 1 (mainnet). + * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. + * Defaults to 10000ms (10s). + * @return An instance of StandardRelayerAPIAssetBuyerManager + */ + constructor( + provider: Provider, + assetDatas: string[], + orderFetcher: OrderFetcher, + networkId?: number, + orderRefreshIntervalMs?: number, + ) { + assert.assert(assetDatas.length > 0, `Expected 'assetDatas' to be a non-empty array.`); + this.assetBuyerMap = _.reduce( + assetDatas, + (accAssetBuyerMap: ObjectMap, assetData: string) => { + accAssetBuyerMap[assetData] = new AssetBuyer( + provider, + assetData, + orderFetcher, + networkId, + orderRefreshIntervalMs, + ); + return accAssetBuyerMap; + }, + {}, + ); + } + /** + * Get a AssetBuyer for the provided assetData + * @param assetData The desired assetData. + * + * @return An instance of AssetBuyer + */ + public getAssetBuyerFromAssetData(assetData: string): AssetBuyer { + const assetBuyer = this.assetBuyerMap[assetData]; + if (_.isUndefined(assetBuyer)) { + throw new Error( + `${StandardRelayerApiAssetBuyerManagerError.AssetBuyerNotFound}: For assetData ${assetData}`, + ); + } + return assetBuyer; + } + /** + * Get a AssetBuyer for the provided ERC20 tokenAddress + * @param tokenAddress The desired tokenAddress. + * + * @return An instance of AssetBuyer + */ + public getAssetBuyerFromERC20TokenAddress(tokenAddress: string): AssetBuyer { + const assetData = assetDataUtils.encodeERC20AssetData(tokenAddress); + return this.getAssetBuyerFromAssetData(assetData); + } +} diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index 339bce52c..c30bdf4bb 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -70,6 +70,13 @@ export enum AssetBuyerError { NoAddressAvailable = 'NO_ADDRESS_AVAILABLE', } +/** + * Possible errors thrown by an StandardRelayerApiAssetBuyerManager instance or associated static methods. + */ +export enum StandardRelayerApiAssetBuyerManagerError { + AssetBuyerNotFound = 'ASSET_BUYER_NOT_FOUND', +} + export interface AssetBuyerOrdersAndFillableAmounts { orders: SignedOrder[]; feeOrders: SignedOrder[]; diff --git a/packages/asset-buyer/src/utils/asset_data_utils.ts b/packages/asset-buyer/src/utils/asset_data_utils.ts new file mode 100644 index 000000000..10ff31057 --- /dev/null +++ b/packages/asset-buyer/src/utils/asset_data_utils.ts @@ -0,0 +1,26 @@ +import { ContractWrappers } from '@0xproject/contract-wrappers'; +import { assetDataUtils as sharedAssetDataUtils } from '@0xproject/order-utils'; +import * as _ from 'lodash'; + +import { AssetBuyerError } from '../types'; + +export const assetDataUtils = { + ...sharedAssetDataUtils, + getEtherTokenAssetData(contractWrappers: ContractWrappers): string { + const etherTokenAddressIfExists = contractWrappers.etherToken.getContractAddressIfExists(); + if (_.isUndefined(etherTokenAddressIfExists)) { + throw new Error(AssetBuyerError.NoEtherTokenContractFound); + } + const etherTokenAssetData = sharedAssetDataUtils.encodeERC20AssetData(etherTokenAddressIfExists); + return etherTokenAssetData; + }, + getZrxTokenAssetData(contractWrappers: ContractWrappers): string { + let zrxTokenAssetData: string; + try { + zrxTokenAssetData = contractWrappers.exchange.getZRXAssetData(); + } catch (err) { + throw new Error(AssetBuyerError.NoZrxTokenContractFound); + } + return zrxTokenAssetData; + }, +}; diff --git a/packages/asset-buyer/src/utils/buy_quote_calculator.ts b/packages/asset-buyer/src/utils/buy_quote_calculator.ts index 52cecf8ad..31823cc02 100644 --- a/packages/asset-buyer/src/utils/buy_quote_calculator.ts +++ b/packages/asset-buyer/src/utils/buy_quote_calculator.ts @@ -59,6 +59,7 @@ export const buyQuoteCalculator = { let maxEthAmount = constants.ZERO_AMOUNT; let cumulativeMakerAmount = constants.ZERO_AMOUNT; _.forEach(allOrders, (order, index) => { + // TODO: Move this logic to order_utils const remainingFillableMakerAssetAmount = allRemainingAmounts[index]; const orderRate = order.takerAssetAmount.div(order.makerAssetAmount); const claimableTakerAssetAmount = orderRate.mul(remainingFillableMakerAssetAmount); -- cgit v1.2.3 From 3238925aa4e13750e5a8e45b3130ce9e148efae8 Mon Sep 17 00:00:00 2001 From: fragosti Date: Thu, 20 Sep 2018 14:09:11 +0200 Subject: Add better description to README --- packages/asset-buyer/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/README.md b/packages/asset-buyer/README.md index 717849bf2..f615b93e1 100644 --- a/packages/asset-buyer/README.md +++ b/packages/asset-buyer/README.md @@ -1,6 +1,8 @@ ## @0xproject/asset-buyer -Convenience package for buying assets +Convenience package for buying assets represented on the Ethereum blockchain using 0x. In its simplest form, the package helps in the usage of the [0x forwarder contract](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/forwarder-specification.md), which allows users to execute Wrapped Ether based 0x orders without having to set allowances, wrap Ether or buy ZRX, meaning they can buy tokens with Ether alone. Given some liquidity (0x signed orders), it helps estimate the Ether cost of buying a certain asset (giving a range) and then buying that asset. + +In its more advanced and useful form, it integrates with the [Standard Relayer API](https://github.com/0xProject/standard-relayer-api) and takes care of sourcing liquidity for you given an SRA compliant endpoint. The final result is a library that tells you what assets are available, provides an Ether based quote for any asset desired, and allows you to buy that asset using Ether alone. ### Read the [Documentation](https://0xproject.com/docs/asset-buyer). -- cgit v1.2.3 From d23487bda9fa918d31bb940fffb74f8be0859be5 Mon Sep 17 00:00:00 2001 From: fragosti Date: Thu, 20 Sep 2018 14:27:20 +0200 Subject: Update descriptions for methods on AssetBuyer --- packages/asset-buyer/src/asset_buyer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 5f57e3d7f..2b25c681c 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -33,7 +33,7 @@ export class AssetBuyer { private _lastRefreshTimeIfExists?: number; private _currentOrdersAndFillableAmountsIfExists?: AssetBuyerOrdersAndFillableAmounts; /** - * Instantiates a new AssetBuyer instance + * Instantiates a new AssetBuyer instance given existing liquidity in the form of orders and feeOrders. * @param provider The Provider instance you would like to use for interacting with the Ethereum network. * @param orders A non-empty array of objects that conform to SignedOrder. All orders must have the same makerAssetData and takerAssetData (WETH). * @param feeOrders A array of objects that conform to SignedOrder. All orders must have the same makerAssetData (ZRX) and takerAssetData (WETH). Defaults to an empty array. @@ -63,7 +63,7 @@ export class AssetBuyer { return assetBuyer; } /** - * Instantiates a new AssetBuyer instance + * Instantiates a new AssetBuyer instance given the desired assetData and a [Standard Relayer API](https://github.com/0xProject/standard-relayer-api) endpoint * @param provider The Provider instance you would like to use for interacting with the Ethereum network. * @param assetData The assetData that identifies the desired asset to buy. * @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from. @@ -89,7 +89,7 @@ export class AssetBuyer { return assetBuyer; } /** - * Instantiates a new AssetBuyer instance + * Instantiates a new AssetBuyer instance given the desired ERC20 token address and a [Standard Relayer API](https://github.com/0xProject/standard-relayer-api) endpoint * @param provider The Provider instance you would like to use for interacting with the Ethereum network. * @param tokenAddress The ERC20 token address that identifies the desired asset to buy. * @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from. -- cgit v1.2.3 From 03e18ff7c60749fd33228b25077891da26e81d41 Mon Sep 17 00:00:00 2001 From: fragosti Date: Thu, 20 Sep 2018 15:10:15 +0200 Subject: Rename OrderFetcher to OrderProvider and other small improvements --- packages/asset-buyer/src/asset_buyer.ts | 28 ++++++++++------------ packages/asset-buyer/src/index.ts | 6 ++--- .../src/order_fetchers/provided_order_fetcher.ts | 6 ++--- .../standard_relayer_api_order_fetcher.ts | 10 ++++---- .../standard_relayer_api_asset_buyer_manager.ts | 21 ++++++++-------- packages/asset-buyer/src/types.ts | 8 +++---- packages/asset-buyer/src/utils/assert.ts | 8 +++---- packages/asset-buyer/src/utils/asset_data_utils.ts | 4 ++-- .../src/utils/order_fetcher_response_processor.ts | 6 ++--- 9 files changed, 48 insertions(+), 49 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 2b25c681c..e74b8eac1 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -14,8 +14,8 @@ import { AssetBuyerOrdersAndFillableAmounts, BuyQuote, BuyQuoteRequestOpts, - OrderFetcher, - OrderFetcherResponse, + OrderProvider, + OrderProviderResponse, } from './types'; import { assert } from './utils/assert'; @@ -26,7 +26,7 @@ import { orderFetcherResponseProcessor } from './utils/order_fetcher_response_pr export class AssetBuyer { public readonly provider: Provider; public readonly assetData: string; - public readonly orderFetcher: OrderFetcher; + public readonly orderFetcher: OrderProvider; public readonly networkId: number; public readonly orderRefreshIntervalMs: number; private readonly _contractWrappers: ContractWrappers; @@ -133,7 +133,7 @@ export class AssetBuyer { constructor( provider: Provider, assetData: string, - orderFetcher: OrderFetcher, + orderFetcher: OrderProvider, networkId: number = constants.MAINNET_NETWORK_ID, orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS, ) { @@ -152,8 +152,8 @@ export class AssetBuyer { }); } /** - * Get a BuyQuote containing all information relevant to fulfilling a buy. - * Pass the BuyQuote to executeBuyQuoteAsync to execute the buy. + * Get a `BuyQuote` containing all information relevant to fulfilling a buy. + * You can then pass the `BuyQuote` to `executeBuyQuoteAsync` to execute the buy. * @param assetBuyAmount The amount of asset to buy. * @param feePercentage The affiliate fee percentage. Defaults to 0. * @param forceOrderRefresh If set to true, new orders and state will be fetched instead of waiting for @@ -251,10 +251,8 @@ export class AssetBuyer { * Ask the order fetcher for orders and process them. */ private async _getLatestOrdersAndFillableAmountsAsync(): Promise { - // find ether token asset data - const etherTokenAssetData = this._getEtherTokenAssetData(); - // find zrx token asset data - const zrxTokenAssetData = this._getZrxTokenAssetData(); + const etherTokenAssetData = this._getEtherTokenAssetDataOrThrow(); + const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow(); // construct order fetcher requests const targetOrderFetcherRequest = { makerAssetData: this.assetData, @@ -269,7 +267,7 @@ export class AssetBuyer { const requests = [targetOrderFetcherRequest, feeOrderFetcherRequest]; // fetch orders and possible fillable amounts const [targetOrderFetcherResponse, feeOrderFetcherResponse] = await Promise.all( - _.map(requests, async request => this.orderFetcher.fetchOrdersAsync(request)), + _.map(requests, async request => this.orderFetcher.getOrdersAsync(request)), ); // process the responses into one object const ordersAndFillableAmounts = await orderFetcherResponseProcessor.processAsync( @@ -284,14 +282,14 @@ export class AssetBuyer { * Get the assetData that represents the WETH token. * Will throw if WETH does not exist for the current network. */ - private _getEtherTokenAssetData(): string { - return assetDataUtils.getEtherTokenAssetData(this._contractWrappers); + private _getEtherTokenAssetDataOrThrow(): string { + return assetDataUtils.getEtherTokenAssetDataOrThrow(this._contractWrappers); } /** * Get the assetData that represents the ZRX token. * Will throw if ZRX does not exist for the current network. */ - private _getZrxTokenAssetData(): string { - return assetDataUtils.getZrxTokenAssetData(this._contractWrappers); + private _getZrxTokenAssetDataOrThrow(): string { + return assetDataUtils.getZrxTokenAssetDataOrThrow(this._contractWrappers); } } diff --git a/packages/asset-buyer/src/index.ts b/packages/asset-buyer/src/index.ts index 2156b7e96..aa10c3af2 100644 --- a/packages/asset-buyer/src/index.ts +++ b/packages/asset-buyer/src/index.ts @@ -9,9 +9,9 @@ export { StandardRelayerAPIAssetBuyerManager } from './standard_relayer_api_asse export { AssetBuyerError, BuyQuote, - OrderFetcher, - OrderFetcherRequest, - OrderFetcherResponse, + OrderProvider, + OrderProviderRequest, + OrderProviderResponse, SignedOrderWithRemainingFillableMakerAssetAmount, StandardRelayerApiAssetBuyerManagerError, } from './types'; diff --git a/packages/asset-buyer/src/order_fetchers/provided_order_fetcher.ts b/packages/asset-buyer/src/order_fetchers/provided_order_fetcher.ts index 7a7ffcdfe..397f296d7 100644 --- a/packages/asset-buyer/src/order_fetchers/provided_order_fetcher.ts +++ b/packages/asset-buyer/src/order_fetchers/provided_order_fetcher.ts @@ -2,10 +2,10 @@ import { schemas } from '@0xproject/json-schemas'; import { SignedOrder } from '@0xproject/types'; import * as _ from 'lodash'; -import { OrderFetcher, OrderFetcherRequest, OrderFetcherResponse } from '../types'; +import { OrderProvider, OrderProviderRequest, OrderProviderResponse } from '../types'; import { assert } from '../utils/assert'; -export class ProvidedOrderFetcher implements OrderFetcher { +export class ProvidedOrderFetcher implements OrderProvider { public readonly providedOrders: SignedOrder[]; /** * Instantiates a new ProvidedOrderFetcher instance @@ -21,7 +21,7 @@ export class ProvidedOrderFetcher implements OrderFetcher { * @param orderFetchRequest An instance of OrderFetcherRequest. See type for more information. * @return An instance of OrderFetcherResponse. See type for more information. */ - public async fetchOrdersAsync(orderFetchRequest: OrderFetcherRequest): Promise { + public async getOrdersAsync(orderFetchRequest: OrderProviderRequest): Promise { assert.isValidOrderFetcherRequest('orderFetchRequest', orderFetchRequest); const { makerAssetData, takerAssetData } = orderFetchRequest; const orders = _.filter(this.providedOrders, order => { diff --git a/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts b/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts index 3b082b68d..730eaf4e0 100644 --- a/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts +++ b/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts @@ -3,15 +3,15 @@ import * as _ from 'lodash'; import { AssetBuyerError, - OrderFetcher, - OrderFetcherRequest, - OrderFetcherResponse, + OrderProvider, + OrderProviderRequest, + OrderProviderResponse, SignedOrderWithRemainingFillableMakerAssetAmount, } from '../types'; import { assert } from '../utils/assert'; import { orderUtils } from '../utils/order_utils'; -export class StandardRelayerAPIOrderFetcher implements OrderFetcher { +export class StandardRelayerAPIOrderFetcher implements OrderProvider { public readonly apiUrl: string; private readonly _sraClient: HttpClient; /** @@ -56,7 +56,7 @@ export class StandardRelayerAPIOrderFetcher implements OrderFetcher { * @param orderFetchRequest An instance of OrderFetcherRequest. See type for more information. * @return An instance of OrderFetcherResponse. See type for more information. */ - public async fetchOrdersAsync(orderFetchRequest: OrderFetcherRequest): Promise { + public async getOrdersAsync(orderFetchRequest: OrderProviderRequest): Promise { assert.isValidOrderFetcherRequest('orderFetchRequest', orderFetchRequest); const { makerAssetData, takerAssetData, networkId } = orderFetchRequest; const orderbookRequest = { baseAssetData: makerAssetData, quoteAssetData: takerAssetData }; diff --git a/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts b/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts index 6cd96fab2..866bee46e 100644 --- a/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts +++ b/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts @@ -9,8 +9,9 @@ import { constants } from './constants'; import { assert } from './utils/assert'; import { assetDataUtils } from './utils/asset_data_utils'; -import { OrderFetcher, StandardRelayerApiAssetBuyerManagerError } from './types'; +import { OrderProvider, StandardRelayerApiAssetBuyerManagerError } from './types'; +// TODO: Read-only list of available assetDatas export class StandardRelayerAPIAssetBuyerManager { // Map of assetData to AssetBuyer for that assetData public readonly assetBuyerMap: ObjectMap; @@ -37,7 +38,7 @@ export class StandardRelayerAPIAssetBuyerManager { * Instantiates a new StandardRelayerAPIAssetBuyerManager instance with all available assetDatas at the provided sraApiUrl * @param provider The Provider instance you would like to use for interacting with the Ethereum network. * @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from. - * @param orderFetcher An object that conforms to OrderFetcher, see type for definition. + * @param orderProvider An object that conforms to OrderProvider, see type for definition. * @param networkId The ethereum network id. Defaults to 1 (mainnet). * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. * Defaults to 10000ms (10s). @@ -46,12 +47,12 @@ export class StandardRelayerAPIAssetBuyerManager { public static async getAssetBuyerManagerWithAllAvailableAssetDatasAsync( provider: Provider, sraApiUrl: string, - orderFetcher: OrderFetcher, + orderProvider: OrderProvider, networkId: number = constants.MAINNET_NETWORK_ID, orderRefreshIntervalMs?: number, ): Promise { const contractWrappers = new ContractWrappers(provider, { networkId }); - const etherTokenAssetData = assetDataUtils.getEtherTokenAssetData(contractWrappers); + const etherTokenAssetData = assetDataUtils.getEtherTokenAssetDataOrThrow(contractWrappers); const assetDatas = await StandardRelayerAPIAssetBuyerManager.getAllAvailableAssetDatasAsync( sraApiUrl, etherTokenAssetData, @@ -59,7 +60,7 @@ export class StandardRelayerAPIAssetBuyerManager { return new StandardRelayerAPIAssetBuyerManager( provider, assetDatas, - orderFetcher, + orderProvider, networkId, orderRefreshIntervalMs, ); @@ -68,7 +69,7 @@ export class StandardRelayerAPIAssetBuyerManager { * Instantiates a new StandardRelayerAPIAssetBuyerManager instance * @param provider The Provider instance you would like to use for interacting with the Ethereum network. * @param assetDatas The assetDatas of the desired assets to buy (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). - * @param orderFetcher An object that conforms to OrderFetcher, see type for definition. + * @param orderProvider An object that conforms to OrderProvider, see type for definition. * @param networkId The ethereum network id. Defaults to 1 (mainnet). * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. * Defaults to 10000ms (10s). @@ -77,7 +78,7 @@ export class StandardRelayerAPIAssetBuyerManager { constructor( provider: Provider, assetDatas: string[], - orderFetcher: OrderFetcher, + orderProvider: OrderProvider, networkId?: number, orderRefreshIntervalMs?: number, ) { @@ -88,7 +89,7 @@ export class StandardRelayerAPIAssetBuyerManager { accAssetBuyerMap[assetData] = new AssetBuyer( provider, assetData, - orderFetcher, + orderProvider, networkId, orderRefreshIntervalMs, ); @@ -98,7 +99,7 @@ export class StandardRelayerAPIAssetBuyerManager { ); } /** - * Get a AssetBuyer for the provided assetData + * Get an AssetBuyer for the provided assetData * @param assetData The desired assetData. * * @return An instance of AssetBuyer @@ -113,7 +114,7 @@ export class StandardRelayerAPIAssetBuyerManager { return assetBuyer; } /** - * Get a AssetBuyer for the provided ERC20 tokenAddress + * Get an AssetBuyer for the provided ERC20 tokenAddress * @param tokenAddress The desired tokenAddress. * * @return An instance of AssetBuyer diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index c30bdf4bb..09319fca4 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -6,7 +6,7 @@ import { BigNumber } from '@0xproject/utils'; * takerAssetData: The assetData representing the desired takerAsset. * networkId: The networkId that the desired orders should be for. */ -export interface OrderFetcherRequest { +export interface OrderProviderRequest { makerAssetData: string; takerAssetData: string; networkId: number; @@ -15,7 +15,7 @@ export interface OrderFetcherRequest { /** * orders: An array of orders with optional remaining fillable makerAsset amounts. See type for more info. */ -export interface OrderFetcherResponse { +export interface OrderProviderResponse { orders: SignedOrderWithRemainingFillableMakerAssetAmount[]; } @@ -29,8 +29,8 @@ export interface SignedOrderWithRemainingFillableMakerAssetAmount extends Signed /** * Given an OrderFetchRequest, get an OrderFetchResponse. */ -export interface OrderFetcher { - fetchOrdersAsync: (orderFetchRequest: OrderFetcherRequest) => Promise; +export interface OrderProvider { + getOrdersAsync: (orderProviderRequest: OrderProviderRequest) => Promise; } /** diff --git a/packages/asset-buyer/src/utils/assert.ts b/packages/asset-buyer/src/utils/assert.ts index 745ba726f..1d57ae9b8 100644 --- a/packages/asset-buyer/src/utils/assert.ts +++ b/packages/asset-buyer/src/utils/assert.ts @@ -3,7 +3,7 @@ import { schemas } from '@0xproject/json-schemas'; import { SignedOrder } from '@0xproject/types'; import * as _ from 'lodash'; -import { BuyQuote, OrderFetcher, OrderFetcherRequest } from '../types'; +import { BuyQuote, OrderProvider, OrderProviderRequest } from '../types'; export const assert = { ...sharedAssert, @@ -18,10 +18,10 @@ export const assert = { sharedAssert.isNumber(`${variableName}.feePercentage`, buyQuote.feePercentage); } }, - isValidOrderFetcher(variableName: string, orderFetcher: OrderFetcher): void { - sharedAssert.isFunction(`${variableName}.fetchOrdersAsync`, orderFetcher.fetchOrdersAsync); + isValidOrderFetcher(variableName: string, orderFetcher: OrderProvider): void { + sharedAssert.isFunction(`${variableName}.fetchOrdersAsync`, orderFetcher.getOrdersAsync); }, - isValidOrderFetcherRequest(variableName: string, orderFetcherRequest: OrderFetcherRequest): void { + isValidOrderFetcherRequest(variableName: string, orderFetcherRequest: OrderProviderRequest): void { sharedAssert.isHexString(`${variableName}.makerAssetData`, orderFetcherRequest.makerAssetData); sharedAssert.isHexString(`${variableName}.takerAssetData`, orderFetcherRequest.takerAssetData); sharedAssert.isNumber(`${variableName}.networkId`, orderFetcherRequest.networkId); diff --git a/packages/asset-buyer/src/utils/asset_data_utils.ts b/packages/asset-buyer/src/utils/asset_data_utils.ts index 10ff31057..d05ff2504 100644 --- a/packages/asset-buyer/src/utils/asset_data_utils.ts +++ b/packages/asset-buyer/src/utils/asset_data_utils.ts @@ -6,7 +6,7 @@ import { AssetBuyerError } from '../types'; export const assetDataUtils = { ...sharedAssetDataUtils, - getEtherTokenAssetData(contractWrappers: ContractWrappers): string { + getEtherTokenAssetDataOrThrow(contractWrappers: ContractWrappers): string { const etherTokenAddressIfExists = contractWrappers.etherToken.getContractAddressIfExists(); if (_.isUndefined(etherTokenAddressIfExists)) { throw new Error(AssetBuyerError.NoEtherTokenContractFound); @@ -14,7 +14,7 @@ export const assetDataUtils = { const etherTokenAssetData = sharedAssetDataUtils.encodeERC20AssetData(etherTokenAddressIfExists); return etherTokenAssetData; }, - getZrxTokenAssetData(contractWrappers: ContractWrappers): string { + getZrxTokenAssetDataOrThrow(contractWrappers: ContractWrappers): string { let zrxTokenAssetData: string; try { zrxTokenAssetData = contractWrappers.exchange.getZRXAssetData(); diff --git a/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts b/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts index f700cadf9..98ab6b9d1 100644 --- a/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts +++ b/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts @@ -8,7 +8,7 @@ import * as _ from 'lodash'; import { constants } from '../constants'; import { AssetBuyerOrdersAndFillableAmounts, - OrderFetcherResponse, + OrderProviderResponse, SignedOrderWithRemainingFillableMakerAssetAmount, } from '../types'; @@ -28,8 +28,8 @@ export const orderFetcherResponseProcessor = { * - Sort by rate */ async processAsync( - targetOrderFetcherResponse: OrderFetcherResponse, - feeOrderFetcherResponse: OrderFetcherResponse, + targetOrderFetcherResponse: OrderProviderResponse, + feeOrderFetcherResponse: OrderProviderResponse, zrxTokenAssetData: string, orderValidator?: OrderValidatorWrapper, ): Promise { -- cgit v1.2.3 From 35c324f67c8022f3a71bc723bf69763bf651cb05 Mon Sep 17 00:00:00 2001 From: fragosti Date: Thu, 20 Sep 2018 19:00:23 +0200 Subject: Add utility to get assetDatas --- .../asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts b/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts index 866bee46e..207713110 100644 --- a/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts +++ b/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts @@ -11,7 +11,6 @@ import { assetDataUtils } from './utils/asset_data_utils'; import { OrderProvider, StandardRelayerApiAssetBuyerManagerError } from './types'; -// TODO: Read-only list of available assetDatas export class StandardRelayerAPIAssetBuyerManager { // Map of assetData to AssetBuyer for that assetData public readonly assetBuyerMap: ObjectMap; @@ -123,4 +122,12 @@ export class StandardRelayerAPIAssetBuyerManager { const assetData = assetDataUtils.encodeERC20AssetData(tokenAddress); return this.getAssetBuyerFromAssetData(assetData); } + /** + * Get a list of all the assetDatas that the instance supports + * + * @return An array of assetData strings + */ + public getAssetDatas(): string[] { + return _.keys(this.assetBuyerMap); + } } -- cgit v1.2.3 From fcca63a2dc17f7745304d02dfaffe60f0d6785c4 Mon Sep 17 00:00:00 2001 From: fragosti Date: Thu, 20 Sep 2018 23:39:55 +0200 Subject: Move some logic to order utils --- packages/asset-buyer/src/utils/buy_quote_calculator.ts | 10 ++++++---- packages/asset-buyer/src/utils/order_utils.ts | 14 ++++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/utils/buy_quote_calculator.ts b/packages/asset-buyer/src/utils/buy_quote_calculator.ts index 31823cc02..aa7159e6c 100644 --- a/packages/asset-buyer/src/utils/buy_quote_calculator.ts +++ b/packages/asset-buyer/src/utils/buy_quote_calculator.ts @@ -3,6 +3,7 @@ import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; import { constants } from '../constants'; +import { orderUtils } from './order_utils'; import { AssetBuyerError, AssetBuyerOrdersAndFillableAmounts, BuyQuote } from '../types'; export const buyQuoteCalculator = { @@ -30,7 +31,7 @@ export const buyQuoteCalculator = { if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) { throw new Error(AssetBuyerError.InsufficientAssetLiquidity); } - // TODO: optimization + // TODO(bmillman): optimization // update this logic to find the minimum amount of feeOrders to cover the worst case as opposed to // finding order that cover all fees, this will help with estimating ETH and minimizing gas usage const { @@ -59,10 +60,11 @@ export const buyQuoteCalculator = { let maxEthAmount = constants.ZERO_AMOUNT; let cumulativeMakerAmount = constants.ZERO_AMOUNT; _.forEach(allOrders, (order, index) => { - // TODO: Move this logic to order_utils const remainingFillableMakerAssetAmount = allRemainingAmounts[index]; - const orderRate = order.takerAssetAmount.div(order.makerAssetAmount); - const claimableTakerAssetAmount = orderRate.mul(remainingFillableMakerAssetAmount); + const claimableTakerAssetAmount = orderUtils.calculateRemainingTakerAssetAmount( + order, + remainingFillableMakerAssetAmount, + ); // taker asset is always assumed to be WETH maxEthAmount = maxEthAmount.plus(claimableTakerAssetAmount); if (cumulativeMakerAmount.lessThan(assetBuyAmount)) { diff --git a/packages/asset-buyer/src/utils/order_utils.ts b/packages/asset-buyer/src/utils/order_utils.ts index bb0bdb80f..27db73257 100644 --- a/packages/asset-buyer/src/utils/order_utils.ts +++ b/packages/asset-buyer/src/utils/order_utils.ts @@ -10,10 +10,16 @@ export const orderUtils = { return order.expirationTimeSeconds.lessThan(currentUnixTimestampSec); }, calculateRemainingMakerAssetAmount(order: SignedOrder, remainingTakerAssetAmount: BigNumber): BigNumber { - const result = remainingTakerAssetAmount.eq(0) - ? constants.ZERO_AMOUNT - : remainingTakerAssetAmount.times(order.makerAssetAmount).dividedToIntegerBy(order.takerAssetAmount); - return result; + if (remainingTakerAssetAmount.eq(0)) { + return constants.ZERO_AMOUNT; + } + return remainingTakerAssetAmount.times(order.makerAssetAmount).dividedToIntegerBy(order.takerAssetAmount); + }, + calculateRemainingTakerAssetAmount(order: SignedOrder, remainingMakerAssetAmount: BigNumber): BigNumber { + if (remainingMakerAssetAmount.eq(0)) { + return constants.ZERO_AMOUNT; + } + return remainingMakerAssetAmount.times(order.takerAssetAmount).dividedToIntegerBy(order.makerAssetAmount); }, isOpenOrder(order: SignedOrder): boolean { return order.takerAddress === constants.NULL_ADDRESS; -- cgit v1.2.3 From 97150cf55f472ca5c77b28d064976d84df9f2436 Mon Sep 17 00:00:00 2001 From: fragosti Date: Fri, 21 Sep 2018 14:56:54 +0200 Subject: Add labels to TODOs --- packages/asset-buyer/src/utils/buy_quote_calculator.ts | 3 ++- packages/asset-buyer/src/utils/order_fetcher_response_processor.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/utils/buy_quote_calculator.ts b/packages/asset-buyer/src/utils/buy_quote_calculator.ts index aa7159e6c..3d90a6ead 100644 --- a/packages/asset-buyer/src/utils/buy_quote_calculator.ts +++ b/packages/asset-buyer/src/utils/buy_quote_calculator.ts @@ -3,9 +3,10 @@ import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; import { constants } from '../constants'; -import { orderUtils } from './order_utils'; import { AssetBuyerError, AssetBuyerOrdersAndFillableAmounts, BuyQuote } from '../types'; +import { orderUtils } from './order_utils'; + export const buyQuoteCalculator = { calculate( ordersAndFillableAmounts: AssetBuyerOrdersAndFillableAmounts, diff --git a/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts b/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts index 98ab6b9d1..ec4a6ce7e 100644 --- a/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts +++ b/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts @@ -41,9 +41,9 @@ export const orderFetcherResponseProcessor = { let unsortedFeeOrders = filteredFeeOrders; // if an orderValidator is provided, use on chain information to calculate remaining fillable makerAsset amounts if (!_.isUndefined(orderValidator)) { - // TODO: critical + // TODO(bmillman): improvement // try catch these requests and throw a more domain specific error - // TODO: optimization + // TODO(bmillman): optimization // reduce this to once RPC call buy combining orders into one array and then splitting up the response const [targetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all( _.map([filteredTargetOrders, filteredFeeOrders], ordersToBeValidated => { @@ -65,7 +65,7 @@ export const orderFetcherResponseProcessor = { ); } // sort orders by rate - // TODO: optimization + // TODO(bmillman): optimization // provide a feeRate to the sorting function to more accurately sort based on the current market for ZRX tokens const sortedTargetOrders = sortingUtils.sortOrdersByFeeAdjustedRate(unsortedTargetOrders); const sortedFeeOrders = sortingUtils.sortFeeOrdersByFeeAdjustedRate(unsortedFeeOrders); -- cgit v1.2.3 From 8dea48ebeff775f7437c45f5a6d39dfc7dbf704e Mon Sep 17 00:00:00 2001 From: fragosti Date: Fri, 21 Sep 2018 15:21:44 +0200 Subject: Renamed orderFetcherResponseProcessor to orderProviderResponseProcessoer --- packages/asset-buyer/src/asset_buyer.ts | 4 +- .../src/utils/order_fetcher_response_processor.ts | 184 --------------------- .../src/utils/order_provider_response_processor.ts | 184 +++++++++++++++++++++ 3 files changed, 186 insertions(+), 186 deletions(-) delete mode 100644 packages/asset-buyer/src/utils/order_fetcher_response_processor.ts create mode 100644 packages/asset-buyer/src/utils/order_provider_response_processor.ts (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index e74b8eac1..74ced0e51 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -21,7 +21,7 @@ import { import { assert } from './utils/assert'; import { assetDataUtils } from './utils/asset_data_utils'; import { buyQuoteCalculator } from './utils/buy_quote_calculator'; -import { orderFetcherResponseProcessor } from './utils/order_fetcher_response_processor'; +import { orderProviderResponseProcessor } from './utils/order_provider_response_processor'; export class AssetBuyer { public readonly provider: Provider; @@ -270,7 +270,7 @@ export class AssetBuyer { _.map(requests, async request => this.orderFetcher.getOrdersAsync(request)), ); // process the responses into one object - const ordersAndFillableAmounts = await orderFetcherResponseProcessor.processAsync( + const ordersAndFillableAmounts = await orderProviderResponseProcessor.processAsync( targetOrderFetcherResponse, feeOrderFetcherResponse, zrxTokenAssetData, diff --git a/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts b/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts deleted file mode 100644 index ec4a6ce7e..000000000 --- a/packages/asset-buyer/src/utils/order_fetcher_response_processor.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { OrderAndTraderInfo, OrderStatus, OrderValidatorWrapper } from '@0xproject/contract-wrappers'; -import { sortingUtils } from '@0xproject/order-utils'; -import { RemainingFillableCalculator } from '@0xproject/order-utils/lib/src/remaining_fillable_calculator'; -import { SignedOrder } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; -import * as _ from 'lodash'; - -import { constants } from '../constants'; -import { - AssetBuyerOrdersAndFillableAmounts, - OrderProviderResponse, - SignedOrderWithRemainingFillableMakerAssetAmount, -} from '../types'; - -import { orderUtils } from './order_utils'; - -interface OrdersAndRemainingFillableMakerAssetAmounts { - orders: SignedOrder[]; - remainingFillableMakerAssetAmounts: BigNumber[]; -} - -export const orderFetcherResponseProcessor = { - /** - * Take the responses for the target orders to buy and fee orders and process them. - * Processing includes: - * - Drop orders that are expired or not open orders (null taker address) - * - If shouldValidateOnChain, attempt to grab fillable amounts from on-chain otherwise assume completely fillable - * - Sort by rate - */ - async processAsync( - targetOrderFetcherResponse: OrderProviderResponse, - feeOrderFetcherResponse: OrderProviderResponse, - zrxTokenAssetData: string, - orderValidator?: OrderValidatorWrapper, - ): Promise { - // drop orders that are expired or not open - const filteredTargetOrders = filterOutExpiredAndNonOpenOrders(targetOrderFetcherResponse.orders); - const filteredFeeOrders = filterOutExpiredAndNonOpenOrders(feeOrderFetcherResponse.orders); - // set the orders to be sorted equal to the filtered orders - let unsortedTargetOrders = filteredTargetOrders; - let unsortedFeeOrders = filteredFeeOrders; - // if an orderValidator is provided, use on chain information to calculate remaining fillable makerAsset amounts - if (!_.isUndefined(orderValidator)) { - // TODO(bmillman): improvement - // try catch these requests and throw a more domain specific error - // TODO(bmillman): optimization - // reduce this to once RPC call buy combining orders into one array and then splitting up the response - const [targetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all( - _.map([filteredTargetOrders, filteredFeeOrders], ordersToBeValidated => { - const takerAddresses = _.map(ordersToBeValidated, () => constants.NULL_ADDRESS); - return orderValidator.getOrdersAndTradersInfoAsync(ordersToBeValidated, takerAddresses); - }), - ); - // take orders + on chain information and find the valid orders and remaining fillable maker asset amounts - unsortedTargetOrders = getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain( - filteredTargetOrders, - targetOrdersAndTradersInfo, - zrxTokenAssetData, - ); - // take orders + on chain information and find the valid orders and remaining fillable maker asset amounts - unsortedFeeOrders = getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain( - filteredFeeOrders, - feeOrdersAndTradersInfo, - zrxTokenAssetData, - ); - } - // sort orders by rate - // TODO(bmillman): optimization - // provide a feeRate to the sorting function to more accurately sort based on the current market for ZRX tokens - const sortedTargetOrders = sortingUtils.sortOrdersByFeeAdjustedRate(unsortedTargetOrders); - const sortedFeeOrders = sortingUtils.sortFeeOrdersByFeeAdjustedRate(unsortedFeeOrders); - // unbundle orders and fillable amounts and compile final result - const targetOrdersAndRemainingFillableMakerAssetAmounts = unbundleOrdersWithAmounts(sortedTargetOrders); - const feeOrdersAndRemainingFillableMakerAssetAmounts = unbundleOrdersWithAmounts(sortedFeeOrders); - return { - orders: targetOrdersAndRemainingFillableMakerAssetAmounts.orders, - feeOrders: feeOrdersAndRemainingFillableMakerAssetAmounts.orders, - remainingFillableMakerAssetAmounts: - targetOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, - remainingFillableFeeAmounts: - feeOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, - }; - }, -}; - -/** - * Given an array of orders, return a new array with expired and non open orders filtered out. - */ -function filterOutExpiredAndNonOpenOrders( - orders: SignedOrderWithRemainingFillableMakerAssetAmount[], -): SignedOrderWithRemainingFillableMakerAssetAmount[] { - const result = _.filter(orders, order => { - return orderUtils.isOpenOrder(order) && !orderUtils.isOrderExpired(order); - }); - return result; -} - -/** - * Given an array of orders and corresponding on-chain infos, return a subset of the orders - * that are still fillable orders with their corresponding remainingFillableMakerAssetAmounts. - */ -function getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain( - inputOrders: SignedOrder[], - ordersAndTradersInfo: OrderAndTraderInfo[], - zrxAssetData: string, -): SignedOrderWithRemainingFillableMakerAssetAmount[] { - // iterate through the input orders and find the ones that are still fillable - // for the orders that are still fillable, calculate the remaining fillable maker asset amount - const result = _.reduce( - inputOrders, - (accOrders, order, index) => { - // get corresponding on-chain state for the order - const { orderInfo, traderInfo } = ordersAndTradersInfo[index]; - // if the order IS NOT fillable, do not add anything to the accumulations and continue iterating - if (orderInfo.orderStatus !== OrderStatus.FILLABLE) { - return accOrders; - } - // if the order IS fillable, add the order and calculate the remaining fillable amount - const transferrableAssetAmount = BigNumber.min([traderInfo.makerAllowance, traderInfo.makerBalance]); - const transferrableFeeAssetAmount = BigNumber.min([ - traderInfo.makerZrxAllowance, - traderInfo.makerZrxBalance, - ]); - const remainingTakerAssetAmount = order.takerAssetAmount.minus(orderInfo.orderTakerAssetFilledAmount); - const remainingMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( - order, - remainingTakerAssetAmount, - ); - const remainingFillableCalculator = new RemainingFillableCalculator( - order.makerFee, - order.makerAssetAmount, - order.makerAssetData === zrxAssetData, - transferrableAssetAmount, - transferrableFeeAssetAmount, - remainingMakerAssetAmount, - ); - const remainingFillableAmount = remainingFillableCalculator.computeRemainingFillable(); - // if the order does not have any remaining fillable makerAsset, do not add anything to the accumulations and continue iterating - if (remainingFillableAmount.lte(constants.ZERO_AMOUNT)) { - return accOrders; - } - const orderWithRemainingFillableMakerAssetAmount = { - ...order, - remainingFillableMakerAssetAmount: remainingFillableAmount, - }; - const newAccOrders = _.concat(accOrders, orderWithRemainingFillableMakerAssetAmount); - return newAccOrders; - }, - [] as SignedOrderWithRemainingFillableMakerAssetAmount[], - ); - return result; -} - -/** - * Given an array of orders with remaining fillable maker asset amounts. Unbundle into an instance of OrdersAndRemainingFillableMakerAssetAmounts. - * If an order is missing a corresponding remainingFillableMakerAssetAmount, assume it is completely fillable. - */ -function unbundleOrdersWithAmounts( - ordersWithAmounts: SignedOrderWithRemainingFillableMakerAssetAmount[], -): OrdersAndRemainingFillableMakerAssetAmounts { - const result = _.reduce( - ordersWithAmounts, - (acc, orderWithAmount) => { - const { orders, remainingFillableMakerAssetAmounts } = acc; - const { remainingFillableMakerAssetAmount, ...order } = orderWithAmount; - // if we are still missing a remainingFillableMakerAssetAmount, assume the order is completely fillable - const newRemainingAmount = remainingFillableMakerAssetAmount || order.makerAssetAmount; - // if remaining amount is less than or equal to zero, do not add it - if (newRemainingAmount.lte(constants.ZERO_AMOUNT)) { - return acc; - } - const newAcc = { - orders: _.concat(orders, order), - remainingFillableMakerAssetAmounts: _.concat(remainingFillableMakerAssetAmounts, newRemainingAmount), - }; - return newAcc; - }, - { - orders: [] as SignedOrder[], - remainingFillableMakerAssetAmounts: [] as BigNumber[], - }, - ); - return result; -} diff --git a/packages/asset-buyer/src/utils/order_provider_response_processor.ts b/packages/asset-buyer/src/utils/order_provider_response_processor.ts new file mode 100644 index 000000000..79e15c3d8 --- /dev/null +++ b/packages/asset-buyer/src/utils/order_provider_response_processor.ts @@ -0,0 +1,184 @@ +import { OrderAndTraderInfo, OrderStatus, OrderValidatorWrapper } from '@0xproject/contract-wrappers'; +import { sortingUtils } from '@0xproject/order-utils'; +import { RemainingFillableCalculator } from '@0xproject/order-utils/lib/src/remaining_fillable_calculator'; +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { constants } from '../constants'; +import { + AssetBuyerOrdersAndFillableAmounts, + OrderProviderResponse, + SignedOrderWithRemainingFillableMakerAssetAmount, +} from '../types'; + +import { orderUtils } from './order_utils'; + +interface OrdersAndRemainingFillableMakerAssetAmounts { + orders: SignedOrder[]; + remainingFillableMakerAssetAmounts: BigNumber[]; +} + +export const orderProviderResponseProcessor = { + /** + * Take the responses for the target orders to buy and fee orders and process them. + * Processing includes: + * - Drop orders that are expired or not open orders (null taker address) + * - If shouldValidateOnChain, attempt to grab fillable amounts from on-chain otherwise assume completely fillable + * - Sort by rate + */ + async processAsync( + targetOrderProviderResponse: OrderProviderResponse, + feeOrderProviderResponse: OrderProviderResponse, + zrxTokenAssetData: string, + orderValidator?: OrderValidatorWrapper, + ): Promise { + // drop orders that are expired or not open + const filteredTargetOrders = filterOutExpiredAndNonOpenOrders(targetOrderProviderResponse.orders); + const filteredFeeOrders = filterOutExpiredAndNonOpenOrders(feeOrderProviderResponse.orders); + // set the orders to be sorted equal to the filtered orders + let unsortedTargetOrders = filteredTargetOrders; + let unsortedFeeOrders = filteredFeeOrders; + // if an orderValidator is provided, use on chain information to calculate remaining fillable makerAsset amounts + if (!_.isUndefined(orderValidator)) { + // TODO(bmillman): improvement + // try catch these requests and throw a more domain specific error + // TODO(bmillman): optimization + // reduce this to once RPC call buy combining orders into one array and then splitting up the response + const [targetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all( + _.map([filteredTargetOrders, filteredFeeOrders], ordersToBeValidated => { + const takerAddresses = _.map(ordersToBeValidated, () => constants.NULL_ADDRESS); + return orderValidator.getOrdersAndTradersInfoAsync(ordersToBeValidated, takerAddresses); + }), + ); + // take orders + on chain information and find the valid orders and remaining fillable maker asset amounts + unsortedTargetOrders = getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain( + filteredTargetOrders, + targetOrdersAndTradersInfo, + zrxTokenAssetData, + ); + // take orders + on chain information and find the valid orders and remaining fillable maker asset amounts + unsortedFeeOrders = getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain( + filteredFeeOrders, + feeOrdersAndTradersInfo, + zrxTokenAssetData, + ); + } + // sort orders by rate + // TODO(bmillman): optimization + // provide a feeRate to the sorting function to more accurately sort based on the current market for ZRX tokens + const sortedTargetOrders = sortingUtils.sortOrdersByFeeAdjustedRate(unsortedTargetOrders); + const sortedFeeOrders = sortingUtils.sortFeeOrdersByFeeAdjustedRate(unsortedFeeOrders); + // unbundle orders and fillable amounts and compile final result + const targetOrdersAndRemainingFillableMakerAssetAmounts = unbundleOrdersWithAmounts(sortedTargetOrders); + const feeOrdersAndRemainingFillableMakerAssetAmounts = unbundleOrdersWithAmounts(sortedFeeOrders); + return { + orders: targetOrdersAndRemainingFillableMakerAssetAmounts.orders, + feeOrders: feeOrdersAndRemainingFillableMakerAssetAmounts.orders, + remainingFillableMakerAssetAmounts: + targetOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, + remainingFillableFeeAmounts: + feeOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, + }; + }, +}; + +/** + * Given an array of orders, return a new array with expired and non open orders filtered out. + */ +function filterOutExpiredAndNonOpenOrders( + orders: SignedOrderWithRemainingFillableMakerAssetAmount[], +): SignedOrderWithRemainingFillableMakerAssetAmount[] { + const result = _.filter(orders, order => { + return orderUtils.isOpenOrder(order) && !orderUtils.isOrderExpired(order); + }); + return result; +} + +/** + * Given an array of orders and corresponding on-chain infos, return a subset of the orders + * that are still fillable orders with their corresponding remainingFillableMakerAssetAmounts. + */ +function getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain( + inputOrders: SignedOrder[], + ordersAndTradersInfo: OrderAndTraderInfo[], + zrxAssetData: string, +): SignedOrderWithRemainingFillableMakerAssetAmount[] { + // iterate through the input orders and find the ones that are still fillable + // for the orders that are still fillable, calculate the remaining fillable maker asset amount + const result = _.reduce( + inputOrders, + (accOrders, order, index) => { + // get corresponding on-chain state for the order + const { orderInfo, traderInfo } = ordersAndTradersInfo[index]; + // if the order IS NOT fillable, do not add anything to the accumulations and continue iterating + if (orderInfo.orderStatus !== OrderStatus.FILLABLE) { + return accOrders; + } + // if the order IS fillable, add the order and calculate the remaining fillable amount + const transferrableAssetAmount = BigNumber.min([traderInfo.makerAllowance, traderInfo.makerBalance]); + const transferrableFeeAssetAmount = BigNumber.min([ + traderInfo.makerZrxAllowance, + traderInfo.makerZrxBalance, + ]); + const remainingTakerAssetAmount = order.takerAssetAmount.minus(orderInfo.orderTakerAssetFilledAmount); + const remainingMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( + order, + remainingTakerAssetAmount, + ); + const remainingFillableCalculator = new RemainingFillableCalculator( + order.makerFee, + order.makerAssetAmount, + order.makerAssetData === zrxAssetData, + transferrableAssetAmount, + transferrableFeeAssetAmount, + remainingMakerAssetAmount, + ); + const remainingFillableAmount = remainingFillableCalculator.computeRemainingFillable(); + // if the order does not have any remaining fillable makerAsset, do not add anything to the accumulations and continue iterating + if (remainingFillableAmount.lte(constants.ZERO_AMOUNT)) { + return accOrders; + } + const orderWithRemainingFillableMakerAssetAmount = { + ...order, + remainingFillableMakerAssetAmount: remainingFillableAmount, + }; + const newAccOrders = _.concat(accOrders, orderWithRemainingFillableMakerAssetAmount); + return newAccOrders; + }, + [] as SignedOrderWithRemainingFillableMakerAssetAmount[], + ); + return result; +} + +/** + * Given an array of orders with remaining fillable maker asset amounts. Unbundle into an instance of OrdersAndRemainingFillableMakerAssetAmounts. + * If an order is missing a corresponding remainingFillableMakerAssetAmount, assume it is completely fillable. + */ +function unbundleOrdersWithAmounts( + ordersWithAmounts: SignedOrderWithRemainingFillableMakerAssetAmount[], +): OrdersAndRemainingFillableMakerAssetAmounts { + const result = _.reduce( + ordersWithAmounts, + (acc, orderWithAmount) => { + const { orders, remainingFillableMakerAssetAmounts } = acc; + const { remainingFillableMakerAssetAmount, ...order } = orderWithAmount; + // if we are still missing a remainingFillableMakerAssetAmount, assume the order is completely fillable + const newRemainingAmount = remainingFillableMakerAssetAmount || order.makerAssetAmount; + // if remaining amount is less than or equal to zero, do not add it + if (newRemainingAmount.lte(constants.ZERO_AMOUNT)) { + return acc; + } + const newAcc = { + orders: _.concat(orders, order), + remainingFillableMakerAssetAmounts: _.concat(remainingFillableMakerAssetAmounts, newRemainingAmount), + }; + return newAcc; + }, + { + orders: [] as SignedOrder[], + remainingFillableMakerAssetAmounts: [] as BigNumber[], + }, + ); + return result; +} -- cgit v1.2.3 From c64dcec772d8f5bfa7a32c5e60bfe808229d4d5e Mon Sep 17 00:00:00 2001 From: fragosti Date: Fri, 21 Sep 2018 15:58:04 +0200 Subject: More renaming --- packages/asset-buyer/src/asset_buyer.ts | 40 +++++------ packages/asset-buyer/src/index.ts | 4 +- .../src/order_fetchers/provided_order_fetcher.ts | 32 --------- .../standard_relayer_api_order_fetcher.ts | 78 ---------------------- .../src/order_providers/basic_order_provider.ts | 32 +++++++++ .../standard_relayer_api_order_provider.ts | 78 ++++++++++++++++++++++ packages/asset-buyer/src/types.ts | 2 +- packages/asset-buyer/src/utils/assert.ts | 4 +- .../asset-buyer/src/utils/buy_quote_calculator.ts | 1 + 9 files changed, 136 insertions(+), 135 deletions(-) delete mode 100644 packages/asset-buyer/src/order_fetchers/provided_order_fetcher.ts delete mode 100644 packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts create mode 100644 packages/asset-buyer/src/order_providers/basic_order_provider.ts create mode 100644 packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 74ced0e51..9774a8d39 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -7,8 +7,8 @@ import { Provider } from 'ethereum-types'; import * as _ from 'lodash'; import { constants } from './constants'; -import { ProvidedOrderFetcher } from './order_fetchers/provided_order_fetcher'; -import { StandardRelayerAPIOrderFetcher } from './order_fetchers/standard_relayer_api_order_fetcher'; +import { BasicOrderProvider } from './order_providers/basic_order_provider'; +import { StandardRelayerAPIOrderProvider } from './order_providers/standard_relayer_api_order_provider'; import { AssetBuyerError, AssetBuyerOrdersAndFillableAmounts, @@ -26,7 +26,7 @@ import { orderProviderResponseProcessor } from './utils/order_provider_response_ export class AssetBuyer { public readonly provider: Provider; public readonly assetData: string; - public readonly orderFetcher: OrderProvider; + public readonly orderProvider: OrderProvider; public readonly networkId: number; public readonly orderRefreshIntervalMs: number; private readonly _contractWrappers: ContractWrappers; @@ -58,8 +58,8 @@ export class AssetBuyer { assert.areValidProvidedOrders('feeOrders', feeOrders); assert.assert(orders.length !== 0, `Expected orders to contain at least one order`); const assetData = orders[0].makerAssetData; - const orderFetcher = new ProvidedOrderFetcher(_.concat(orders, feeOrders)); - const assetBuyer = new AssetBuyer(provider, assetData, orderFetcher, networkId, orderRefreshIntervalMs); + const orderProvider = new BasicOrderProvider(_.concat(orders, feeOrders)); + const assetBuyer = new AssetBuyer(provider, assetData, orderProvider, networkId, orderRefreshIntervalMs); return assetBuyer; } /** @@ -84,8 +84,8 @@ export class AssetBuyer { assert.isWebUri('sraApiUrl', sraApiUrl); assert.isNumber('networkId', networkId); assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs); - const orderFetcher = new StandardRelayerAPIOrderFetcher(sraApiUrl); - const assetBuyer = new AssetBuyer(provider, assetData, orderFetcher, networkId, orderRefreshIntervalMs); + const orderProvider = new StandardRelayerAPIOrderProvider(sraApiUrl); + const assetBuyer = new AssetBuyer(provider, assetData, orderProvider, networkId, orderRefreshIntervalMs); return assetBuyer; } /** @@ -124,7 +124,7 @@ export class AssetBuyer { * Instantiates a new AssetBuyer instance * @param provider The Provider instance you would like to use for interacting with the Ethereum network. * @param assetData The assetData of the desired asset to buy (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). - * @param orderFetcher An object that conforms to OrderFetcher, see type for definition. + * @param orderProvider An object that conforms to OrderProvider, see type for definition. * @param networkId The ethereum network id. Defaults to 1 (mainnet). * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. * Defaults to 10000ms (10s). @@ -133,18 +133,18 @@ export class AssetBuyer { constructor( provider: Provider, assetData: string, - orderFetcher: OrderProvider, + orderProvider: OrderProvider, networkId: number = constants.MAINNET_NETWORK_ID, orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS, ) { assert.isWeb3Provider('provider', provider); assert.isString('assetData', assetData); - assert.isValidOrderFetcher('orderFetcher', orderFetcher); + assert.isValidOrderProvider('orderProvider', orderProvider); assert.isNumber('networkId', networkId); assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs); this.provider = provider; this.assetData = assetData; - this.orderFetcher = orderFetcher; + this.orderProvider = orderProvider; this.networkId = networkId; this.orderRefreshIntervalMs = orderRefreshIntervalMs; this._contractWrappers = new ContractWrappers(this.provider, { @@ -248,31 +248,31 @@ export class AssetBuyer { return txHash; } /** - * Ask the order fetcher for orders and process them. + * Ask the order Provider for orders and process them. */ private async _getLatestOrdersAndFillableAmountsAsync(): Promise { const etherTokenAssetData = this._getEtherTokenAssetDataOrThrow(); const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow(); - // construct order fetcher requests - const targetOrderFetcherRequest = { + // construct order Provider requests + const targetOrderProviderRequest = { makerAssetData: this.assetData, takerAssetData: etherTokenAssetData, networkId: this.networkId, }; - const feeOrderFetcherRequest = { + const feeOrderProviderRequest = { makerAssetData: zrxTokenAssetData, takerAssetData: etherTokenAssetData, networkId: this.networkId, }; - const requests = [targetOrderFetcherRequest, feeOrderFetcherRequest]; + const requests = [targetOrderProviderRequest, feeOrderProviderRequest]; // fetch orders and possible fillable amounts - const [targetOrderFetcherResponse, feeOrderFetcherResponse] = await Promise.all( - _.map(requests, async request => this.orderFetcher.getOrdersAsync(request)), + const [targetOrderProviderResponse, feeOrderProviderResponse] = await Promise.all( + _.map(requests, async request => this.orderProvider.getOrdersAsync(request)), ); // process the responses into one object const ordersAndFillableAmounts = await orderProviderResponseProcessor.processAsync( - targetOrderFetcherResponse, - feeOrderFetcherResponse, + targetOrderProviderResponse, + feeOrderProviderResponse, zrxTokenAssetData, this._contractWrappers.orderValidator, ); diff --git a/packages/asset-buyer/src/index.ts b/packages/asset-buyer/src/index.ts index aa10c3af2..8ef529ac0 100644 --- a/packages/asset-buyer/src/index.ts +++ b/packages/asset-buyer/src/index.ts @@ -3,8 +3,8 @@ export { SignedOrder } from '@0xproject/types'; export { BigNumber } from '@0xproject/utils'; export { AssetBuyer } from './asset_buyer'; -export { ProvidedOrderFetcher } from './order_fetchers/provided_order_fetcher'; -export { StandardRelayerAPIOrderFetcher } from './order_fetchers/standard_relayer_api_order_fetcher'; +export { BasicOrderProvider } from './order_providers/basic_order_provider'; +export { StandardRelayerAPIOrderProvider } from './order_providers/standard_relayer_api_order_provider'; export { StandardRelayerAPIAssetBuyerManager } from './standard_relayer_api_asset_buyer_manager'; export { AssetBuyerError, diff --git a/packages/asset-buyer/src/order_fetchers/provided_order_fetcher.ts b/packages/asset-buyer/src/order_fetchers/provided_order_fetcher.ts deleted file mode 100644 index 397f296d7..000000000 --- a/packages/asset-buyer/src/order_fetchers/provided_order_fetcher.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { schemas } from '@0xproject/json-schemas'; -import { SignedOrder } from '@0xproject/types'; -import * as _ from 'lodash'; - -import { OrderProvider, OrderProviderRequest, OrderProviderResponse } from '../types'; -import { assert } from '../utils/assert'; - -export class ProvidedOrderFetcher implements OrderProvider { - public readonly providedOrders: SignedOrder[]; - /** - * Instantiates a new ProvidedOrderFetcher instance - * @param providedOrders An array of objects that conform to SignedOrder to fetch from. - * @return An instance of ProvidedOrderFetcher - */ - constructor(providedOrders: SignedOrder[]) { - assert.doesConformToSchema('providedOrders', providedOrders, schemas.signedOrdersSchema); - this.providedOrders = providedOrders; - } - /** - * Given an object that conforms to OrderFetcherRequest, return the corresponding OrderFetcherResponse that satisfies the request. - * @param orderFetchRequest An instance of OrderFetcherRequest. See type for more information. - * @return An instance of OrderFetcherResponse. See type for more information. - */ - public async getOrdersAsync(orderFetchRequest: OrderProviderRequest): Promise { - assert.isValidOrderFetcherRequest('orderFetchRequest', orderFetchRequest); - const { makerAssetData, takerAssetData } = orderFetchRequest; - const orders = _.filter(this.providedOrders, order => { - return order.makerAssetData === makerAssetData && order.takerAssetData === takerAssetData; - }); - return { orders }; - } -} diff --git a/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts b/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts deleted file mode 100644 index 730eaf4e0..000000000 --- a/packages/asset-buyer/src/order_fetchers/standard_relayer_api_order_fetcher.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { APIOrder, HttpClient, OrderbookResponse } from '@0xproject/connect'; -import * as _ from 'lodash'; - -import { - AssetBuyerError, - OrderProvider, - OrderProviderRequest, - OrderProviderResponse, - SignedOrderWithRemainingFillableMakerAssetAmount, -} from '../types'; -import { assert } from '../utils/assert'; -import { orderUtils } from '../utils/order_utils'; - -export class StandardRelayerAPIOrderFetcher implements OrderProvider { - public readonly apiUrl: string; - private readonly _sraClient: HttpClient; - /** - * Given an array of APIOrder objects from a standard relayer api, return an array - * of SignedOrderWithRemainingFillableMakerAssetAmounts - */ - private static _getSignedOrderWithRemainingFillableMakerAssetAmountFromApi( - apiOrders: APIOrder[], - ): SignedOrderWithRemainingFillableMakerAssetAmount[] { - const result = _.map(apiOrders, apiOrder => { - const { order, metaData } = apiOrder; - // calculate remainingFillableMakerAssetAmount from api metadata, else assume order is completely fillable - const remainingFillableTakerAssetAmount = _.get( - metaData, - 'remainingTakerAssetAmount', - order.takerAssetAmount, - ); - const remainingFillableMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( - order, - remainingFillableTakerAssetAmount, - ); - const newOrder = { - ...order, - remainingFillableMakerAssetAmount, - }; - return newOrder; - }); - return result; - } - /** - * Instantiates a new StandardRelayerAPIOrderFetcher instance - * @param apiUrl The standard relayer API base HTTP url you would like to source orders from. - * @return An instance of StandardRelayerAPIOrderFetcher - */ - constructor(apiUrl: string) { - assert.isWebUri('apiUrl', apiUrl); - this.apiUrl = apiUrl; - this._sraClient = new HttpClient(apiUrl); - } - /** - * Given an object that conforms to OrderFetcherRequest, return the corresponding OrderFetcherResponse that satisfies the request. - * @param orderFetchRequest An instance of OrderFetcherRequest. See type for more information. - * @return An instance of OrderFetcherResponse. See type for more information. - */ - public async getOrdersAsync(orderFetchRequest: OrderProviderRequest): Promise { - assert.isValidOrderFetcherRequest('orderFetchRequest', orderFetchRequest); - const { makerAssetData, takerAssetData, networkId } = orderFetchRequest; - const orderbookRequest = { baseAssetData: makerAssetData, quoteAssetData: takerAssetData }; - const requestOpts = { networkId }; - let orderbook: OrderbookResponse; - try { - orderbook = await this._sraClient.getOrderbookAsync(orderbookRequest, requestOpts); - } catch (err) { - throw new Error(AssetBuyerError.StandardRelayerApiError); - } - const apiOrders = orderbook.asks.records; - const orders = StandardRelayerAPIOrderFetcher._getSignedOrderWithRemainingFillableMakerAssetAmountFromApi( - apiOrders, - ); - return { - orders, - }; - } -} diff --git a/packages/asset-buyer/src/order_providers/basic_order_provider.ts b/packages/asset-buyer/src/order_providers/basic_order_provider.ts new file mode 100644 index 000000000..9bb2d90ac --- /dev/null +++ b/packages/asset-buyer/src/order_providers/basic_order_provider.ts @@ -0,0 +1,32 @@ +import { schemas } from '@0xproject/json-schemas'; +import { SignedOrder } from '@0xproject/types'; +import * as _ from 'lodash'; + +import { OrderProvider, OrderProviderRequest, OrderProviderResponse } from '../types'; +import { assert } from '../utils/assert'; + +export class BasicOrderProvider implements OrderProvider { + public readonly orders: SignedOrder[]; + /** + * Instantiates a new BasicOrderProvider instance + * @param orders An array of objects that conform to SignedOrder to fetch from. + * @return An instance of BasicOrderProvider + */ + constructor(orders: SignedOrder[]) { + assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); + this.orders = orders; + } + /** + * Given an object that conforms to OrderFetcherRequest, return the corresponding OrderProviderResponse that satisfies the request. + * @param orderProviderRequest An instance of OrderFetcherRequest. See type for more information. + * @return An instance of OrderProviderResponse. See type for more information. + */ + public async getOrdersAsync(orderProviderRequest: OrderProviderRequest): Promise { + assert.isValidOrderProviderRequest('orderProviderRequest', orderProviderRequest); + const { makerAssetData, takerAssetData } = orderProviderRequest; + const orders = _.filter(this.orders, order => { + return order.makerAssetData === makerAssetData && order.takerAssetData === takerAssetData; + }); + return { orders }; + } +} diff --git a/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts b/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts new file mode 100644 index 000000000..be3e9da50 --- /dev/null +++ b/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts @@ -0,0 +1,78 @@ +import { APIOrder, HttpClient, OrderbookResponse } from '@0xproject/connect'; +import * as _ from 'lodash'; + +import { + AssetBuyerError, + OrderProvider, + OrderProviderRequest, + OrderProviderResponse, + SignedOrderWithRemainingFillableMakerAssetAmount, +} from '../types'; +import { assert } from '../utils/assert'; +import { orderUtils } from '../utils/order_utils'; + +export class StandardRelayerAPIOrderProvider implements OrderProvider { + public readonly apiUrl: string; + private readonly _sraClient: HttpClient; + /** + * Given an array of APIOrder objects from a standard relayer api, return an array + * of SignedOrderWithRemainingFillableMakerAssetAmounts + */ + private static _getSignedOrderWithRemainingFillableMakerAssetAmountFromApi( + apiOrders: APIOrder[], + ): SignedOrderWithRemainingFillableMakerAssetAmount[] { + const result = _.map(apiOrders, apiOrder => { + const { order, metaData } = apiOrder; + // calculate remainingFillableMakerAssetAmount from api metadata, else assume order is completely fillable + const remainingFillableTakerAssetAmount = _.get( + metaData, + 'remainingTakerAssetAmount', + order.takerAssetAmount, + ); + const remainingFillableMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( + order, + remainingFillableTakerAssetAmount, + ); + const newOrder = { + ...order, + remainingFillableMakerAssetAmount, + }; + return newOrder; + }); + return result; + } + /** + * Instantiates a new StandardRelayerAPIOrderProvider instance + * @param apiUrl The standard relayer API base HTTP url you would like to source orders from. + * @return An instance of StandardRelayerAPIOrderProvider + */ + constructor(apiUrl: string) { + assert.isWebUri('apiUrl', apiUrl); + this.apiUrl = apiUrl; + this._sraClient = new HttpClient(apiUrl); + } + /** + * Given an object that conforms to OrderProviderRequest, return the corresponding OrderProviderResponse that satisfies the request. + * @param orderProviderRequest An instance of OrderProviderRequest. See type for more information. + * @return An instance of OrderProviderResponse. See type for more information. + */ + public async getOrdersAsync(orderProviderRequest: OrderProviderRequest): Promise { + assert.isValidOrderProviderRequest('orderProviderRequest', orderProviderRequest); + const { makerAssetData, takerAssetData, networkId } = orderProviderRequest; + const orderbookRequest = { baseAssetData: makerAssetData, quoteAssetData: takerAssetData }; + const requestOpts = { networkId }; + let orderbook: OrderbookResponse; + try { + orderbook = await this._sraClient.getOrderbookAsync(orderbookRequest, requestOpts); + } catch (err) { + throw new Error(AssetBuyerError.StandardRelayerApiError); + } + const apiOrders = orderbook.asks.records; + const orders = StandardRelayerAPIOrderProvider._getSignedOrderWithRemainingFillableMakerAssetAmountFromApi( + apiOrders, + ); + return { + orders, + }; + } +} diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index 09319fca4..5df8e632a 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -27,7 +27,7 @@ export interface SignedOrderWithRemainingFillableMakerAssetAmount extends Signed remainingFillableMakerAssetAmount?: BigNumber; } /** - * Given an OrderFetchRequest, get an OrderFetchResponse. + * Given an OrderProviderRequest, get an OrderProviderResponse. */ export interface OrderProvider { getOrdersAsync: (orderProviderRequest: OrderProviderRequest) => Promise; diff --git a/packages/asset-buyer/src/utils/assert.ts b/packages/asset-buyer/src/utils/assert.ts index 1d57ae9b8..2c3426894 100644 --- a/packages/asset-buyer/src/utils/assert.ts +++ b/packages/asset-buyer/src/utils/assert.ts @@ -18,10 +18,10 @@ export const assert = { sharedAssert.isNumber(`${variableName}.feePercentage`, buyQuote.feePercentage); } }, - isValidOrderFetcher(variableName: string, orderFetcher: OrderProvider): void { + isValidOrderProvider(variableName: string, orderFetcher: OrderProvider): void { sharedAssert.isFunction(`${variableName}.fetchOrdersAsync`, orderFetcher.getOrdersAsync); }, - isValidOrderFetcherRequest(variableName: string, orderFetcherRequest: OrderProviderRequest): void { + isValidOrderProviderRequest(variableName: string, orderFetcherRequest: OrderProviderRequest): void { sharedAssert.isHexString(`${variableName}.makerAssetData`, orderFetcherRequest.makerAssetData); sharedAssert.isHexString(`${variableName}.takerAssetData`, orderFetcherRequest.takerAssetData); sharedAssert.isNumber(`${variableName}.networkId`, orderFetcherRequest.networkId); diff --git a/packages/asset-buyer/src/utils/buy_quote_calculator.ts b/packages/asset-buyer/src/utils/buy_quote_calculator.ts index 3d90a6ead..9946924ef 100644 --- a/packages/asset-buyer/src/utils/buy_quote_calculator.ts +++ b/packages/asset-buyer/src/utils/buy_quote_calculator.ts @@ -7,6 +7,7 @@ import { AssetBuyerError, AssetBuyerOrdersAndFillableAmounts, BuyQuote } from '. import { orderUtils } from './order_utils'; +// Calculates a buy quote for orders that have WETH as the takerAsset export const buyQuoteCalculator = { calculate( ordersAndFillableAmounts: AssetBuyerOrdersAndFillableAmounts, -- cgit v1.2.3 From c48cf3ab3b74a9082eb30540115534c7432667ac Mon Sep 17 00:00:00 2001 From: fragosti Date: Fri, 21 Sep 2018 16:03:47 +0200 Subject: Make asset buyer map private --- .../asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts b/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts index 207713110..947c738a1 100644 --- a/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts +++ b/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts @@ -13,7 +13,7 @@ import { OrderProvider, StandardRelayerApiAssetBuyerManagerError } from './types export class StandardRelayerAPIAssetBuyerManager { // Map of assetData to AssetBuyer for that assetData - public readonly assetBuyerMap: ObjectMap; + private readonly _assetBuyerMap: ObjectMap; /** * Returns an array of all assetDatas available at the provided sraApiUrl * @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from. @@ -82,7 +82,7 @@ export class StandardRelayerAPIAssetBuyerManager { orderRefreshIntervalMs?: number, ) { assert.assert(assetDatas.length > 0, `Expected 'assetDatas' to be a non-empty array.`); - this.assetBuyerMap = _.reduce( + this._assetBuyerMap = _.reduce( assetDatas, (accAssetBuyerMap: ObjectMap, assetData: string) => { accAssetBuyerMap[assetData] = new AssetBuyer( @@ -104,7 +104,7 @@ export class StandardRelayerAPIAssetBuyerManager { * @return An instance of AssetBuyer */ public getAssetBuyerFromAssetData(assetData: string): AssetBuyer { - const assetBuyer = this.assetBuyerMap[assetData]; + const assetBuyer = this._assetBuyerMap[assetData]; if (_.isUndefined(assetBuyer)) { throw new Error( `${StandardRelayerApiAssetBuyerManagerError.AssetBuyerNotFound}: For assetData ${assetData}`, @@ -128,6 +128,6 @@ export class StandardRelayerAPIAssetBuyerManager { * @return An array of assetData strings */ public getAssetDatas(): string[] { - return _.keys(this.assetBuyerMap); + return _.keys(this._assetBuyerMap); } } -- cgit v1.2.3 From 1bfaefb240d08eb0a8a5d6743d529604383f6e43 Mon Sep 17 00:00:00 2001 From: fragosti Date: Fri, 21 Sep 2018 16:24:45 +0200 Subject: Add order provider response validation --- packages/asset-buyer/src/asset_buyer.ts | 4 ++++ packages/asset-buyer/src/types.ts | 1 + .../asset-buyer/src/utils/order_provider_response_processor.ts | 10 ++++++++++ 3 files changed, 15 insertions(+) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 9774a8d39..03f9b5a2b 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -269,6 +269,10 @@ export class AssetBuyer { const [targetOrderProviderResponse, feeOrderProviderResponse] = await Promise.all( _.map(requests, async request => this.orderProvider.getOrdersAsync(request)), ); + // since the order provider is an injected dependency, validate that it respects the API + // ie. it should only return maker/taker assetDatas that are specified + orderProviderResponseProcessor.throwIfInvalidResponse(targetOrderProviderResponse, targetOrderProviderRequest); + orderProviderResponseProcessor.throwIfInvalidResponse(feeOrderProviderResponse, feeOrderProviderRequest); // process the responses into one object const ordersAndFillableAmounts = await orderProviderResponseProcessor.processAsync( targetOrderProviderResponse, diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index 5df8e632a..ee6858525 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -68,6 +68,7 @@ export enum AssetBuyerError { InsufficientAssetLiquidity = 'INSUFFICIENT_ASSET_LIQUIDITY', InsufficientZrxLiquidity = 'INSUFFICIENT_ZRX_LIQUIDITY', NoAddressAvailable = 'NO_ADDRESS_AVAILABLE', + InvalidOrderProviderResponse = 'INVALID_ORDER_PROVIDER_RESPONSE', } /** diff --git a/packages/asset-buyer/src/utils/order_provider_response_processor.ts b/packages/asset-buyer/src/utils/order_provider_response_processor.ts index 79e15c3d8..2f3208a55 100644 --- a/packages/asset-buyer/src/utils/order_provider_response_processor.ts +++ b/packages/asset-buyer/src/utils/order_provider_response_processor.ts @@ -7,7 +7,9 @@ import * as _ from 'lodash'; import { constants } from '../constants'; import { + AssetBuyerError, AssetBuyerOrdersAndFillableAmounts, + OrderProviderRequest, OrderProviderResponse, SignedOrderWithRemainingFillableMakerAssetAmount, } from '../types'; @@ -20,6 +22,14 @@ interface OrdersAndRemainingFillableMakerAssetAmounts { } export const orderProviderResponseProcessor = { + throwIfInvalidResponse(response: OrderProviderResponse, request: OrderProviderRequest): void { + const { makerAssetData, takerAssetData } = request; + _.forEach(response.orders, order => { + if (order.makerAssetData !== makerAssetData || order.takerAssetData !== takerAssetData) { + throw new Error(AssetBuyerError.InvalidOrderProviderResponse); + } + }); + }, /** * Take the responses for the target orders to buy and fee orders and process them. * Processing includes: -- cgit v1.2.3 From 5e84e9689f0899a39dbfaffe13b80fa07fec5ba8 Mon Sep 17 00:00:00 2001 From: fragosti Date: Fri, 21 Sep 2018 16:53:18 +0200 Subject: Small typos --- packages/asset-buyer/src/utils/assert.ts | 2 +- packages/asset-buyer/src/utils/order_provider_response_processor.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/utils/assert.ts b/packages/asset-buyer/src/utils/assert.ts index 2c3426894..04f425237 100644 --- a/packages/asset-buyer/src/utils/assert.ts +++ b/packages/asset-buyer/src/utils/assert.ts @@ -19,7 +19,7 @@ export const assert = { } }, isValidOrderProvider(variableName: string, orderFetcher: OrderProvider): void { - sharedAssert.isFunction(`${variableName}.fetchOrdersAsync`, orderFetcher.getOrdersAsync); + sharedAssert.isFunction(`${variableName}.getOrdersAsync`, orderFetcher.getOrdersAsync); }, isValidOrderProviderRequest(variableName: string, orderFetcherRequest: OrderProviderRequest): void { sharedAssert.isHexString(`${variableName}.makerAssetData`, orderFetcherRequest.makerAssetData); diff --git a/packages/asset-buyer/src/utils/order_provider_response_processor.ts b/packages/asset-buyer/src/utils/order_provider_response_processor.ts index 2f3208a55..9e54300b3 100644 --- a/packages/asset-buyer/src/utils/order_provider_response_processor.ts +++ b/packages/asset-buyer/src/utils/order_provider_response_processor.ts @@ -52,7 +52,7 @@ export const orderProviderResponseProcessor = { // if an orderValidator is provided, use on chain information to calculate remaining fillable makerAsset amounts if (!_.isUndefined(orderValidator)) { // TODO(bmillman): improvement - // try catch these requests and throw a more domain specific error + // try/catch these requests and throw a more domain specific error // TODO(bmillman): optimization // reduce this to once RPC call buy combining orders into one array and then splitting up the response const [targetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all( -- cgit v1.2.3 From e4b664bafa24c2ee788a343edecd0124f784780d Mon Sep 17 00:00:00 2001 From: fragosti Date: Mon, 24 Sep 2018 13:17:38 +0200 Subject: Add expiry buffer --- packages/asset-buyer/src/asset_buyer.ts | 45 +++++++++++++++++----- packages/asset-buyer/src/constants.ts | 1 + .../src/utils/order_provider_response_processor.ts | 14 +++++-- packages/asset-buyer/src/utils/order_utils.ts | 5 ++- 4 files changed, 51 insertions(+), 14 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 03f9b5a2b..409e34e74 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -29,6 +29,7 @@ export class AssetBuyer { public readonly orderProvider: OrderProvider; public readonly networkId: number; public readonly orderRefreshIntervalMs: number; + public readonly expiryBufferSeconds: number; private readonly _contractWrappers: ContractWrappers; private _lastRefreshTimeIfExists?: number; private _currentOrdersAndFillableAmountsIfExists?: AssetBuyerOrdersAndFillableAmounts; @@ -38,8 +39,9 @@ export class AssetBuyer { * @param orders A non-empty array of objects that conform to SignedOrder. All orders must have the same makerAssetData and takerAssetData (WETH). * @param feeOrders A array of objects that conform to SignedOrder. All orders must have the same makerAssetData (ZRX) and takerAssetData (WETH). Defaults to an empty array. * @param networkId The ethereum network id. Defaults to 1 (mainnet). - * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. - * Defaults to 10000ms (10s). + * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s). + * @param expiryBufferSeconds The number of seconds to add when calculating whether an order is expired or not. Defaults to 15s. + * * @return An instance of AssetBuyer */ public static getAssetBuyerForProvidedOrders( @@ -48,6 +50,7 @@ export class AssetBuyer { feeOrders: SignedOrder[] = [], networkId: number = constants.MAINNET_NETWORK_ID, orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS, + expiryBufferSeconds: number = constants.DEFAULT_EXPIRY_BUFFER_SECONDS, ): AssetBuyer { assert.isWeb3Provider('provider', provider); assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); @@ -59,7 +62,14 @@ export class AssetBuyer { assert.assert(orders.length !== 0, `Expected orders to contain at least one order`); const assetData = orders[0].makerAssetData; const orderProvider = new BasicOrderProvider(_.concat(orders, feeOrders)); - const assetBuyer = new AssetBuyer(provider, assetData, orderProvider, networkId, orderRefreshIntervalMs); + const assetBuyer = new AssetBuyer( + provider, + assetData, + orderProvider, + networkId, + orderRefreshIntervalMs, + expiryBufferSeconds, + ); return assetBuyer; } /** @@ -68,8 +78,9 @@ export class AssetBuyer { * @param assetData The assetData that identifies the desired asset to buy. * @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from. * @param networkId The ethereum network id. Defaults to 1 (mainnet). - * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. - * Defaults to 10000ms (10s). + * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s). + * @param expiryBufferSeconds The number of seconds to add when calculating whether an order is expired or not. Defaults to 15s. + * * @return An instance of AssetBuyer */ public static getAssetBuyerForAssetData( @@ -78,6 +89,7 @@ export class AssetBuyer { sraApiUrl: string, networkId: number = constants.MAINNET_NETWORK_ID, orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS, + expiryBufferSeconds: number = constants.DEFAULT_EXPIRY_BUFFER_SECONDS, ): AssetBuyer { assert.isWeb3Provider('provider', provider); assert.isHexString('assetData', assetData); @@ -85,7 +97,14 @@ export class AssetBuyer { assert.isNumber('networkId', networkId); assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs); const orderProvider = new StandardRelayerAPIOrderProvider(sraApiUrl); - const assetBuyer = new AssetBuyer(provider, assetData, orderProvider, networkId, orderRefreshIntervalMs); + const assetBuyer = new AssetBuyer( + provider, + assetData, + orderProvider, + networkId, + orderRefreshIntervalMs, + expiryBufferSeconds, + ); return assetBuyer; } /** @@ -94,8 +113,8 @@ export class AssetBuyer { * @param tokenAddress The ERC20 token address that identifies the desired asset to buy. * @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from. * @param networkId The ethereum network id. Defaults to 1 (mainnet). - * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. - * Defaults to 10000ms (10s). + * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s). + * @param expiryBufferSeconds The number of seconds to add when calculating whether an order is expired or not. Defaults to 15s. * @return An instance of AssetBuyer */ public static getAssetBuyerForERC20TokenAddress( @@ -104,6 +123,7 @@ export class AssetBuyer { sraApiUrl: string, networkId: number = constants.MAINNET_NETWORK_ID, orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS, + expiryBufferSeconds: number = constants.DEFAULT_EXPIRY_BUFFER_SECONDS, ): AssetBuyer { assert.isWeb3Provider('provider', provider); assert.isETHAddressHex('tokenAddress', tokenAddress); @@ -117,6 +137,7 @@ export class AssetBuyer { sraApiUrl, networkId, orderRefreshIntervalMs, + expiryBufferSeconds, ); return assetBuyer; } @@ -126,8 +147,9 @@ export class AssetBuyer { * @param assetData The assetData of the desired asset to buy (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). * @param orderProvider An object that conforms to OrderProvider, see type for definition. * @param networkId The ethereum network id. Defaults to 1 (mainnet). - * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. - * Defaults to 10000ms (10s). + * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s). + * @param expiryBufferSeconds The number of seconds to add when calculating whether an order is expired or not. Defaults to 15s. + * * @return An instance of AssetBuyer */ constructor( @@ -136,6 +158,7 @@ export class AssetBuyer { orderProvider: OrderProvider, networkId: number = constants.MAINNET_NETWORK_ID, orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS, + expiryBufferSeconds: number = constants.DEFAULT_EXPIRY_BUFFER_SECONDS, ) { assert.isWeb3Provider('provider', provider); assert.isString('assetData', assetData); @@ -146,6 +169,7 @@ export class AssetBuyer { this.assetData = assetData; this.orderProvider = orderProvider; this.networkId = networkId; + this.expiryBufferSeconds = expiryBufferSeconds; this.orderRefreshIntervalMs = orderRefreshIntervalMs; this._contractWrappers = new ContractWrappers(this.provider, { networkId, @@ -278,6 +302,7 @@ export class AssetBuyer { targetOrderProviderResponse, feeOrderProviderResponse, zrxTokenAssetData, + this.expiryBufferSeconds, this._contractWrappers.orderValidator, ); return ordersAndFillableAmounts; diff --git a/packages/asset-buyer/src/constants.ts b/packages/asset-buyer/src/constants.ts index 0ebe0f8e2..79b5d9052 100644 --- a/packages/asset-buyer/src/constants.ts +++ b/packages/asset-buyer/src/constants.ts @@ -16,4 +16,5 @@ export const constants = { ETHER_TOKEN_DECIMALS: 18, DEFAULT_BUY_QUOTE_REQUEST_OPTS, MAX_PER_PAGE: 10000, + DEFAULT_EXPIRY_BUFFER_SECONDS: 15, }; diff --git a/packages/asset-buyer/src/utils/order_provider_response_processor.ts b/packages/asset-buyer/src/utils/order_provider_response_processor.ts index 9e54300b3..31fdcc182 100644 --- a/packages/asset-buyer/src/utils/order_provider_response_processor.ts +++ b/packages/asset-buyer/src/utils/order_provider_response_processor.ts @@ -41,11 +41,18 @@ export const orderProviderResponseProcessor = { targetOrderProviderResponse: OrderProviderResponse, feeOrderProviderResponse: OrderProviderResponse, zrxTokenAssetData: string, + expiryBufferSeconds: number, orderValidator?: OrderValidatorWrapper, ): Promise { // drop orders that are expired or not open - const filteredTargetOrders = filterOutExpiredAndNonOpenOrders(targetOrderProviderResponse.orders); - const filteredFeeOrders = filterOutExpiredAndNonOpenOrders(feeOrderProviderResponse.orders); + const filteredTargetOrders = filterOutExpiredAndNonOpenOrders( + targetOrderProviderResponse.orders, + expiryBufferSeconds, + ); + const filteredFeeOrders = filterOutExpiredAndNonOpenOrders( + feeOrderProviderResponse.orders, + expiryBufferSeconds, + ); // set the orders to be sorted equal to the filtered orders let unsortedTargetOrders = filteredTargetOrders; let unsortedFeeOrders = filteredFeeOrders; @@ -98,9 +105,10 @@ export const orderProviderResponseProcessor = { */ function filterOutExpiredAndNonOpenOrders( orders: SignedOrderWithRemainingFillableMakerAssetAmount[], + expiryBufferSeconds: number, ): SignedOrderWithRemainingFillableMakerAssetAmount[] { const result = _.filter(orders, order => { - return orderUtils.isOpenOrder(order) && !orderUtils.isOrderExpired(order); + return orderUtils.isOpenOrder(order) && !orderUtils.willOrderExpire(order, expiryBufferSeconds); }); return result; } diff --git a/packages/asset-buyer/src/utils/order_utils.ts b/packages/asset-buyer/src/utils/order_utils.ts index 27db73257..62166eb76 100644 --- a/packages/asset-buyer/src/utils/order_utils.ts +++ b/packages/asset-buyer/src/utils/order_utils.ts @@ -5,9 +5,12 @@ import { constants } from '../constants'; export const orderUtils = { isOrderExpired(order: SignedOrder): boolean { + return orderUtils.willOrderExpire(order, 0); + }, + willOrderExpire(order: SignedOrder, secondsFromNow: number): boolean { const millisecondsInSecond = 1000; const currentUnixTimestampSec = new BigNumber(Date.now() / millisecondsInSecond).round(); - return order.expirationTimeSeconds.lessThan(currentUnixTimestampSec); + return order.expirationTimeSeconds.lessThan(currentUnixTimestampSec.minus(secondsFromNow)); }, calculateRemainingMakerAssetAmount(order: SignedOrder, remainingTakerAssetAmount: BigNumber): BigNumber { if (remainingTakerAssetAmount.eq(0)) { -- cgit v1.2.3 From 7a43e1911615d4f59a2cecdb2a98cac95f205f86 Mon Sep 17 00:00:00 2001 From: fragosti Date: Mon, 24 Sep 2018 15:43:02 +0200 Subject: Improve README --- packages/asset-buyer/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/README.md b/packages/asset-buyer/README.md index f615b93e1..5f7f26f30 100644 --- a/packages/asset-buyer/README.md +++ b/packages/asset-buyer/README.md @@ -1,11 +1,9 @@ ## @0xproject/asset-buyer -Convenience package for buying assets represented on the Ethereum blockchain using 0x. In its simplest form, the package helps in the usage of the [0x forwarder contract](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/forwarder-specification.md), which allows users to execute Wrapped Ether based 0x orders without having to set allowances, wrap Ether or buy ZRX, meaning they can buy tokens with Ether alone. Given some liquidity (0x signed orders), it helps estimate the Ether cost of buying a certain asset (giving a range) and then buying that asset. +Convenience package for buying assets represented on the Ethereum blockchain using 0x. In its simplest form, the package helps in the usage of the [0x forwarder contract](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/forwarder-specification.md), which allows users to execute [Wrapped Ether](https://weth.io/) based 0x orders without having to set allowances, wrap Ether or buy ZRX, meaning they can buy tokens with Ether alone. Given some liquidity (0x signed orders), it helps estimate the Ether cost of buying a certain asset (giving a range) and then buying that asset. In its more advanced and useful form, it integrates with the [Standard Relayer API](https://github.com/0xProject/standard-relayer-api) and takes care of sourcing liquidity for you given an SRA compliant endpoint. The final result is a library that tells you what assets are available, provides an Ether based quote for any asset desired, and allows you to buy that asset using Ether alone. -### Read the [Documentation](https://0xproject.com/docs/asset-buyer). - ## Installation ```bash -- cgit v1.2.3 From d8d1c98a40f5cc895329f73af4d8d000c6a5195b Mon Sep 17 00:00:00 2001 From: fragosti Date: Mon, 24 Sep 2018 15:53:34 +0200 Subject: Upgrade all deps --- packages/asset-buyer/package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/package.json b/packages/asset-buyer/package.json index f80f8db7e..2b5136e31 100644 --- a/packages/asset-buyer/package.json +++ b/packages/asset-buyer/package.json @@ -36,16 +36,16 @@ }, "homepage": "https://github.com/0xProject/0x-monorepo/packages/asset-buyer/README.md", "dependencies": { - "@0xproject/assert": "^1.0.8", - "@0xproject/connect": "^2.0.0", - "@0xproject/contract-wrappers": "^1.0.1", - "@0xproject/json-schemas": "^1.0.1", + "@0xproject/assert": "^1.0.9", + "@0xproject/connect": "^2.0.2", + "@0xproject/contract-wrappers": "^1.0.4", + "@0xproject/json-schemas": "^1.0.2", "@0xproject/order-utils": "^1.0.1", "@0xproject/subproviders": "^2.0.2", - "@0xproject/types": "^1.0.1", - "@0xproject/typescript-typings": "^2.0.0", - "@0xproject/utils": "^1.0.8", - "@0xproject/web3-wrapper": "^2.0.2", + "@0xproject/types": "^1.0.2", + "@0xproject/typescript-typings": "^2.0.1", + "@0xproject/utils": "^1.0.9", + "@0xproject/web3-wrapper": "^2.0.3", "ethereum-types": "^1.0.6", "lodash": "^4.17.10" }, -- cgit v1.2.3 From b85db17e75e818b5b554cc2b8068aba980d95804 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Tue, 25 Sep 2018 12:47:31 +0200 Subject: Move SRA types from @0xproject/connect to @0xproject/sra-types --- .../src/order_providers/standard_relayer_api_order_provider.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts b/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts index be3e9da50..31942c25b 100644 --- a/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts +++ b/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts @@ -1,4 +1,5 @@ -import { APIOrder, HttpClient, OrderbookResponse } from '@0xproject/connect'; +import { HttpClient } from '@0xproject/connect'; +import { APIOrder, OrderbookResponse } from '@0xproject/types'; import * as _ from 'lodash'; import { -- cgit v1.2.3 From 411813d8d9f23d535f10dd3fdd9e824b244bf187 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Tue, 25 Sep 2018 13:48:44 +0200 Subject: Updated CHANGELOGS --- packages/asset-buyer/CHANGELOG.json | 9 +++++++++ packages/asset-buyer/CHANGELOG.md | 13 +++++++++++++ 2 files changed, 22 insertions(+) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/CHANGELOG.json b/packages/asset-buyer/CHANGELOG.json index 2e4db1f6e..50b965f2d 100644 --- a/packages/asset-buyer/CHANGELOG.json +++ b/packages/asset-buyer/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "timestamp": 1537875740, + "version": "1.0.0", + "changes": [ + { + "note": "Dependencies updated" + } + ] + }, { "version": "1.0.0-rc.1", "changes": [ diff --git a/packages/asset-buyer/CHANGELOG.md b/packages/asset-buyer/CHANGELOG.md index 8b1378917..b27795d18 100644 --- a/packages/asset-buyer/CHANGELOG.md +++ b/packages/asset-buyer/CHANGELOG.md @@ -1 +1,14 @@ + +CHANGELOG + +## v1.0.0 - _September 25, 2018_ + + * Dependencies updated + +## v1.0.0-rc.1 - _Invalid date_ + + * Init -- cgit v1.2.3 From 78ef98c27ce954f7e46b261b0809ff9d8d70519b Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Tue, 25 Sep 2018 13:48:55 +0200 Subject: Publish - 0x.js@1.0.5 - @0xproject/abi-gen@1.0.10 - @0xproject/assert@1.0.10 - @0xproject/asset-buyer@1.0.0 - @0xproject/base-contract@2.0.4 - @0xproject/connect@2.0.3 - @0xproject/contract-wrappers@1.0.5 - contracts@2.1.46 - @0xproject/dev-utils@1.0.9 - @0xproject/fill-scenarios@1.0.4 - @0xproject/json-schemas@1.0.3 - @0xproject/metacoin@0.0.20 - @0xproject/migrations@1.0.11 - @0xproject/order-utils@1.0.4 - @0xproject/order-watcher@1.0.5 - @0xproject/react-docs@1.0.10 - @0xproject/react-shared@1.0.11 - @0xproject/sol-compiler@1.1.4 - @0xproject/sol-cov@2.1.4 - @0xproject/sol-resolver@1.0.10 - @0xproject/sra-report@1.0.10 - @0xproject/sra-spec@1.0.3 - @0xproject/subproviders@2.0.4 - @0xproject/testnet-faucets@1.0.48 - @0xproject/types@1.1.0 - @0xproject/utils@1.0.10 - @0xproject/web3-wrapper@3.0.0 - @0xproject/website@0.0.51 --- packages/asset-buyer/package.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/package.json b/packages/asset-buyer/package.json index 2b5136e31..d15213e0b 100644 --- a/packages/asset-buyer/package.json +++ b/packages/asset-buyer/package.json @@ -1,6 +1,6 @@ { "name": "@0xproject/asset-buyer", - "version": "1.0.0-rc.1", + "version": "1.0.0", "engines": { "node": ">=6.12" }, @@ -36,16 +36,16 @@ }, "homepage": "https://github.com/0xProject/0x-monorepo/packages/asset-buyer/README.md", "dependencies": { - "@0xproject/assert": "^1.0.9", - "@0xproject/connect": "^2.0.2", - "@0xproject/contract-wrappers": "^1.0.4", - "@0xproject/json-schemas": "^1.0.2", - "@0xproject/order-utils": "^1.0.1", - "@0xproject/subproviders": "^2.0.2", - "@0xproject/types": "^1.0.2", + "@0xproject/assert": "^1.0.10", + "@0xproject/connect": "^2.0.3", + "@0xproject/contract-wrappers": "^1.0.5", + "@0xproject/json-schemas": "^1.0.3", + "@0xproject/order-utils": "^1.0.4", + "@0xproject/subproviders": "^2.0.4", + "@0xproject/types": "^1.1.0", "@0xproject/typescript-typings": "^2.0.1", - "@0xproject/utils": "^1.0.9", - "@0xproject/web3-wrapper": "^2.0.3", + "@0xproject/utils": "^1.0.10", + "@0xproject/web3-wrapper": "^3.0.0", "ethereum-types": "^1.0.6", "lodash": "^4.17.10" }, -- cgit v1.2.3 From 900a8aee76237778398a5940e723ce30c4af1860 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Tue, 25 Sep 2018 21:33:55 +0100 Subject: Updated CHANGELOGS --- packages/asset-buyer/CHANGELOG.json | 9 +++++++++ packages/asset-buyer/CHANGELOG.md | 4 ++++ 2 files changed, 13 insertions(+) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/CHANGELOG.json b/packages/asset-buyer/CHANGELOG.json index 50b965f2d..d673f0a45 100644 --- a/packages/asset-buyer/CHANGELOG.json +++ b/packages/asset-buyer/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "timestamp": 1537907159, + "version": "1.0.1", + "changes": [ + { + "note": "Dependencies updated" + } + ] + }, { "timestamp": 1537875740, "version": "1.0.0", diff --git a/packages/asset-buyer/CHANGELOG.md b/packages/asset-buyer/CHANGELOG.md index b27795d18..bea4a551b 100644 --- a/packages/asset-buyer/CHANGELOG.md +++ b/packages/asset-buyer/CHANGELOG.md @@ -5,6 +5,10 @@ Edit the package's CHANGELOG.json file only. CHANGELOG +## v1.0.1 - _September 25, 2018_ + + * Dependencies updated + ## v1.0.0 - _September 25, 2018_ * Dependencies updated -- cgit v1.2.3 From 1b35a6e3b5d050fa33097b35e1d739ca13c6806e Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Tue, 25 Sep 2018 21:34:14 +0100 Subject: Publish - 0x.js@1.0.6 - @0xproject/abi-gen@1.0.11 - @0xproject/assert@1.0.11 - @0xproject/asset-buyer@1.0.1 - @0xproject/base-contract@2.0.5 - @0xproject/connect@2.0.4 - @0xproject/contract-wrappers@2.0.0 - contracts@2.1.47 - @0xproject/dev-utils@1.0.10 - ethereum-types@1.0.8 - @0xproject/fill-scenarios@1.0.5 - @0xproject/json-schemas@1.0.4 - @0xproject/metacoin@0.0.21 - @0xproject/migrations@1.0.12 - @0xproject/order-utils@1.0.5 - @0xproject/order-watcher@2.0.0 - @0xproject/react-docs@1.0.11 - @0xproject/react-shared@1.0.12 - @0xproject/sol-compiler@1.1.5 - @0xproject/sol-cov@2.1.5 - @0xproject/sol-resolver@1.0.11 - @0xproject/sra-report@1.0.11 - @0xproject/sra-spec@1.0.4 - @0xproject/subproviders@2.0.5 - @0xproject/testnet-faucets@1.0.49 - @0xproject/types@1.1.1 - @0xproject/typescript-typings@2.0.2 - @0xproject/utils@1.0.11 - @0xproject/web3-wrapper@3.0.1 - @0xproject/website@0.0.52 --- packages/asset-buyer/package.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) (limited to 'packages/asset-buyer') diff --git a/packages/asset-buyer/package.json b/packages/asset-buyer/package.json index d15213e0b..ff0afb782 100644 --- a/packages/asset-buyer/package.json +++ b/packages/asset-buyer/package.json @@ -1,6 +1,6 @@ { "name": "@0xproject/asset-buyer", - "version": "1.0.0", + "version": "1.0.1", "engines": { "node": ">=6.12" }, @@ -36,17 +36,17 @@ }, "homepage": "https://github.com/0xProject/0x-monorepo/packages/asset-buyer/README.md", "dependencies": { - "@0xproject/assert": "^1.0.10", - "@0xproject/connect": "^2.0.3", - "@0xproject/contract-wrappers": "^1.0.5", - "@0xproject/json-schemas": "^1.0.3", - "@0xproject/order-utils": "^1.0.4", - "@0xproject/subproviders": "^2.0.4", - "@0xproject/types": "^1.1.0", - "@0xproject/typescript-typings": "^2.0.1", - "@0xproject/utils": "^1.0.10", - "@0xproject/web3-wrapper": "^3.0.0", - "ethereum-types": "^1.0.6", + "@0xproject/assert": "^1.0.11", + "@0xproject/connect": "^2.0.4", + "@0xproject/contract-wrappers": "^2.0.0", + "@0xproject/json-schemas": "^1.0.4", + "@0xproject/order-utils": "^1.0.5", + "@0xproject/subproviders": "^2.0.5", + "@0xproject/types": "^1.1.1", + "@0xproject/typescript-typings": "^2.0.2", + "@0xproject/utils": "^1.0.11", + "@0xproject/web3-wrapper": "^3.0.1", + "ethereum-types": "^1.0.8", "lodash": "^4.17.10" }, "devDependencies": { -- cgit v1.2.3