diff --git a/packages/asset-buyer/src/quote_consumers/exchange_swap_quote_consumer.ts b/packages/asset-buyer/src/quote_consumers/exchange_swap_quote_consumer.ts index 1cfff664be..369209bc6a 100644 --- a/packages/asset-buyer/src/quote_consumers/exchange_swap_quote_consumer.ts +++ b/packages/asset-buyer/src/quote_consumers/exchange_swap_quote_consumer.ts @@ -7,13 +7,10 @@ import * as _ from 'lodash'; import { constants } from '../constants'; import { CalldataInfo, - ExchangeMarketBuySmartContractParams, - ExchangeMarketSellSmartContractParams, - MarketBuySwapQuote, - MarketSellSwapQuote, + ExchangeSmartContractParams, SmartContractParamsInfo, SwapQuote, - SwapQuoteConsumer, + SwapQuoteConsumerBase, SwapQuoteConsumerError, SwapQuoteConsumerOpts, SwapQuoteExecutionOpts, @@ -23,8 +20,7 @@ import { assert } from '../utils/assert'; import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils'; import { utils } from '../utils/utils'; -export class ExchangeSwapQuoteConsumer - implements SwapQuoteConsumer { +export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase { public readonly provider: ZeroExProvider; public readonly networkId: number; @@ -48,19 +44,18 @@ export class ExchangeSwapQuoteConsumer ): Promise { assert.isValidSwapQuote('quote', quote); - const consumableQuote = (quote as any) as (MarketBuySwapQuote | MarketSellSwapQuote); - const smartContractParamsInfo = await this.getSmartContractParamsOrThrowAsync(consumableQuote, opts); - const { to, methodAbi, ethAmount } = smartContractParamsInfo; + const { to, methodAbi, ethAmount, params } = await this.getSmartContractParamsOrThrowAsync(quote, opts); const abiEncoder = new AbiEncoder.Method(methodAbi); + const { orders, signatures } = params; let args: any[]; - if (utils.isSwapQuoteMarketBuy(consumableQuote)) { - const marketBuyParams = (smartContractParamsInfo.params as any) as ExchangeMarketBuySmartContractParams; - args = [marketBuyParams.orders, marketBuyParams.makerAssetFillAmount, marketBuyParams.signatures]; + if (params.type === 'marketBuy') { + const { makerAssetFillAmount } = params; + args = [orders, makerAssetFillAmount, signatures]; } else { - const marketSellParams = (smartContractParamsInfo.params as any) as ExchangeMarketSellSmartContractParams; - args = [marketSellParams.orders, marketSellParams.takerAssetFillAmount, marketSellParams.signatures]; + const { takerAssetFillAmount } = params; + args = [orders, takerAssetFillAmount, signatures]; } const calldataHexString = abiEncoder.encode(args); return { @@ -73,36 +68,36 @@ export class ExchangeSwapQuoteConsumer public async getSmartContractParamsOrThrowAsync( quote: SwapQuote, - opts: Partial, - ): Promise> { + _opts: Partial, + ): Promise> { assert.isValidSwapQuote('quote', quote); - const consumableQuote = (quote as any) as (MarketBuySwapQuote | MarketSellSwapQuote); - - const { orders } = consumableQuote; + const { orders } = quote; const signatures = _.map(orders, o => o.signature); - let params: ExchangeMarketBuySmartContractParams | ExchangeMarketSellSmartContractParams; + let params: ExchangeSmartContractParams; let methodName: string; - if (utils.isSwapQuoteMarketBuy(consumableQuote)) { - const { makerAssetFillAmount } = consumableQuote; + if (quote.type === 'marketBuy') { + const { makerAssetFillAmount } = quote; params = { orders, signatures, makerAssetFillAmount, + type: 'marketBuy', }; methodName = 'marketBuyOrders'; } else { - const { takerAssetFillAmount } = consumableQuote; + const { takerAssetFillAmount } = quote; params = { orders, signatures, takerAssetFillAmount, + type: 'marketSell', }; methodName = 'marketSellOrders'; @@ -138,16 +133,14 @@ export class ExchangeSwapQuoteConsumer assert.isBigNumber('gasPrice', gasPrice); } - const consumableQuote = (quote as any) as (MarketBuySwapQuote | MarketSellSwapQuote); - - const { orders } = consumableQuote; + const { orders } = quote; const finalTakerAddress = await swapQuoteConsumerUtils.getTakerAddressOrThrowAsync(this.provider, opts); try { let txHash: string; - if (utils.isSwapQuoteMarketBuy(consumableQuote)) { - const { makerAssetFillAmount } = consumableQuote; + if (quote.type === 'marketBuy') { + const { makerAssetFillAmount } = quote; txHash = await this._contractWrappers.exchange.marketBuyOrdersNoThrowAsync( orders, makerAssetFillAmount, @@ -159,7 +152,7 @@ export class ExchangeSwapQuoteConsumer }, ); } else { - const { takerAssetFillAmount } = consumableQuote; + const { takerAssetFillAmount } = quote; txHash = await this._contractWrappers.exchange.marketSellOrdersNoThrowAsync( orders, takerAssetFillAmount, diff --git a/packages/asset-buyer/src/quote_consumers/forwarder_swap_quote_consumer.ts b/packages/asset-buyer/src/quote_consumers/forwarder_swap_quote_consumer.ts index caad19fd31..9ae67c394a 100644 --- a/packages/asset-buyer/src/quote_consumers/forwarder_swap_quote_consumer.ts +++ b/packages/asset-buyer/src/quote_consumers/forwarder_swap_quote_consumer.ts @@ -7,15 +7,12 @@ import * as _ from 'lodash'; import { constants } from '../constants'; import { CalldataInfo, - ForwarderMarketBuySmartContractParams, - ForwarderMarketSellSmartContractParams, + ForwarderSmartContractParams, ForwarderSwapQuoteExecutionOpts, ForwarderSwapQuoteGetOutputOpts, - MarketBuySwapQuote, - MarketSellSwapQuote, SmartContractParamsInfo, SwapQuote, - SwapQuoteConsumer, + SwapQuoteConsumerBase, SwapQuoteConsumerError, SwapQuoteConsumerOpts, } from '../types'; @@ -25,8 +22,7 @@ import { assetDataUtils } from '../utils/asset_data_utils'; import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils'; import { utils } from '../utils/utils'; -export class ForwarderSwapQuoteConsumer - implements SwapQuoteConsumer { +export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase { public readonly provider: ZeroExProvider; public readonly networkId: number; @@ -55,34 +51,18 @@ export class ForwarderSwapQuoteConsumer ): Promise { assert.isValidForwarderSwapQuote('quote', quote, this._getEtherTokenAssetDataOrThrow()); - const consumableQuote = (quote as any) as (MarketBuySwapQuote | MarketSellSwapQuote); - const smartContractParamsInfo = await this.getSmartContractParamsOrThrowAsync(consumableQuote, opts); - const { to, methodAbi, ethAmount } = smartContractParamsInfo; + const { to, methodAbi, ethAmount, params } = await this.getSmartContractParamsOrThrowAsync(quote, opts); const abiEncoder = new AbiEncoder.Method(methodAbi); + const { orders, signatures, feeOrders, feeSignatures, feePercentage, feeRecipient } = params; + let args: any[]; - if (utils.isSwapQuoteMarketBuy(consumableQuote)) { - const marketBuyParams = (smartContractParamsInfo.params as any) as ForwarderMarketBuySmartContractParams; - args = [ - marketBuyParams.orders, - marketBuyParams.makerAssetFillAmount, - marketBuyParams.signatures, - marketBuyParams.feeOrders, - marketBuyParams.feeSignatures, - marketBuyParams.feePercentage, - marketBuyParams.feeRecipient, - ]; + if (params.type === 'marketBuy') { + const { makerAssetFillAmount } = params; + args = [orders, makerAssetFillAmount, signatures, feeOrders, feeSignatures, feePercentage, feeRecipient]; } else { - const marketSellParams = (smartContractParamsInfo.params as any) as ForwarderMarketSellSmartContractParams; - args = [ - marketSellParams.orders, - marketSellParams.signatures, - marketSellParams.feeOrders, - marketSellParams.feeSignatures, - marketSellParams.feePercentage, - marketSellParams.feeRecipient, - ]; + args = [orders, signatures, feeOrders, feeSignatures, feePercentage, feeRecipient]; } const calldataHexString = abiEncoder.encode(args); return { @@ -101,9 +81,7 @@ export class ForwarderSwapQuoteConsumer public async getSmartContractParamsOrThrowAsync( quote: SwapQuote, opts: Partial, - ): Promise< - SmartContractParamsInfo - > { + ): Promise> { assert.isValidForwarderSwapQuote('quote', quote, this._getEtherTokenAssetDataOrThrow()); const { ethAmount, feeRecipient, feePercentage: unFormattedFeePercentage } = _.merge( @@ -118,27 +96,20 @@ export class ForwarderSwapQuoteConsumer assert.isBigNumber('ethAmount', ethAmount); } - const swapQuoteWithAffiliateFee = affiliateFeeUtils.getSwapQuoteWithAffiliateFee( - quote, - unFormattedFeePercentage, - ); + const quoteWithAffiliateFee = affiliateFeeUtils.getSwapQuoteWithAffiliateFee(quote, unFormattedFeePercentage); - const consumableQuoteWithAffiliateFee = (swapQuoteWithAffiliateFee as any) as ( - | MarketBuySwapQuote - | MarketSellSwapQuote); - - const { orders, feeOrders, worstCaseQuoteInfo } = swapQuoteWithAffiliateFee; + const { orders, feeOrders, worstCaseQuoteInfo } = quoteWithAffiliateFee; const signatures = _.map(orders, o => o.signature); const feeSignatures = _.map(feeOrders, o => o.signature); const feePercentage = utils.numberPercentageToEtherTokenAmountPercentage(unFormattedFeePercentage); - let params: ForwarderMarketBuySmartContractParams | ForwarderMarketSellSmartContractParams; + let params: ForwarderSmartContractParams; let methodName: string; - if (utils.isSwapQuoteMarketBuy(consumableQuoteWithAffiliateFee)) { - const { makerAssetFillAmount } = consumableQuoteWithAffiliateFee; + if (quoteWithAffiliateFee.type === 'marketBuy') { + const { makerAssetFillAmount } = quoteWithAffiliateFee; params = { orders, @@ -148,20 +119,19 @@ export class ForwarderSwapQuoteConsumer feeSignatures, feePercentage, feeRecipient, + type: 'marketBuy', }; methodName = 'marketBuyOrdersWithEth'; } else { - const { takerAssetFillAmount } = consumableQuoteWithAffiliateFee; - params = { orders, - takerAssetFillAmount, signatures, feeOrders, feeSignatures, feePercentage, feeRecipient, + type: 'marketSell', }; methodName = 'marketSellOrdersWithEth'; } @@ -210,20 +180,16 @@ export class ForwarderSwapQuoteConsumer assert.isBigNumber('gasPrice', gasPrice); } - const swapQuoteWithAffiliateFee = affiliateFeeUtils.getSwapQuoteWithAffiliateFee(quote, feePercentage); + const quoteWithAffiliateFee = affiliateFeeUtils.getSwapQuoteWithAffiliateFee(quote, feePercentage); - const consumableQuoteWithAffiliateFee = (swapQuoteWithAffiliateFee as any) as ( - | MarketBuySwapQuote - | MarketSellSwapQuote); - - const { orders, feeOrders, worstCaseQuoteInfo } = consumableQuoteWithAffiliateFee; + const { orders, feeOrders, worstCaseQuoteInfo } = quoteWithAffiliateFee; const finalTakerAddress = await swapQuoteConsumerUtils.getTakerAddressOrThrowAsync(this.provider, opts); try { let txHash: string; - if (utils.isSwapQuoteMarketBuy(consumableQuoteWithAffiliateFee)) { - const { makerAssetFillAmount } = consumableQuoteWithAffiliateFee; + if (quoteWithAffiliateFee.type === 'marketBuy') { + const { makerAssetFillAmount } = quoteWithAffiliateFee; txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync( orders, makerAssetFillAmount, diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index 20ca938456..d17baf44af 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -66,46 +66,48 @@ export interface SmartContractParamsInfo { methodAbi: MethodAbi; } +export interface SmartContractParamsBase { + orders: SignedOrder[]; + signatures: string[]; +} + +export type MarketOperation = 'marketBuy' | 'marketSell'; + /** * orders: An array of objects conforming to SignedOrder. These orders can be used to cover the requested assetBuyAmount plus slippage. * makerAssetFillAmount: The amount of makerAsset to swap for. * signatures: An array of signatures that attest that the maker of the orders in fact made the orders. */ -export interface ExchangeMarketBuySmartContractParams { - orders: SignedOrder[]; +export interface ExchangeMarketBuySmartContractParams extends SmartContractParamsBase { makerAssetFillAmount: BigNumber; - signatures: string[]; + type: 'marketBuy'; } -export interface ExchangeMarketSellSmartContractParams { - orders: SignedOrder[]; +export interface ExchangeMarketSellSmartContractParams extends SmartContractParamsBase { takerAssetFillAmount: BigNumber; - signatures: string[]; + type: 'marketSell'; } -/** - * orders: An array of objects conforming to SignedOrder. These orders can be used to cover the requested assetBuyAmount plus slippage. - * makerAssetFillAmount: The amount of makerAsset to swap for. - * feeOrders: An array of objects conforming to SignedOrder. These orders can be used to cover the fees for the orders param above. - * signatures: An array of signatures that attest that the maker of the orders in fact made the orders. - * feeOrders: An array of objects conforming to SignedOrder. These orders can be used to cover the fees for the orders param above. - * feeSignatures: An array of signatures that attest that the maker of the fee orders in fact made the orders. - * feePercentage: percentage (up to 5%) of the taker asset paid to feeRecipient - * feeRecipient: address of the receiver of the feePercentage of taker asset - */ -export interface ForwarderMarketBuySmartContractParams extends ExchangeMarketBuySmartContractParams { +export type ExchangeSmartContractParams = ExchangeMarketBuySmartContractParams | ExchangeMarketSellSmartContractParams; + +export interface ForwarderSmartContractParamsBase { feeOrders: SignedOrder[]; feeSignatures: string[]; feePercentage: BigNumber; feeRecipient: string; } -export interface ForwarderMarketSellSmartContractParams extends ExchangeMarketSellSmartContractParams { - feeOrders: SignedOrder[]; - feeSignatures: string[]; - feePercentage: BigNumber; - feeRecipient: string; -} +export interface ForwarderMarketBuySmartContractParams + extends ExchangeMarketBuySmartContractParams, + ForwarderSmartContractParamsBase {} + +export interface ForwarderMarketSellSmartContractParams + extends Omit, + ForwarderSmartContractParamsBase {} + +export type ForwarderSmartContractParams = + | ForwarderMarketBuySmartContractParams + | ForwarderMarketSellSmartContractParams; /** * Interface that varying SwapQuoteConsumers adhere to (exchange consumer, router consumer, forwarder consumer, coordinator consumer) @@ -113,7 +115,7 @@ export interface ForwarderMarketSellSmartContractParams extends ExchangeMarketSe * getSmartContractParamsOrThrow: Get SmartContractParamsInfo to swap for tokens with provided SwapQuote. Throws if invalid SwapQuote is provided. * executeSwapQuoteOrThrowAsync: Executes a web3 transaction to swap for tokens with provided SwapQuote. Throws if invalid SwapQuote is provided. */ -export interface SwapQuoteConsumer { +export interface SwapQuoteConsumerBase { getCalldataOrThrowAsync(quote: SwapQuote, opts: Partial): Promise; getSmartContractParamsOrThrowAsync( quote: SwapQuote, @@ -161,6 +163,8 @@ export interface ForwarderSwapQuoteGetOutputOpts extends SwapQuoteGetOutputOpts */ export interface ForwarderSwapQuoteExecutionOpts extends ForwarderSwapQuoteGetOutputOpts, SwapQuoteExecutionOpts {} +export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote; + /** * takerAssetData: String that represents a specific taker asset (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). * makerAssetData: String that represents a specific maker asset (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). @@ -170,7 +174,7 @@ export interface ForwarderSwapQuoteExecutionOpts extends ForwarderSwapQuoteGetOu * bestCaseQuoteInfo: Info about the best case price for the asset. * worstCaseQuoteInfo: Info about the worst case price for the asset. */ -export interface SwapQuote { +export interface SwapQuoteBase { takerAssetData: string; makerAssetData: string; orders: SignedOrder[]; @@ -179,22 +183,25 @@ export interface SwapQuote { worstCaseQuoteInfo: SwapQuoteInfo; } -export interface MarketSellSwapQuote extends SwapQuote { +export interface MarketSellSwapQuote extends SwapQuoteBase { takerAssetFillAmount: BigNumber; - bestCaseQuoteInfo: SwapQuoteInfo; - worstCaseQuoteInfo: SwapQuoteInfo; + type: 'marketSell'; } -export interface MarketBuySwapQuote extends SwapQuote { +export interface MarketBuySwapQuote extends SwapQuoteBase { makerAssetFillAmount: BigNumber; - bestCaseQuoteInfo: SwapQuoteInfo; - worstCaseQuoteInfo: SwapQuoteInfo; + type: 'marketBuy'; } -export interface SwapQuoteWithAffiliateFee extends SwapQuote { +export interface SwapQuoteWithAffiliateFeeBase { feePercentage: number; } +export interface MarketSellSwapQuoteWithAffiliateFee extends SwapQuoteWithAffiliateFeeBase, MarketSellSwapQuote {} + +export interface MarketBuySwapQuoteWithAffiliateFee extends SwapQuoteWithAffiliateFeeBase, MarketBuySwapQuote {} + +export type SwapQuoteWithAffiliateFee = MarketBuySwapQuoteWithAffiliateFee | MarketSellSwapQuoteWithAffiliateFee; /** * assetEthAmount: The amount of eth required to pay for the requested asset. * feeEthAmount: The amount of eth required to pay any fee concerned with completing the swap. diff --git a/packages/asset-buyer/src/utils/assert.ts b/packages/asset-buyer/src/utils/assert.ts index a1b45cc5e4..8341a48a26 100644 --- a/packages/asset-buyer/src/utils/assert.ts +++ b/packages/asset-buyer/src/utils/assert.ts @@ -3,8 +3,7 @@ import { schemas } from '@0x/json-schemas'; import { SignedOrder } from '@0x/types'; import * as _ from 'lodash'; -import { OrderProvider, OrderProviderRequest, SwapQuote, SwapQuoteConsumerError, SwapQuoteInfo } from '../types'; -import { utils } from '../utils/utils'; +import { OrderProvider, OrderProviderRequest, SwapQuote, SwapQuoteInfo } from '../types'; export const assert = { ...sharedAssert, @@ -15,12 +14,10 @@ export const assert = { sharedAssert.doesConformToSchema(`${variableName}.feeOrders`, swapQuote.feeOrders, schemas.signedOrdersSchema); assert.isValidSwapQuoteInfo(`${variableName}.bestCaseQuoteInfo`, swapQuote.bestCaseQuoteInfo); assert.isValidSwapQuoteInfo(`${variableName}.worstCaseQuoteInfo`, swapQuote.worstCaseQuoteInfo); - if (utils.isSwapQuoteMarketBuy(swapQuote)) { + if (swapQuote.type === 'marketBuy') { sharedAssert.isBigNumber(`${variableName}.makerAssetFillAmount`, swapQuote.makerAssetFillAmount); - } else if (utils.isSwapQuoteMarketSell(swapQuote)) { - sharedAssert.isBigNumber(`${variableName}.takerAssetFillAmount`, swapQuote.takerAssetFillAmount); } else { - throw new Error(SwapQuoteConsumerError.InvalidMarketSellOrMarketBuySwapQuote); + sharedAssert.isBigNumber(`${variableName}.takerAssetFillAmount`, swapQuote.takerAssetFillAmount); } }, isValidForwarderSwapQuote(variableName: string, swapQuote: SwapQuote, wethAssetData: string): void { diff --git a/packages/asset-buyer/src/utils/swap_quote_calculator.ts b/packages/asset-buyer/src/utils/swap_quote_calculator.ts index 0eabfdf7c7..c60907d947 100644 --- a/packages/asset-buyer/src/utils/swap_quote_calculator.ts +++ b/packages/asset-buyer/src/utils/swap_quote_calculator.ts @@ -6,8 +6,10 @@ import { constants } from '../constants'; import { InsufficientAssetLiquidityError } from '../errors'; import { MarketBuySwapQuote, + MarketOperation, MarketSellSwapQuote, OrdersAndFillableAmounts, + SwapQuote, SwapQuoteInfo, SwapQuoterError, } from '../types'; @@ -21,109 +23,14 @@ export const swapQuoteCalculator = { slippagePercentage: number, isMakerAssetZrxToken: boolean, ): MarketSellSwapQuote { - const orders = ordersAndFillableAmounts.orders; - const remainingFillableMakerAssetAmounts = ordersAndFillableAmounts.remainingFillableMakerAssetAmounts; - const remainingFillableTakerAssetAmounts = remainingFillableMakerAssetAmounts.map( - (makerAssetAmount: BigNumber, index: number) => { - return orderCalculationUtils.getTakerFillAmount(orders[index], makerAssetAmount); - }, - ); - const feeOrders = feeOrdersAndFillableAmounts.orders; - const remainingFillableFeeAmounts = feeOrdersAndFillableAmounts.remainingFillableMakerAssetAmounts; - const slippageBufferAmount = takerAssetFillAmount.multipliedBy(slippagePercentage).integerValue(); - // find the orders that cover the desired assetBuyAmount (with slippage) - const { - resultOrders, - remainingFillAmount, - ordersRemainingFillableTakerAssetAmounts, - } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(orders, takerAssetFillAmount, { - remainingFillableTakerAssetAmounts, - slippageBufferAmount, - }); - const ordersRemainingFillableMakerAssetAmounts = _.map( - ordersRemainingFillableTakerAssetAmounts, - (takerAssetAmount: BigNumber, index: number) => { - return orderCalculationUtils.getMakerFillAmount(resultOrders[index], takerAssetAmount); - }, - ); - // if we do not have enough orders to cover the desired assetBuyAmount, throw - if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) { - // We needed the amount they requested to buy, plus the amount for slippage - const totalAmountRequested = takerAssetFillAmount.plus(slippageBufferAmount); - const amountAbleToFill = totalAmountRequested.minus(remainingFillAmount); - // multiplierNeededWithSlippage represents what we need to multiply the assetBuyAmount by - // in order to get the total amount needed considering slippage - // i.e. if slippagePercent was 0.2 (20%), multiplierNeededWithSlippage would be 1.2 - const multiplierNeededWithSlippage = new BigNumber(1).plus(slippagePercentage); - // Given amountAvailableToFillConsideringSlippage * multiplierNeededWithSlippage = amountAbleToFill - // We divide amountUnableToFill by multiplierNeededWithSlippage to determine amountAvailableToFillConsideringSlippage - const amountAvailableToFillConsideringSlippage = amountAbleToFill - .div(multiplierNeededWithSlippage) - .integerValue(BigNumber.ROUND_FLOOR); - - throw new InsufficientAssetLiquidityError(amountAvailableToFillConsideringSlippage); - } - // if we are not buying ZRX: - // given the orders calculated above, find the fee-orders that cover the desired assetBuyAmount (with slippage) - // 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 - let resultFeeOrders = [] as SignedOrder[]; - let feeOrdersRemainingFillableMakerAssetAmounts = [] as BigNumber[]; - if (!isMakerAssetZrxToken) { - const feeOrdersAndRemainingFeeAmount = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( - resultOrders, - feeOrders, - { - remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts, - remainingFillableFeeAmounts, - }, - ); - // if we do not have enough feeOrders to cover the fees, throw - if (feeOrdersAndRemainingFeeAmount.remainingFeeAmount.gt(constants.ZERO_AMOUNT)) { - throw new Error(SwapQuoterError.InsufficientZrxLiquidity); - } - resultFeeOrders = feeOrdersAndRemainingFeeAmount.resultFeeOrders; - feeOrdersRemainingFillableMakerAssetAmounts = - feeOrdersAndRemainingFeeAmount.feeOrdersRemainingFillableMakerAssetAmounts; - } - - // assetData information for the result - const takerAssetData = orders[0].takerAssetData; - const makerAssetData = orders[0].makerAssetData; - - // compile the resulting trimmed set of orders for makerAsset and feeOrders that are needed for assetBuyAmount - const trimmedOrdersAndFillableAmounts: OrdersAndFillableAmounts = { - orders: resultOrders, - remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts, - }; - const trimmedFeeOrdersAndFillableAmounts: OrdersAndFillableAmounts = { - orders: resultFeeOrders, - remainingFillableMakerAssetAmounts: feeOrdersRemainingFillableMakerAssetAmounts, - }; - const bestCaseQuoteInfo = calculateMarketSellQuoteInfo( - trimmedOrdersAndFillableAmounts, - trimmedFeeOrdersAndFillableAmounts, + return calculateSwapQuote( + ordersAndFillableAmounts, + feeOrdersAndFillableAmounts, takerAssetFillAmount, + slippagePercentage, isMakerAssetZrxToken, - ); - // in order to calculate the maxRate, reverse the ordersAndFillableAmounts such that they are sorted from worst rate to best rate - const worstCaseQuoteInfo = calculateMarketSellQuoteInfo( - reverseOrdersAndFillableAmounts(trimmedOrdersAndFillableAmounts), - reverseOrdersAndFillableAmounts(trimmedFeeOrdersAndFillableAmounts), - takerAssetFillAmount, - isMakerAssetZrxToken, - ); - - return { - takerAssetData, - makerAssetData, - takerAssetFillAmount, - orders: resultOrders, - feeOrders: resultFeeOrders, - bestCaseQuoteInfo, - worstCaseQuoteInfo, - }; + 'marketSell', + ) as MarketSellSwapQuote; }, calculateMarketBuySwapQuote( ordersAndFillableAmounts: OrdersAndFillableAmounts, @@ -132,120 +39,201 @@ export const swapQuoteCalculator = { slippagePercentage: number, isMakerAssetZrxToken: boolean, ): MarketBuySwapQuote { - const orders = ordersAndFillableAmounts.orders; - const remainingFillableMakerAssetAmounts = ordersAndFillableAmounts.remainingFillableMakerAssetAmounts; - const feeOrders = feeOrdersAndFillableAmounts.orders; - const remainingFillableFeeAmounts = feeOrdersAndFillableAmounts.remainingFillableMakerAssetAmounts; - const slippageBufferAmount = makerAssetFillAmount.multipliedBy(slippagePercentage).integerValue(); - // find the orders that cover the desired assetBuyAmount (with slippage) - const { - resultOrders, - remainingFillAmount, - ordersRemainingFillableMakerAssetAmounts, - } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(orders, makerAssetFillAmount, { - remainingFillableMakerAssetAmounts, - slippageBufferAmount, - }); - // if we do not have enough orders to cover the desired assetBuyAmount, throw - if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) { - // We needed the amount they requested to buy, plus the amount for slippage - const totalAmountRequested = makerAssetFillAmount.plus(slippageBufferAmount); - const amountAbleToFill = totalAmountRequested.minus(remainingFillAmount); - // multiplierNeededWithSlippage represents what we need to multiply the assetBuyAmount by - // in order to get the total amount needed considering slippage - // i.e. if slippagePercent was 0.2 (20%), multiplierNeededWithSlippage would be 1.2 - const multiplierNeededWithSlippage = new BigNumber(1).plus(slippagePercentage); - // Given amountAvailableToFillConsideringSlippage * multiplierNeededWithSlippage = amountAbleToFill - // We divide amountUnableToFill by multiplierNeededWithSlippage to determine amountAvailableToFillConsideringSlippage - const amountAvailableToFillConsideringSlippage = amountAbleToFill - .div(multiplierNeededWithSlippage) - .integerValue(BigNumber.ROUND_FLOOR); - - throw new InsufficientAssetLiquidityError(amountAvailableToFillConsideringSlippage); - } - // if we are not buying ZRX: - // given the orders calculated above, find the fee-orders that cover the desired assetBuyAmount (with slippage) - // 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 - let resultFeeOrders = [] as SignedOrder[]; - let feeOrdersRemainingFillableMakerAssetAmounts = [] as BigNumber[]; - if (!isMakerAssetZrxToken) { - const feeOrdersAndRemainingFeeAmount = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( - resultOrders, - feeOrders, - { - remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts, - remainingFillableFeeAmounts, - }, - ); - // if we do not have enough feeOrders to cover the fees, throw - if (feeOrdersAndRemainingFeeAmount.remainingFeeAmount.gt(constants.ZERO_AMOUNT)) { - throw new Error(SwapQuoterError.InsufficientZrxLiquidity); - } - resultFeeOrders = feeOrdersAndRemainingFeeAmount.resultFeeOrders; - feeOrdersRemainingFillableMakerAssetAmounts = - feeOrdersAndRemainingFeeAmount.feeOrdersRemainingFillableMakerAssetAmounts; - } - - // assetData information for the result - const takerAssetData = orders[0].takerAssetData; - const makerAssetData = orders[0].makerAssetData; - - // compile the resulting trimmed set of orders for makerAsset and feeOrders that are needed for assetBuyAmount - const trimmedOrdersAndFillableAmounts: OrdersAndFillableAmounts = { - orders: resultOrders, - remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts, - }; - const trimmedFeeOrdersAndFillableAmounts: OrdersAndFillableAmounts = { - orders: resultFeeOrders, - remainingFillableMakerAssetAmounts: feeOrdersRemainingFillableMakerAssetAmounts, - }; - const bestCaseQuoteInfo = calculateMarketBuyQuoteInfo( - trimmedOrdersAndFillableAmounts, - trimmedFeeOrdersAndFillableAmounts, + return calculateSwapQuote( + ordersAndFillableAmounts, + feeOrdersAndFillableAmounts, makerAssetFillAmount, + slippagePercentage, isMakerAssetZrxToken, - ); - // in order to calculate the maxRate, reverse the ordersAndFillableAmounts such that they are sorted from worst rate to best rate - const worstCaseQuoteInfo = calculateMarketBuyQuoteInfo( - reverseOrdersAndFillableAmounts(trimmedOrdersAndFillableAmounts), - reverseOrdersAndFillableAmounts(trimmedFeeOrdersAndFillableAmounts), - makerAssetFillAmount, - isMakerAssetZrxToken, - ); - - return { - takerAssetData, - makerAssetData, - makerAssetFillAmount, - orders: resultOrders, - feeOrders: resultFeeOrders, - bestCaseQuoteInfo, - worstCaseQuoteInfo, - }; + 'marketBuy', + ) as MarketBuySwapQuote; }, }; -function calculateMarketBuyQuoteInfo( +function calculateSwapQuote( ordersAndFillableAmounts: OrdersAndFillableAmounts, feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, - makerTokenAmount: BigNumber, + assetFillAmount: BigNumber, + slippagePercentage: number, isMakerAssetZrxToken: boolean, + marketOperation: MarketOperation, +): SwapQuote { + const orders = ordersAndFillableAmounts.orders; + const remainingFillableMakerAssetAmounts = ordersAndFillableAmounts.remainingFillableMakerAssetAmounts; + const remainingFillableTakerAssetAmounts = remainingFillableMakerAssetAmounts.map( + (makerAssetAmount: BigNumber, index: number) => { + return orderCalculationUtils.getTakerFillAmount(orders[index], makerAssetAmount); + }, + ); + const feeOrders = feeOrdersAndFillableAmounts.orders; + const remainingFillableFeeAmounts = feeOrdersAndFillableAmounts.remainingFillableMakerAssetAmounts; + + const slippageBufferAmount = assetFillAmount.multipliedBy(slippagePercentage).integerValue(); + + let resultOrders: SignedOrder[]; + let remainingFillAmount: BigNumber; + let ordersRemainingFillableMakerAssetAmounts: BigNumber[]; + + if (marketOperation === 'marketBuy') { + // find the orders that cover the desired assetBuyAmount (with slippage) + ({ + resultOrders, + remainingFillAmount, + ordersRemainingFillableMakerAssetAmounts, + } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(orders, assetFillAmount, { + remainingFillableMakerAssetAmounts, + slippageBufferAmount, + })); + } else { + let ordersRemainingFillableTakerAssetAmounts: BigNumber[]; + // find the orders that cover the desired assetBuyAmount (with slippage) + ({ + resultOrders, + remainingFillAmount, + ordersRemainingFillableTakerAssetAmounts, + } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(orders, assetFillAmount, { + remainingFillableTakerAssetAmounts, + slippageBufferAmount, + })); + + ordersRemainingFillableMakerAssetAmounts = _.map( + ordersRemainingFillableTakerAssetAmounts, + (takerAssetAmount: BigNumber, index: number) => { + return orderCalculationUtils.getMakerFillAmount(resultOrders[index], takerAssetAmount); + }, + ); + } + + // if we do not have enough orders to cover the desired assetBuyAmount, throw + if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) { + // We needed the amount they requested to buy, plus the amount for slippage + const totalAmountRequested = assetFillAmount.plus(slippageBufferAmount); + const amountAbleToFill = totalAmountRequested.minus(remainingFillAmount); + // multiplierNeededWithSlippage represents what we need to multiply the assetBuyAmount by + // in order to get the total amount needed considering slippage + // i.e. if slippagePercent was 0.2 (20%), multiplierNeededWithSlippage would be 1.2 + const multiplierNeededWithSlippage = new BigNumber(1).plus(slippagePercentage); + // Given amountAvailableToFillConsideringSlippage * multiplierNeededWithSlippage = amountAbleToFill + // We divide amountUnableToFill by multiplierNeededWithSlippage to determine amountAvailableToFillConsideringSlippage + const amountAvailableToFillConsideringSlippage = amountAbleToFill + .div(multiplierNeededWithSlippage) + .integerValue(BigNumber.ROUND_FLOOR); + + throw new InsufficientAssetLiquidityError(amountAvailableToFillConsideringSlippage); + } + // if we are not buying ZRX: + // given the orders calculated above, find the fee-orders that cover the desired assetBuyAmount (with slippage) + // 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 + let resultFeeOrders = [] as SignedOrder[]; + let feeOrdersRemainingFillableMakerAssetAmounts = [] as BigNumber[]; + if (!isMakerAssetZrxToken) { + const feeOrdersAndRemainingFeeAmount = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + resultOrders, + feeOrders, + { + remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts, + remainingFillableFeeAmounts, + }, + ); + // if we do not have enough feeOrders to cover the fees, throw + if (feeOrdersAndRemainingFeeAmount.remainingFeeAmount.gt(constants.ZERO_AMOUNT)) { + throw new Error(SwapQuoterError.InsufficientZrxLiquidity); + } + resultFeeOrders = feeOrdersAndRemainingFeeAmount.resultFeeOrders; + feeOrdersRemainingFillableMakerAssetAmounts = + feeOrdersAndRemainingFeeAmount.feeOrdersRemainingFillableMakerAssetAmounts; + } + + // assetData information for the result + const takerAssetData = orders[0].takerAssetData; + const makerAssetData = orders[0].makerAssetData; + + // compile the resulting trimmed set of orders for makerAsset and feeOrders that are needed for assetBuyAmount + const trimmedOrdersAndFillableAmounts: OrdersAndFillableAmounts = { + orders: resultOrders, + remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts, + }; + const trimmedFeeOrdersAndFillableAmounts: OrdersAndFillableAmounts = { + orders: resultFeeOrders, + remainingFillableMakerAssetAmounts: feeOrdersRemainingFillableMakerAssetAmounts, + }; + + const bestCaseQuoteInfo = calculateQuoteInfo( + trimmedOrdersAndFillableAmounts, + trimmedFeeOrdersAndFillableAmounts, + assetFillAmount, + isMakerAssetZrxToken, + marketOperation, + ); + // in order to calculate the maxRate, reverse the ordersAndFillableAmounts such that they are sorted from worst rate to best rate + const worstCaseQuoteInfo = calculateQuoteInfo( + reverseOrdersAndFillableAmounts(trimmedOrdersAndFillableAmounts), + reverseOrdersAndFillableAmounts(trimmedFeeOrdersAndFillableAmounts), + assetFillAmount, + isMakerAssetZrxToken, + marketOperation, + ); + + const quoteBase = { + takerAssetData, + makerAssetData, + orders: resultOrders, + feeOrders: resultFeeOrders, + bestCaseQuoteInfo, + worstCaseQuoteInfo, + }; + + if (marketOperation === 'marketBuy') { + return { + ...quoteBase, + type: 'marketBuy', + makerAssetFillAmount: assetFillAmount, + }; + } else { + return { + ...quoteBase, + type: 'marketSell', + takerAssetFillAmount: assetFillAmount, + }; + } +} + +function calculateQuoteInfo( + ordersAndFillableAmounts: OrdersAndFillableAmounts, + feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, + tokenAmount: BigNumber, + isMakerAssetZrxToken: boolean, + marketOperation: MarketOperation, ): SwapQuoteInfo { // find the total eth and zrx needed to buy assetAmount from the resultOrders from left to right - let takerTokenAmount = constants.ZERO_AMOUNT; + let makerTokenAmount = marketOperation === 'marketBuy' ? tokenAmount : constants.ZERO_AMOUNT; + let takerTokenAmount = marketOperation === 'marketSell' ? tokenAmount : constants.ZERO_AMOUNT; let zrxTakerTokenAmount = constants.ZERO_AMOUNT; + if (isMakerAssetZrxToken) { - takerTokenAmount = findTakerTokenAmountNeededToBuyZrx(ordersAndFillableAmounts, makerTokenAmount); + if (marketOperation === 'marketBuy') { + takerTokenAmount = findTakerTokenAmountNeededToBuyZrx(ordersAndFillableAmounts, makerTokenAmount); + } else { + makerTokenAmount = findZrxTokenAmountFromSellingTakerTokenAmount( + ordersAndFillableAmounts, + takerTokenAmount, + ); + } } else { + const findTokenAndZrxAmount = + marketOperation === 'marketBuy' + ? findTakerTokenAndZrxAmountNeededToBuyAsset + : findMakerTokenAmountReceivedAndZrxAmountNeededToSellAsset; // find eth and zrx amounts needed to buy - const takerTokenAndZrxAmountToBuyAsset = findTakerTokenAndZrxAmountNeededToBuyAsset( - ordersAndFillableAmounts, - makerTokenAmount, - ); - takerTokenAmount = takerTokenAndZrxAmountToBuyAsset[0]; - const zrxAmountToBuyAsset = takerTokenAndZrxAmountToBuyAsset[1]; + const tokenAndZrxAmountToBuyAsset = findTokenAndZrxAmount(ordersAndFillableAmounts, makerTokenAmount); + if (marketOperation === 'marketBuy') { + takerTokenAmount = tokenAndZrxAmountToBuyAsset[0]; + } else { + makerTokenAmount = tokenAndZrxAmountToBuyAsset[0]; + } + const zrxAmountToBuyAsset = tokenAndZrxAmountToBuyAsset[1]; // find eth amount needed to buy zrx zrxTakerTokenAmount = findTakerTokenAmountNeededToBuyZrx(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset); } @@ -261,42 +249,6 @@ function calculateMarketBuyQuoteInfo( totalTakerTokenAmount, }; } - -function calculateMarketSellQuoteInfo( - ordersAndFillableAmounts: OrdersAndFillableAmounts, - feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, - takerTokenAmount: BigNumber, - isMakerAssetZrxToken: boolean, -): SwapQuoteInfo { - // find the total eth and zrx needed to buy assetAmount from the resultOrders from left to right - let makerTokenAmount = constants.ZERO_AMOUNT; - let zrxTakerTokenAmount = constants.ZERO_AMOUNT; - if (isMakerAssetZrxToken) { - makerTokenAmount = findZrxTokenAmountFromSellingTakerTokenAmount(ordersAndFillableAmounts, takerTokenAmount); - } else { - // find eth and zrx amounts needed to buy - const takerTokenAndZrxAmountToBuyAsset = findMakerTokenAmountReceivedAndZrxAmountNeededToSellAsset( - ordersAndFillableAmounts, - takerTokenAmount, - ); - makerTokenAmount = takerTokenAndZrxAmountToBuyAsset[0]; - const zrxAmountToSellAsset = takerTokenAndZrxAmountToBuyAsset[1]; - // find eth amount needed to buy zrx - zrxTakerTokenAmount = findTakerTokenAmountNeededToBuyZrx(feeOrdersAndFillableAmounts, zrxAmountToSellAsset); - } - - const feeTakerTokenAmount = zrxTakerTokenAmount; - - // eth amount needed in total is the sum of the amount needed for the asset and the amount needed for fees - const totalTakerTokenAmount = takerTokenAmount.plus(feeTakerTokenAmount); - return { - makerTokenAmount, - takerTokenAmount, - feeTakerTokenAmount, - totalTakerTokenAmount, - }; -} - // given an OrdersAndFillableAmounts, reverse the orders and remainingFillableMakerAssetAmounts properties function reverseOrdersAndFillableAmounts(ordersAndFillableAmounts: OrdersAndFillableAmounts): OrdersAndFillableAmounts { const ordersCopy = _.clone(ordersAndFillableAmounts.orders); diff --git a/packages/asset-buyer/src/utils/utils.ts b/packages/asset-buyer/src/utils/utils.ts index afb8acbb37..b08bb0e152 100644 --- a/packages/asset-buyer/src/utils/utils.ts +++ b/packages/asset-buyer/src/utils/utils.ts @@ -4,7 +4,6 @@ import { AbiDefinition, ContractAbi, MethodAbi } from 'ethereum-types'; import * as _ from 'lodash'; import { constants } from '../constants'; -import { MarketBuySwapQuote, MarketSellSwapQuote, SwapQuote } from '../types'; // tslint:disable:no-unnecessary-type-assertion export const utils = { @@ -26,10 +25,4 @@ export const utils = { }, ) as MethodAbi | undefined; }, - isSwapQuoteMarketBuy(quote: SwapQuote): quote is MarketBuySwapQuote { - return (quote as MarketSellSwapQuote).takerAssetFillAmount !== undefined; - }, - isSwapQuoteMarketSell(quote: SwapQuote): quote is MarketSellSwapQuote { - return (quote as MarketBuySwapQuote).makerAssetFillAmount !== undefined; - }, }; diff --git a/packages/asset-buyer/test/utils/swap_quote.ts b/packages/asset-buyer/test/utils/swap_quote.ts index 0f21724050..cf07ca39ef 100644 --- a/packages/asset-buyer/test/utils/swap_quote.ts +++ b/packages/asset-buyer/test/utils/swap_quote.ts @@ -3,7 +3,7 @@ import { SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; -import { MarketBuySwapQuote, MarketSellSwapQuote } from '../../src'; +import { MarketOperation, SwapQuote } from '../../src/types'; const ZERO_BIG_NUMBER = new BigNumber(0); @@ -25,11 +25,12 @@ export const getSignedOrdersWithNoFees = ( ); }; -export const getFullyFillableMarketBuySwapQuoteWithNoFees = ( +export const getFullyFillableSwapQuoteWithNoFees = ( makerAssetData: string, takerAssetData: string, orders: SignedOrder[], -): MarketBuySwapQuote => { + operation: MarketOperation, +): SwapQuote => { const makerAssetFillAmount = _.reduce( orders, (a: BigNumber, c: SignedOrder) => a.plus(c.makerAssetAmount), @@ -47,46 +48,26 @@ export const getFullyFillableMarketBuySwapQuoteWithNoFees = ( totalTakerTokenAmount, }; - return { + const quoteBase = { makerAssetData, takerAssetData, orders, feeOrders: [], - makerAssetFillAmount, bestCaseQuoteInfo: quoteInfo, worstCaseQuoteInfo: quoteInfo, }; -}; -export const getFullyFillableMarketSellSwapQuoteWithNoFees = ( - makerAssetData: string, - takerAssetData: string, - orders: SignedOrder[], -): MarketSellSwapQuote => { - const makerAssetFillAmount = _.reduce( - orders, - (a: BigNumber, c: SignedOrder) => a.plus(c.makerAssetAmount), - ZERO_BIG_NUMBER, - ); - const totalTakerTokenAmount = _.reduce( - orders, - (a: BigNumber, c: SignedOrder) => a.plus(c.takerAssetAmount), - ZERO_BIG_NUMBER, - ); - const quoteInfo = { - makerTokenAmount: makerAssetFillAmount, - takerTokenAmount: totalTakerTokenAmount, - feeTakerTokenAmount: ZERO_BIG_NUMBER, - totalTakerTokenAmount, - }; - - return { - makerAssetData, - takerAssetData, - orders, - feeOrders: [], - takerAssetFillAmount: totalTakerTokenAmount, - bestCaseQuoteInfo: quoteInfo, - worstCaseQuoteInfo: quoteInfo, - }; + if (operation === 'marketBuy') { + return { + ...quoteBase, + type: 'marketBuy', + makerAssetFillAmount, + }; + } else { + return { + ...quoteBase, + type: 'marketSell', + takerAssetFillAmount: totalTakerTokenAmount, + }; + } }; diff --git a/packages/order-utils/src/market_utils.ts b/packages/order-utils/src/market_utils.ts index 76db1d4881..90059cf09b 100644 --- a/packages/order-utils/src/market_utils.ts +++ b/packages/order-utils/src/market_utils.ts @@ -10,6 +10,7 @@ import { FindFeeOrdersThatCoverFeesForTargetOrdersOpts, FindOrdersThatCoverMakerAssetFillAmountOpts, FindOrdersThatCoverTakerAssetFillAmountOpts, + MarketOperation, OrdersAndRemainingMakerFillAmount, OrdersAndRemainingTakerFillAmount, } from './types'; @@ -20,60 +21,12 @@ export const marketUtils = { takerAssetFillAmount: BigNumber, opts?: FindOrdersThatCoverTakerAssetFillAmountOpts, ): OrdersAndRemainingTakerFillAmount { - assert.doesConformToSchema('orders', orders, schemas.ordersSchema); - assert.isValidBaseUnitAmount('takerAssetFillAmount', takerAssetFillAmount); - // try to get remainingFillableTakerAssetAmounts from opts, if it's not there, use takerAssetAmount values from orders - const remainingFillableTakerAssetAmounts = _.get( - opts, - 'remainingFillableTakerAssetAmounts', - _.map(orders, order => order.takerAssetAmount), - ) as BigNumber[]; - _.forEach(remainingFillableTakerAssetAmounts, (amount, index) => - assert.isValidBaseUnitAmount(`remainingFillableTakerAssetAmount[${index}]`, amount), - ); - assert.assert( - orders.length === remainingFillableTakerAssetAmounts.length, - 'Expected orders.length to equal opts.remainingFillableMakerAssetAmounts.length', - ); - // try to get slippageBufferAmount from opts, if it's not there, default to 0 - const slippageBufferAmount = _.get(opts, 'slippageBufferAmount', constants.ZERO_AMOUNT) as BigNumber; - assert.isValidBaseUnitAmount('opts.slippageBufferAmount', slippageBufferAmount); - // calculate total amount of makerAsset needed to be filled - const totalFillAmount = takerAssetFillAmount.plus(slippageBufferAmount); - // iterate through the orders input from left to right until we have enough makerAsset to fill totalFillAmount - const result = _.reduce( + return findOrdersThatCoverAssetFillAmount( orders, - ({ resultOrders, remainingFillAmount, ordersRemainingFillableTakerAssetAmounts }, order, index) => { - if (remainingFillAmount.isLessThanOrEqualTo(constants.ZERO_AMOUNT)) { - return { - resultOrders, - remainingFillAmount: constants.ZERO_AMOUNT, - ordersRemainingFillableTakerAssetAmounts, - }; - } else { - const takerAssetAmountAvailable = remainingFillableTakerAssetAmounts[index]; - const shouldIncludeOrder = takerAssetAmountAvailable.gt(constants.ZERO_AMOUNT); - // if there is no makerAssetAmountAvailable do not append order to resultOrders - // if we have exceeded the total amount we want to fill set remainingFillAmount to 0 - return { - resultOrders: shouldIncludeOrder ? _.concat(resultOrders, order) : resultOrders, - ordersRemainingFillableTakerAssetAmounts: shouldIncludeOrder - ? _.concat(ordersRemainingFillableTakerAssetAmounts, takerAssetAmountAvailable) - : ordersRemainingFillableTakerAssetAmounts, - remainingFillAmount: BigNumber.max( - constants.ZERO_AMOUNT, - remainingFillAmount.minus(takerAssetAmountAvailable), - ), - }; - } - }, - { - resultOrders: [] as T[], - remainingFillAmount: totalFillAmount, - ordersRemainingFillableTakerAssetAmounts: [] as BigNumber[], - }, - ); - return result; + takerAssetFillAmount, + 'marketSell', + opts, + ) as OrdersAndRemainingTakerFillAmount; }, /** * Takes an array of orders and returns a subset of those orders that has enough makerAssetAmount @@ -90,60 +43,12 @@ export const marketUtils = { makerAssetFillAmount: BigNumber, opts?: FindOrdersThatCoverMakerAssetFillAmountOpts, ): OrdersAndRemainingMakerFillAmount { - assert.doesConformToSchema('orders', orders, schemas.ordersSchema); - assert.isValidBaseUnitAmount('makerAssetFillAmount', makerAssetFillAmount); - // try to get remainingFillableMakerAssetAmounts from opts, if it's not there, use makerAssetAmount values from orders - const remainingFillableMakerAssetAmounts = _.get( - opts, - 'remainingFillableMakerAssetAmounts', - _.map(orders, order => order.makerAssetAmount), - ) as BigNumber[]; - _.forEach(remainingFillableMakerAssetAmounts, (amount, index) => - assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount), - ); - assert.assert( - orders.length === remainingFillableMakerAssetAmounts.length, - 'Expected orders.length to equal opts.remainingFillableMakerAssetAmounts.length', - ); - // try to get slippageBufferAmount from opts, if it's not there, default to 0 - const slippageBufferAmount = _.get(opts, 'slippageBufferAmount', constants.ZERO_AMOUNT) as BigNumber; - assert.isValidBaseUnitAmount('opts.slippageBufferAmount', slippageBufferAmount); - // calculate total amount of makerAsset needed to be filled - const totalFillAmount = makerAssetFillAmount.plus(slippageBufferAmount); - // iterate through the orders input from left to right until we have enough makerAsset to fill totalFillAmount - const result = _.reduce( + return findOrdersThatCoverAssetFillAmount( orders, - ({ resultOrders, remainingFillAmount, ordersRemainingFillableMakerAssetAmounts }, order, index) => { - if (remainingFillAmount.isLessThanOrEqualTo(constants.ZERO_AMOUNT)) { - return { - resultOrders, - remainingFillAmount: constants.ZERO_AMOUNT, - ordersRemainingFillableMakerAssetAmounts, - }; - } else { - const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index]; - const shouldIncludeOrder = makerAssetAmountAvailable.gt(constants.ZERO_AMOUNT); - // if there is no makerAssetAmountAvailable do not append order to resultOrders - // if we have exceeded the total amount we want to fill set remainingFillAmount to 0 - return { - resultOrders: shouldIncludeOrder ? _.concat(resultOrders, order) : resultOrders, - ordersRemainingFillableMakerAssetAmounts: shouldIncludeOrder - ? _.concat(ordersRemainingFillableMakerAssetAmounts, makerAssetAmountAvailable) - : ordersRemainingFillableMakerAssetAmounts, - remainingFillAmount: BigNumber.max( - constants.ZERO_AMOUNT, - remainingFillAmount.minus(makerAssetAmountAvailable), - ), - }; - } - }, - { - resultOrders: [] as T[], - remainingFillAmount: totalFillAmount, - ordersRemainingFillableMakerAssetAmounts: [] as BigNumber[], - }, - ); - return result; + makerAssetFillAmount, + 'marketBuy', + opts, + ) as OrdersAndRemainingMakerFillAmount; }, /** * Takes an array of orders and an array of feeOrders. Returns a subset of the feeOrders that has enough ZRX @@ -222,3 +127,82 @@ export const marketUtils = { // https://github.com/0xProject/0x-protocol-specification/blob/master/v2/forwarding-contract-specification.md#over-buying-zrx }, }; + +function findOrdersThatCoverAssetFillAmount( + orders: T[], + assetFillAmount: BigNumber, + operation: MarketOperation, + opts?: FindOrdersThatCoverTakerAssetFillAmountOpts, +): OrdersAndRemainingTakerFillAmount | OrdersAndRemainingMakerFillAmount { + const variablePrefix = operation === 'marketBuy' ? 'maker' : 'taker'; + assert.doesConformToSchema('orders', orders, schemas.ordersSchema); + assert.isValidBaseUnitAmount(`${variablePrefix}AssetFillAmount}`, assetFillAmount); + // try to get remainingFillableTakerAssetAmounts from opts, if it's not there, use takerAssetAmount values from orders + const remainingFillableAssetAmounts = _.get( + opts, + `remainingFillable${variablePrefix}AssetAmounts`, + _.map(orders, order => (operation === 'marketBuy' ? order.makerAssetAmount : order.takerAssetAmount)), + ) as BigNumber[]; + _.forEach(remainingFillableAssetAmounts, (amount, index) => + assert.isValidBaseUnitAmount(`remainingFillable${variablePrefix}AssetAmount[${index}]`, amount), + ); + assert.assert( + orders.length === remainingFillableAssetAmounts.length, + `Expected orders.length to equal opts.remainingFillable${variablePrefix}AssetAmounts.length`, + ); + // try to get slippageBufferAmount from opts, if it's not there, default to 0 + const slippageBufferAmount = _.get(opts, 'slippageBufferAmount', constants.ZERO_AMOUNT) as BigNumber; + assert.isValidBaseUnitAmount('opts.slippageBufferAmount', slippageBufferAmount); + // calculate total amount of asset needed to be filled + const totalFillAmount = assetFillAmount.plus(slippageBufferAmount); + // iterate through the orders input from left to right until we have enough makerAsset to fill totalFillAmount + const result = _.reduce( + orders, + ({ resultOrders, remainingFillAmount, ordersRemainingFillableAssetAmounts }, order, index) => { + if (remainingFillAmount.isLessThanOrEqualTo(constants.ZERO_AMOUNT)) { + return { + resultOrders, + remainingFillAmount: constants.ZERO_AMOUNT, + ordersRemainingFillableAssetAmounts, + }; + } else { + const assetAmountAvailable = remainingFillableAssetAmounts[index]; + const shouldIncludeOrder = assetAmountAvailable.gt(constants.ZERO_AMOUNT); + // if there is no assetAmountAvailable do not append order to resultOrders + // if we have exceeded the total amount we want to fill set remainingFillAmount to 0 + return { + resultOrders: shouldIncludeOrder ? _.concat(resultOrders, order) : resultOrders, + ordersRemainingFillableAssetAmounts: shouldIncludeOrder + ? _.concat(ordersRemainingFillableAssetAmounts, assetAmountAvailable) + : ordersRemainingFillableAssetAmounts, + remainingFillAmount: BigNumber.max( + constants.ZERO_AMOUNT, + remainingFillAmount.minus(assetAmountAvailable), + ), + }; + } + }, + { + resultOrders: [] as T[], + remainingFillAmount: totalFillAmount, + ordersRemainingFillableAssetAmounts: [] as BigNumber[], + }, + ); + + const { + ordersRemainingFillableAssetAmounts: resultOrdersRemainingFillableAssetAmounts, + ...ordersAndRemainingFillAmount + } = result; + + if (operation === 'marketBuy') { + return { + ...ordersAndRemainingFillAmount, + ordersRemainingFillableMakerAssetAmounts: resultOrdersRemainingFillableAssetAmounts, + }; + } else { + return { + ...ordersAndRemainingFillAmount, + ordersRemainingFillableMakerAssetAmounts: resultOrdersRemainingFillableAssetAmounts, + }; + } +} diff --git a/packages/order-utils/src/types.ts b/packages/order-utils/src/types.ts index c58eafe036..fd48114270 100644 --- a/packages/order-utils/src/types.ts +++ b/packages/order-utils/src/types.ts @@ -49,6 +49,8 @@ export interface FindOrdersThatCoverTakerAssetFillAmountOpts { slippageBufferAmount?: BigNumber; } +export type MarketOperation = 'marketSell' | 'marketBuy'; + /** * remainingFillableMakerAssetAmount: An array of BigNumbers corresponding to the `orders` parameter. * You can use `OrderStateUtils` `@0x/order-utils` to perform blockchain lookups for these values.