Merge pull request #2735 from 0xProject/rfqt_comparison_feature_flag
Rfqt comparison feature flag
This commit is contained in:
commit
3213131cad
@ -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;
|
||||||
|
@ -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,
|
||||||
@ -696,6 +696,31 @@ export class SwapQuoter {
|
|||||||
opts.rfqt = undefined;
|
opts.rfqt = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!IS_PRICE_AWARE_RFQ_ENABLED && // Price-aware RFQ is disabled.
|
||||||
|
opts.rfqt && // This is an RFQT-enabled API request
|
||||||
|
opts.rfqt.intentOnFilling && // The requestor is asking for a firm quote
|
||||||
|
opts.rfqt.apiKey &&
|
||||||
|
this._isApiKeyWhitelisted(opts.rfqt.apiKey) && // A valid API key was provided
|
||||||
|
sourceFilters.isAllowed(ERC20BridgeSource.Native) // Native liquidity is not excluded
|
||||||
|
) {
|
||||||
|
if (!opts.rfqt.takerAddress || opts.rfqt.takerAddress === constants.NULL_ADDRESS) {
|
||||||
|
throw new Error('RFQ-T requests must specify a taker address');
|
||||||
|
}
|
||||||
|
orderBatchPromises.push(
|
||||||
|
quoteRequestor
|
||||||
|
.requestRfqtFirmQuotesAsync(
|
||||||
|
makerAssetData,
|
||||||
|
takerAssetData,
|
||||||
|
assetFillAmount,
|
||||||
|
marketOperation,
|
||||||
|
undefined,
|
||||||
|
opts.rfqt,
|
||||||
|
)
|
||||||
|
.then(firmQuotes => firmQuotes.map(quote => quote.signedOrder)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
@ -5,6 +5,7 @@ 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 { IS_PRICE_AWARE_RFQ_ENABLED } from '../../constants';
|
||||||
import { MarketOperation, Omit } from '../../types';
|
import { MarketOperation, Omit } from '../../types';
|
||||||
import { QuoteRequestor } from '../quote_requestor';
|
import { QuoteRequestor } from '../quote_requestor';
|
||||||
|
|
||||||
@ -216,6 +217,18 @@ export class MarketOperationUtils {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const rfqtPromise =
|
||||||
|
!IS_PRICE_AWARE_RFQ_ENABLED && quoteSourceFilters.isAllowed(ERC20BridgeSource.Native)
|
||||||
|
? getRfqtIndicativeQuotesAsync(
|
||||||
|
nativeOrders[0].makerAssetData,
|
||||||
|
nativeOrders[0].takerAssetData,
|
||||||
|
MarketOperation.Sell,
|
||||||
|
takerAmount,
|
||||||
|
undefined,
|
||||||
|
_opts,
|
||||||
|
)
|
||||||
|
: Promise.resolve([]);
|
||||||
|
|
||||||
const offChainBalancerPromise = sampleBalancerOffChain
|
const offChainBalancerPromise = sampleBalancerOffChain
|
||||||
? this._sampler.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, sampleAmounts)
|
? this._sampler.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, sampleAmounts)
|
||||||
: Promise.resolve([]);
|
: Promise.resolve([]);
|
||||||
@ -230,10 +243,17 @@ export class MarketOperationUtils {
|
|||||||
|
|
||||||
const [
|
const [
|
||||||
[tokenDecimals, orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes],
|
[tokenDecimals, orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes],
|
||||||
|
rfqtIndicativeQuotes,
|
||||||
offChainBalancerQuotes,
|
offChainBalancerQuotes,
|
||||||
offChainCreamQuotes,
|
offChainCreamQuotes,
|
||||||
offChainBancorQuotes,
|
offChainBancorQuotes,
|
||||||
] = await Promise.all([samplerPromise, offChainBalancerPromise, offChainCreamPromise, offChainBancorPromise]);
|
] = await Promise.all([
|
||||||
|
samplerPromise,
|
||||||
|
rfqtPromise,
|
||||||
|
offChainBalancerPromise,
|
||||||
|
offChainCreamPromise,
|
||||||
|
offChainBancorPromise,
|
||||||
|
]);
|
||||||
|
|
||||||
const [makerTokenDecimals, takerTokenDecimals] = tokenDecimals;
|
const [makerTokenDecimals, takerTokenDecimals] = tokenDecimals;
|
||||||
return {
|
return {
|
||||||
@ -246,7 +266,7 @@ export class MarketOperationUtils {
|
|||||||
orderFillableAmounts,
|
orderFillableAmounts,
|
||||||
ethToOutputRate: ethToMakerAssetRate,
|
ethToOutputRate: ethToMakerAssetRate,
|
||||||
ethToInputRate: ethToTakerAssetRate,
|
ethToInputRate: ethToTakerAssetRate,
|
||||||
rfqtIndicativeQuotes: [],
|
rfqtIndicativeQuotes,
|
||||||
twoHopQuotes,
|
twoHopQuotes,
|
||||||
quoteSourceFilters,
|
quoteSourceFilters,
|
||||||
makerTokenDecimals: makerTokenDecimals.toNumber(),
|
makerTokenDecimals: makerTokenDecimals.toNumber(),
|
||||||
@ -345,7 +365,17 @@ export class MarketOperationUtils {
|
|||||||
this._liquidityProviderRegistry,
|
this._liquidityProviderRegistry,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const rfqtPromise =
|
||||||
|
!IS_PRICE_AWARE_RFQ_ENABLED && quoteSourceFilters.isAllowed(ERC20BridgeSource.Native)
|
||||||
|
? getRfqtIndicativeQuotesAsync(
|
||||||
|
nativeOrders[0].makerAssetData,
|
||||||
|
nativeOrders[0].takerAssetData,
|
||||||
|
MarketOperation.Buy,
|
||||||
|
makerAmount,
|
||||||
|
undefined,
|
||||||
|
_opts,
|
||||||
|
)
|
||||||
|
: Promise.resolve([]);
|
||||||
const offChainBalancerPromise = sampleBalancerOffChain
|
const offChainBalancerPromise = sampleBalancerOffChain
|
||||||
? this._sampler.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, sampleAmounts)
|
? this._sampler.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, sampleAmounts)
|
||||||
: Promise.resolve([]);
|
: Promise.resolve([]);
|
||||||
@ -356,9 +386,10 @@ export class MarketOperationUtils {
|
|||||||
|
|
||||||
const [
|
const [
|
||||||
[tokenDecimals, orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes],
|
[tokenDecimals, orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes],
|
||||||
|
rfqtIndicativeQuotes,
|
||||||
offChainBalancerQuotes,
|
offChainBalancerQuotes,
|
||||||
offChainCreamQuotes,
|
offChainCreamQuotes,
|
||||||
] = await Promise.all([samplerPromise, offChainBalancerPromise, offChainCreamPromise]);
|
] = await Promise.all([samplerPromise, rfqtPromise, offChainBalancerPromise, offChainCreamPromise]);
|
||||||
// Attach the MultiBridge address to the sample fillData
|
// Attach the MultiBridge address to the sample fillData
|
||||||
(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 }),
|
||||||
@ -374,7 +405,7 @@ export class MarketOperationUtils {
|
|||||||
orderFillableAmounts,
|
orderFillableAmounts,
|
||||||
ethToOutputRate: ethToTakerAssetRate,
|
ethToOutputRate: ethToTakerAssetRate,
|
||||||
ethToInputRate: ethToMakerAssetRate,
|
ethToInputRate: ethToMakerAssetRate,
|
||||||
rfqtIndicativeQuotes: [],
|
rfqtIndicativeQuotes,
|
||||||
twoHopQuotes,
|
twoHopQuotes,
|
||||||
quoteSourceFilters,
|
quoteSourceFilters,
|
||||||
makerTokenDecimals: makerTokenDecimals.toNumber(),
|
makerTokenDecimals: makerTokenDecimals.toNumber(),
|
||||||
@ -646,7 +677,12 @@ export class MarketOperationUtils {
|
|||||||
|
|
||||||
// If RFQ liquidity is enabled, make a request to check RFQ liquidity
|
// If RFQ liquidity is enabled, make a request to check RFQ liquidity
|
||||||
const { rfqt } = _opts;
|
const { rfqt } = _opts;
|
||||||
if (rfqt && rfqt.quoteRequestor && marketSideLiquidity.quoteSourceFilters.isAllowed(ERC20BridgeSource.Native)) {
|
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.
|
// Calculate a suggested price. For now, this is simply the overall price of the aggregation.
|
||||||
let comparisonPrice: BigNumber | undefined;
|
let comparisonPrice: BigNumber | undefined;
|
||||||
if (optimizerResult) {
|
if (optimizerResult) {
|
||||||
|
@ -17,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 {
|
||||||
@ -724,326 +725,379 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getMarketSellOrdersAsync() optimizer will be called once only if RFQ if not defined', async () => {
|
it(
|
||||||
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
|
'getMarketSellOrdersAsync() optimizer will be called once only if RFQ if not defined',
|
||||||
MarketOperationUtils,
|
IS_PRICE_AWARE_RFQ_ENABLED
|
||||||
TypeMoq.MockBehavior.Loose,
|
? async () => {
|
||||||
false,
|
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
|
||||||
MOCK_SAMPLER,
|
MarketOperationUtils,
|
||||||
contractAddresses,
|
TypeMoq.MockBehavior.Loose,
|
||||||
ORDER_DOMAIN,
|
false,
|
||||||
);
|
MOCK_SAMPLER,
|
||||||
mockedMarketOpUtils.callBase = true;
|
contractAddresses,
|
||||||
|
ORDER_DOMAIN,
|
||||||
|
);
|
||||||
|
mockedMarketOpUtils.callBase = true;
|
||||||
|
|
||||||
// Ensure that `_generateOptimizedOrdersAsync` is only called once
|
// Ensure that `_generateOptimizedOrdersAsync` is only called once
|
||||||
mockedMarketOpUtils
|
mockedMarketOpUtils
|
||||||
.setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
.setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||||
.returns(async (a, b) => mockedMarketOpUtils.target._generateOptimizedOrdersAsync(a, b))
|
.returns(async (a, b) => mockedMarketOpUtils.target._generateOptimizedOrdersAsync(a, b))
|
||||||
.verifiable(TypeMoq.Times.once());
|
.verifiable(TypeMoq.Times.once());
|
||||||
|
|
||||||
const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b));
|
const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b));
|
||||||
await mockedMarketOpUtils.object.getMarketSellOrdersAsync(ORDERS, totalAssetAmount, DEFAULT_OPTS);
|
await mockedMarketOpUtils.object.getMarketSellOrdersAsync(
|
||||||
mockedMarketOpUtils.verifyAll();
|
ORDERS,
|
||||||
});
|
totalAssetAmount,
|
||||||
|
DEFAULT_OPTS,
|
||||||
|
);
|
||||||
|
mockedMarketOpUtils.verifyAll();
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
|
||||||
it('optimizer will send in a comparison price to RFQ providers', async () => {
|
it(
|
||||||
// Set up mocked quote requestor, will return an order that is better
|
'optimizer will send in a comparison price to RFQ providers',
|
||||||
// than the best of the orders.
|
IS_PRICE_AWARE_RFQ_ENABLED
|
||||||
const mockedQuoteRequestor = TypeMoq.Mock.ofType(QuoteRequestor, TypeMoq.MockBehavior.Loose, false, {});
|
? 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;
|
let requestedComparisonPrice: BigNumber | undefined;
|
||||||
mockedQuoteRequestor
|
mockedQuoteRequestor
|
||||||
.setup(mqr =>
|
.setup(mqr =>
|
||||||
mqr.requestRfqtFirmQuotesAsync(
|
mqr.requestRfqtFirmQuotesAsync(
|
||||||
TypeMoq.It.isAny(),
|
TypeMoq.It.isAny(),
|
||||||
TypeMoq.It.isAny(),
|
TypeMoq.It.isAny(),
|
||||||
TypeMoq.It.isAny(),
|
TypeMoq.It.isAny(),
|
||||||
TypeMoq.It.isAny(),
|
TypeMoq.It.isAny(),
|
||||||
TypeMoq.It.isAny(),
|
TypeMoq.It.isAny(),
|
||||||
TypeMoq.It.isAny(),
|
TypeMoq.It.isAny(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.callback(
|
.callback(
|
||||||
(
|
(
|
||||||
_makerAssetData: string,
|
_makerAssetData: string,
|
||||||
_takerAssetData: string,
|
_takerAssetData: string,
|
||||||
_assetFillAmount: BigNumber,
|
_assetFillAmount: BigNumber,
|
||||||
_marketOperation: MarketOperation,
|
_marketOperation: MarketOperation,
|
||||||
comparisonPrice: BigNumber | undefined,
|
comparisonPrice: BigNumber | undefined,
|
||||||
_options: RfqtRequestOpts,
|
_options: RfqtRequestOpts,
|
||||||
) => {
|
) => {
|
||||||
requestedComparisonPrice = comparisonPrice;
|
requestedComparisonPrice = comparisonPrice;
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.returns(async () => {
|
.returns(async () => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
signedOrder: createOrder({
|
signedOrder: createOrder({
|
||||||
makerAssetData: MAKER_ASSET_DATA,
|
makerAssetData: MAKER_ASSET_DATA,
|
||||||
takerAssetData: TAKER_ASSET_DATA,
|
takerAssetData: TAKER_ASSET_DATA,
|
||||||
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(321, 6),
|
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(321, 6),
|
||||||
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 18),
|
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 18),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up sampler, will only return 1 on-chain order
|
// Set up sampler, will only return 1 on-chain order
|
||||||
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
|
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
|
||||||
MarketOperationUtils,
|
MarketOperationUtils,
|
||||||
TypeMoq.MockBehavior.Loose,
|
TypeMoq.MockBehavior.Loose,
|
||||||
false,
|
false,
|
||||||
MOCK_SAMPLER,
|
MOCK_SAMPLER,
|
||||||
contractAddresses,
|
contractAddresses,
|
||||||
ORDER_DOMAIN,
|
ORDER_DOMAIN,
|
||||||
);
|
);
|
||||||
mockedMarketOpUtils.callBase = true;
|
mockedMarketOpUtils.callBase = true;
|
||||||
mockedMarketOpUtils
|
mockedMarketOpUtils
|
||||||
.setup(mou =>
|
.setup(mou =>
|
||||||
mou.getMarketSellLiquidityAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()),
|
mou.getMarketSellLiquidityAsync(
|
||||||
)
|
TypeMoq.It.isAny(),
|
||||||
.returns(async () => {
|
TypeMoq.It.isAny(),
|
||||||
return {
|
TypeMoq.It.isAny(),
|
||||||
dexQuotes: [],
|
),
|
||||||
ethToInputRate: Web3Wrapper.toBaseUnitAmount(1, 18),
|
)
|
||||||
ethToOutputRate: Web3Wrapper.toBaseUnitAmount(1, 6),
|
.returns(async () => {
|
||||||
inputAmount: Web3Wrapper.toBaseUnitAmount(1, 18),
|
return {
|
||||||
inputToken: MAKER_TOKEN,
|
dexQuotes: [],
|
||||||
outputToken: TAKER_TOKEN,
|
ethToInputRate: Web3Wrapper.toBaseUnitAmount(1, 18),
|
||||||
nativeOrders: [
|
ethToOutputRate: Web3Wrapper.toBaseUnitAmount(1, 6),
|
||||||
createOrder({
|
inputAmount: Web3Wrapper.toBaseUnitAmount(1, 18),
|
||||||
makerAssetData: MAKER_ASSET_DATA,
|
inputToken: MAKER_TOKEN,
|
||||||
takerAssetData: TAKER_ASSET_DATA,
|
outputToken: TAKER_TOKEN,
|
||||||
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(320, 6),
|
nativeOrders: [
|
||||||
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 18),
|
createOrder({
|
||||||
}),
|
makerAssetData: MAKER_ASSET_DATA,
|
||||||
],
|
takerAssetData: TAKER_ASSET_DATA,
|
||||||
orderFillableAmounts: [Web3Wrapper.toBaseUnitAmount(1, 18)],
|
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(320, 6),
|
||||||
rfqtIndicativeQuotes: [],
|
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 18),
|
||||||
side: MarketOperation.Sell,
|
}),
|
||||||
twoHopQuotes: [],
|
],
|
||||||
quoteSourceFilters: new SourceFilters(),
|
orderFillableAmounts: [Web3Wrapper.toBaseUnitAmount(1, 18)],
|
||||||
makerTokenDecimals: 6,
|
rfqtIndicativeQuotes: [],
|
||||||
takerTokenDecimals: 18,
|
side: MarketOperation.Sell,
|
||||||
};
|
twoHopQuotes: [],
|
||||||
});
|
quoteSourceFilters: new SourceFilters(),
|
||||||
const result = await mockedMarketOpUtils.object.getMarketSellOrdersAsync(
|
makerTokenDecimals: 6,
|
||||||
ORDERS,
|
takerTokenDecimals: 18,
|
||||||
Web3Wrapper.toBaseUnitAmount(1, 18),
|
};
|
||||||
{
|
});
|
||||||
...DEFAULT_OPTS,
|
const result = await mockedMarketOpUtils.object.getMarketSellOrdersAsync(
|
||||||
rfqt: {
|
ORDERS,
|
||||||
isIndicative: false,
|
Web3Wrapper.toBaseUnitAmount(1, 18),
|
||||||
apiKey: 'foo',
|
{
|
||||||
takerAddress: randomAddress(),
|
...DEFAULT_OPTS,
|
||||||
intentOnFilling: true,
|
rfqt: {
|
||||||
quoteRequestor: {
|
isIndicative: false,
|
||||||
requestRfqtFirmQuotesAsync: mockedQuoteRequestor.object.requestRfqtFirmQuotesAsync,
|
apiKey: 'foo',
|
||||||
} as any,
|
takerAddress: randomAddress(),
|
||||||
},
|
intentOnFilling: true,
|
||||||
},
|
quoteRequestor: {
|
||||||
);
|
requestRfqtFirmQuotesAsync:
|
||||||
expect(result.optimizedOrders.length).to.eql(1);
|
mockedQuoteRequestor.object.requestRfqtFirmQuotesAsync,
|
||||||
// tslint:disable-next-line:no-unnecessary-type-assertion
|
} as any,
|
||||||
expect(requestedComparisonPrice!.toString()).to.eql('320');
|
},
|
||||||
expect(result.optimizedOrders[0].makerAssetAmount.toString()).to.eql('321000000');
|
},
|
||||||
expect(result.optimizedOrders[0].takerAssetAmount.toString()).to.eql('1000000000000000000');
|
);
|
||||||
});
|
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', async () => {
|
it(
|
||||||
// Ensure that `_generateOptimizedOrdersAsync` is only called once
|
'getMarketSellOrdersAsync() will not rerun the optimizer if no orders are returned',
|
||||||
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
|
IS_PRICE_AWARE_RFQ_ENABLED
|
||||||
MarketOperationUtils,
|
? async () => {
|
||||||
TypeMoq.MockBehavior.Loose,
|
// Ensure that `_generateOptimizedOrdersAsync` is only called once
|
||||||
false,
|
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
|
||||||
MOCK_SAMPLER,
|
MarketOperationUtils,
|
||||||
contractAddresses,
|
TypeMoq.MockBehavior.Loose,
|
||||||
ORDER_DOMAIN,
|
false,
|
||||||
);
|
MOCK_SAMPLER,
|
||||||
mockedMarketOpUtils.callBase = true;
|
contractAddresses,
|
||||||
mockedMarketOpUtils
|
ORDER_DOMAIN,
|
||||||
.setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
);
|
||||||
.returns(async (a, b) => mockedMarketOpUtils.target._generateOptimizedOrdersAsync(a, b))
|
mockedMarketOpUtils.callBase = true;
|
||||||
.verifiable(TypeMoq.Times.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 requestor = getMockedQuoteRequestor('firm', [], TypeMoq.Times.once());
|
const requestor = getMockedQuoteRequestor('firm', [], TypeMoq.Times.once());
|
||||||
|
|
||||||
const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b));
|
const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b));
|
||||||
await mockedMarketOpUtils.object.getMarketSellOrdersAsync(ORDERS, totalAssetAmount, {
|
await mockedMarketOpUtils.object.getMarketSellOrdersAsync(ORDERS, totalAssetAmount, {
|
||||||
...DEFAULT_OPTS,
|
...DEFAULT_OPTS,
|
||||||
rfqt: {
|
rfqt: {
|
||||||
isIndicative: false,
|
isIndicative: false,
|
||||||
apiKey: 'foo',
|
apiKey: 'foo',
|
||||||
takerAddress: randomAddress(),
|
takerAddress: randomAddress(),
|
||||||
intentOnFilling: true,
|
intentOnFilling: true,
|
||||||
quoteRequestor: {
|
quoteRequestor: {
|
||||||
requestRfqtFirmQuotesAsync: requestor.object.requestRfqtFirmQuotesAsync,
|
requestRfqtFirmQuotesAsync: requestor.object.requestRfqtFirmQuotesAsync,
|
||||||
} as any,
|
} as any,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
mockedMarketOpUtils.verifyAll();
|
mockedMarketOpUtils.verifyAll();
|
||||||
requestor.verifyAll();
|
requestor.verifyAll();
|
||||||
});
|
}
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
|
||||||
it('getMarketSellOrdersAsync() will rerun the optimizer if one or more indicative are returned', async () => {
|
it(
|
||||||
const requestor = getMockedQuoteRequestor('indicative', [ORDERS[0], ORDERS[1]], TypeMoq.Times.once());
|
'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 numOrdersInCall: number[] = [];
|
||||||
const numIndicativeQuotesInCall: number[] = [];
|
const numIndicativeQuotesInCall: number[] = [];
|
||||||
|
|
||||||
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
|
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
|
||||||
MarketOperationUtils,
|
MarketOperationUtils,
|
||||||
TypeMoq.MockBehavior.Loose,
|
TypeMoq.MockBehavior.Loose,
|
||||||
false,
|
false,
|
||||||
MOCK_SAMPLER,
|
MOCK_SAMPLER,
|
||||||
contractAddresses,
|
contractAddresses,
|
||||||
ORDER_DOMAIN,
|
ORDER_DOMAIN,
|
||||||
);
|
);
|
||||||
mockedMarketOpUtils.callBase = true;
|
mockedMarketOpUtils.callBase = true;
|
||||||
mockedMarketOpUtils
|
mockedMarketOpUtils
|
||||||
.setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
.setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||||
.callback(async (msl: MarketSideLiquidity, _opts: GenerateOptimizedOrdersOpts) => {
|
.callback(async (msl: MarketSideLiquidity, _opts: GenerateOptimizedOrdersOpts) => {
|
||||||
numOrdersInCall.push(msl.nativeOrders.length);
|
numOrdersInCall.push(msl.nativeOrders.length);
|
||||||
numIndicativeQuotesInCall.push(msl.rfqtIndicativeQuotes.length);
|
numIndicativeQuotesInCall.push(msl.rfqtIndicativeQuotes.length);
|
||||||
})
|
})
|
||||||
.returns(async (a, b) => mockedMarketOpUtils.target._generateOptimizedOrdersAsync(a, b))
|
.returns(async (a, b) => mockedMarketOpUtils.target._generateOptimizedOrdersAsync(a, b))
|
||||||
.verifiable(TypeMoq.Times.exactly(2));
|
.verifiable(TypeMoq.Times.exactly(2));
|
||||||
|
|
||||||
const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b));
|
const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b));
|
||||||
await mockedMarketOpUtils.object.getMarketSellOrdersAsync(
|
await mockedMarketOpUtils.object.getMarketSellOrdersAsync(
|
||||||
ORDERS.slice(2, ORDERS.length),
|
ORDERS.slice(2, ORDERS.length),
|
||||||
totalAssetAmount,
|
totalAssetAmount,
|
||||||
{
|
{
|
||||||
...DEFAULT_OPTS,
|
...DEFAULT_OPTS,
|
||||||
rfqt: {
|
rfqt: {
|
||||||
isIndicative: true,
|
isIndicative: true,
|
||||||
apiKey: 'foo',
|
apiKey: 'foo',
|
||||||
takerAddress: randomAddress(),
|
takerAddress: randomAddress(),
|
||||||
intentOnFilling: true,
|
intentOnFilling: true,
|
||||||
quoteRequestor: {
|
quoteRequestor: {
|
||||||
requestRfqtIndicativeQuotesAsync: requestor.object.requestRfqtIndicativeQuotesAsync,
|
requestRfqtIndicativeQuotesAsync:
|
||||||
} as any,
|
requestor.object.requestRfqtIndicativeQuotesAsync,
|
||||||
},
|
} as any,
|
||||||
},
|
},
|
||||||
);
|
},
|
||||||
mockedMarketOpUtils.verifyAll();
|
);
|
||||||
requestor.verifyAll();
|
mockedMarketOpUtils.verifyAll();
|
||||||
|
requestor.verifyAll();
|
||||||
|
|
||||||
// The first and second optimizer call contains same number of RFQ orders.
|
// The first and second optimizer call contains same number of RFQ orders.
|
||||||
expect(numOrdersInCall.length).to.eql(2);
|
expect(numOrdersInCall.length).to.eql(2);
|
||||||
expect(numOrdersInCall[0]).to.eql(1);
|
expect(numOrdersInCall[0]).to.eql(1);
|
||||||
expect(numOrdersInCall[1]).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
|
// The first call to optimizer will have no RFQ indicative quotes. The second call will have
|
||||||
// two indicative quotes.
|
// two indicative quotes.
|
||||||
expect(numIndicativeQuotesInCall.length).to.eql(2);
|
expect(numIndicativeQuotesInCall.length).to.eql(2);
|
||||||
expect(numIndicativeQuotesInCall[0]).to.eql(0);
|
expect(numIndicativeQuotesInCall[0]).to.eql(0);
|
||||||
expect(numIndicativeQuotesInCall[1]).to.eql(2);
|
expect(numIndicativeQuotesInCall[1]).to.eql(2);
|
||||||
});
|
}
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
|
||||||
it('getMarketSellOrdersAsync() will rerun the optimizer if one or more RFQ orders are returned', async () => {
|
it(
|
||||||
const requestor = getMockedQuoteRequestor('firm', [ORDERS[0]], TypeMoq.Times.once());
|
'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
|
// Ensure that `_generateOptimizedOrdersAsync` is only called once
|
||||||
|
|
||||||
// TODO: Ensure fillable amounts increase too
|
// TODO: Ensure fillable amounts increase too
|
||||||
const numOrdersInCall: number[] = [];
|
const numOrdersInCall: number[] = [];
|
||||||
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
|
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
|
||||||
MarketOperationUtils,
|
MarketOperationUtils,
|
||||||
TypeMoq.MockBehavior.Loose,
|
TypeMoq.MockBehavior.Loose,
|
||||||
false,
|
false,
|
||||||
MOCK_SAMPLER,
|
MOCK_SAMPLER,
|
||||||
contractAddresses,
|
contractAddresses,
|
||||||
ORDER_DOMAIN,
|
ORDER_DOMAIN,
|
||||||
);
|
);
|
||||||
mockedMarketOpUtils.callBase = true;
|
mockedMarketOpUtils.callBase = true;
|
||||||
mockedMarketOpUtils
|
mockedMarketOpUtils
|
||||||
.setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
.setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||||
.callback(async (msl: MarketSideLiquidity, _opts: GenerateOptimizedOrdersOpts) => {
|
.callback(async (msl: MarketSideLiquidity, _opts: GenerateOptimizedOrdersOpts) => {
|
||||||
numOrdersInCall.push(msl.nativeOrders.length);
|
numOrdersInCall.push(msl.nativeOrders.length);
|
||||||
})
|
})
|
||||||
.returns(async (a, b) => mockedMarketOpUtils.target._generateOptimizedOrdersAsync(a, b))
|
.returns(async (a, b) => mockedMarketOpUtils.target._generateOptimizedOrdersAsync(a, b))
|
||||||
.verifiable(TypeMoq.Times.exactly(2));
|
.verifiable(TypeMoq.Times.exactly(2));
|
||||||
|
|
||||||
const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b));
|
const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b));
|
||||||
await mockedMarketOpUtils.object.getMarketSellOrdersAsync(
|
await mockedMarketOpUtils.object.getMarketSellOrdersAsync(
|
||||||
ORDERS.slice(1, ORDERS.length),
|
ORDERS.slice(1, ORDERS.length),
|
||||||
totalAssetAmount,
|
totalAssetAmount,
|
||||||
{
|
{
|
||||||
...DEFAULT_OPTS,
|
...DEFAULT_OPTS,
|
||||||
rfqt: {
|
rfqt: {
|
||||||
isIndicative: false,
|
isIndicative: false,
|
||||||
apiKey: 'foo',
|
apiKey: 'foo',
|
||||||
takerAddress: randomAddress(),
|
takerAddress: randomAddress(),
|
||||||
intentOnFilling: true,
|
intentOnFilling: true,
|
||||||
quoteRequestor: {
|
quoteRequestor: {
|
||||||
requestRfqtFirmQuotesAsync: requestor.object.requestRfqtFirmQuotesAsync,
|
requestRfqtFirmQuotesAsync: requestor.object.requestRfqtFirmQuotesAsync,
|
||||||
} as any,
|
} as any,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
mockedMarketOpUtils.verifyAll();
|
mockedMarketOpUtils.verifyAll();
|
||||||
requestor.verifyAll();
|
requestor.verifyAll();
|
||||||
expect(numOrdersInCall.length).to.eql(2);
|
expect(numOrdersInCall.length).to.eql(2);
|
||||||
|
|
||||||
// The first call to optimizer was without an RFQ order.
|
// The first call to optimizer was without an RFQ order.
|
||||||
// The first call to optimizer was with an extra RFQ order.
|
// The first call to optimizer was with an extra RFQ order.
|
||||||
expect(numOrdersInCall[0]).to.eql(2);
|
expect(numOrdersInCall[0]).to.eql(2);
|
||||||
expect(numOrdersInCall[1]).to.eql(3);
|
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', async () => {
|
it(
|
||||||
let hasFirstOptimizationRun = false;
|
'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',
|
||||||
let hasSecondOptimizationRun = false;
|
IS_PRICE_AWARE_RFQ_ENABLED
|
||||||
const requestor = getMockedQuoteRequestor('firm', [ORDERS[0], ORDERS[1]], TypeMoq.Times.once());
|
? async () => {
|
||||||
|
let hasFirstOptimizationRun = false;
|
||||||
|
let hasSecondOptimizationRun = false;
|
||||||
|
const requestor = getMockedQuoteRequestor(
|
||||||
|
'firm',
|
||||||
|
[ORDERS[0], ORDERS[1]],
|
||||||
|
TypeMoq.Times.once(),
|
||||||
|
);
|
||||||
|
|
||||||
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
|
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
|
||||||
MarketOperationUtils,
|
MarketOperationUtils,
|
||||||
TypeMoq.MockBehavior.Loose,
|
TypeMoq.MockBehavior.Loose,
|
||||||
false,
|
false,
|
||||||
MOCK_SAMPLER,
|
MOCK_SAMPLER,
|
||||||
contractAddresses,
|
contractAddresses,
|
||||||
ORDER_DOMAIN,
|
ORDER_DOMAIN,
|
||||||
);
|
);
|
||||||
mockedMarketOpUtils.callBase = true;
|
mockedMarketOpUtils.callBase = true;
|
||||||
mockedMarketOpUtils
|
mockedMarketOpUtils
|
||||||
.setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
.setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||||
.returns(async (msl: MarketSideLiquidity, _opts: GenerateOptimizedOrdersOpts) => {
|
.returns(async (msl: MarketSideLiquidity, _opts: GenerateOptimizedOrdersOpts) => {
|
||||||
if (msl.nativeOrders.length === 1) {
|
if (msl.nativeOrders.length === 1) {
|
||||||
hasFirstOptimizationRun = true;
|
hasFirstOptimizationRun = true;
|
||||||
throw new Error(AggregationError.NoOptimalPath);
|
throw new Error(AggregationError.NoOptimalPath);
|
||||||
} else if (msl.nativeOrders.length === 3) {
|
} else if (msl.nativeOrders.length === 3) {
|
||||||
hasSecondOptimizationRun = true;
|
hasSecondOptimizationRun = true;
|
||||||
return mockedMarketOpUtils.target._generateOptimizedOrdersAsync(msl, _opts);
|
return mockedMarketOpUtils.target._generateOptimizedOrdersAsync(msl, _opts);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid path. this error message should never appear');
|
throw new Error('Invalid path. this error message should never appear');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.verifiable(TypeMoq.Times.exactly(2));
|
.verifiable(TypeMoq.Times.exactly(2));
|
||||||
|
|
||||||
const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b));
|
const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b));
|
||||||
await mockedMarketOpUtils.object.getMarketSellOrdersAsync(
|
await mockedMarketOpUtils.object.getMarketSellOrdersAsync(
|
||||||
ORDERS.slice(2, ORDERS.length),
|
ORDERS.slice(2, ORDERS.length),
|
||||||
totalAssetAmount,
|
totalAssetAmount,
|
||||||
{
|
{
|
||||||
...DEFAULT_OPTS,
|
...DEFAULT_OPTS,
|
||||||
rfqt: {
|
rfqt: {
|
||||||
isIndicative: false,
|
isIndicative: false,
|
||||||
apiKey: 'foo',
|
apiKey: 'foo',
|
||||||
takerAddress: randomAddress(),
|
takerAddress: randomAddress(),
|
||||||
intentOnFilling: true,
|
intentOnFilling: true,
|
||||||
quoteRequestor: {
|
quoteRequestor: {
|
||||||
requestRfqtFirmQuotesAsync: requestor.object.requestRfqtFirmQuotesAsync,
|
requestRfqtFirmQuotesAsync: requestor.object.requestRfqtFirmQuotesAsync,
|
||||||
} as any,
|
} as any,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
mockedMarketOpUtils.verifyAll();
|
mockedMarketOpUtils.verifyAll();
|
||||||
requestor.verifyAll();
|
requestor.verifyAll();
|
||||||
|
|
||||||
expect(hasFirstOptimizationRun).to.eql(true);
|
expect(hasFirstOptimizationRun).to.eql(true);
|
||||||
expect(hasSecondOptimizationRun).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 () => {
|
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(
|
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user