Merge pull request #2720 from 0xProject/refactor_rfq_comparison_price_integration

feat: Refactor rfq comparison price integration
This commit is contained in:
Daniel Pyrathon 2020-10-20 16:17:04 -07:00 committed by GitHub
commit 0571244e9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 853 additions and 144 deletions

View File

@ -8,7 +8,7 @@
"evmVersion": "istanbul", "evmVersion": "istanbul",
"optimizer": { "optimizer": {
"enabled": true, "enabled": true,
"runs": 1000000, "runs": 62500,
"details": { "yul": true, "deduplicate": true, "cse": true, "constantOptimizer": true } "details": { "yul": true, "deduplicate": true, "cse": true, "constantOptimizer": true }
}, },
"outputSelection": { "outputSelection": {

View File

@ -20,6 +20,7 @@ pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol";
import "@0x/contracts-exchange/contracts/src/interfaces/IExchange.sol"; import "@0x/contracts-exchange/contracts/src/interfaces/IExchange.sol";
import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol"; import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol";
import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol";
@ -36,6 +37,19 @@ contract NativeOrderSampler {
/// @dev Gas limit for calls to `getOrderFillableTakerAmount()`. /// @dev Gas limit for calls to `getOrderFillableTakerAmount()`.
uint256 constant internal DEFAULT_CALL_GAS = 200e3; // 200k uint256 constant internal DEFAULT_CALL_GAS = 200e3; // 200k
function getTokenDecimals(
address makerTokenAddress,
address takerTokenAddress
)
public
view
returns (uint256, uint256)
{
uint256 fromTokenDecimals = LibERC20Token.decimals(makerTokenAddress);
uint256 toTokenDecimals = LibERC20Token.decimals(takerTokenAddress);
return (fromTokenDecimals, toTokenDecimals);
}
/// @dev Queries the fillable taker asset amounts of native orders. /// @dev Queries the fillable taker asset amounts of native orders.
/// Effectively ignores orders that have empty signatures or /// Effectively ignores orders that have empty signatures or
/// maker/taker asset amounts (returning 0). /// maker/taker asset amounts (returning 0).

View File

@ -64,7 +64,7 @@
"@0x/json-schemas": "^5.1.0", "@0x/json-schemas": "^5.1.0",
"@0x/order-utils": "^10.3.0", "@0x/order-utils": "^10.3.0",
"@0x/orderbook": "^2.2.7", "@0x/orderbook": "^2.2.7",
"@0x/quote-server": "^2.0.2", "@0x/quote-server": "^3.1.0",
"@0x/types": "^3.2.0", "@0x/types": "^3.2.0",
"@0x/typescript-typings": "^5.1.1", "@0x/typescript-typings": "^5.1.1",
"@0x/utils": "^5.5.1", "@0x/utils": "^5.5.1",

View File

@ -122,3 +122,12 @@ export const constants = {
DEFAULT_INFO_LOGGER, DEFAULT_INFO_LOGGER,
DEFAULT_WARNING_LOGGER, DEFAULT_WARNING_LOGGER,
}; };
// This feature flag allows us to merge the price-aware RFQ pricing
// project while still controlling when to activate the feature. We plan to do some
// data analysis work and address some of the issues with maker fillable amounts
// in later milestones. Once the feature is fully rolled out and is providing value
// and we have assessed that there is no user impact, we will proceed in cleaning up
// the feature flag. When that time comes, follow this PR to "undo" the feature flag:
// https://github.com/0xProject/0x-monorepo/pull/2735
export const IS_PRICE_AWARE_RFQ_ENABLED: boolean = false;

View File

@ -18,7 +18,7 @@ export {
SRAPollingOrderProviderOpts, SRAPollingOrderProviderOpts,
SRAWebsocketOrderProviderOpts, SRAWebsocketOrderProviderOpts,
} from '@0x/orderbook'; } from '@0x/orderbook';
export { RFQTFirmQuote, RFQTIndicativeQuote } from '@0x/quote-server'; export { RFQTFirmQuote, RFQTIndicativeQuote, TakerRequestQueryParams } from '@0x/quote-server';
export { export {
APIOrder, APIOrder,
Asset, Asset,

View File

@ -8,7 +8,7 @@ import { BlockParamLiteral, SupportedProvider, ZeroExProvider } from 'ethereum-t
import * as _ from 'lodash'; import * as _ from 'lodash';
import { artifacts } from './artifacts'; import { artifacts } from './artifacts';
import { constants } from './constants'; import { constants, IS_PRICE_AWARE_RFQ_ENABLED } from './constants';
import { import {
CalculateSwapQuoteOpts, CalculateSwapQuoteOpts,
LiquidityForTakerMakerAssetDataPair, LiquidityForTakerMakerAssetDataPair,
@ -683,7 +683,21 @@ export class SwapQuoter {
this.expiryBufferMs, this.expiryBufferMs,
); );
// If an API key was provided, but the key is not whitelisted, raise a warning and disable RFQ
if (opts.rfqt && opts.rfqt.apiKey && !this._isApiKeyWhitelisted(opts.rfqt.apiKey)) {
if (rfqtOptions && rfqtOptions.warningLogger) {
rfqtOptions.warningLogger(
{
apiKey: opts.rfqt.apiKey,
},
'Attempt at using an RFQ API key that is not whitelisted. Disabling RFQ for the request lifetime.',
);
}
opts.rfqt = undefined;
}
if ( if (
!IS_PRICE_AWARE_RFQ_ENABLED && // Price-aware RFQ is disabled.
opts.rfqt && // This is an RFQT-enabled API request opts.rfqt && // This is an RFQT-enabled API request
opts.rfqt.intentOnFilling && // The requestor is asking for a firm quote opts.rfqt.intentOnFilling && // The requestor is asking for a firm quote
opts.rfqt.apiKey && opts.rfqt.apiKey &&
@ -700,6 +714,7 @@ export class SwapQuoter {
takerAssetData, takerAssetData,
assetFillAmount, assetFillAmount,
marketOperation, marketOperation,
undefined,
opts.rfqt, opts.rfqt,
) )
.then(firmQuotes => firmQuotes.map(quote => quote.signedOrder)), .then(firmQuotes => firmQuotes.map(quote => quote.signedOrder)),
@ -707,9 +722,7 @@ export class SwapQuoter {
} }
const orderBatches: SignedOrder[][] = await Promise.all(orderBatchPromises); const orderBatches: SignedOrder[][] = await Promise.all(orderBatchPromises);
const unsortedOrders: SignedOrder[] = orderBatches.reduce((_orders, batch) => _orders.concat(...batch), []); const unsortedOrders: SignedOrder[] = orderBatches.reduce((_orders, batch) => _orders.concat(...batch), []);
const orders = sortingUtils.sortOrders(unsortedOrders); const orders = sortingUtils.sortOrders(unsortedOrders);
// if no native orders, pass in a dummy order for the sampler to have required metadata for sampling // if no native orders, pass in a dummy order for the sampler to have required metadata for sampling

View File

@ -1,4 +1,5 @@
import { BlockParam, ContractAddresses, GethCallOverrides } from '@0x/contract-wrappers'; import { BlockParam, ContractAddresses, GethCallOverrides } from '@0x/contract-wrappers';
import { TakerRequestQueryParams } from '@0x/quote-server';
import { SignedOrder } from '@0x/types'; import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
@ -357,9 +358,7 @@ export enum OrderPrunerPermittedFeeTypes {
export interface MockedRfqtFirmQuoteResponse { export interface MockedRfqtFirmQuoteResponse {
endpoint: string; endpoint: string;
requestApiKey: string; requestApiKey: string;
requestParams: { requestParams: TakerRequestQueryParams;
[key: string]: string | undefined;
};
responseData: any; responseData: any;
responseCode: number; responseCode: number;
} }
@ -370,9 +369,7 @@ export interface MockedRfqtFirmQuoteResponse {
export interface MockedRfqtIndicativeQuoteResponse { export interface MockedRfqtIndicativeQuoteResponse {
endpoint: string; endpoint: string;
requestApiKey: string; requestApiKey: string;
requestParams: { requestParams: TakerRequestQueryParams;
[key: string]: string | undefined;
};
responseData: any; responseData: any;
responseCode: number; responseCode: number;
} }
@ -381,3 +378,5 @@ export interface SamplerOverrides {
overrides: GethCallOverrides; overrides: GethCallOverrides;
block: BlockParam; block: BlockParam;
} }
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

View File

@ -282,3 +282,4 @@ export const ONE_HOUR_IN_SECONDS = 60 * 60;
export const ONE_SECOND_MS = 1000; export const ONE_SECOND_MS = 1000;
export const NULL_BYTES = '0x'; export const NULL_BYTES = '0x';
export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
export const COMPARISON_PRICE_DECIMALS = 5;

View File

@ -1,15 +1,18 @@
import { ContractAddresses } from '@0x/contract-addresses'; import { ContractAddresses } from '@0x/contract-addresses';
import { Web3Wrapper } from '@0x/dev-utils';
import { RFQTIndicativeQuote } from '@0x/quote-server'; import { RFQTIndicativeQuote } from '@0x/quote-server';
import { SignedOrder } from '@0x/types'; import { SignedOrder } from '@0x/types';
import { BigNumber, NULL_ADDRESS } from '@0x/utils'; import { BigNumber, NULL_ADDRESS } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { MarketOperation } from '../../types'; import { IS_PRICE_AWARE_RFQ_ENABLED } from '../../constants';
import { MarketOperation, Omit } from '../../types';
import { QuoteRequestor } from '../quote_requestor'; import { QuoteRequestor } from '../quote_requestor';
import { generateQuoteReport, QuoteReport } from './../quote_report_generator'; import { generateQuoteReport, QuoteReport } from './../quote_report_generator';
import { import {
BUY_SOURCE_FILTER, BUY_SOURCE_FILTER,
COMPARISON_PRICE_DECIMALS,
DEFAULT_GET_MARKET_ORDERS_OPTS, DEFAULT_GET_MARKET_ORDERS_OPTS,
FEE_QUOTE_SOURCES, FEE_QUOTE_SOURCES,
ONE_ETHER, ONE_ETHER,
@ -33,12 +36,12 @@ import {
CollapsedFill, CollapsedFill,
DexSample, DexSample,
ERC20BridgeSource, ERC20BridgeSource,
ExchangeProxyOverhead, GenerateOptimizedOrdersOpts,
FeeSchedule,
GetMarketOrdersOpts, GetMarketOrdersOpts,
MarketSideLiquidity, MarketSideLiquidity,
OptimizedMarketOrder, OptimizedMarketOrder,
OptimizerResult, OptimizerResult,
OptimizerResultWithReport,
OrderDomain, OrderDomain,
TokenAdjacencyGraph, TokenAdjacencyGraph,
} from './types'; } from './types';
@ -58,6 +61,7 @@ export async function getRfqtIndicativeQuotesAsync(
takerAssetData: string, takerAssetData: string,
marketOperation: MarketOperation, marketOperation: MarketOperation,
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
comparisonPrice: BigNumber | undefined,
opts: Partial<GetMarketOrdersOpts>, opts: Partial<GetMarketOrdersOpts>,
): Promise<RFQTIndicativeQuote[]> { ): Promise<RFQTIndicativeQuote[]> {
if (opts.rfqt && opts.rfqt.isIndicative === true && opts.rfqt.quoteRequestor) { if (opts.rfqt && opts.rfqt.isIndicative === true && opts.rfqt.quoteRequestor) {
@ -66,6 +70,7 @@ export async function getRfqtIndicativeQuotesAsync(
takerAssetData, takerAssetData,
assetFillAmount, assetFillAmount,
marketOperation, marketOperation,
comparisonPrice,
opts.rfqt, opts.rfqt,
); );
} else { } else {
@ -168,6 +173,7 @@ export class MarketOperationUtils {
// Call the sampler contract. // Call the sampler contract.
const samplerPromise = this._sampler.executeAsync( const samplerPromise = this._sampler.executeAsync(
this._sampler.getTokenDecimals(makerToken, takerToken),
// Get native order fillable amounts. // Get native order fillable amounts.
this._sampler.getOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchange), this._sampler.getOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchange),
// Get ETH -> maker token price. // Get ETH -> maker token price.
@ -211,12 +217,14 @@ export class MarketOperationUtils {
), ),
); );
const rfqtPromise = quoteSourceFilters.isAllowed(ERC20BridgeSource.Native) const rfqtPromise =
!IS_PRICE_AWARE_RFQ_ENABLED && quoteSourceFilters.isAllowed(ERC20BridgeSource.Native)
? getRfqtIndicativeQuotesAsync( ? getRfqtIndicativeQuotesAsync(
nativeOrders[0].makerAssetData, nativeOrders[0].makerAssetData,
nativeOrders[0].takerAssetData, nativeOrders[0].takerAssetData,
MarketOperation.Sell, MarketOperation.Sell,
takerAmount, takerAmount,
undefined,
_opts, _opts,
) )
: Promise.resolve([]); : Promise.resolve([]);
@ -234,7 +242,7 @@ export class MarketOperationUtils {
: Promise.resolve([]); : Promise.resolve([]);
const [ const [
[orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes], [tokenDecimals, orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes],
rfqtIndicativeQuotes, rfqtIndicativeQuotes,
offChainBalancerQuotes, offChainBalancerQuotes,
offChainCreamQuotes, offChainCreamQuotes,
@ -247,6 +255,7 @@ export class MarketOperationUtils {
offChainBancorPromise, offChainBancorPromise,
]); ]);
const [makerTokenDecimals, takerTokenDecimals] = tokenDecimals;
return { return {
side: MarketOperation.Sell, side: MarketOperation.Sell,
inputAmount: takerAmount, inputAmount: takerAmount,
@ -259,6 +268,9 @@ export class MarketOperationUtils {
ethToInputRate: ethToTakerAssetRate, ethToInputRate: ethToTakerAssetRate,
rfqtIndicativeQuotes, rfqtIndicativeQuotes,
twoHopQuotes, twoHopQuotes,
quoteSourceFilters,
makerTokenDecimals: makerTokenDecimals.toNumber(),
takerTokenDecimals: takerTokenDecimals.toNumber(),
}; };
} }
@ -311,6 +323,7 @@ export class MarketOperationUtils {
// Call the sampler contract. // Call the sampler contract.
const samplerPromise = this._sampler.executeAsync( const samplerPromise = this._sampler.executeAsync(
this._sampler.getTokenDecimals(makerToken, takerToken),
// Get native order fillable amounts. // Get native order fillable amounts.
this._sampler.getOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchange), this._sampler.getOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchange),
// Get ETH -> makerToken token price. // Get ETH -> makerToken token price.
@ -352,17 +365,17 @@ export class MarketOperationUtils {
this._liquidityProviderRegistry, this._liquidityProviderRegistry,
), ),
); );
const rfqtPromise =
const rfqtPromise = quoteSourceFilters.isAllowed(ERC20BridgeSource.Native) !IS_PRICE_AWARE_RFQ_ENABLED && quoteSourceFilters.isAllowed(ERC20BridgeSource.Native)
? getRfqtIndicativeQuotesAsync( ? getRfqtIndicativeQuotesAsync(
nativeOrders[0].makerAssetData, nativeOrders[0].makerAssetData,
nativeOrders[0].takerAssetData, nativeOrders[0].takerAssetData,
MarketOperation.Buy, MarketOperation.Buy,
makerAmount, makerAmount,
undefined,
_opts, _opts,
) )
: Promise.resolve([]); : Promise.resolve([]);
const offChainBalancerPromise = sampleBalancerOffChain const offChainBalancerPromise = sampleBalancerOffChain
? this._sampler.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, sampleAmounts) ? this._sampler.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, sampleAmounts)
: Promise.resolve([]); : Promise.resolve([]);
@ -372,7 +385,7 @@ export class MarketOperationUtils {
: Promise.resolve([]); : Promise.resolve([]);
const [ const [
[orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes], [tokenDecimals, orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes],
rfqtIndicativeQuotes, rfqtIndicativeQuotes,
offChainBalancerQuotes, offChainBalancerQuotes,
offChainCreamQuotes, offChainCreamQuotes,
@ -381,6 +394,7 @@ export class MarketOperationUtils {
(dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.MultiBridge) || []).forEach( (dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.MultiBridge) || []).forEach(
q => (q.fillData = { poolAddress: this._multiBridge }), q => (q.fillData = { poolAddress: this._multiBridge }),
); );
const [makerTokenDecimals, takerTokenDecimals] = tokenDecimals;
return { return {
side: MarketOperation.Buy, side: MarketOperation.Buy,
inputAmount: makerAmount, inputAmount: makerAmount,
@ -393,6 +407,9 @@ export class MarketOperationUtils {
ethToInputRate: ethToMakerAssetRate, ethToInputRate: ethToMakerAssetRate,
rfqtIndicativeQuotes, rfqtIndicativeQuotes,
twoHopQuotes, twoHopQuotes,
quoteSourceFilters,
makerTokenDecimals: makerTokenDecimals.toNumber(),
takerTokenDecimals: takerTokenDecimals.toNumber(),
}; };
} }
@ -408,29 +425,8 @@ export class MarketOperationUtils {
nativeOrders: SignedOrder[], nativeOrders: SignedOrder[],
takerAmount: BigNumber, takerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>, opts?: Partial<GetMarketOrdersOpts>,
): Promise<OptimizerResult> { ): Promise<OptimizerResultWithReport> {
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; return this._getMarketSideOrdersAsync(nativeOrders, takerAmount, MarketOperation.Sell, opts);
const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, _opts);
const optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, {
bridgeSlippage: _opts.bridgeSlippage,
maxFallbackSlippage: _opts.maxFallbackSlippage,
excludedSources: _opts.excludedSources,
feeSchedule: _opts.feeSchedule,
exchangeProxyOverhead: _opts.exchangeProxyOverhead,
allowFallback: _opts.allowFallback,
});
// Compute Quote Report and return the results.
let quoteReport: QuoteReport | undefined;
if (_opts.shouldGenerateQuoteReport) {
quoteReport = MarketOperationUtils._computeQuoteReport(
nativeOrders,
_opts.rfqt ? _opts.rfqt.quoteRequestor : undefined,
marketSideLiquidity,
optimizerResult,
);
}
return { ...optimizerResult, quoteReport };
} }
/** /**
@ -445,27 +441,8 @@ export class MarketOperationUtils {
nativeOrders: SignedOrder[], nativeOrders: SignedOrder[],
makerAmount: BigNumber, makerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>, opts?: Partial<GetMarketOrdersOpts>,
): Promise<OptimizerResult> { ): Promise<OptimizerResultWithReport> {
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; return this._getMarketSideOrdersAsync(nativeOrders, makerAmount, MarketOperation.Buy, opts);
const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, _opts);
const optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, {
bridgeSlippage: _opts.bridgeSlippage,
maxFallbackSlippage: _opts.maxFallbackSlippage,
excludedSources: _opts.excludedSources,
feeSchedule: _opts.feeSchedule,
exchangeProxyOverhead: _opts.exchangeProxyOverhead,
allowFallback: _opts.allowFallback,
});
let quoteReport: QuoteReport | undefined;
if (_opts.shouldGenerateQuoteReport) {
quoteReport = MarketOperationUtils._computeQuoteReport(
nativeOrders,
_opts.rfqt ? _opts.rfqt.quoteRequestor : undefined,
marketSideLiquidity,
optimizerResult,
);
}
return { ...optimizerResult, quoteReport };
} }
/** /**
@ -548,6 +525,7 @@ export class MarketOperationUtils {
inputToken: makerToken, inputToken: makerToken,
outputToken: takerToken, outputToken: takerToken,
twoHopQuotes: [], twoHopQuotes: [],
quoteSourceFilters,
}, },
{ {
bridgeSlippage: _opts.bridgeSlippage, bridgeSlippage: _opts.bridgeSlippage,
@ -567,18 +545,9 @@ export class MarketOperationUtils {
); );
} }
private async _generateOptimizedOrdersAsync( public async _generateOptimizedOrdersAsync(
marketSideLiquidity: MarketSideLiquidity, marketSideLiquidity: Omit<MarketSideLiquidity, 'makerTokenDecimals' | 'takerTokenDecimals'>,
opts: { opts: GenerateOptimizedOrdersOpts,
runLimit?: number;
bridgeSlippage?: number;
maxFallbackSlippage?: number;
excludedSources?: ERC20BridgeSource[];
feeSchedule?: FeeSchedule;
exchangeProxyOverhead?: ExchangeProxyOverhead;
allowFallback?: boolean;
shouldBatchBridgeOrders?: boolean;
},
): Promise<OptimizerResult> { ): Promise<OptimizerResult> {
const { const {
inputToken, inputToken,
@ -671,6 +640,151 @@ export class MarketOperationUtils {
sourceFlags: collapsedPath.sourceFlags, sourceFlags: collapsedPath.sourceFlags,
}; };
} }
private async _getMarketSideOrdersAsync(
nativeOrders: SignedOrder[],
amount: BigNumber,
side: MarketOperation,
opts?: Partial<GetMarketOrdersOpts>,
): Promise<OptimizerResultWithReport> {
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const optimizerOpts: GenerateOptimizedOrdersOpts = {
bridgeSlippage: _opts.bridgeSlippage,
maxFallbackSlippage: _opts.maxFallbackSlippage,
excludedSources: _opts.excludedSources,
feeSchedule: _opts.feeSchedule,
allowFallback: _opts.allowFallback,
exchangeProxyOverhead: _opts.exchangeProxyOverhead,
};
// Compute an optimized path for on-chain DEX and open-orderbook. This should not include RFQ liquidity.
const marketLiquidityFnAsync =
side === MarketOperation.Sell
? this.getMarketSellLiquidityAsync.bind(this)
: this.getMarketBuyLiquidityAsync.bind(this);
const marketSideLiquidity = await marketLiquidityFnAsync(nativeOrders, amount, _opts);
let optimizerResult: OptimizerResult | undefined;
try {
optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, optimizerOpts);
} catch (e) {
// If no on-chain or off-chain Open Orderbook orders are present, a `NoOptimalPath` will be thrown.
// If this happens at this stage, there is still a chance that an RFQ order is fillable, therefore
// we catch the error and continue.
if (e.message !== AggregationError.NoOptimalPath) {
throw e;
}
}
// If RFQ liquidity is enabled, make a request to check RFQ liquidity
const { rfqt } = _opts;
if (
IS_PRICE_AWARE_RFQ_ENABLED &&
rfqt &&
rfqt.quoteRequestor &&
marketSideLiquidity.quoteSourceFilters.isAllowed(ERC20BridgeSource.Native)
) {
// Calculate a suggested price. For now, this is simply the overall price of the aggregation.
let comparisonPrice: BigNumber | undefined;
if (optimizerResult) {
const totalMakerAmount = BigNumber.sum(
...optimizerResult.optimizedOrders.map(order => order.makerAssetAmount),
);
const totalTakerAmount = BigNumber.sum(
...optimizerResult.optimizedOrders.map(order => order.takerAssetAmount),
);
if (totalMakerAmount.gt(0)) {
const totalMakerAmountUnitAmount = Web3Wrapper.toUnitAmount(
totalMakerAmount,
marketSideLiquidity.makerTokenDecimals,
);
const totalTakerAmountUnitAmount = Web3Wrapper.toUnitAmount(
totalTakerAmount,
marketSideLiquidity.takerTokenDecimals,
);
comparisonPrice = totalMakerAmountUnitAmount
.div(totalTakerAmountUnitAmount)
.decimalPlaces(COMPARISON_PRICE_DECIMALS);
}
}
// If we are making an indicative quote, make the RFQT request and then re-run the sampler if new orders come back.
if (rfqt.isIndicative) {
const indicativeQuotes = await getRfqtIndicativeQuotesAsync(
nativeOrders[0].makerAssetData,
nativeOrders[0].takerAssetData,
side,
amount,
comparisonPrice,
_opts,
);
// Re-run optimizer with the new indicative quote
if (indicativeQuotes.length > 0) {
optimizerResult = await this._generateOptimizedOrdersAsync(
{
...marketSideLiquidity,
rfqtIndicativeQuotes: indicativeQuotes,
},
optimizerOpts,
);
}
} else {
// A firm quote is being requested. Ensure that `intentOnFilling` is enabled.
if (rfqt.intentOnFilling) {
// Extra validation happens when requesting a firm quote, such as ensuring that the takerAddress
// is indeed valid.
if (!rfqt.takerAddress || rfqt.takerAddress === NULL_ADDRESS) {
throw new Error('RFQ-T requests must specify a taker address');
}
const firmQuotes = await rfqt.quoteRequestor.requestRfqtFirmQuotesAsync(
nativeOrders[0].makerAssetData,
nativeOrders[0].takerAssetData,
amount,
side,
comparisonPrice,
rfqt,
);
if (firmQuotes.length > 0) {
// Re-run optimizer with the new firm quote. This is the second and last time
// we run the optimized in a block of code. In this case, we don't catch a potential `NoOptimalPath` exception
// and we let it bubble up if it happens.
//
// NOTE: as of now, we assume that RFQ orders are 100% fillable because these are trusted market makers, therefore
// we do not perform an extra check to get fillable taker amounts.
optimizerResult = await this._generateOptimizedOrdersAsync(
{
...marketSideLiquidity,
nativeOrders: marketSideLiquidity.nativeOrders.concat(
firmQuotes.map(quote => quote.signedOrder),
),
orderFillableAmounts: marketSideLiquidity.orderFillableAmounts.concat(
firmQuotes.map(quote => quote.signedOrder.takerAssetAmount),
),
},
optimizerOpts,
);
}
}
}
}
// At this point we should have at least one valid optimizer result, therefore we manually raise
// `NoOptimalPath` if no optimizer result was ever set.
if (optimizerResult === undefined) {
throw new Error(AggregationError.NoOptimalPath);
}
// Compute Quote Report and return the results.
let quoteReport: QuoteReport | undefined;
if (_opts.shouldGenerateQuoteReport) {
quoteReport = MarketOperationUtils._computeQuoteReport(
nativeOrders,
_opts.rfqt ? _opts.rfqt.quoteRequestor : undefined,
marketSideLiquidity,
optimizerResult,
);
}
return { ...optimizerResult, quoteReport };
}
} }
// tslint:disable: max-file-line-count // tslint:disable: max-file-line-count

View File

@ -1,6 +1,8 @@
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { Omit } from '../../types';
import { ZERO_AMOUNT } from './constants'; import { ZERO_AMOUNT } from './constants';
import { getTwoHopAdjustedRate } from './rate_utils'; import { getTwoHopAdjustedRate } from './rate_utils';
import { import {
@ -41,7 +43,7 @@ export function getIntermediateTokens(
* Returns the best two-hop quote and the fee-adjusted rate of that quote. * Returns the best two-hop quote and the fee-adjusted rate of that quote.
*/ */
export function getBestTwoHopQuote( export function getBestTwoHopQuote(
marketSideLiquidity: MarketSideLiquidity, marketSideLiquidity: Omit<MarketSideLiquidity, 'makerTokenDecimals' | 'takerTokenDecimals'>,
feeSchedule?: FeeSchedule, feeSchedule?: FeeSchedule,
exchangeProxyOverhead?: ExchangeProxyOverhead, exchangeProxyOverhead?: ExchangeProxyOverhead,
): { quote: DexSample<MultiHopFillData> | undefined; adjustedRate: BigNumber } { ): { quote: DexSample<MultiHopFillData> | undefined; adjustedRate: BigNumber } {

View File

@ -97,6 +97,15 @@ export function createSignedOrdersWithFillableAmounts(
orders: SignedOrder[], orders: SignedOrder[],
fillableAmounts: BigNumber[], fillableAmounts: BigNumber[],
): SignedOrderWithFillableAmounts[] { ): SignedOrderWithFillableAmounts[] {
// Quick safety check: ensures that orders maps perfectly to fillable amounts.
if (orders.length !== fillableAmounts.length) {
throw new Error(
`Number of orders was ${orders.length} but fillable amounts was ${
fillableAmounts.length
}. This should never happen`,
);
}
return orders return orders
.map((order: SignedOrder, i: number) => { .map((order: SignedOrder, i: number) => {
const fillableAmount = fillableAmounts[i]; const fillableAmount = fillableAmounts[i];

View File

@ -93,6 +93,15 @@ export class SamplerOperations {
return this._bancorService; return this._bancorService;
} }
public getTokenDecimals(makerTokenAddress: string, takerTokenAddress: string): BatchedOperation<BigNumber[]> {
return new SamplerContractOperation({
source: ERC20BridgeSource.Native,
contract: this._samplerContract,
function: this._samplerContract.getTokenDecimals,
params: [makerTokenAddress, takerTokenAddress],
});
}
public getOrderFillableTakerAmounts(orders: SignedOrder[], exchangeAddress: string): BatchedOperation<BigNumber[]> { public getOrderFillableTakerAmounts(orders: SignedOrder[], exchangeAddress: string): BatchedOperation<BigNumber[]> {
return new SamplerContractOperation({ return new SamplerContractOperation({
source: ERC20BridgeSource.Native, source: ERC20BridgeSource.Native,

View File

@ -6,6 +6,8 @@ import { RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../../types';
import { QuoteRequestor } from '../../utils/quote_requestor'; import { QuoteRequestor } from '../../utils/quote_requestor';
import { QuoteReport } from '../quote_report_generator'; import { QuoteReport } from '../quote_report_generator';
import { SourceFilters } from './source_filters';
/** /**
* Order domain keys: chainId and exchange * Order domain keys: chainId and exchange
*/ */
@ -331,6 +333,9 @@ export interface OptimizerResult {
optimizedOrders: OptimizedMarketOrder[]; optimizedOrders: OptimizedMarketOrder[];
sourceFlags: number; sourceFlags: number;
liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>; liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>;
}
export interface OptimizerResultWithReport extends OptimizerResult {
quoteReport?: QuoteReport; quoteReport?: QuoteReport;
} }
@ -353,8 +358,22 @@ export interface MarketSideLiquidity {
ethToInputRate: BigNumber; ethToInputRate: BigNumber;
rfqtIndicativeQuotes: RFQTIndicativeQuote[]; rfqtIndicativeQuotes: RFQTIndicativeQuote[];
twoHopQuotes: Array<DexSample<MultiHopFillData>>; twoHopQuotes: Array<DexSample<MultiHopFillData>>;
quoteSourceFilters: SourceFilters;
makerTokenDecimals: number;
takerTokenDecimals: number;
} }
export interface TokenAdjacencyGraph { export interface TokenAdjacencyGraph {
[token: string]: string[]; [token: string]: string[];
} }
export interface GenerateOptimizedOrdersOpts {
runLimit?: number;
bridgeSlippage?: number;
maxFallbackSlippage?: number;
excludedSources?: ERC20BridgeSource[];
feeSchedule?: FeeSchedule;
exchangeProxyOverhead?: ExchangeProxyOverhead;
allowFallback?: boolean;
shouldBatchBridgeOrders?: boolean;
}

View File

@ -1,6 +1,6 @@
import { schemas, SchemaValidator } from '@0x/json-schemas'; import { schemas, SchemaValidator } from '@0x/json-schemas';
import { assetDataUtils, orderCalculationUtils, SignedOrder } from '@0x/order-utils'; import { assetDataUtils, orderCalculationUtils, SignedOrder } from '@0x/order-utils';
import { RFQTFirmQuote, RFQTIndicativeQuote, TakerRequest } from '@0x/quote-server'; import { RFQTFirmQuote, RFQTIndicativeQuote, TakerRequestQueryParams } from '@0x/quote-server';
import { ERC20AssetData } from '@0x/types'; import { ERC20AssetData } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import Axios, { AxiosInstance } from 'axios'; import Axios, { AxiosInstance } from 'axios';
@ -44,29 +44,6 @@ function getTokenAddressOrThrow(assetData: string): string {
throw new Error(`Decoded asset data (${JSON.stringify(decodedAssetData)}) does not contain a token address`); throw new Error(`Decoded asset data (${JSON.stringify(decodedAssetData)}) does not contain a token address`);
} }
function inferQueryParams(
marketOperation: MarketOperation,
makerAssetData: string,
takerAssetData: string,
assetFillAmount: BigNumber,
): Pick<TakerRequest, 'buyTokenAddress' | 'sellTokenAddress' | 'buyAmountBaseUnits' | 'sellAmountBaseUnits'> {
if (marketOperation === MarketOperation.Buy) {
return {
buyTokenAddress: getTokenAddressOrThrow(makerAssetData),
sellTokenAddress: getTokenAddressOrThrow(takerAssetData),
buyAmountBaseUnits: assetFillAmount,
sellAmountBaseUnits: undefined,
};
} else {
return {
buyTokenAddress: getTokenAddressOrThrow(makerAssetData),
sellTokenAddress: getTokenAddressOrThrow(takerAssetData),
sellAmountBaseUnits: assetFillAmount,
buyAmountBaseUnits: undefined,
};
}
}
function hasExpectedAssetData( function hasExpectedAssetData(
expectedMakerAssetData: string, expectedMakerAssetData: string,
expectedTakerAssetData: string, expectedTakerAssetData: string,
@ -111,6 +88,54 @@ export class QuoteRequestor {
private readonly _schemaValidator: SchemaValidator = new SchemaValidator(); private readonly _schemaValidator: SchemaValidator = new SchemaValidator();
private readonly _orderSignatureToMakerUri: { [orderSignature: string]: string } = {}; private readonly _orderSignatureToMakerUri: { [orderSignature: string]: string } = {};
public static makeQueryParameters(
takerAddress: string,
marketOperation: MarketOperation,
makerAssetData: string,
takerAssetData: string,
assetFillAmount: BigNumber,
comparisonPrice?: BigNumber,
): TakerRequestQueryParams {
const buyTokenAddress = getTokenAddressOrThrow(makerAssetData);
const sellTokenAddress = getTokenAddressOrThrow(takerAssetData);
const { buyAmountBaseUnits, sellAmountBaseUnits } =
marketOperation === MarketOperation.Buy
? {
buyAmountBaseUnits: assetFillAmount,
sellAmountBaseUnits: undefined,
}
: {
sellAmountBaseUnits: assetFillAmount,
buyAmountBaseUnits: undefined,
};
const requestParamsWithBigNumbers: Pick<
TakerRequestQueryParams,
'buyTokenAddress' | 'sellTokenAddress' | 'takerAddress' | 'comparisonPrice'
> = {
takerAddress,
comparisonPrice: comparisonPrice === undefined ? undefined : comparisonPrice.toString(),
buyTokenAddress,
sellTokenAddress,
};
// convert BigNumbers to strings
// so they are digestible by axios
if (sellAmountBaseUnits) {
return {
...requestParamsWithBigNumbers,
sellAmountBaseUnits: sellAmountBaseUnits.toString(),
};
} else if (buyAmountBaseUnits) {
return {
...requestParamsWithBigNumbers,
buyAmountBaseUnits: buyAmountBaseUnits.toString(),
};
} else {
throw new Error('Neither "buyAmountBaseUnits" or "sellAmountBaseUnits" were defined');
}
}
constructor( constructor(
private readonly _rfqtAssetOfferings: RfqtMakerAssetOfferings, private readonly _rfqtAssetOfferings: RfqtMakerAssetOfferings,
private readonly _warningLogger: LogFunction = constants.DEFAULT_WARNING_LOGGER, private readonly _warningLogger: LogFunction = constants.DEFAULT_WARNING_LOGGER,
@ -125,6 +150,7 @@ export class QuoteRequestor {
takerAssetData: string, takerAssetData: string,
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
marketOperation: MarketOperation, marketOperation: MarketOperation,
comparisonPrice: BigNumber | undefined,
options: RfqtRequestOpts, options: RfqtRequestOpts,
): Promise<RFQTFirmQuote[]> { ): Promise<RFQTFirmQuote[]> {
const _opts: RfqtRequestOpts = { ...constants.DEFAULT_RFQT_REQUEST_OPTS, ...options }; const _opts: RfqtRequestOpts = { ...constants.DEFAULT_RFQT_REQUEST_OPTS, ...options };
@ -143,6 +169,7 @@ export class QuoteRequestor {
takerAssetData, takerAssetData,
assetFillAmount, assetFillAmount,
marketOperation, marketOperation,
comparisonPrice,
_opts, _opts,
'firm', 'firm',
); );
@ -213,6 +240,7 @@ export class QuoteRequestor {
takerAssetData: string, takerAssetData: string,
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
marketOperation: MarketOperation, marketOperation: MarketOperation,
comparisonPrice: BigNumber | undefined,
options: RfqtRequestOpts, options: RfqtRequestOpts,
): Promise<RFQTIndicativeQuote[]> { ): Promise<RFQTIndicativeQuote[]> {
const _opts: RfqtRequestOpts = { ...constants.DEFAULT_RFQT_REQUEST_OPTS, ...options }; const _opts: RfqtRequestOpts = { ...constants.DEFAULT_RFQT_REQUEST_OPTS, ...options };
@ -231,6 +259,7 @@ export class QuoteRequestor {
takerAssetData, takerAssetData,
assetFillAmount, assetFillAmount,
marketOperation, marketOperation,
comparisonPrice,
_opts, _opts,
'indicative', 'indicative',
); );
@ -331,25 +360,18 @@ export class QuoteRequestor {
takerAssetData: string, takerAssetData: string,
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
marketOperation: MarketOperation, marketOperation: MarketOperation,
comparisonPrice: BigNumber | undefined,
options: RfqtRequestOpts, options: RfqtRequestOpts,
quoteType: 'firm' | 'indicative', quoteType: 'firm' | 'indicative',
): Promise<Array<{ response: ResponseT; makerUri: string }>> { ): Promise<Array<{ response: ResponseT; makerUri: string }>> {
const requestParamsWithBigNumbers = { const requestParams = QuoteRequestor.makeQueryParameters(
takerAddress: options.takerAddress, options.takerAddress,
...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount), marketOperation,
}; makerAssetData,
takerAssetData,
// convert BigNumbers to strings assetFillAmount,
// so they are digestible by axios comparisonPrice,
const requestParams = { );
...requestParamsWithBigNumbers,
sellAmountBaseUnits: requestParamsWithBigNumbers.sellAmountBaseUnits
? requestParamsWithBigNumbers.sellAmountBaseUnits.toString()
: undefined,
buyAmountBaseUnits: requestParamsWithBigNumbers.buyAmountBaseUnits
? requestParamsWithBigNumbers.buyAmountBaseUnits.toString()
: undefined,
};
const result: Array<{ response: ResponseT; makerUri: string }> = []; const result: Array<{ response: ResponseT; makerUri: string }> = [];
await Promise.all( await Promise.all(

View File

@ -1,3 +1,4 @@
import { artifacts as erc20Artifacts, DummyERC20TokenContract } from '@0x/contracts-erc20';
import { import {
assertIntegerRoughlyEquals, assertIntegerRoughlyEquals,
blockchainTests, blockchainTests,
@ -130,6 +131,36 @@ blockchainTests.resets('NativeOrderSampler contract', env => {
.awaitTransactionSuccessAsync(); .awaitTransactionSuccessAsync();
} }
describe('getTokenDecimals()', () => {
it('correctly returns the token balances', async () => {
const newMakerToken = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
erc20Artifacts.DummyERC20Token,
env.provider,
env.txDefaults,
artifacts,
constants.DUMMY_TOKEN_NAME,
constants.DUMMY_TOKEN_SYMBOL,
new BigNumber(18),
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
);
const newTakerToken = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
erc20Artifacts.DummyERC20Token,
env.provider,
env.txDefaults,
artifacts,
constants.DUMMY_TOKEN_NAME,
constants.DUMMY_TOKEN_SYMBOL,
new BigNumber(6),
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
);
const [makerDecimals, takerDecimals] = await testContract
.getTokenDecimals(newMakerToken.address, newTakerToken.address)
.callAsync();
expect(makerDecimals.toString()).to.eql('18');
expect(takerDecimals.toString()).to.eql('6');
});
});
describe('getOrderFillableTakerAmount()', () => { describe('getOrderFillableTakerAmount()', () => {
it('returns the full amount for a fully funded order', async () => { it('returns the full amount for a fully funded order', async () => {
const order = createOrder(); const order = createOrder();

View File

@ -1,3 +1,4 @@
// tslint:disable: no-unbound-method
import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses'; import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
import { import {
assertRoughlyEquals, assertRoughlyEquals,
@ -16,6 +17,7 @@ import * as _ from 'lodash';
import * as TypeMoq from 'typemoq'; import * as TypeMoq from 'typemoq';
import { MarketOperation, QuoteRequestor, RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../src'; import { MarketOperation, QuoteRequestor, RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../src';
import { IS_PRICE_AWARE_RFQ_ENABLED } from '../src/constants';
import { getRfqtIndicativeQuotesAsync, MarketOperationUtils } from '../src/utils/market_operation_utils/'; import { getRfqtIndicativeQuotesAsync, MarketOperationUtils } from '../src/utils/market_operation_utils/';
import { BalancerPoolsCache } from '../src/utils/market_operation_utils/balancer_utils'; import { BalancerPoolsCache } from '../src/utils/market_operation_utils/balancer_utils';
import { import {
@ -29,7 +31,16 @@ import { CreamPoolsCache } from '../src/utils/market_operation_utils/cream_utils
import { createFills } from '../src/utils/market_operation_utils/fills'; import { createFills } from '../src/utils/market_operation_utils/fills';
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler'; import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations'; import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations';
import { DexSample, ERC20BridgeSource, FillData, NativeFillData } from '../src/utils/market_operation_utils/types'; import { SourceFilters } from '../src/utils/market_operation_utils/source_filters';
import {
AggregationError,
DexSample,
ERC20BridgeSource,
FillData,
GenerateOptimizedOrdersOpts,
MarketSideLiquidity,
NativeFillData,
} from '../src/utils/market_operation_utils/types';
const MAKER_TOKEN = randomAddress(); const MAKER_TOKEN = randomAddress();
const TAKER_TOKEN = randomAddress(); const TAKER_TOKEN = randomAddress();
@ -58,6 +69,34 @@ describe('MarketOperationUtils tests', () => {
const CHAIN_ID = 1; const CHAIN_ID = 1;
const contractAddresses = { ...getContractAddressesForChainOrThrow(CHAIN_ID), multiBridge: NULL_ADDRESS }; const contractAddresses = { ...getContractAddressesForChainOrThrow(CHAIN_ID), multiBridge: NULL_ADDRESS };
function getMockedQuoteRequestor(
type: 'indicative' | 'firm',
results: SignedOrder[],
verifiable: TypeMoq.Times,
): TypeMoq.IMock<QuoteRequestor> {
const args: [any, any, any, any, any, any] = [
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
];
const requestor = TypeMoq.Mock.ofType(QuoteRequestor, TypeMoq.MockBehavior.Loose, true);
if (type === 'firm') {
requestor
.setup(r => r.requestRfqtFirmQuotesAsync(...args))
.returns(async () => results.map(result => ({ signedOrder: result })))
.verifiable(verifiable);
} else {
requestor
.setup(r => r.requestRfqtIndicativeQuotesAsync(...args))
.returns(async () => results)
.verifiable(verifiable);
}
return requestor;
}
function createOrder(overrides?: Partial<SignedOrder>): SignedOrder { function createOrder(overrides?: Partial<SignedOrder>): SignedOrder {
return { return {
chainId: CHAIN_ID, chainId: CHAIN_ID,
@ -353,6 +392,10 @@ describe('MarketOperationUtils tests', () => {
}; };
const DEFAULT_OPS = { const DEFAULT_OPS = {
getTokenDecimals(_makerAddress: string, _takerAddress: string): BigNumber[] {
const result = new BigNumber(18);
return [result, result];
},
getOrderFillableTakerAmounts(orders: SignedOrder[]): BigNumber[] { getOrderFillableTakerAmounts(orders: SignedOrder[]): BigNumber[] {
return orders.map(o => o.takerAssetAmount); return orders.map(o => o.takerAssetAmount);
}, },
@ -447,6 +490,7 @@ describe('MarketOperationUtils tests', () => {
TypeMoq.It.isAny(), TypeMoq.It.isAny(),
TypeMoq.It.isAny(), TypeMoq.It.isAny(),
TypeMoq.It.isAny(), TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
), ),
) )
.returns(() => Promise.resolve([])) .returns(() => Promise.resolve([]))
@ -456,6 +500,7 @@ describe('MarketOperationUtils tests', () => {
TAKER_ASSET_DATA, TAKER_ASSET_DATA,
MarketOperation.Sell, MarketOperation.Sell,
new BigNumber('100e18'), new BigNumber('100e18'),
undefined,
{ {
rfqt: { quoteRequestor: requestor.object, ...partialRfqt }, rfqt: { quoteRequestor: requestor.object, ...partialRfqt },
}, },
@ -693,6 +738,412 @@ describe('MarketOperationUtils tests', () => {
} }
}); });
it(
'getMarketSellOrdersAsync() optimizer will be called once only if RFQ if not defined',
IS_PRICE_AWARE_RFQ_ENABLED
? async () => {
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
MarketOperationUtils,
TypeMoq.MockBehavior.Loose,
false,
MOCK_SAMPLER,
contractAddresses,
ORDER_DOMAIN,
);
mockedMarketOpUtils.callBase = true;
// Ensure that `_generateOptimizedOrdersAsync` is only called once
mockedMarketOpUtils
.setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(async (a, b) => mockedMarketOpUtils.target._generateOptimizedOrdersAsync(a, b))
.verifiable(TypeMoq.Times.once());
const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b));
await mockedMarketOpUtils.object.getMarketSellOrdersAsync(
ORDERS,
totalAssetAmount,
DEFAULT_OPTS,
);
mockedMarketOpUtils.verifyAll();
}
: undefined,
);
it(
'optimizer will send in a comparison price to RFQ providers',
IS_PRICE_AWARE_RFQ_ENABLED
? async () => {
// Set up mocked quote requestor, will return an order that is better
// than the best of the orders.
const mockedQuoteRequestor = TypeMoq.Mock.ofType(
QuoteRequestor,
TypeMoq.MockBehavior.Loose,
false,
{},
);
let requestedComparisonPrice: BigNumber | undefined;
mockedQuoteRequestor
.setup(mqr =>
mqr.requestRfqtFirmQuotesAsync(
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
),
)
.callback(
(
_makerAssetData: string,
_takerAssetData: string,
_assetFillAmount: BigNumber,
_marketOperation: MarketOperation,
comparisonPrice: BigNumber | undefined,
_options: RfqtRequestOpts,
) => {
requestedComparisonPrice = comparisonPrice;
},
)
.returns(async () => {
return [
{
signedOrder: createOrder({
makerAssetData: MAKER_ASSET_DATA,
takerAssetData: TAKER_ASSET_DATA,
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(321, 6),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 18),
}),
},
];
});
// Set up sampler, will only return 1 on-chain order
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
MarketOperationUtils,
TypeMoq.MockBehavior.Loose,
false,
MOCK_SAMPLER,
contractAddresses,
ORDER_DOMAIN,
);
mockedMarketOpUtils.callBase = true;
mockedMarketOpUtils
.setup(mou =>
mou.getMarketSellLiquidityAsync(
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
),
)
.returns(async () => {
return {
dexQuotes: [],
ethToInputRate: Web3Wrapper.toBaseUnitAmount(1, 18),
ethToOutputRate: Web3Wrapper.toBaseUnitAmount(1, 6),
inputAmount: Web3Wrapper.toBaseUnitAmount(1, 18),
inputToken: MAKER_TOKEN,
outputToken: TAKER_TOKEN,
nativeOrders: [
createOrder({
makerAssetData: MAKER_ASSET_DATA,
takerAssetData: TAKER_ASSET_DATA,
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(320, 6),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 18),
}),
],
orderFillableAmounts: [Web3Wrapper.toBaseUnitAmount(1, 18)],
rfqtIndicativeQuotes: [],
side: MarketOperation.Sell,
twoHopQuotes: [],
quoteSourceFilters: new SourceFilters(),
makerTokenDecimals: 6,
takerTokenDecimals: 18,
};
});
const result = await mockedMarketOpUtils.object.getMarketSellOrdersAsync(
ORDERS,
Web3Wrapper.toBaseUnitAmount(1, 18),
{
...DEFAULT_OPTS,
rfqt: {
isIndicative: false,
apiKey: 'foo',
takerAddress: randomAddress(),
intentOnFilling: true,
quoteRequestor: {
requestRfqtFirmQuotesAsync:
mockedQuoteRequestor.object.requestRfqtFirmQuotesAsync,
} as any,
},
},
);
expect(result.optimizedOrders.length).to.eql(1);
// tslint:disable-next-line:no-unnecessary-type-assertion
expect(requestedComparisonPrice!.toString()).to.eql('320');
expect(result.optimizedOrders[0].makerAssetAmount.toString()).to.eql('321000000');
expect(result.optimizedOrders[0].takerAssetAmount.toString()).to.eql('1000000000000000000');
}
: undefined,
);
it(
'getMarketSellOrdersAsync() will not rerun the optimizer if no orders are returned',
IS_PRICE_AWARE_RFQ_ENABLED
? async () => {
// Ensure that `_generateOptimizedOrdersAsync` is only called once
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
MarketOperationUtils,
TypeMoq.MockBehavior.Loose,
false,
MOCK_SAMPLER,
contractAddresses,
ORDER_DOMAIN,
);
mockedMarketOpUtils.callBase = true;
mockedMarketOpUtils
.setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(async (a, b) => mockedMarketOpUtils.target._generateOptimizedOrdersAsync(a, b))
.verifiable(TypeMoq.Times.once());
const requestor = getMockedQuoteRequestor('firm', [], TypeMoq.Times.once());
const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b));
await mockedMarketOpUtils.object.getMarketSellOrdersAsync(ORDERS, totalAssetAmount, {
...DEFAULT_OPTS,
rfqt: {
isIndicative: false,
apiKey: 'foo',
takerAddress: randomAddress(),
intentOnFilling: true,
quoteRequestor: {
requestRfqtFirmQuotesAsync: requestor.object.requestRfqtFirmQuotesAsync,
} as any,
},
});
mockedMarketOpUtils.verifyAll();
requestor.verifyAll();
}
: undefined,
);
it(
'getMarketSellOrdersAsync() will rerun the optimizer if one or more indicative are returned',
IS_PRICE_AWARE_RFQ_ENABLED
? async () => {
const requestor = getMockedQuoteRequestor(
'indicative',
[ORDERS[0], ORDERS[1]],
TypeMoq.Times.once(),
);
const numOrdersInCall: number[] = [];
const numIndicativeQuotesInCall: number[] = [];
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
MarketOperationUtils,
TypeMoq.MockBehavior.Loose,
false,
MOCK_SAMPLER,
contractAddresses,
ORDER_DOMAIN,
);
mockedMarketOpUtils.callBase = true;
mockedMarketOpUtils
.setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.callback(async (msl: MarketSideLiquidity, _opts: GenerateOptimizedOrdersOpts) => {
numOrdersInCall.push(msl.nativeOrders.length);
numIndicativeQuotesInCall.push(msl.rfqtIndicativeQuotes.length);
})
.returns(async (a, b) => mockedMarketOpUtils.target._generateOptimizedOrdersAsync(a, b))
.verifiable(TypeMoq.Times.exactly(2));
const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b));
await mockedMarketOpUtils.object.getMarketSellOrdersAsync(
ORDERS.slice(2, ORDERS.length),
totalAssetAmount,
{
...DEFAULT_OPTS,
rfqt: {
isIndicative: true,
apiKey: 'foo',
takerAddress: randomAddress(),
intentOnFilling: true,
quoteRequestor: {
requestRfqtIndicativeQuotesAsync:
requestor.object.requestRfqtIndicativeQuotesAsync,
} as any,
},
},
);
mockedMarketOpUtils.verifyAll();
requestor.verifyAll();
// The first and second optimizer call contains same number of RFQ orders.
expect(numOrdersInCall.length).to.eql(2);
expect(numOrdersInCall[0]).to.eql(1);
expect(numOrdersInCall[1]).to.eql(1);
// The first call to optimizer will have no RFQ indicative quotes. The second call will have
// two indicative quotes.
expect(numIndicativeQuotesInCall.length).to.eql(2);
expect(numIndicativeQuotesInCall[0]).to.eql(0);
expect(numIndicativeQuotesInCall[1]).to.eql(2);
}
: undefined,
);
it(
'getMarketSellOrdersAsync() will rerun the optimizer if one or more RFQ orders are returned',
IS_PRICE_AWARE_RFQ_ENABLED
? async () => {
const requestor = getMockedQuoteRequestor('firm', [ORDERS[0]], TypeMoq.Times.once());
// Ensure that `_generateOptimizedOrdersAsync` is only called once
// TODO: Ensure fillable amounts increase too
const numOrdersInCall: number[] = [];
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
MarketOperationUtils,
TypeMoq.MockBehavior.Loose,
false,
MOCK_SAMPLER,
contractAddresses,
ORDER_DOMAIN,
);
mockedMarketOpUtils.callBase = true;
mockedMarketOpUtils
.setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.callback(async (msl: MarketSideLiquidity, _opts: GenerateOptimizedOrdersOpts) => {
numOrdersInCall.push(msl.nativeOrders.length);
})
.returns(async (a, b) => mockedMarketOpUtils.target._generateOptimizedOrdersAsync(a, b))
.verifiable(TypeMoq.Times.exactly(2));
const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b));
await mockedMarketOpUtils.object.getMarketSellOrdersAsync(
ORDERS.slice(1, ORDERS.length),
totalAssetAmount,
{
...DEFAULT_OPTS,
rfqt: {
isIndicative: false,
apiKey: 'foo',
takerAddress: randomAddress(),
intentOnFilling: true,
quoteRequestor: {
requestRfqtFirmQuotesAsync: requestor.object.requestRfqtFirmQuotesAsync,
} as any,
},
},
);
mockedMarketOpUtils.verifyAll();
requestor.verifyAll();
expect(numOrdersInCall.length).to.eql(2);
// The first call to optimizer was without an RFQ order.
// The first call to optimizer was with an extra RFQ order.
expect(numOrdersInCall[0]).to.eql(2);
expect(numOrdersInCall[1]).to.eql(3);
}
: undefined,
);
it(
'getMarketSellOrdersAsync() will not raise a NoOptimalPath error if no initial path was found during on-chain DEX optimization, but a path was found after RFQ optimization',
IS_PRICE_AWARE_RFQ_ENABLED
? async () => {
let hasFirstOptimizationRun = false;
let hasSecondOptimizationRun = false;
const requestor = getMockedQuoteRequestor(
'firm',
[ORDERS[0], ORDERS[1]],
TypeMoq.Times.once(),
);
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
MarketOperationUtils,
TypeMoq.MockBehavior.Loose,
false,
MOCK_SAMPLER,
contractAddresses,
ORDER_DOMAIN,
);
mockedMarketOpUtils.callBase = true;
mockedMarketOpUtils
.setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(async (msl: MarketSideLiquidity, _opts: GenerateOptimizedOrdersOpts) => {
if (msl.nativeOrders.length === 1) {
hasFirstOptimizationRun = true;
throw new Error(AggregationError.NoOptimalPath);
} else if (msl.nativeOrders.length === 3) {
hasSecondOptimizationRun = true;
return mockedMarketOpUtils.target._generateOptimizedOrdersAsync(msl, _opts);
} else {
throw new Error('Invalid path. this error message should never appear');
}
})
.verifiable(TypeMoq.Times.exactly(2));
const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b));
await mockedMarketOpUtils.object.getMarketSellOrdersAsync(
ORDERS.slice(2, ORDERS.length),
totalAssetAmount,
{
...DEFAULT_OPTS,
rfqt: {
isIndicative: false,
apiKey: 'foo',
takerAddress: randomAddress(),
intentOnFilling: true,
quoteRequestor: {
requestRfqtFirmQuotesAsync: requestor.object.requestRfqtFirmQuotesAsync,
} as any,
},
},
);
mockedMarketOpUtils.verifyAll();
requestor.verifyAll();
expect(hasFirstOptimizationRun).to.eql(true);
expect(hasSecondOptimizationRun).to.eql(true);
}
: undefined,
);
it('getMarketSellOrdersAsync() will raise a NoOptimalPath error if no path was found during on-chain DEX optimization and RFQ optimization', async () => {
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
MarketOperationUtils,
TypeMoq.MockBehavior.Loose,
false,
MOCK_SAMPLER,
contractAddresses,
ORDER_DOMAIN,
);
mockedMarketOpUtils.callBase = true;
mockedMarketOpUtils
.setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(async (msl: MarketSideLiquidity, _opts: GenerateOptimizedOrdersOpts) => {
throw new Error(AggregationError.NoOptimalPath);
})
.verifiable(TypeMoq.Times.exactly(1));
try {
await mockedMarketOpUtils.object.getMarketSellOrdersAsync(
ORDERS.slice(2, ORDERS.length),
ORDERS[0].takerAssetAmount,
DEFAULT_OPTS,
);
expect.fail(`Call should have thrown "${AggregationError.NoOptimalPath}" but instead succeded`);
} catch (e) {
if (e.message !== AggregationError.NoOptimalPath) {
expect.fail(e);
}
}
mockedMarketOpUtils.verifyAll();
});
it('generates bridge orders with correct taker amount', async () => { it('generates bridge orders with correct taker amount', async () => {
const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
// Pass in empty orders to prevent native orders from being used. // Pass in empty orders to prevent native orders from being used.

View File

@ -1,5 +1,6 @@
import { tokenUtils } from '@0x/dev-utils'; import { tokenUtils } from '@0x/dev-utils';
import { assetDataUtils } from '@0x/order-utils'; import { assetDataUtils } from '@0x/order-utils';
import { TakerRequestQueryParams } from '@0x/quote-server';
import { StatusCodes } from '@0x/types'; import { StatusCodes } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as chai from 'chai'; import * as chai from 'chai';
@ -35,11 +36,11 @@ describe('QuoteRequestor', async () => {
// Set up RFQT responses // Set up RFQT responses
// tslint:disable-next-line:array-type // tslint:disable-next-line:array-type
const mockedRequests: MockedRfqtFirmQuoteResponse[] = []; const mockedRequests: MockedRfqtFirmQuoteResponse[] = [];
const expectedParams = { const expectedParams: TakerRequestQueryParams = {
sellTokenAddress: takerToken, sellTokenAddress: takerToken,
buyTokenAddress: makerToken, buyTokenAddress: makerToken,
sellAmountBaseUnits: '10000', sellAmountBaseUnits: '10000',
buyAmountBaseUnits: undefined, comparisonPrice: undefined,
takerAddress, takerAddress,
}; };
// Successful response // Successful response
@ -174,6 +175,7 @@ describe('QuoteRequestor', async () => {
takerAssetData, takerAssetData,
new BigNumber(10000), new BigNumber(10000),
MarketOperation.Sell, MarketOperation.Sell,
undefined,
{ {
apiKey, apiKey,
takerAddress, takerAddress,
@ -189,6 +191,17 @@ describe('QuoteRequestor', async () => {
}); });
}); });
describe('requestRfqtIndicativeQuotesAsync for Indicative quotes', async () => { describe('requestRfqtIndicativeQuotesAsync for Indicative quotes', async () => {
it('should optionally accept a "comparisonPrice" parameter', async () => {
const response = QuoteRequestor.makeQueryParameters(
otherToken1,
MarketOperation.Sell,
makerAssetData,
takerAssetData,
new BigNumber(1000),
new BigNumber(300.2),
);
expect(response.comparisonPrice).to.eql('300.2');
});
it('should return successful RFQT requests', async () => { it('should return successful RFQT requests', async () => {
const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a'; const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
const apiKey = 'my-ko0l-api-key'; const apiKey = 'my-ko0l-api-key';
@ -196,11 +209,11 @@ describe('QuoteRequestor', async () => {
// Set up RFQT responses // Set up RFQT responses
// tslint:disable-next-line:array-type // tslint:disable-next-line:array-type
const mockedRequests: MockedRfqtIndicativeQuoteResponse[] = []; const mockedRequests: MockedRfqtIndicativeQuoteResponse[] = [];
const expectedParams = { const expectedParams: TakerRequestQueryParams = {
sellTokenAddress: takerToken, sellTokenAddress: takerToken,
buyTokenAddress: makerToken, buyTokenAddress: makerToken,
sellAmountBaseUnits: '10000', sellAmountBaseUnits: '10000',
buyAmountBaseUnits: undefined, comparisonPrice: undefined,
takerAddress, takerAddress,
}; };
// Successful response // Successful response
@ -276,6 +289,7 @@ describe('QuoteRequestor', async () => {
takerAssetData, takerAssetData,
new BigNumber(10000), new BigNumber(10000),
MarketOperation.Sell, MarketOperation.Sell,
undefined,
{ {
apiKey, apiKey,
takerAddress, takerAddress,
@ -294,11 +308,11 @@ describe('QuoteRequestor', async () => {
// Set up RFQT responses // Set up RFQT responses
// tslint:disable-next-line:array-type // tslint:disable-next-line:array-type
const mockedRequests: MockedRfqtIndicativeQuoteResponse[] = []; const mockedRequests: MockedRfqtIndicativeQuoteResponse[] = [];
const expectedParams = { const expectedParams: TakerRequestQueryParams = {
sellTokenAddress: takerToken, sellTokenAddress: takerToken,
buyTokenAddress: makerToken, buyTokenAddress: makerToken,
buyAmountBaseUnits: '10000', buyAmountBaseUnits: '10000',
sellAmountBaseUnits: undefined, comparisonPrice: undefined,
takerAddress, takerAddress,
}; };
// Successful response // Successful response
@ -326,6 +340,7 @@ describe('QuoteRequestor', async () => {
takerAssetData, takerAssetData,
new BigNumber(10000), new BigNumber(10000),
MarketOperation.Buy, MarketOperation.Buy,
undefined,
{ {
apiKey, apiKey,
takerAddress, takerAddress,

View File

@ -748,9 +748,10 @@
uuid "^3.3.2" uuid "^3.3.2"
websocket "^1.0.29" websocket "^1.0.29"
"@0x/quote-server@^2.0.2": "@0x/quote-server@^3.1.0":
version "2.0.2" version "3.1.0"
resolved "https://registry.yarnpkg.com/@0x/quote-server/-/quote-server-2.0.2.tgz#60d0665c1cad378c9abb89b5491bdc55b4c8412c" resolved "https://registry.yarnpkg.com/@0x/quote-server/-/quote-server-3.1.0.tgz#ba5c0de9f88fedfd522ec1ef608dd8eebb868509"
integrity sha512-o9n7wE9XmV/YMjAcIt3EJMnc0xony8VhqNtO7dGAREi/WQxJBlNAHNZxu4wQ0wV03wroH58eJTOpn4fk+kuXqQ==
dependencies: dependencies:
"@0x/json-schemas" "^5.0.7" "@0x/json-schemas" "^5.0.7"
"@0x/order-utils" "^10.2.4" "@0x/order-utils" "^10.2.4"