Merge pull request #2720 from 0xProject/refactor_rfq_comparison_price_integration
feat: Refactor rfq comparison price integration
This commit is contained in:
commit
0571244e9e
@ -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": {
|
||||||
|
@ -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).
|
||||||
|
@ -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",
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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>>;
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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 } {
|
||||||
|
@ -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];
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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();
|
||||||
|
@ -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.
|
||||||
|
@ -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,
|
||||||
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user