Merge pull request #2735 from 0xProject/rfqt_comparison_feature_flag

Rfqt comparison feature flag
This commit is contained in:
Daniel Pyrathon 2020-10-20 15:48:29 -07:00 committed by GitHub
commit 3213131cad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 428 additions and 304 deletions

View File

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

View File

@ -8,7 +8,7 @@ import { BlockParamLiteral, SupportedProvider, ZeroExProvider } from 'ethereum-t
import * as _ from 'lodash';
import { artifacts } from './artifacts';
import { constants } from './constants';
import { constants, IS_PRICE_AWARE_RFQ_ENABLED } from './constants';
import {
CalculateSwapQuoteOpts,
LiquidityForTakerMakerAssetDataPair,
@ -696,6 +696,31 @@ export class SwapQuoter {
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 unsortedOrders: SignedOrder[] = orderBatches.reduce((_orders, batch) => _orders.concat(...batch), []);
const orders = sortingUtils.sortOrders(unsortedOrders);

View File

@ -5,6 +5,7 @@ import { SignedOrder } from '@0x/types';
import { BigNumber, NULL_ADDRESS } from '@0x/utils';
import * as _ from 'lodash';
import { IS_PRICE_AWARE_RFQ_ENABLED } from '../../constants';
import { MarketOperation, Omit } from '../../types';
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
? this._sampler.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, sampleAmounts)
: Promise.resolve([]);
@ -230,10 +243,17 @@ export class MarketOperationUtils {
const [
[tokenDecimals, orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes],
rfqtIndicativeQuotes,
offChainBalancerQuotes,
offChainCreamQuotes,
offChainBancorQuotes,
] = await Promise.all([samplerPromise, offChainBalancerPromise, offChainCreamPromise, offChainBancorPromise]);
] = await Promise.all([
samplerPromise,
rfqtPromise,
offChainBalancerPromise,
offChainCreamPromise,
offChainBancorPromise,
]);
const [makerTokenDecimals, takerTokenDecimals] = tokenDecimals;
return {
@ -246,7 +266,7 @@ export class MarketOperationUtils {
orderFillableAmounts,
ethToOutputRate: ethToMakerAssetRate,
ethToInputRate: ethToTakerAssetRate,
rfqtIndicativeQuotes: [],
rfqtIndicativeQuotes,
twoHopQuotes,
quoteSourceFilters,
makerTokenDecimals: makerTokenDecimals.toNumber(),
@ -345,7 +365,17 @@ export class MarketOperationUtils {
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
? this._sampler.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, sampleAmounts)
: Promise.resolve([]);
@ -356,9 +386,10 @@ export class MarketOperationUtils {
const [
[tokenDecimals, orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes],
rfqtIndicativeQuotes,
offChainBalancerQuotes,
offChainCreamQuotes,
] = await Promise.all([samplerPromise, offChainBalancerPromise, offChainCreamPromise]);
] = await Promise.all([samplerPromise, rfqtPromise, offChainBalancerPromise, offChainCreamPromise]);
// Attach the MultiBridge address to the sample fillData
(dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.MultiBridge) || []).forEach(
q => (q.fillData = { poolAddress: this._multiBridge }),
@ -374,7 +405,7 @@ export class MarketOperationUtils {
orderFillableAmounts,
ethToOutputRate: ethToTakerAssetRate,
ethToInputRate: ethToMakerAssetRate,
rfqtIndicativeQuotes: [],
rfqtIndicativeQuotes,
twoHopQuotes,
quoteSourceFilters,
makerTokenDecimals: makerTokenDecimals.toNumber(),
@ -646,7 +677,12 @@ export class MarketOperationUtils {
// If RFQ liquidity is enabled, make a request to check RFQ liquidity
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.
let comparisonPrice: BigNumber | undefined;
if (optimizerResult) {

View File

@ -17,6 +17,7 @@ import * as _ from 'lodash';
import * as TypeMoq from 'typemoq';
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 { BalancerPoolsCache } from '../src/utils/market_operation_utils/balancer_utils';
import {
@ -724,326 +725,379 @@ describe('MarketOperationUtils tests', () => {
}
});
it('getMarketSellOrdersAsync() optimizer will be called once only if RFQ if not defined', async () => {
const mockedMarketOpUtils = TypeMoq.Mock.ofType(
MarketOperationUtils,
TypeMoq.MockBehavior.Loose,
false,
MOCK_SAMPLER,
contractAddresses,
ORDER_DOMAIN,
);
mockedMarketOpUtils.callBase = true;
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());
// 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();
});
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', 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, {});
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),
}),
},
];
});
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');
});
// 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', 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());
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 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();
});
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', async () => {
const requestor = getMockedQuoteRequestor('indicative', [ORDERS[0], ORDERS[1]], TypeMoq.Times.once());
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 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 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();
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 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);
});
// 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', async () => {
const requestor = getMockedQuoteRequestor('firm', [ORDERS[0]], TypeMoq.Times.once());
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
// 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));
// 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);
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);
});
// 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', async () => {
let hasFirstOptimizationRun = false;
let hasSecondOptimizationRun = false;
const requestor = getMockedQuoteRequestor('firm', [ORDERS[0], ORDERS[1]], TypeMoq.Times.once());
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 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();
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);
});
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(