feat/alt RFQ MM implementation (#139)
* baseline adapter code [WIP] * fixed adapter logic, quote_requester instantiation * modified quote-requestor test to include alt implementation * type changes, fixes to quote requestor test * small fixes * working tests, made alt utils more readable * lint errors * added alt indicative quote tests, minor fixes * export alt MM market offering types * altered alt market offering to have id instead of symbols * addressed minor comments * updated changelog * got rid of unnecessary, large if-block, fixed the buy-sell assignment to be from the MM's perspective * extra logging for debugging * fixed existingOrder size * get rid of only flag on test, get rid of extra logging * prettier
This commit is contained in:
parent
fa78d1092a
commit
514f9d2621
@ -33,6 +33,10 @@
|
||||
{
|
||||
"note": "Create `FakeTaker` contract to get result data and gas used",
|
||||
"pr": 151
|
||||
},
|
||||
{
|
||||
"note": "Add an alternative RFQ market making implementation",
|
||||
"pr": 139
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -24,6 +24,7 @@ const ONE_MINUTE_SECS = 60;
|
||||
const ONE_MINUTE_MS = ONE_SECOND_MS * ONE_MINUTE_SECS;
|
||||
const DEFAULT_PER_PAGE = 1000;
|
||||
const ZERO_AMOUNT = new BigNumber(0);
|
||||
const ALT_MM_IMPUTED_INDICATIVE_EXPIRY_SECONDS = 180;
|
||||
|
||||
const DEFAULT_ORDER_PRUNER_OPTS: OrderPrunerOpts = {
|
||||
expiryBufferMs: 120000, // 2 minutes
|
||||
@ -111,4 +112,5 @@ export const constants = {
|
||||
DEFAULT_INFO_LOGGER,
|
||||
DEFAULT_WARNING_LOGGER,
|
||||
EMPTY_BYTES32,
|
||||
ALT_MM_IMPUTED_INDICATIVE_EXPIRY_SECONDS,
|
||||
};
|
||||
|
@ -75,6 +75,8 @@ export { SwapQuoteConsumer } from './quote_consumers/swap_quote_consumer';
|
||||
export { SwapQuoter, Orderbook } from './swap_quoter';
|
||||
export {
|
||||
AffiliateFee,
|
||||
AltOffering,
|
||||
AltRfqtMakerAssetOfferings,
|
||||
AssetSwapperContractAddresses,
|
||||
CalldataInfo,
|
||||
ExchangeProxyContractOpts,
|
||||
|
@ -348,6 +348,7 @@ export class SwapQuoter {
|
||||
if (calcOpts.rfqt !== undefined) {
|
||||
calcOpts.rfqt.quoteRequestor = new QuoteRequestor(
|
||||
rfqtOptions ? rfqtOptions.makerAssetOfferings || {} : {},
|
||||
rfqtOptions ? rfqtOptions.altRfqCreds : undefined,
|
||||
rfqtOptions ? rfqtOptions.warningLogger : undefined,
|
||||
rfqtOptions ? rfqtOptions.infoLogger : undefined,
|
||||
this.expiryBufferMs,
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
RfqOrderFields,
|
||||
Signature,
|
||||
} from '@0x/protocol-utils';
|
||||
import { TakerRequestQueryParams } from '@0x/quote-server';
|
||||
import { TakerRequestQueryParams, V4SignedRfqOrder } from '@0x/quote-server';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
import {
|
||||
@ -228,6 +228,7 @@ export interface RfqtRequestOpts {
|
||||
isIndicative?: boolean;
|
||||
makerEndpointMaxResponseTimeMs?: number;
|
||||
nativeExclusivelyRFQT?: boolean;
|
||||
altRfqtAssetOfferings?: AltRfqtMakerAssetOfferings;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -246,6 +247,25 @@ export interface RfqtMakerAssetOfferings {
|
||||
[endpoint: string]: Array<[string, string]>;
|
||||
}
|
||||
|
||||
export interface AltOffering {
|
||||
id: string;
|
||||
baseAsset: string;
|
||||
quoteAsset: string;
|
||||
baseAssetDecimals: number;
|
||||
quoteAssetDecimals: number;
|
||||
}
|
||||
export interface AltRfqtMakerAssetOfferings {
|
||||
[endpoint: string]: AltOffering[];
|
||||
}
|
||||
export enum RfqPairType {
|
||||
Standard = 'standard',
|
||||
Alt = 'alt',
|
||||
}
|
||||
export interface TypedMakerUrl {
|
||||
url: string;
|
||||
pairType: RfqPairType;
|
||||
}
|
||||
|
||||
export type LogFunction = (obj: object, msg?: string, ...args: any[]) => void;
|
||||
|
||||
export interface RfqtFirmQuoteValidator {
|
||||
@ -255,6 +275,10 @@ export interface RfqtFirmQuoteValidator {
|
||||
export interface SwapQuoterRfqtOpts {
|
||||
takerApiKeyWhitelist: string[];
|
||||
makerAssetOfferings: RfqtMakerAssetOfferings;
|
||||
altRfqCreds?: {
|
||||
altRfqApiKey: string;
|
||||
altRfqProfile: string;
|
||||
};
|
||||
warningLogger?: LogFunction;
|
||||
infoLogger?: LogFunction;
|
||||
}
|
||||
@ -333,6 +357,17 @@ export interface MockedRfqtQuoteResponse {
|
||||
responseCode: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a mocked RFQT maker responses.
|
||||
*/
|
||||
export interface AltMockedRfqtQuoteResponse {
|
||||
endpoint: string;
|
||||
mmApiKey: string;
|
||||
requestData: AltQuoteRequestData;
|
||||
responseData: any;
|
||||
responseCode: number;
|
||||
}
|
||||
|
||||
export interface SamplerOverrides {
|
||||
overrides: GethCallOverrides;
|
||||
block: BlockParam;
|
||||
@ -344,3 +379,50 @@ export interface SamplerCallResult {
|
||||
}
|
||||
|
||||
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
||||
export enum AltQuoteModel {
|
||||
Firm = 'firm',
|
||||
Indicative = 'indicative',
|
||||
}
|
||||
|
||||
export enum AltQuoteSide {
|
||||
Buy = 'buy',
|
||||
Sell = 'sell',
|
||||
}
|
||||
|
||||
export interface AltQuoteRequestData {
|
||||
market: string;
|
||||
model: AltQuoteModel;
|
||||
profile: string;
|
||||
side: AltQuoteSide;
|
||||
value?: string;
|
||||
amount?: string;
|
||||
meta: {
|
||||
txOrigin: string;
|
||||
taker: string;
|
||||
client: string;
|
||||
existingOrder?: {
|
||||
price: string;
|
||||
value?: string;
|
||||
amount?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface AltBaseRfqResponse extends AltQuoteRequestData {
|
||||
id: string;
|
||||
price?: string;
|
||||
}
|
||||
|
||||
export interface AltIndicativeQuoteResponse extends AltBaseRfqResponse {
|
||||
model: AltQuoteModel.Indicative;
|
||||
status: 'live' | 'rejected';
|
||||
}
|
||||
|
||||
export interface AltFirmQuoteResponse extends AltBaseRfqResponse {
|
||||
model: AltQuoteModel.Firm;
|
||||
data: {
|
||||
'0xv4order': V4SignedRfqOrder;
|
||||
};
|
||||
status: 'active' | 'rejected';
|
||||
}
|
||||
|
233
packages/asset-swapper/src/utils/alt_mm_implementation_utils.ts
Normal file
233
packages/asset-swapper/src/utils/alt_mm_implementation_utils.ts
Normal file
@ -0,0 +1,233 @@
|
||||
import { Web3Wrapper } from '@0x/dev-utils';
|
||||
import { TakerRequestQueryParams, V4RFQFirmQuote, V4RFQIndicativeQuote } from '@0x/quote-server';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { AxiosInstance } from 'axios';
|
||||
|
||||
import { constants } from '../constants';
|
||||
import {
|
||||
AltFirmQuoteResponse,
|
||||
AltIndicativeQuoteResponse,
|
||||
AltOffering,
|
||||
AltQuoteModel,
|
||||
AltQuoteRequestData,
|
||||
AltQuoteSide,
|
||||
AltRfqtMakerAssetOfferings,
|
||||
} from '../types';
|
||||
|
||||
function getAltMarketInfo(
|
||||
offerings: AltOffering[],
|
||||
buyTokenAddress: string,
|
||||
sellTokenAddress: string,
|
||||
): AltOffering | undefined {
|
||||
for (const offering of offerings) {
|
||||
if (
|
||||
(buyTokenAddress.toLowerCase() === offering.baseAsset.toLowerCase() &&
|
||||
sellTokenAddress.toLowerCase() === offering.quoteAsset.toLowerCase()) ||
|
||||
(sellTokenAddress.toLowerCase() === offering.baseAsset.toLowerCase() &&
|
||||
buyTokenAddress.toLowerCase() === offering.quoteAsset.toLowerCase())
|
||||
) {
|
||||
return offering;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseFirmQuoteResponseFromAltMM(altFirmQuoteReponse: AltFirmQuoteResponse): V4RFQFirmQuote {
|
||||
return {
|
||||
signedOrder: altFirmQuoteReponse.data['0xv4order'],
|
||||
};
|
||||
}
|
||||
|
||||
function parseIndicativeQuoteResponseFromAltMM(
|
||||
altIndicativeQuoteResponse: AltIndicativeQuoteResponse,
|
||||
altPair: AltOffering,
|
||||
makerToken: string,
|
||||
takerToken: string,
|
||||
): V4RFQIndicativeQuote {
|
||||
let makerAmount: BigNumber;
|
||||
let takerAmount: BigNumber;
|
||||
let quoteAmount: BigNumber;
|
||||
let baseAmount: BigNumber;
|
||||
|
||||
if (!altIndicativeQuoteResponse.price) {
|
||||
throw new Error('Price not returned by alt MM');
|
||||
}
|
||||
if (altIndicativeQuoteResponse.amount) {
|
||||
// if amount is specified, amount is the base token amount
|
||||
baseAmount = Web3Wrapper.toBaseUnitAmount(
|
||||
new BigNumber(altIndicativeQuoteResponse.amount),
|
||||
altPair.baseAssetDecimals,
|
||||
);
|
||||
// if amount is specified, use the price (quote/base) to get the quote amount
|
||||
quoteAmount = Web3Wrapper.toBaseUnitAmount(
|
||||
new BigNumber(altIndicativeQuoteResponse.amount)
|
||||
.times(new BigNumber(altIndicativeQuoteResponse.price))
|
||||
.decimalPlaces(altPair.quoteAssetDecimals, BigNumber.ROUND_DOWN),
|
||||
altPair.quoteAssetDecimals,
|
||||
);
|
||||
} else if (altIndicativeQuoteResponse.value) {
|
||||
// if value is specified, value is the quote token amount
|
||||
quoteAmount = Web3Wrapper.toBaseUnitAmount(
|
||||
new BigNumber(altIndicativeQuoteResponse.value),
|
||||
altPair.quoteAssetDecimals,
|
||||
);
|
||||
// if value is specified, use the price (quote/base) to get the base amount
|
||||
baseAmount = Web3Wrapper.toBaseUnitAmount(
|
||||
new BigNumber(altIndicativeQuoteResponse.value)
|
||||
.dividedBy(new BigNumber(altIndicativeQuoteResponse.price))
|
||||
.decimalPlaces(altPair.baseAssetDecimals, BigNumber.ROUND_DOWN),
|
||||
altPair.baseAssetDecimals,
|
||||
);
|
||||
} else {
|
||||
throw new Error('neither amount or value were specified');
|
||||
}
|
||||
if (makerToken.toLowerCase() === altPair.baseAsset.toLowerCase()) {
|
||||
makerAmount = baseAmount;
|
||||
takerAmount = quoteAmount;
|
||||
} else if (makerToken.toLowerCase() === altPair.quoteAsset.toLowerCase()) {
|
||||
makerAmount = quoteAmount;
|
||||
takerAmount = baseAmount;
|
||||
} else {
|
||||
throw new Error(`Base, quote tokens don't align with maker, taker tokens`);
|
||||
}
|
||||
|
||||
return {
|
||||
makerToken,
|
||||
makerAmount,
|
||||
takerToken,
|
||||
takerAmount,
|
||||
// HACK: alt implementation does not return an expiration with indicative quotes
|
||||
// return now + { IMPUTED EXPIRY SECONDS } to have it included after order checks
|
||||
expiry:
|
||||
// tslint:disable-next-line:custom-no-magic-numbers
|
||||
new BigNumber(Date.now() / 1000)
|
||||
.integerValue(BigNumber.ROUND_DOWN)
|
||||
.plus(constants.ALT_MM_IMPUTED_INDICATIVE_EXPIRY_SECONDS),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a standard quote request into an alt quote request
|
||||
* and return the appropriate standard quote response
|
||||
*/
|
||||
export async function returnQuoteFromAltMMAsync<ResponseT>(
|
||||
url: string,
|
||||
apiKey: string,
|
||||
profile: string,
|
||||
integratorKey: string,
|
||||
quoteModel: AltQuoteModel,
|
||||
makerToken: string,
|
||||
takerToken: string,
|
||||
maxResponseTimeMs: number,
|
||||
altRfqtAssetOfferings: AltRfqtMakerAssetOfferings,
|
||||
takerRequestQueryParams: TakerRequestQueryParams,
|
||||
axiosInstance: AxiosInstance,
|
||||
): Promise<{ data: ResponseT; status: number }> {
|
||||
const altPair = getAltMarketInfo(
|
||||
altRfqtAssetOfferings[url],
|
||||
takerRequestQueryParams.buyTokenAddress,
|
||||
takerRequestQueryParams.sellTokenAddress,
|
||||
);
|
||||
|
||||
if (!altPair) {
|
||||
throw new Error(`Alt pair not found`);
|
||||
}
|
||||
const side = altPair.baseAsset === takerRequestQueryParams.buyTokenAddress ? AltQuoteSide.Sell : AltQuoteSide.Buy;
|
||||
|
||||
// comparison price needs to be quote/base
|
||||
// in the standard implementation, it's maker/taker
|
||||
let altComparisonPrice: string | undefined;
|
||||
if (altPair.quoteAsset === makerToken) {
|
||||
altComparisonPrice = takerRequestQueryParams.comparisonPrice
|
||||
? takerRequestQueryParams.comparisonPrice
|
||||
: undefined;
|
||||
} else {
|
||||
altComparisonPrice = takerRequestQueryParams.comparisonPrice
|
||||
? new BigNumber(takerRequestQueryParams.comparisonPrice).pow(-1).toString()
|
||||
: undefined;
|
||||
}
|
||||
|
||||
let data: AltQuoteRequestData;
|
||||
data = {
|
||||
market: `${altPair.id}`,
|
||||
model: quoteModel,
|
||||
profile,
|
||||
side,
|
||||
meta: {
|
||||
txOrigin: takerRequestQueryParams.txOrigin!,
|
||||
taker: takerRequestQueryParams.takerAddress,
|
||||
client: integratorKey,
|
||||
},
|
||||
};
|
||||
|
||||
// specify a comparison price if it exists
|
||||
if (altComparisonPrice) {
|
||||
data.meta.existingOrder = {
|
||||
price: altComparisonPrice,
|
||||
};
|
||||
}
|
||||
|
||||
// need to specify amount or value
|
||||
// amount is units of the base asset
|
||||
// value is units of the quote asset
|
||||
let requestSize: string;
|
||||
if (takerRequestQueryParams.buyAmountBaseUnits) {
|
||||
requestSize = Web3Wrapper.toUnitAmount(
|
||||
new BigNumber(takerRequestQueryParams.buyAmountBaseUnits),
|
||||
takerRequestQueryParams.buyTokenAddress === altPair.baseAsset
|
||||
? altPair.baseAssetDecimals
|
||||
: altPair.quoteAssetDecimals,
|
||||
).toString();
|
||||
if (takerRequestQueryParams.buyTokenAddress === altPair.baseAsset) {
|
||||
data.amount = requestSize;
|
||||
// add to 'existing order' if there is a comparison price
|
||||
if (data.meta.existingOrder) {
|
||||
data.meta.existingOrder.amount = requestSize;
|
||||
}
|
||||
} else {
|
||||
data.value = requestSize;
|
||||
// add to 'existing order' if there is a comparison price
|
||||
if (data.meta.existingOrder) {
|
||||
data.meta.existingOrder.value = requestSize;
|
||||
}
|
||||
}
|
||||
} else if (takerRequestQueryParams.sellAmountBaseUnits) {
|
||||
requestSize = Web3Wrapper.toUnitAmount(
|
||||
new BigNumber(takerRequestQueryParams.sellAmountBaseUnits),
|
||||
takerRequestQueryParams.sellTokenAddress === altPair.baseAsset
|
||||
? altPair.baseAssetDecimals
|
||||
: altPair.quoteAssetDecimals,
|
||||
).toString();
|
||||
if (takerRequestQueryParams.sellTokenAddress === altPair.baseAsset) {
|
||||
data.amount = requestSize;
|
||||
if (data.meta.existingOrder) {
|
||||
data.meta.existingOrder.amount = requestSize;
|
||||
}
|
||||
} else {
|
||||
data.value = requestSize;
|
||||
if (data.meta.existingOrder) {
|
||||
data.meta.existingOrder.value = requestSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await axiosInstance.post(`${url}/quotes`, data, {
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
timeout: maxResponseTimeMs,
|
||||
});
|
||||
|
||||
if (response.data.status === 'rejected') {
|
||||
throw new Error('alt MM rejected quote');
|
||||
}
|
||||
|
||||
const parsedResponse =
|
||||
quoteModel === 'firm'
|
||||
? parseFirmQuoteResponseFromAltMM(response.data)
|
||||
: parseIndicativeQuoteResponseFromAltMM(response.data, altPair, makerToken, takerToken);
|
||||
|
||||
return {
|
||||
// hack to appease type checking
|
||||
data: (parsedResponse as unknown) as ResponseT,
|
||||
status: response.status,
|
||||
};
|
||||
}
|
@ -7,8 +7,19 @@ import { Agent as HttpAgent } from 'http';
|
||||
import { Agent as HttpsAgent } from 'https';
|
||||
|
||||
import { constants } from '../constants';
|
||||
import { LogFunction, MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts, SignedNativeOrder } from '../types';
|
||||
import {
|
||||
AltQuoteModel,
|
||||
AltRfqtMakerAssetOfferings,
|
||||
LogFunction,
|
||||
MarketOperation,
|
||||
RfqPairType,
|
||||
RfqtMakerAssetOfferings,
|
||||
RfqtRequestOpts,
|
||||
SignedNativeOrder,
|
||||
TypedMakerUrl,
|
||||
} from '../types';
|
||||
|
||||
import { returnQuoteFromAltMMAsync } from './alt_mm_implementation_utils';
|
||||
import { ONE_SECOND_MS } from './market_operation_utils/constants';
|
||||
import { RfqMakerBlacklist } from './rfq_maker_blacklist';
|
||||
|
||||
@ -126,6 +137,7 @@ export class QuoteRequestor {
|
||||
|
||||
constructor(
|
||||
private readonly _rfqtAssetOfferings: RfqtMakerAssetOfferings,
|
||||
private readonly _altRfqCreds?: { altRfqApiKey: string; altRfqProfile: string },
|
||||
private readonly _warningLogger: LogFunction = constants.DEFAULT_WARNING_LOGGER,
|
||||
private readonly _infoLogger: LogFunction = constants.DEFAULT_INFO_LOGGER,
|
||||
private readonly _expiryBufferMs: number = constants.DEFAULT_SWAP_QUOTER_OPTS.expiryBufferMs,
|
||||
@ -311,13 +323,29 @@ export class QuoteRequestor {
|
||||
return true;
|
||||
}
|
||||
|
||||
private _makerSupportsPair(makerUrl: string, makerToken: string, takerToken: string): boolean {
|
||||
for (const assetPair of this._rfqtAssetOfferings[makerUrl]) {
|
||||
if (
|
||||
(assetPair[0] === makerToken && assetPair[1] === takerToken) ||
|
||||
(assetPair[0] === takerToken && assetPair[1] === makerToken)
|
||||
) {
|
||||
return true;
|
||||
private _makerSupportsPair(
|
||||
typedMakerUrl: TypedMakerUrl,
|
||||
makerToken: string,
|
||||
takerToken: string,
|
||||
altMakerAssetOfferings: AltRfqtMakerAssetOfferings | undefined,
|
||||
): boolean {
|
||||
if (typedMakerUrl.pairType === RfqPairType.Standard) {
|
||||
for (const assetPair of this._rfqtAssetOfferings[typedMakerUrl.url]) {
|
||||
if (
|
||||
(assetPair[0] === makerToken && assetPair[1] === takerToken) ||
|
||||
(assetPair[0] === takerToken && assetPair[1] === makerToken)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (typedMakerUrl.pairType === RfqPairType.Alt && altMakerAssetOfferings) {
|
||||
for (const altAssetPair of altMakerAssetOfferings[typedMakerUrl.url]) {
|
||||
if (
|
||||
(altAssetPair.baseAsset === makerToken && altAssetPair.quoteAsset === takerToken) ||
|
||||
(altAssetPair.baseAsset === takerToken && altAssetPair.quoteAsset === makerToken)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@ -347,6 +375,7 @@ export class QuoteRequestor {
|
||||
assetFillAmount,
|
||||
comparisonPrice,
|
||||
);
|
||||
|
||||
const quotePath = (() => {
|
||||
switch (quoteType) {
|
||||
case 'firm':
|
||||
@ -358,45 +387,95 @@ export class QuoteRequestor {
|
||||
}
|
||||
})();
|
||||
|
||||
const makerUrls = Object.keys(this._rfqtAssetOfferings);
|
||||
const quotePromises = makerUrls.map(async url => {
|
||||
const standardUrls = Object.keys(this._rfqtAssetOfferings).map(
|
||||
(mm: string): TypedMakerUrl => {
|
||||
return { pairType: RfqPairType.Standard, url: mm };
|
||||
},
|
||||
);
|
||||
const altUrls = options.altRfqtAssetOfferings
|
||||
? Object.keys(options.altRfqtAssetOfferings).map(
|
||||
(mm: string): TypedMakerUrl => {
|
||||
return { pairType: RfqPairType.Alt, url: mm };
|
||||
},
|
||||
)
|
||||
: [];
|
||||
|
||||
const typedMakerUrls = standardUrls.concat(altUrls);
|
||||
|
||||
const quotePromises = typedMakerUrls.map(async typedMakerUrl => {
|
||||
// filter out requests to skip
|
||||
const isBlacklisted = rfqMakerBlacklist.isMakerBlacklisted(url);
|
||||
const partialLogEntry = { url, quoteType, requestParams, isBlacklisted };
|
||||
const isBlacklisted = rfqMakerBlacklist.isMakerBlacklisted(typedMakerUrl.url);
|
||||
const partialLogEntry = { url: typedMakerUrl.url, quoteType, requestParams, isBlacklisted };
|
||||
if (isBlacklisted) {
|
||||
this._infoLogger({ rfqtMakerInteraction: { ...partialLogEntry } });
|
||||
return;
|
||||
} else if (!this._makerSupportsPair(url, makerToken, takerToken)) {
|
||||
} else if (!this._makerSupportsPair(typedMakerUrl, makerToken, takerToken, options.altRfqtAssetOfferings)) {
|
||||
return;
|
||||
} else {
|
||||
// make request to MMs
|
||||
// make request to MM
|
||||
const timeBeforeAwait = Date.now();
|
||||
const maxResponseTimeMs =
|
||||
options.makerEndpointMaxResponseTimeMs === undefined
|
||||
? constants.DEFAULT_RFQT_REQUEST_OPTS.makerEndpointMaxResponseTimeMs!
|
||||
: options.makerEndpointMaxResponseTimeMs;
|
||||
try {
|
||||
const response = await quoteRequestorHttpClient.get<ResponseT>(`${url}/${quotePath}`, {
|
||||
headers: { '0x-api-key': options.apiKey },
|
||||
params: requestParams,
|
||||
timeout: maxResponseTimeMs,
|
||||
});
|
||||
const latencyMs = Date.now() - timeBeforeAwait;
|
||||
this._infoLogger({
|
||||
rfqtMakerInteraction: {
|
||||
...partialLogEntry,
|
||||
response: {
|
||||
included: true,
|
||||
apiKey: options.apiKey,
|
||||
takerAddress: requestParams.takerAddress,
|
||||
txOrigin: requestParams.txOrigin,
|
||||
statusCode: response.status,
|
||||
latencyMs,
|
||||
if (typedMakerUrl.pairType === RfqPairType.Standard) {
|
||||
const response = await quoteRequestorHttpClient.get(`${typedMakerUrl.url}/${quotePath}`, {
|
||||
headers: { '0x-api-key': options.apiKey },
|
||||
params: requestParams,
|
||||
timeout: maxResponseTimeMs,
|
||||
});
|
||||
const latencyMs = Date.now() - timeBeforeAwait;
|
||||
this._infoLogger({
|
||||
rfqtMakerInteraction: {
|
||||
...partialLogEntry,
|
||||
response: {
|
||||
included: true,
|
||||
apiKey: options.apiKey,
|
||||
takerAddress: requestParams.takerAddress,
|
||||
txOrigin: requestParams.txOrigin,
|
||||
statusCode: response.status,
|
||||
latencyMs,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs >= maxResponseTimeMs);
|
||||
return { response: response.data, makerUri: url };
|
||||
});
|
||||
rfqMakerBlacklist.logTimeoutOrLackThereof(typedMakerUrl.url, latencyMs >= maxResponseTimeMs);
|
||||
return { response: response.data, makerUri: typedMakerUrl.url };
|
||||
} else {
|
||||
if (this._altRfqCreds === undefined) {
|
||||
throw new Error(`don't have credentials for alt MM`);
|
||||
}
|
||||
const quote = await returnQuoteFromAltMMAsync<ResponseT>(
|
||||
typedMakerUrl.url,
|
||||
this._altRfqCreds.altRfqApiKey,
|
||||
this._altRfqCreds.altRfqProfile,
|
||||
options.apiKey,
|
||||
quoteType === 'firm' ? AltQuoteModel.Firm : AltQuoteModel.Indicative,
|
||||
makerToken,
|
||||
takerToken,
|
||||
maxResponseTimeMs,
|
||||
options.altRfqtAssetOfferings || {},
|
||||
requestParams,
|
||||
quoteRequestorHttpClient,
|
||||
);
|
||||
|
||||
const latencyMs = Date.now() - timeBeforeAwait;
|
||||
this._infoLogger({
|
||||
rfqtMakerInteraction: {
|
||||
...partialLogEntry,
|
||||
response: {
|
||||
included: true,
|
||||
apiKey: options.apiKey,
|
||||
takerAddress: requestParams.takerAddress,
|
||||
txOrigin: requestParams.txOrigin,
|
||||
statusCode: quote.status,
|
||||
latencyMs,
|
||||
},
|
||||
},
|
||||
});
|
||||
rfqMakerBlacklist.logTimeoutOrLackThereof(typedMakerUrl.url, latencyMs >= maxResponseTimeMs);
|
||||
return { response: quote.data, makerUri: typedMakerUrl.url };
|
||||
}
|
||||
} catch (err) {
|
||||
// log error if any
|
||||
const latencyMs = Date.now() - timeBeforeAwait;
|
||||
@ -413,12 +492,14 @@ export class QuoteRequestor {
|
||||
},
|
||||
},
|
||||
});
|
||||
rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs >= maxResponseTimeMs);
|
||||
rfqMakerBlacklist.logTimeoutOrLackThereof(typedMakerUrl.url, latencyMs >= maxResponseTimeMs);
|
||||
this._warningLogger(
|
||||
convertIfAxiosError(err),
|
||||
`Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${url} for API key ${
|
||||
options.apiKey
|
||||
} for taker address ${options.takerAddress} and tx origin ${options.txOrigin}`,
|
||||
`Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${
|
||||
typedMakerUrl.url
|
||||
} for API key ${options.apiKey} for taker address ${options.takerAddress} and tx origin ${
|
||||
options.txOrigin
|
||||
}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -1,14 +1,22 @@
|
||||
import { tokenUtils } from '@0x/dev-utils';
|
||||
import { FillQuoteTransformerOrderType, SignatureType } from '@0x/protocol-utils';
|
||||
import { TakerRequestQueryParams } from '@0x/quote-server';
|
||||
import { TakerRequestQueryParams, V4RFQIndicativeQuote } from '@0x/quote-server';
|
||||
import { StatusCodes } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { BigNumber, logUtils } from '@0x/utils';
|
||||
import * as chai from 'chai';
|
||||
import _ = require('lodash');
|
||||
import 'mocha';
|
||||
|
||||
import { constants } from '../src/constants';
|
||||
import { MarketOperation, MockedRfqtQuoteResponse } from '../src/types';
|
||||
import {
|
||||
AltMockedRfqtQuoteResponse,
|
||||
AltQuoteModel,
|
||||
AltQuoteRequestData,
|
||||
AltQuoteSide,
|
||||
AltRfqtMakerAssetOfferings,
|
||||
MarketOperation,
|
||||
MockedRfqtQuoteResponse,
|
||||
} from '../src/types';
|
||||
import { NULL_ADDRESS } from '../src/utils/market_operation_utils/constants';
|
||||
import { QuoteRequestor, quoteRequestorHttpClient } from '../src/utils/quote_requestor';
|
||||
|
||||
@ -17,6 +25,12 @@ import { RfqtQuoteEndpoint, testHelpers } from './utils/test_helpers';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
const ALT_MM_API_KEY = 'averysecurekey';
|
||||
const ALT_PROFILE = 'acoolprofile';
|
||||
const ALT_RFQ_CREDS = {
|
||||
altRfqApiKey: ALT_MM_API_KEY,
|
||||
altRfqProfile: ALT_PROFILE,
|
||||
};
|
||||
|
||||
function makeThreeMinuteExpiry(): BigNumber {
|
||||
const expiry = new Date(Date.now());
|
||||
@ -28,6 +42,18 @@ describe('QuoteRequestor', async () => {
|
||||
const [makerToken, takerToken, otherToken1] = tokenUtils.getDummyERC20TokenAddresses();
|
||||
const validSignature = { v: 28, r: '0x', s: '0x', signatureType: SignatureType.EthSign };
|
||||
|
||||
const altRfqtAssetOfferings: AltRfqtMakerAssetOfferings = {
|
||||
'https://132.0.0.1': [
|
||||
{
|
||||
id: 'XYZ-123',
|
||||
baseAsset: makerToken,
|
||||
quoteAsset: takerToken,
|
||||
baseAssetDecimals: 2,
|
||||
quoteAssetDecimals: 3,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('requestRfqtFirmQuotesAsync for firm quotes', async () => {
|
||||
it('should return successful RFQT requests', async () => {
|
||||
const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
|
||||
@ -37,6 +63,8 @@ describe('QuoteRequestor', async () => {
|
||||
// Set up RFQT responses
|
||||
// tslint:disable-next-line:array-type
|
||||
const mockedRequests: MockedRfqtQuoteResponse[] = [];
|
||||
const altMockedRequests: AltMockedRfqtQuoteResponse[] = [];
|
||||
|
||||
const expectedParams: TakerRequestQueryParams = {
|
||||
sellTokenAddress: takerToken,
|
||||
buyTokenAddress: makerToken,
|
||||
@ -66,6 +94,30 @@ describe('QuoteRequestor', async () => {
|
||||
expiry: makeThreeMinuteExpiry(),
|
||||
signature: validSignature,
|
||||
};
|
||||
// request is to sell 10000 units of the base token
|
||||
// 10 units at 3 decimals
|
||||
const altFirmRequestData = {
|
||||
market: 'XYZ-123',
|
||||
model: AltQuoteModel.Firm,
|
||||
profile: ALT_PROFILE,
|
||||
side: AltQuoteSide.Sell,
|
||||
meta: {
|
||||
txOrigin,
|
||||
taker: takerAddress,
|
||||
client: apiKey,
|
||||
},
|
||||
value: '10',
|
||||
};
|
||||
const altFirmResponse = {
|
||||
...altFirmRequestData,
|
||||
id: 'random_id',
|
||||
// tslint:disable-next-line:custom-no-magic-numbers
|
||||
price: new BigNumber(10 / 100).toString(),
|
||||
status: 'active',
|
||||
data: {
|
||||
'0xv4order': validSignedOrder,
|
||||
},
|
||||
};
|
||||
|
||||
// Successful response
|
||||
mockedRequests.push({
|
||||
@ -124,6 +176,14 @@ describe('QuoteRequestor', async () => {
|
||||
endpoint: 'https://425.0.0.1',
|
||||
responseData: { signedOrder: { ...validSignedOrder, txOrigin: NULL_ADDRESS } },
|
||||
});
|
||||
// A successful response code and order from an alt RFQ implementation
|
||||
altMockedRequests.push({
|
||||
endpoint: 'https://132.0.0.1',
|
||||
mmApiKey: ALT_MM_API_KEY,
|
||||
responseCode: StatusCodes.Success,
|
||||
requestData: altFirmRequestData,
|
||||
responseData: altFirmResponse,
|
||||
});
|
||||
|
||||
const normalizedSuccessfulOrder = {
|
||||
order: {
|
||||
@ -139,20 +199,24 @@ describe('QuoteRequestor', async () => {
|
||||
|
||||
return testHelpers.withMockedRfqtQuotes(
|
||||
mockedRequests,
|
||||
altMockedRequests,
|
||||
RfqtQuoteEndpoint.Firm,
|
||||
async () => {
|
||||
const qr = new QuoteRequestor({
|
||||
'https://1337.0.0.1': [[makerToken, takerToken]],
|
||||
'https://420.0.0.1': [[makerToken, takerToken]],
|
||||
'https://421.0.0.1': [[makerToken, takerToken]],
|
||||
'https://421.1.0.1': [[makerToken, takerToken]],
|
||||
'https://422.0.0.1': [[makerToken, takerToken]],
|
||||
'https://423.0.0.1': [[makerToken, takerToken]],
|
||||
'https://424.0.0.1': [[makerToken, takerToken]],
|
||||
'https://425.0.0.1': [[makerToken, takerToken]],
|
||||
'https://426.0.0.1': [] /* Shouldn't ping an RFQ-T provider when they don't support the requested asset pair. */,
|
||||
'https://37.0.0.1': [[makerToken, takerToken]],
|
||||
});
|
||||
const qr = new QuoteRequestor(
|
||||
{
|
||||
'https://1337.0.0.1': [[makerToken, takerToken]],
|
||||
'https://420.0.0.1': [[makerToken, takerToken]],
|
||||
'https://421.0.0.1': [[makerToken, takerToken]],
|
||||
'https://421.1.0.1': [[makerToken, takerToken]],
|
||||
'https://422.0.0.1': [[makerToken, takerToken]],
|
||||
'https://423.0.0.1': [[makerToken, takerToken]],
|
||||
'https://424.0.0.1': [[makerToken, takerToken]],
|
||||
'https://425.0.0.1': [[makerToken, takerToken]],
|
||||
'https://426.0.0.1': [] /* Shouldn't ping an RFQ-T provider when they don't support the requested asset pair. */,
|
||||
'https://37.0.0.1': [[makerToken, takerToken]],
|
||||
},
|
||||
ALT_RFQ_CREDS,
|
||||
);
|
||||
const resp = await qr.requestRfqtFirmQuotesAsync(
|
||||
makerToken,
|
||||
takerToken,
|
||||
@ -164,9 +228,14 @@ describe('QuoteRequestor', async () => {
|
||||
takerAddress,
|
||||
txOrigin: takerAddress,
|
||||
intentOnFilling: true,
|
||||
altRfqtAssetOfferings,
|
||||
},
|
||||
);
|
||||
expect(resp).to.deep.eq([normalizedSuccessfulOrder, normalizedSuccessfulOrder]);
|
||||
expect(resp).to.deep.eq([
|
||||
normalizedSuccessfulOrder,
|
||||
normalizedSuccessfulOrder,
|
||||
normalizedSuccessfulOrder,
|
||||
]);
|
||||
},
|
||||
quoteRequestorHttpClient,
|
||||
);
|
||||
@ -255,6 +324,7 @@ describe('QuoteRequestor', async () => {
|
||||
|
||||
return testHelpers.withMockedRfqtQuotes(
|
||||
mockedRequests,
|
||||
[],
|
||||
RfqtQuoteEndpoint.Indicative,
|
||||
async () => {
|
||||
const qr = new QuoteRequestor({
|
||||
@ -318,6 +388,7 @@ describe('QuoteRequestor', async () => {
|
||||
|
||||
return testHelpers.withMockedRfqtQuotes(
|
||||
mockedRequests,
|
||||
[],
|
||||
RfqtQuoteEndpoint.Indicative,
|
||||
async () => {
|
||||
const qr = new QuoteRequestor({ 'https://1337.0.0.1': [[makerToken, takerToken]] });
|
||||
@ -339,5 +410,241 @@ describe('QuoteRequestor', async () => {
|
||||
quoteRequestorHttpClient,
|
||||
);
|
||||
});
|
||||
it('should return successful alt indicative quotes', async () => {
|
||||
const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
|
||||
const txOrigin = '0xf209925defc99488e3afff1174e48b4fa628302a';
|
||||
const apiKey = 'my-ko0l-api-key';
|
||||
|
||||
// base token has 2 decimals
|
||||
// quote token has 3 decimals
|
||||
const baseToken = makerToken;
|
||||
const quoteToken = takerToken;
|
||||
|
||||
// Set up RFQT responses
|
||||
const altMockedRequests: AltMockedRfqtQuoteResponse[] = [];
|
||||
const altScenarios: Array<{
|
||||
successfulQuote: V4RFQIndicativeQuote;
|
||||
requestedMakerToken: string;
|
||||
requestedTakerToken: string;
|
||||
requestedAmount: BigNumber;
|
||||
requestedOperation: MarketOperation;
|
||||
}> = [];
|
||||
|
||||
// SCENARIO 1
|
||||
// buy, base asset specified
|
||||
// requesting to buy 100 units (10000 base units) of the base token
|
||||
// returning a price of 0.01, which should mean 10000 maker, 1000 taker amount
|
||||
const buyAmountAltRequest: AltQuoteRequestData = {
|
||||
market: 'XYZ-123',
|
||||
model: AltQuoteModel.Indicative,
|
||||
profile: ALT_PROFILE,
|
||||
side: AltQuoteSide.Sell,
|
||||
meta: {
|
||||
txOrigin,
|
||||
taker: takerAddress,
|
||||
client: apiKey,
|
||||
},
|
||||
amount: '100',
|
||||
};
|
||||
// Successful response
|
||||
const buyAmountAltResponse = {
|
||||
...buyAmountAltRequest,
|
||||
id: 'random_id',
|
||||
// tslint:disable-next-line:custom-no-magic-numbers
|
||||
price: new BigNumber(0.01).toString(),
|
||||
status: 'live',
|
||||
};
|
||||
const successfulBuyAmountQuote: V4RFQIndicativeQuote = {
|
||||
makerToken: baseToken,
|
||||
takerToken: quoteToken,
|
||||
makerAmount: new BigNumber(10000),
|
||||
takerAmount: new BigNumber(1000),
|
||||
expiry: new BigNumber(0),
|
||||
};
|
||||
altMockedRequests.push({
|
||||
endpoint: 'https://132.0.0.1',
|
||||
mmApiKey: ALT_MM_API_KEY,
|
||||
responseCode: StatusCodes.Success,
|
||||
requestData: buyAmountAltRequest,
|
||||
responseData: buyAmountAltResponse,
|
||||
});
|
||||
altScenarios.push({
|
||||
successfulQuote: successfulBuyAmountQuote,
|
||||
requestedMakerToken: baseToken,
|
||||
requestedTakerToken: quoteToken,
|
||||
requestedAmount: new BigNumber(10000),
|
||||
requestedOperation: MarketOperation.Buy,
|
||||
});
|
||||
|
||||
// SCENARIO 2
|
||||
// alt buy, quote asset specified
|
||||
// user is requesting to sell 1 unit of the quote token, or 1000 base units
|
||||
// returning a price of 0.01, which should mean 10000 maker amount, 1000 taker amount
|
||||
const buyValueAltRequest: AltQuoteRequestData = {
|
||||
market: 'XYZ-123',
|
||||
model: AltQuoteModel.Indicative,
|
||||
profile: ALT_PROFILE,
|
||||
side: AltQuoteSide.Sell,
|
||||
meta: {
|
||||
txOrigin,
|
||||
taker: takerAddress,
|
||||
client: apiKey,
|
||||
},
|
||||
value: '1',
|
||||
};
|
||||
// Successful response
|
||||
const buyValueAltResponse = {
|
||||
...buyValueAltRequest,
|
||||
id: 'random_id',
|
||||
// tslint:disable-next-line:custom-no-magic-numbers
|
||||
price: new BigNumber(0.01).toString(),
|
||||
status: 'live',
|
||||
};
|
||||
const successfulBuyValueQuote: V4RFQIndicativeQuote = {
|
||||
makerToken: baseToken,
|
||||
takerToken: quoteToken,
|
||||
makerAmount: new BigNumber(10000),
|
||||
takerAmount: new BigNumber(1000),
|
||||
expiry: new BigNumber(0),
|
||||
};
|
||||
altMockedRequests.push({
|
||||
endpoint: 'https://132.0.0.1',
|
||||
mmApiKey: ALT_MM_API_KEY,
|
||||
responseCode: StatusCodes.Success,
|
||||
requestData: buyValueAltRequest,
|
||||
responseData: buyValueAltResponse,
|
||||
});
|
||||
altScenarios.push({
|
||||
successfulQuote: successfulBuyValueQuote,
|
||||
requestedMakerToken: baseToken,
|
||||
requestedTakerToken: quoteToken,
|
||||
requestedAmount: new BigNumber(1000),
|
||||
requestedOperation: MarketOperation.Sell,
|
||||
});
|
||||
|
||||
// SCENARIO 3
|
||||
// alt sell, base asset specified
|
||||
// user is requesting to sell 100 units (10000 base units) of the base token
|
||||
// returning a price of 0.01, which should mean 10000 taker amount, 1000 maker amount
|
||||
const sellAmountAltRequest: AltQuoteRequestData = {
|
||||
market: 'XYZ-123',
|
||||
model: AltQuoteModel.Indicative,
|
||||
profile: ALT_PROFILE,
|
||||
side: AltQuoteSide.Buy,
|
||||
meta: {
|
||||
txOrigin,
|
||||
taker: takerAddress,
|
||||
client: apiKey,
|
||||
},
|
||||
amount: '100',
|
||||
};
|
||||
// Successful response
|
||||
const sellAmountAltResponse = {
|
||||
...sellAmountAltRequest,
|
||||
id: 'random_id',
|
||||
// tslint:disable-next-line:custom-no-magic-numbers
|
||||
price: new BigNumber(0.01).toString(),
|
||||
status: 'live',
|
||||
};
|
||||
const successfulSellAmountQuote: V4RFQIndicativeQuote = {
|
||||
makerToken: quoteToken,
|
||||
takerToken: baseToken,
|
||||
makerAmount: new BigNumber(1000),
|
||||
takerAmount: new BigNumber(10000),
|
||||
expiry: new BigNumber(0),
|
||||
};
|
||||
altMockedRequests.push({
|
||||
endpoint: 'https://132.0.0.1',
|
||||
mmApiKey: ALT_MM_API_KEY,
|
||||
responseCode: StatusCodes.Success,
|
||||
requestData: sellAmountAltRequest,
|
||||
responseData: sellAmountAltResponse,
|
||||
});
|
||||
altScenarios.push({
|
||||
successfulQuote: successfulSellAmountQuote,
|
||||
requestedMakerToken: quoteToken,
|
||||
requestedTakerToken: baseToken,
|
||||
requestedAmount: new BigNumber(10000),
|
||||
requestedOperation: MarketOperation.Sell,
|
||||
});
|
||||
|
||||
// SCENARIO 4
|
||||
// alt sell, quote asset specified
|
||||
// user is requesting to buy 1 unit (1000 base units) of the quote token
|
||||
// returning a price of 0.01, which should mean 10000 taker amount, 1000 maker amount
|
||||
const sellValueAltRequest: AltQuoteRequestData = {
|
||||
market: 'XYZ-123',
|
||||
model: AltQuoteModel.Indicative,
|
||||
profile: ALT_PROFILE,
|
||||
side: AltQuoteSide.Buy,
|
||||
meta: {
|
||||
txOrigin,
|
||||
taker: takerAddress,
|
||||
client: apiKey,
|
||||
},
|
||||
value: '1',
|
||||
};
|
||||
// Successful response
|
||||
const sellValueAltResponse = {
|
||||
...sellValueAltRequest,
|
||||
id: 'random_id',
|
||||
// tslint:disable-next-line:custom-no-magic-numbers
|
||||
price: new BigNumber(0.01).toString(),
|
||||
status: 'live',
|
||||
};
|
||||
const successfulSellValueQuote: V4RFQIndicativeQuote = {
|
||||
makerToken: quoteToken,
|
||||
takerToken: baseToken,
|
||||
makerAmount: new BigNumber(1000),
|
||||
takerAmount: new BigNumber(10000),
|
||||
expiry: new BigNumber(0),
|
||||
};
|
||||
altMockedRequests.push({
|
||||
endpoint: 'https://132.0.0.1',
|
||||
mmApiKey: ALT_MM_API_KEY,
|
||||
responseCode: StatusCodes.Success,
|
||||
requestData: sellValueAltRequest,
|
||||
responseData: sellValueAltResponse,
|
||||
});
|
||||
altScenarios.push({
|
||||
successfulQuote: successfulSellValueQuote,
|
||||
requestedMakerToken: quoteToken,
|
||||
requestedTakerToken: baseToken,
|
||||
requestedAmount: new BigNumber(1000),
|
||||
requestedOperation: MarketOperation.Buy,
|
||||
});
|
||||
|
||||
let scenarioCounter = 1;
|
||||
for (const altScenario of altScenarios) {
|
||||
logUtils.log(`Alt MM indicative scenario ${scenarioCounter}`);
|
||||
scenarioCounter += 1;
|
||||
await testHelpers.withMockedRfqtQuotes(
|
||||
[],
|
||||
altMockedRequests,
|
||||
RfqtQuoteEndpoint.Indicative,
|
||||
async () => {
|
||||
const qr = new QuoteRequestor({}, ALT_RFQ_CREDS);
|
||||
const resp = await qr.requestRfqtIndicativeQuotesAsync(
|
||||
altScenario.requestedMakerToken,
|
||||
altScenario.requestedTakerToken,
|
||||
altScenario.requestedAmount,
|
||||
altScenario.requestedOperation,
|
||||
undefined,
|
||||
{
|
||||
apiKey,
|
||||
takerAddress,
|
||||
txOrigin,
|
||||
intentOnFilling: true,
|
||||
altRfqtAssetOfferings,
|
||||
},
|
||||
);
|
||||
// hack to get the expiry right, since it's dependent on the current timestamp
|
||||
const expected = { ...altScenario.successfulQuote, expiry: resp[0].expiry };
|
||||
expect(resp.sort()).to.eql([expected].sort());
|
||||
},
|
||||
quoteRequestorHttpClient,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import AxiosMockAdapter from 'axios-mock-adapter';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { InsufficientAssetLiquidityError } from '../../src/errors';
|
||||
import { MockedRfqtQuoteResponse } from '../../src/types';
|
||||
import { AltMockedRfqtQuoteResponse, MockedRfqtQuoteResponse } from '../../src/types';
|
||||
|
||||
export enum RfqtQuoteEndpoint {
|
||||
Indicative = 'price',
|
||||
@ -36,21 +37,44 @@ export const testHelpers = {
|
||||
* requests to RFQ-t providers
|
||||
*/
|
||||
withMockedRfqtQuotes: async (
|
||||
mockedResponses: MockedRfqtQuoteResponse[],
|
||||
standardMockedResponses: MockedRfqtQuoteResponse[],
|
||||
altMockedResponses: AltMockedRfqtQuoteResponse[],
|
||||
quoteType: RfqtQuoteEndpoint,
|
||||
afterResponseCallback: () => Promise<void>,
|
||||
axiosClient: AxiosInstance = axios,
|
||||
): Promise<void> => {
|
||||
const mockedAxios = new AxiosMockAdapter(axiosClient);
|
||||
const mockedAxios = new AxiosMockAdapter(axiosClient, { onNoMatch: 'throwException' });
|
||||
try {
|
||||
// Mock out RFQT responses
|
||||
for (const mockedResponse of mockedResponses) {
|
||||
// Mock out Standard RFQT responses
|
||||
for (const mockedResponse of standardMockedResponses) {
|
||||
const { endpoint, requestApiKey, requestParams, responseData, responseCode } = mockedResponse;
|
||||
const requestHeaders = { Accept: 'application/json, text/plain, */*', '0x-api-key': requestApiKey };
|
||||
mockedAxios
|
||||
.onGet(`${endpoint}/${quoteType}`, { params: requestParams }, requestHeaders)
|
||||
.replyOnce(responseCode, responseData);
|
||||
}
|
||||
// Mock out Alt RFQT responses
|
||||
for (const mockedResponse of altMockedResponses) {
|
||||
const { endpoint, mmApiKey, requestData, responseData, responseCode } = mockedResponse;
|
||||
const requestHeaders = {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
Authorization: `Bearer ${mmApiKey}`,
|
||||
};
|
||||
mockedAxios
|
||||
.onPost(
|
||||
`${endpoint}/quotes`,
|
||||
// hack to get AxiosMockAdapter to recognize the match
|
||||
// b/t the mock data and the request data
|
||||
{
|
||||
asymmetricMatch: (x: any) => {
|
||||
return _.isEqual(requestData, x);
|
||||
},
|
||||
},
|
||||
requestHeaders,
|
||||
)
|
||||
.replyOnce(responseCode, responseData);
|
||||
}
|
||||
// Perform the callback function, e.g. a test validation
|
||||
await afterResponseCallback();
|
||||
} finally {
|
||||
|
Loading…
x
Reference in New Issue
Block a user