diff options
Diffstat (limited to 'packages/asset-buyer/src/utils')
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; +} |