refactored types and market sell operation

This commit is contained in:
David Sun 2019-07-03 14:29:28 -07:00
parent d0ea74e180
commit b4ac6d3439
9 changed files with 386 additions and 511 deletions

View File

@ -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<ExchangeMarketBuySmartContractParams | ExchangeMarketSellSmartContractParams> {
export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase<ExchangeSmartContractParams> {
public readonly provider: ZeroExProvider;
public readonly networkId: number;
@ -48,19 +44,18 @@ export class ExchangeSwapQuoteConsumer
): Promise<CalldataInfo> {
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<SwapQuoteGetOutputOpts>,
): Promise<SmartContractParamsInfo<ExchangeMarketBuySmartContractParams | ExchangeMarketSellSmartContractParams>> {
_opts: Partial<SwapQuoteGetOutputOpts>,
): Promise<SmartContractParamsInfo<ExchangeSmartContractParams>> {
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,

View File

@ -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<ForwarderMarketBuySmartContractParams | ForwarderMarketSellSmartContractParams> {
export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<ForwarderSmartContractParams> {
public readonly provider: ZeroExProvider;
public readonly networkId: number;
@ -55,34 +51,18 @@ export class ForwarderSwapQuoteConsumer
): Promise<CalldataInfo> {
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<ForwarderSwapQuoteGetOutputOpts>,
): Promise<
SmartContractParamsInfo<ForwarderMarketBuySmartContractParams | ForwarderMarketSellSmartContractParams>
> {
): Promise<SmartContractParamsInfo<ForwarderSmartContractParams>> {
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,

View File

@ -66,46 +66,48 @@ export interface SmartContractParamsInfo<T> {
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<ExchangeMarketSellSmartContractParams, 'takerAssetFillAmount'>,
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<T> {
export interface SwapQuoteConsumerBase<T> {
getCalldataOrThrowAsync(quote: SwapQuote, opts: Partial<SwapQuoteGetOutputOpts>): Promise<CalldataInfo>;
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.

View File

@ -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 {

View File

@ -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);

View File

@ -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;
},
};

View File

@ -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,
};
}
};

View File

@ -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<T> {
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<T>;
},
/**
* 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<T> {
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<T>;
},
/**
* 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<T extends Order>(
orders: T[],
assetFillAmount: BigNumber,
operation: MarketOperation,
opts?: FindOrdersThatCoverTakerAssetFillAmountOpts,
): OrdersAndRemainingTakerFillAmount<T> | OrdersAndRemainingMakerFillAmount<T> {
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,
};
}
}

View File

@ -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.