diff options
author | Leonid Logvinov <logvinov.leon@gmail.com> | 2018-09-26 20:55:52 +0800 |
---|---|---|
committer | Leonid Logvinov <logvinov.leon@gmail.com> | 2018-09-26 20:55:52 +0800 |
commit | 5d73eebf6abe52763ea6984f85102157abea5b6c (patch) | |
tree | 8cec1c58fe22ba62d979dbc1c6ff366e81985b8d /packages/asset-buyer/src/utils | |
parent | f3deabccf4e6caec57351a09f82b3f786122b5ea (diff) | |
parent | 13aa98f0f3431e4ea4db07794a06304c237e8d45 (diff) | |
download | dexon-0x-contracts-5d73eebf6abe52763ea6984f85102157abea5b6c.tar dexon-0x-contracts-5d73eebf6abe52763ea6984f85102157abea5b6c.tar.gz dexon-0x-contracts-5d73eebf6abe52763ea6984f85102157abea5b6c.tar.bz2 dexon-0x-contracts-5d73eebf6abe52763ea6984f85102157abea5b6c.tar.lz dexon-0x-contracts-5d73eebf6abe52763ea6984f85102157abea5b6c.tar.xz dexon-0x-contracts-5d73eebf6abe52763ea6984f85102157abea5b6c.tar.zst dexon-0x-contracts-5d73eebf6abe52763ea6984f85102157abea5b6c.zip |
Merge branch 'development' into feature/ts-ethers
Diffstat (limited to 'packages/asset-buyer/src/utils')
-rw-r--r-- | packages/asset-buyer/src/utils/assert.ts | 51 | ||||
-rw-r--r-- | packages/asset-buyer/src/utils/asset_data_utils.ts | 26 | ||||
-rw-r--r-- | packages/asset-buyer/src/utils/buy_quote_calculator.ts | 89 | ||||
-rw-r--r-- | packages/asset-buyer/src/utils/order_provider_response_processor.ts | 202 | ||||
-rw-r--r-- | packages/asset-buyer/src/utils/order_utils.ts | 30 |
5 files changed, 398 insertions, 0 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..04f425237 --- /dev/null +++ b/packages/asset-buyer/src/utils/assert.ts @@ -0,0 +1,51 @@ +import { assert as sharedAssert } from '@0xproject/assert'; +import { schemas } from '@0xproject/json-schemas'; +import { SignedOrder } from '@0xproject/types'; +import * as _ from 'lodash'; + +import { BuyQuote, OrderProvider, OrderProviderRequest } 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); + } + }, + isValidOrderProvider(variableName: string, orderFetcher: OrderProvider): void { + sharedAssert.isFunction(`${variableName}.getOrdersAsync`, orderFetcher.getOrdersAsync); + }, + isValidOrderProviderRequest(variableName: string, orderFetcherRequest: OrderProviderRequest): void { + sharedAssert.isHexString(`${variableName}.makerAssetData`, orderFetcherRequest.makerAssetData); + 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.`, + ); + }, + 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}`, + ); + }, +}; 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..d05ff2504 --- /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, + getEtherTokenAssetDataOrThrow(contractWrappers: ContractWrappers): string { + const etherTokenAddressIfExists = contractWrappers.etherToken.getContractAddressIfExists(); + if (_.isUndefined(etherTokenAddressIfExists)) { + throw new Error(AssetBuyerError.NoEtherTokenContractFound); + } + const etherTokenAssetData = sharedAssetDataUtils.encodeERC20AssetData(etherTokenAddressIfExists); + return etherTokenAssetData; + }, + getZrxTokenAssetDataOrThrow(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 new file mode 100644 index 000000000..9946924ef --- /dev/null +++ b/packages/asset-buyer/src/utils/buy_quote_calculator.ts @@ -0,0 +1,89 @@ +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'; + +import { orderUtils } from './order_utils'; + +// Calculates a buy quote for orders that have WETH as the takerAsset +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, + ordersRemainingFillableMakerAssetAmounts, + } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(orders, assetBuyAmount, { + remainingFillableMakerAssetAmounts, + slippageBufferAmount, + }); + if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) { + throw new Error(AssetBuyerError.InsufficientAssetLiquidity); + } + // 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 { + 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; + + // 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 claimableTakerAssetAmount = orderUtils.calculateRemainingTakerAssetAmount( + order, + 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: feeAdjustedMinRate, + maxRate: feeAdjustedMaxRate, + assetBuyAmount, + feePercentage, + }; + }, +}; 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..31fdcc182 --- /dev/null +++ b/packages/asset-buyer/src/utils/order_provider_response_processor.ts @@ -0,0 +1,202 @@ +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 { + AssetBuyerError, + AssetBuyerOrdersAndFillableAmounts, + OrderProviderRequest, + OrderProviderResponse, + SignedOrderWithRemainingFillableMakerAssetAmount, +} from '../types'; + +import { orderUtils } from './order_utils'; + +interface OrdersAndRemainingFillableMakerAssetAmounts { + orders: SignedOrder[]; + remainingFillableMakerAssetAmounts: BigNumber[]; +} + +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: + * - 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, + expiryBufferSeconds: number, + orderValidator?: OrderValidatorWrapper, + ): Promise<AssetBuyerOrdersAndFillableAmounts> { + // drop orders that are expired or not open + 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; + // 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[], + expiryBufferSeconds: number, +): SignedOrderWithRemainingFillableMakerAssetAmount[] { + const result = _.filter(orders, order => { + return orderUtils.isOpenOrder(order) && !orderUtils.willOrderExpire(order, expiryBufferSeconds); + }); + 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_utils.ts b/packages/asset-buyer/src/utils/order_utils.ts new file mode 100644 index 000000000..62166eb76 --- /dev/null +++ b/packages/asset-buyer/src/utils/order_utils.ts @@ -0,0 +1,30 @@ +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; + +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.minus(secondsFromNow)); + }, + calculateRemainingMakerAssetAmount(order: SignedOrder, remainingTakerAssetAmount: BigNumber): BigNumber { + 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; + }, +}; |