aboutsummaryrefslogtreecommitdiffstats
path: root/packages/asset-buyer/src/utils
diff options
context:
space:
mode:
Diffstat (limited to 'packages/asset-buyer/src/utils')
-rw-r--r--packages/asset-buyer/src/utils/assert.ts23
-rw-r--r--packages/asset-buyer/src/utils/buy_quote_calculator.ts60
-rw-r--r--packages/asset-buyer/src/utils/forwarder_helper_impl_config_utils.ts92
-rw-r--r--packages/asset-buyer/src/utils/order_fetcher_response_processor.ts180
4 files changed, 263 insertions, 92 deletions
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<AssetBuyerOrdersAndFillableAmounts> {
+ // 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;
+}