diff options
Diffstat (limited to 'packages')
21 files changed, 595 insertions, 16 deletions
diff --git a/packages/forwarder-helper/.npmignore b/packages/forwarder-helper/.npmignore new file mode 100644 index 000000000..5333847e7 --- /dev/null +++ b/packages/forwarder-helper/.npmignore @@ -0,0 +1,8 @@ +.* +yarn-error.log +/src/ +/scripts/ +/schemas/ +test/ +tsconfig.json +/lib/src/monorepo_scripts/ diff --git a/packages/forwarder-helper/CHANGELOG.json b/packages/forwarder-helper/CHANGELOG.json new file mode 100644 index 000000000..f95e7a2db --- /dev/null +++ b/packages/forwarder-helper/CHANGELOG.json @@ -0,0 +1,11 @@ +[ + { + "version": "1.0.0-rc.1", + "changes": [ + { + "note": "Add initial forwarderHelperFactory", + "pr": 997 + } + ] + } +] diff --git a/packages/forwarder-helper/CHANGELOG.md b/packages/forwarder-helper/CHANGELOG.md new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/packages/forwarder-helper/CHANGELOG.md @@ -0,0 +1 @@ + diff --git a/packages/forwarder-helper/README.md b/packages/forwarder-helper/README.md new file mode 100644 index 000000000..c74526910 --- /dev/null +++ b/packages/forwarder-helper/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/forwarder-helper/package.json b/packages/forwarder-helper/package.json new file mode 100644 index 000000000..3a84e4620 --- /dev/null +++ b/packages/forwarder-helper/package.json @@ -0,0 +1,81 @@ +{ + "name": "@0xproject/forwarder-helper", + "version": "1.0.0-rc.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 --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": [], + "docPublishConfigs": { + "extraFileIncludes": [ + "../types/src/index.ts", + "../ethereum-types/src/index.ts" + ], + "s3BucketPath": "s3://doc-jsons/forwarder-helper/", + "s3StagingBucketPath": "s3://staging-doc-jsons/forwarder-helper/" + } + } + }, + "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.5", + "@0xproject/json-schemas": "^1.0.1-rc.4", + "@0xproject/order-utils": "^1.0.1-rc.3", + "@0xproject/types": "^1.0.1-rc.4", + "@0xproject/typescript-typings": "^1.0.4", + "@0xproject/utils": "^1.0.5", + "@types/node": "^8.0.53", + "lodash": "^4.17.10" + }, + "devDependencies": { + "@0xproject/monorepo-scripts": "^1.0.5", + "@0xproject/tslint-config": "^1.0.5", + "@types/lodash": "^4.14.116", + "@types/mocha": "^2.2.42", + "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": "0xProject/typedoc", + "typescript": "3.0.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/forwarder-helper/src/constants.ts b/packages/forwarder-helper/src/constants.ts new file mode 100644 index 000000000..0ad30e4c0 --- /dev/null +++ b/packages/forwarder-helper/src/constants.ts @@ -0,0 +1,5 @@ +import { BigNumber } from '@0xproject/utils'; + +export const constants = { + ZERO_AMOUNT: new BigNumber(0), +}; diff --git a/packages/forwarder-helper/src/forwarder_helper_factory.ts b/packages/forwarder-helper/src/forwarder_helper_factory.ts new file mode 100644 index 000000000..95f11f555 --- /dev/null +++ b/packages/forwarder-helper/src/forwarder_helper_factory.ts @@ -0,0 +1,25 @@ +import { assert } from '@0xproject/assert'; +import { schemas } from '@0xproject/json-schemas'; +import { SignedOrder } from '@0xproject/types'; + +import { ForwarderHelperImpl, ForwarderHelperImplConfig } from './forwarder_helper_impl'; +import { ForwarderHelper } from './types'; + +export const forwarderHelperFactory = { + /** + * Given an array of orders and an array of feeOrders + * @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); + const config: ForwarderHelperImplConfig = { + orders, + feeOrders, + }; + const helper = new ForwarderHelperImpl(config); + return helper; + }, +}; diff --git a/packages/forwarder-helper/src/forwarder_helper_impl.ts b/packages/forwarder-helper/src/forwarder_helper_impl.ts new file mode 100644 index 000000000..0e59aa7d6 --- /dev/null +++ b/packages/forwarder-helper/src/forwarder_helper_impl.ts @@ -0,0 +1,125 @@ +import { marketUtils, sortingUtils } 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'; + +const SLIPPAGE_PERCENTAGE = new BigNumber(0.2); // 20% slippage protection, possibly move this into request interface + +interface SignedOrderWithAmount extends SignedOrder { + remainingFillAmount?: BigNumber; +} + +export interface ForwarderHelperImplConfig { + orders: SignedOrder[]; + feeOrders: SignedOrder[]; + remainingFillableMakerAssetAmounts?: BigNumber[]; + remainingFillableFeeAmounts?: BigNumber[]; +} + +export class ForwarderHelperImpl implements ForwarderHelper { + public readonly config: ForwarderHelperImplConfig; + private static _createSignedOrderWithAmounts( + orders: SignedOrder[], + amounts?: BigNumber[], + ): SignedOrderWithAmount[] { + const ordersAndAmounts = _.map(orders, (order, index) => { + return { + ...order, + remainingFillAmount: _.nth(amounts, index), + }; + }); + return ordersAndAmounts; + } + private static _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; + }); + const compactAmounts = _.compact(amounts); + return { + orders, + amounts: compactAmounts.length > 0 ? compactAmounts : undefined, + }; + } + private static _sortConfig(opts: ForwarderHelperImplConfig): ForwarderHelperImplConfig { + const { orders, feeOrders, remainingFillableMakerAssetAmounts, remainingFillableFeeAmounts } = opts; + // Bundle orders together with their remainingFillAmounts so that we can sort them together + const orderWithAmounts = ForwarderHelperImpl._createSignedOrderWithAmounts( + orders, + remainingFillableMakerAssetAmounts, + ); + // TODO: provide a feeRate to the sorting function to more accurately sort based on the current market for ZRX tokens + const sortedOrderWithAmounts = sortingUtils.sortOrdersByFeeAdjustedRate(orderWithAmounts); + // Unbundle after sorting + const unbundledSortedOrderWithAmounts = ForwarderHelperImpl._unbundleSignedOrderWithAmounts( + sortedOrderWithAmounts, + ); + // Do the same bundling + unbundling for feeOrder sorting + const feeOrderWithAmounts = ForwarderHelperImpl._createSignedOrderWithAmounts( + feeOrders, + remainingFillableFeeAmounts, + ); + const sortedFeeOrderWithAmounts = sortingUtils.sortFeeOrdersByFeeAdjustedRate(feeOrderWithAmounts); + const unbundledSortedFeeOrderWithAmounts = ForwarderHelperImpl._unbundleSignedOrderWithAmounts( + sortedFeeOrderWithAmounts, + ); + return { + orders: unbundledSortedOrderWithAmounts.orders, + feeOrders: unbundledSortedFeeOrderWithAmounts.orders, + remainingFillableMakerAssetAmounts: unbundledSortedOrderWithAmounts.amounts, + remainingFillableFeeAmounts: unbundledSortedFeeOrderWithAmounts.amounts, + }; + } + constructor(opts: ForwarderHelperImplConfig) { + this.config = ForwarderHelperImpl._sortConfig(opts); + } + 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/forwarder-helper/src/globals.d.ts b/packages/forwarder-helper/src/globals.d.ts new file mode 100644 index 000000000..94e63a32d --- /dev/null +++ b/packages/forwarder-helper/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/forwarder-helper/src/index.ts b/packages/forwarder-helper/src/index.ts new file mode 100644 index 000000000..eb3a34bd5 --- /dev/null +++ b/packages/forwarder-helper/src/index.ts @@ -0,0 +1,2 @@ +export { forwarderHelperFactory } from './forwarder_helper_factory'; +export { ForwarderHelper, ForwarderHelperError, MarketBuyOrdersInfoRequest, MarketBuyOrdersInfo } from './types'; diff --git a/packages/forwarder-helper/src/monorepo_scripts/postpublish.ts b/packages/forwarder-helper/src/monorepo_scripts/postpublish.ts new file mode 100644 index 000000000..dcb99d0f7 --- /dev/null +++ b/packages/forwarder-helper/src/monorepo_scripts/postpublish.ts @@ -0,0 +1,8 @@ +import { postpublishUtils } from '@0xproject/monorepo-scripts'; + +import * as packageJSON from '../package.json'; +import * as tsConfigJSON from '../tsconfig.json'; + +const cwd = `${__dirname}/..`; +// tslint:disable-next-line:no-floating-promises +postpublishUtils.runAsync(packageJSON, tsConfigJSON, cwd); diff --git a/packages/forwarder-helper/src/monorepo_scripts/stage_docs.ts b/packages/forwarder-helper/src/monorepo_scripts/stage_docs.ts new file mode 100644 index 000000000..e732ac8eb --- /dev/null +++ b/packages/forwarder-helper/src/monorepo_scripts/stage_docs.ts @@ -0,0 +1,8 @@ +import { postpublishUtils } from '@0xproject/monorepo-scripts'; + +import * as packageJSON from '../package.json'; +import * as tsConfigJSON from '../tsconfig.json'; + +const cwd = `${__dirname}/..`; +// tslint:disable-next-line:no-floating-promises +postpublishUtils.publishDocsToStagingAsync(packageJSON, tsConfigJSON, cwd); diff --git a/packages/forwarder-helper/src/types.ts b/packages/forwarder-helper/src/types.ts new file mode 100644 index 000000000..fb171cc90 --- /dev/null +++ b/packages/forwarder-helper/src/types.ts @@ -0,0 +1,43 @@ +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; + +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/forwarder-helper/test/forwarder_helper_impl_test.ts b/packages/forwarder-helper/test/forwarder_helper_impl_test.ts new file mode 100644 index 000000000..3c3b6db92 --- /dev/null +++ b/packages/forwarder-helper/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/forwarder-helper/test/utils/chai_setup.ts b/packages/forwarder-helper/test/utils/chai_setup.ts new file mode 100644 index 000000000..1a8733093 --- /dev/null +++ b/packages/forwarder-helper/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/forwarder-helper/tsconfig.json b/packages/forwarder-helper/tsconfig.json new file mode 100644 index 000000000..e35816553 --- /dev/null +++ b/packages/forwarder-helper/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "outDir": "lib" + }, + "include": ["./src/**/*", "./test/**/*"] +} diff --git a/packages/forwarder-helper/tslint.json b/packages/forwarder-helper/tslint.json new file mode 100644 index 000000000..ffaefe83a --- /dev/null +++ b/packages/forwarder-helper/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": ["@0xproject/tslint-config"] +} diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index 86f0da65a..149086857 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -14,6 +14,20 @@ "note": "Update marketUtils api such that all optional parameters are bundled into one optional param and more defaults are provided", "pr": 954 + }, + { + "note": + "Rename `resultOrders` to `resultFeeOrders` for object returned by `findFeeOrdersThatCoverFeesForTargetOrders` in `marketUtils` api", + "pr": 997 + }, + { + "note": "Make `sortFeeOrdersByFeeAdjustedRate` in `sortingUtils` generic", + "pr": 997 + }, + { + "note": + "Update `findFeeOrdersThatCoverFeesForTargetOrders` to round the the nearest integer when calculating required fees", + "pr": 997 } ] }, diff --git a/packages/order-utils/src/market_utils.ts b/packages/order-utils/src/market_utils.ts index a0a827546..b31a2b135 100644 --- a/packages/order-utils/src/market_utils.ts +++ b/packages/order-utils/src/market_utils.ts @@ -84,7 +84,7 @@ export const marketUtils = { orders: T[], feeOrders: T[], opts?: FindFeeOrdersThatCoverFeesForTargetOrdersOpts, - ): { resultOrders: T[]; remainingFeeAmount: BigNumber } { + ): { resultFeeOrders: T[]; remainingFeeAmount: BigNumber } { assert.doesConformToSchema('orders', orders, schemas.ordersSchema); assert.doesConformToSchema('feeOrders', feeOrders, schemas.ordersSchema); // try to get remainingFillableMakerAssetAmounts from opts, if it's not there, use makerAssetAmount values from orders @@ -123,7 +123,7 @@ export const marketUtils = { const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index]; const feeToFillMakerAssetAmountAvailable = makerAssetAmountAvailable .mul(order.takerFee) - .div(order.makerAssetAmount); + .dividedToIntegerBy(order.makerAssetAmount); return accFees.plus(feeToFillMakerAssetAmountAvailable); }, constants.ZERO_AMOUNT, @@ -137,7 +137,7 @@ export const marketUtils = { }, ); return { - resultOrders, + resultFeeOrders: resultOrders, remainingFeeAmount: remainingFillAmount, }; // TODO: add more orders here to cover rounding diff --git a/packages/order-utils/src/sorting_utils.ts b/packages/order-utils/src/sorting_utils.ts index 8811bcaf8..cd5163cf6 100644 --- a/packages/order-utils/src/sorting_utils.ts +++ b/packages/order-utils/src/sorting_utils.ts @@ -32,7 +32,7 @@ export const sortingUtils = { * the makerAsset and WETH as the takerAsset. * @return The input orders sorted by rate in ascending order */ - sortFeeOrdersByFeeAdjustedRate(feeOrders: Order[]): Order[] { + sortFeeOrdersByFeeAdjustedRate<T extends Order>(feeOrders: T[]): T[] { assert.doesConformToSchema('feeOrders', feeOrders, schemas.ordersSchema); const rateCalculator = rateUtils.getFeeAdjustedRateOfFeeOrder.bind(rateUtils); const sortedOrders = sortOrders(feeOrders, rateCalculator); diff --git a/packages/order-utils/test/market_utils_test.ts b/packages/order-utils/test/market_utils_test.ts index 109420a02..0c0151e57 100644 --- a/packages/order-utils/test/market_utils_test.ts +++ b/packages/order-utils/test/market_utils_test.ts @@ -139,11 +139,11 @@ describe('marketUtils', () => { ); describe('no target orders', () => { it('returns empty and zero remainingFeeAmount', async () => { - const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + const { resultFeeOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( [], inputFeeOrders, ); - expect(resultOrders).to.be.empty; + expect(resultFeeOrders).to.be.empty; expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); }); @@ -162,14 +162,14 @@ describe('marketUtils', () => { // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount]; it('returns empty and non-zero remainingFeeAmount', async () => { - const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + const { resultFeeOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( inputOrders, [], { remainingFillableMakerAssetAmounts, }, ); - expect(resultOrders).to.be.empty; + expect(resultFeeOrders).to.be.empty; expect(remainingFeeAmount).to.be.bignumber.equal(new BigNumber(30)); }); }); @@ -183,11 +183,11 @@ describe('marketUtils', () => { 3, ); it('returns empty and zero remainingFeeAmount', async () => { - const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + const { resultFeeOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( inputOrders, inputFeeOrders, ); - expect(resultOrders).to.be.empty; + expect(resultFeeOrders).to.be.empty; expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); }); @@ -204,11 +204,11 @@ describe('marketUtils', () => { 3, ); it('returns input fee orders and zero remainingFeeAmount', async () => { - const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + const { resultFeeOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( inputOrders, inputFeeOrders, ); - expect(resultOrders).to.be.deep.equal(inputFeeOrders); + expect(resultFeeOrders).to.be.deep.equal(inputFeeOrders); expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); }); @@ -230,14 +230,14 @@ describe('marketUtils', () => { // 3. order is completely fillable const remainingFillableMakerAssetAmounts = [constants.ZERO_AMOUNT, new BigNumber(5), makerAssetAmount]; it('returns first two input fee orders and zero remainingFeeAmount', async () => { - const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + const { resultFeeOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( inputOrders, inputFeeOrders, { remainingFillableMakerAssetAmounts, }, ); - expect(resultOrders).to.be.deep.equal([inputFeeOrders[0], inputFeeOrders[1]]); + expect(resultFeeOrders).to.be.deep.equal([inputFeeOrders[0], inputFeeOrders[1]]); expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); }); @@ -254,11 +254,11 @@ describe('marketUtils', () => { 3, ); it('returns input fee orders and non-zero remainingFeeAmount', async () => { - const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + const { resultFeeOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( inputOrders, inputFeeOrders, ); - expect(resultOrders).to.be.deep.equal(inputFeeOrders); + expect(resultFeeOrders).to.be.deep.equal(inputFeeOrders); expect(remainingFeeAmount).to.be.bignumber.equal(new BigNumber(30)); }); }); |