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",
|
"note": "Create `FakeTaker` contract to get result data and gas used",
|
||||||
"pr": 151
|
"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 ONE_MINUTE_MS = ONE_SECOND_MS * ONE_MINUTE_SECS;
|
||||||
const DEFAULT_PER_PAGE = 1000;
|
const DEFAULT_PER_PAGE = 1000;
|
||||||
const ZERO_AMOUNT = new BigNumber(0);
|
const ZERO_AMOUNT = new BigNumber(0);
|
||||||
|
const ALT_MM_IMPUTED_INDICATIVE_EXPIRY_SECONDS = 180;
|
||||||
|
|
||||||
const DEFAULT_ORDER_PRUNER_OPTS: OrderPrunerOpts = {
|
const DEFAULT_ORDER_PRUNER_OPTS: OrderPrunerOpts = {
|
||||||
expiryBufferMs: 120000, // 2 minutes
|
expiryBufferMs: 120000, // 2 minutes
|
||||||
@ -111,4 +112,5 @@ export const constants = {
|
|||||||
DEFAULT_INFO_LOGGER,
|
DEFAULT_INFO_LOGGER,
|
||||||
DEFAULT_WARNING_LOGGER,
|
DEFAULT_WARNING_LOGGER,
|
||||||
EMPTY_BYTES32,
|
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 { SwapQuoter, Orderbook } from './swap_quoter';
|
||||||
export {
|
export {
|
||||||
AffiliateFee,
|
AffiliateFee,
|
||||||
|
AltOffering,
|
||||||
|
AltRfqtMakerAssetOfferings,
|
||||||
AssetSwapperContractAddresses,
|
AssetSwapperContractAddresses,
|
||||||
CalldataInfo,
|
CalldataInfo,
|
||||||
ExchangeProxyContractOpts,
|
ExchangeProxyContractOpts,
|
||||||
|
@ -348,6 +348,7 @@ export class SwapQuoter {
|
|||||||
if (calcOpts.rfqt !== undefined) {
|
if (calcOpts.rfqt !== undefined) {
|
||||||
calcOpts.rfqt.quoteRequestor = new QuoteRequestor(
|
calcOpts.rfqt.quoteRequestor = new QuoteRequestor(
|
||||||
rfqtOptions ? rfqtOptions.makerAssetOfferings || {} : {},
|
rfqtOptions ? rfqtOptions.makerAssetOfferings || {} : {},
|
||||||
|
rfqtOptions ? rfqtOptions.altRfqCreds : undefined,
|
||||||
rfqtOptions ? rfqtOptions.warningLogger : undefined,
|
rfqtOptions ? rfqtOptions.warningLogger : undefined,
|
||||||
rfqtOptions ? rfqtOptions.infoLogger : undefined,
|
rfqtOptions ? rfqtOptions.infoLogger : undefined,
|
||||||
this.expiryBufferMs,
|
this.expiryBufferMs,
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
RfqOrderFields,
|
RfqOrderFields,
|
||||||
Signature,
|
Signature,
|
||||||
} from '@0x/protocol-utils';
|
} from '@0x/protocol-utils';
|
||||||
import { TakerRequestQueryParams } from '@0x/quote-server';
|
import { TakerRequestQueryParams, V4SignedRfqOrder } from '@0x/quote-server';
|
||||||
import { BigNumber } from '@0x/utils';
|
import { BigNumber } from '@0x/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -228,6 +228,7 @@ export interface RfqtRequestOpts {
|
|||||||
isIndicative?: boolean;
|
isIndicative?: boolean;
|
||||||
makerEndpointMaxResponseTimeMs?: number;
|
makerEndpointMaxResponseTimeMs?: number;
|
||||||
nativeExclusivelyRFQT?: boolean;
|
nativeExclusivelyRFQT?: boolean;
|
||||||
|
altRfqtAssetOfferings?: AltRfqtMakerAssetOfferings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -246,6 +247,25 @@ export interface RfqtMakerAssetOfferings {
|
|||||||
[endpoint: string]: Array<[string, string]>;
|
[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 type LogFunction = (obj: object, msg?: string, ...args: any[]) => void;
|
||||||
|
|
||||||
export interface RfqtFirmQuoteValidator {
|
export interface RfqtFirmQuoteValidator {
|
||||||
@ -255,6 +275,10 @@ export interface RfqtFirmQuoteValidator {
|
|||||||
export interface SwapQuoterRfqtOpts {
|
export interface SwapQuoterRfqtOpts {
|
||||||
takerApiKeyWhitelist: string[];
|
takerApiKeyWhitelist: string[];
|
||||||
makerAssetOfferings: RfqtMakerAssetOfferings;
|
makerAssetOfferings: RfqtMakerAssetOfferings;
|
||||||
|
altRfqCreds?: {
|
||||||
|
altRfqApiKey: string;
|
||||||
|
altRfqProfile: string;
|
||||||
|
};
|
||||||
warningLogger?: LogFunction;
|
warningLogger?: LogFunction;
|
||||||
infoLogger?: LogFunction;
|
infoLogger?: LogFunction;
|
||||||
}
|
}
|
||||||
@ -333,6 +357,17 @@ export interface MockedRfqtQuoteResponse {
|
|||||||
responseCode: number;
|
responseCode: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a mocked RFQT maker responses.
|
||||||
|
*/
|
||||||
|
export interface AltMockedRfqtQuoteResponse {
|
||||||
|
endpoint: string;
|
||||||
|
mmApiKey: string;
|
||||||
|
requestData: AltQuoteRequestData;
|
||||||
|
responseData: any;
|
||||||
|
responseCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SamplerOverrides {
|
export interface SamplerOverrides {
|
||||||
overrides: GethCallOverrides;
|
overrides: GethCallOverrides;
|
||||||
block: BlockParam;
|
block: BlockParam;
|
||||||
@ -344,3 +379,50 @@ export interface SamplerCallResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
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 { Agent as HttpsAgent } from 'https';
|
||||||
|
|
||||||
import { constants } from '../constants';
|
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 { ONE_SECOND_MS } from './market_operation_utils/constants';
|
||||||
import { RfqMakerBlacklist } from './rfq_maker_blacklist';
|
import { RfqMakerBlacklist } from './rfq_maker_blacklist';
|
||||||
|
|
||||||
@ -126,6 +137,7 @@ export class QuoteRequestor {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly _rfqtAssetOfferings: RfqtMakerAssetOfferings,
|
private readonly _rfqtAssetOfferings: RfqtMakerAssetOfferings,
|
||||||
|
private readonly _altRfqCreds?: { altRfqApiKey: string; altRfqProfile: string },
|
||||||
private readonly _warningLogger: LogFunction = constants.DEFAULT_WARNING_LOGGER,
|
private readonly _warningLogger: LogFunction = constants.DEFAULT_WARNING_LOGGER,
|
||||||
private readonly _infoLogger: LogFunction = constants.DEFAULT_INFO_LOGGER,
|
private readonly _infoLogger: LogFunction = constants.DEFAULT_INFO_LOGGER,
|
||||||
private readonly _expiryBufferMs: number = constants.DEFAULT_SWAP_QUOTER_OPTS.expiryBufferMs,
|
private readonly _expiryBufferMs: number = constants.DEFAULT_SWAP_QUOTER_OPTS.expiryBufferMs,
|
||||||
@ -311,13 +323,29 @@ export class QuoteRequestor {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _makerSupportsPair(makerUrl: string, makerToken: string, takerToken: string): boolean {
|
private _makerSupportsPair(
|
||||||
for (const assetPair of this._rfqtAssetOfferings[makerUrl]) {
|
typedMakerUrl: TypedMakerUrl,
|
||||||
if (
|
makerToken: string,
|
||||||
(assetPair[0] === makerToken && assetPair[1] === takerToken) ||
|
takerToken: string,
|
||||||
(assetPair[0] === takerToken && assetPair[1] === makerToken)
|
altMakerAssetOfferings: AltRfqtMakerAssetOfferings | undefined,
|
||||||
) {
|
): boolean {
|
||||||
return true;
|
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;
|
return false;
|
||||||
@ -347,6 +375,7 @@ export class QuoteRequestor {
|
|||||||
assetFillAmount,
|
assetFillAmount,
|
||||||
comparisonPrice,
|
comparisonPrice,
|
||||||
);
|
);
|
||||||
|
|
||||||
const quotePath = (() => {
|
const quotePath = (() => {
|
||||||
switch (quoteType) {
|
switch (quoteType) {
|
||||||
case 'firm':
|
case 'firm':
|
||||||
@ -358,45 +387,95 @@ export class QuoteRequestor {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const makerUrls = Object.keys(this._rfqtAssetOfferings);
|
const standardUrls = Object.keys(this._rfqtAssetOfferings).map(
|
||||||
const quotePromises = makerUrls.map(async url => {
|
(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
|
// filter out requests to skip
|
||||||
const isBlacklisted = rfqMakerBlacklist.isMakerBlacklisted(url);
|
const isBlacklisted = rfqMakerBlacklist.isMakerBlacklisted(typedMakerUrl.url);
|
||||||
const partialLogEntry = { url, quoteType, requestParams, isBlacklisted };
|
const partialLogEntry = { url: typedMakerUrl.url, quoteType, requestParams, isBlacklisted };
|
||||||
if (isBlacklisted) {
|
if (isBlacklisted) {
|
||||||
this._infoLogger({ rfqtMakerInteraction: { ...partialLogEntry } });
|
this._infoLogger({ rfqtMakerInteraction: { ...partialLogEntry } });
|
||||||
return;
|
return;
|
||||||
} else if (!this._makerSupportsPair(url, makerToken, takerToken)) {
|
} else if (!this._makerSupportsPair(typedMakerUrl, makerToken, takerToken, options.altRfqtAssetOfferings)) {
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// make request to MMs
|
// make request to MM
|
||||||
const timeBeforeAwait = Date.now();
|
const timeBeforeAwait = Date.now();
|
||||||
const maxResponseTimeMs =
|
const maxResponseTimeMs =
|
||||||
options.makerEndpointMaxResponseTimeMs === undefined
|
options.makerEndpointMaxResponseTimeMs === undefined
|
||||||
? constants.DEFAULT_RFQT_REQUEST_OPTS.makerEndpointMaxResponseTimeMs!
|
? constants.DEFAULT_RFQT_REQUEST_OPTS.makerEndpointMaxResponseTimeMs!
|
||||||
: options.makerEndpointMaxResponseTimeMs;
|
: options.makerEndpointMaxResponseTimeMs;
|
||||||
try {
|
try {
|
||||||
const response = await quoteRequestorHttpClient.get<ResponseT>(`${url}/${quotePath}`, {
|
if (typedMakerUrl.pairType === RfqPairType.Standard) {
|
||||||
headers: { '0x-api-key': options.apiKey },
|
const response = await quoteRequestorHttpClient.get(`${typedMakerUrl.url}/${quotePath}`, {
|
||||||
params: requestParams,
|
headers: { '0x-api-key': options.apiKey },
|
||||||
timeout: maxResponseTimeMs,
|
params: requestParams,
|
||||||
});
|
timeout: maxResponseTimeMs,
|
||||||
const latencyMs = Date.now() - timeBeforeAwait;
|
});
|
||||||
this._infoLogger({
|
const latencyMs = Date.now() - timeBeforeAwait;
|
||||||
rfqtMakerInteraction: {
|
this._infoLogger({
|
||||||
...partialLogEntry,
|
rfqtMakerInteraction: {
|
||||||
response: {
|
...partialLogEntry,
|
||||||
included: true,
|
response: {
|
||||||
apiKey: options.apiKey,
|
included: true,
|
||||||
takerAddress: requestParams.takerAddress,
|
apiKey: options.apiKey,
|
||||||
txOrigin: requestParams.txOrigin,
|
takerAddress: requestParams.takerAddress,
|
||||||
statusCode: response.status,
|
txOrigin: requestParams.txOrigin,
|
||||||
latencyMs,
|
statusCode: response.status,
|
||||||
|
latencyMs,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
rfqMakerBlacklist.logTimeoutOrLackThereof(typedMakerUrl.url, latencyMs >= maxResponseTimeMs);
|
||||||
rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs >= maxResponseTimeMs);
|
return { response: response.data, makerUri: typedMakerUrl.url };
|
||||||
return { response: response.data, makerUri: 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) {
|
} catch (err) {
|
||||||
// log error if any
|
// log error if any
|
||||||
const latencyMs = Date.now() - timeBeforeAwait;
|
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(
|
this._warningLogger(
|
||||||
convertIfAxiosError(err),
|
convertIfAxiosError(err),
|
||||||
`Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${url} for API key ${
|
`Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${
|
||||||
options.apiKey
|
typedMakerUrl.url
|
||||||
} for taker address ${options.takerAddress} and tx origin ${options.txOrigin}`,
|
} for API key ${options.apiKey} for taker address ${options.takerAddress} and tx origin ${
|
||||||
|
options.txOrigin
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,22 @@
|
|||||||
import { tokenUtils } from '@0x/dev-utils';
|
import { tokenUtils } from '@0x/dev-utils';
|
||||||
import { FillQuoteTransformerOrderType, SignatureType } from '@0x/protocol-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 { StatusCodes } from '@0x/types';
|
||||||
import { BigNumber } from '@0x/utils';
|
import { BigNumber, logUtils } from '@0x/utils';
|
||||||
import * as chai from 'chai';
|
import * as chai from 'chai';
|
||||||
import _ = require('lodash');
|
import _ = require('lodash');
|
||||||
import 'mocha';
|
import 'mocha';
|
||||||
|
|
||||||
import { constants } from '../src/constants';
|
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 { NULL_ADDRESS } from '../src/utils/market_operation_utils/constants';
|
||||||
import { QuoteRequestor, quoteRequestorHttpClient } from '../src/utils/quote_requestor';
|
import { QuoteRequestor, quoteRequestorHttpClient } from '../src/utils/quote_requestor';
|
||||||
|
|
||||||
@ -17,6 +25,12 @@ import { RfqtQuoteEndpoint, testHelpers } from './utils/test_helpers';
|
|||||||
|
|
||||||
chaiSetup.configure();
|
chaiSetup.configure();
|
||||||
const expect = chai.expect;
|
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 {
|
function makeThreeMinuteExpiry(): BigNumber {
|
||||||
const expiry = new Date(Date.now());
|
const expiry = new Date(Date.now());
|
||||||
@ -28,6 +42,18 @@ describe('QuoteRequestor', async () => {
|
|||||||
const [makerToken, takerToken, otherToken1] = tokenUtils.getDummyERC20TokenAddresses();
|
const [makerToken, takerToken, otherToken1] = tokenUtils.getDummyERC20TokenAddresses();
|
||||||
const validSignature = { v: 28, r: '0x', s: '0x', signatureType: SignatureType.EthSign };
|
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 () => {
|
describe('requestRfqtFirmQuotesAsync for firm quotes', async () => {
|
||||||
it('should return successful RFQT requests', async () => {
|
it('should return successful RFQT requests', async () => {
|
||||||
const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
|
const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
|
||||||
@ -37,6 +63,8 @@ describe('QuoteRequestor', async () => {
|
|||||||
// Set up RFQT responses
|
// Set up RFQT responses
|
||||||
// tslint:disable-next-line:array-type
|
// tslint:disable-next-line:array-type
|
||||||
const mockedRequests: MockedRfqtQuoteResponse[] = [];
|
const mockedRequests: MockedRfqtQuoteResponse[] = [];
|
||||||
|
const altMockedRequests: AltMockedRfqtQuoteResponse[] = [];
|
||||||
|
|
||||||
const expectedParams: TakerRequestQueryParams = {
|
const expectedParams: TakerRequestQueryParams = {
|
||||||
sellTokenAddress: takerToken,
|
sellTokenAddress: takerToken,
|
||||||
buyTokenAddress: makerToken,
|
buyTokenAddress: makerToken,
|
||||||
@ -66,6 +94,30 @@ describe('QuoteRequestor', async () => {
|
|||||||
expiry: makeThreeMinuteExpiry(),
|
expiry: makeThreeMinuteExpiry(),
|
||||||
signature: validSignature,
|
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
|
// Successful response
|
||||||
mockedRequests.push({
|
mockedRequests.push({
|
||||||
@ -124,6 +176,14 @@ describe('QuoteRequestor', async () => {
|
|||||||
endpoint: 'https://425.0.0.1',
|
endpoint: 'https://425.0.0.1',
|
||||||
responseData: { signedOrder: { ...validSignedOrder, txOrigin: NULL_ADDRESS } },
|
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 = {
|
const normalizedSuccessfulOrder = {
|
||||||
order: {
|
order: {
|
||||||
@ -139,20 +199,24 @@ describe('QuoteRequestor', async () => {
|
|||||||
|
|
||||||
return testHelpers.withMockedRfqtQuotes(
|
return testHelpers.withMockedRfqtQuotes(
|
||||||
mockedRequests,
|
mockedRequests,
|
||||||
|
altMockedRequests,
|
||||||
RfqtQuoteEndpoint.Firm,
|
RfqtQuoteEndpoint.Firm,
|
||||||
async () => {
|
async () => {
|
||||||
const qr = new QuoteRequestor({
|
const qr = new QuoteRequestor(
|
||||||
'https://1337.0.0.1': [[makerToken, takerToken]],
|
{
|
||||||
'https://420.0.0.1': [[makerToken, takerToken]],
|
'https://1337.0.0.1': [[makerToken, takerToken]],
|
||||||
'https://421.0.0.1': [[makerToken, takerToken]],
|
'https://420.0.0.1': [[makerToken, takerToken]],
|
||||||
'https://421.1.0.1': [[makerToken, takerToken]],
|
'https://421.0.0.1': [[makerToken, takerToken]],
|
||||||
'https://422.0.0.1': [[makerToken, takerToken]],
|
'https://421.1.0.1': [[makerToken, takerToken]],
|
||||||
'https://423.0.0.1': [[makerToken, takerToken]],
|
'https://422.0.0.1': [[makerToken, takerToken]],
|
||||||
'https://424.0.0.1': [[makerToken, takerToken]],
|
'https://423.0.0.1': [[makerToken, takerToken]],
|
||||||
'https://425.0.0.1': [[makerToken, takerToken]],
|
'https://424.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://425.0.0.1': [[makerToken, takerToken]],
|
||||||
'https://37.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(
|
const resp = await qr.requestRfqtFirmQuotesAsync(
|
||||||
makerToken,
|
makerToken,
|
||||||
takerToken,
|
takerToken,
|
||||||
@ -164,9 +228,14 @@ describe('QuoteRequestor', async () => {
|
|||||||
takerAddress,
|
takerAddress,
|
||||||
txOrigin: takerAddress,
|
txOrigin: takerAddress,
|
||||||
intentOnFilling: true,
|
intentOnFilling: true,
|
||||||
|
altRfqtAssetOfferings,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(resp).to.deep.eq([normalizedSuccessfulOrder, normalizedSuccessfulOrder]);
|
expect(resp).to.deep.eq([
|
||||||
|
normalizedSuccessfulOrder,
|
||||||
|
normalizedSuccessfulOrder,
|
||||||
|
normalizedSuccessfulOrder,
|
||||||
|
]);
|
||||||
},
|
},
|
||||||
quoteRequestorHttpClient,
|
quoteRequestorHttpClient,
|
||||||
);
|
);
|
||||||
@ -255,6 +324,7 @@ describe('QuoteRequestor', async () => {
|
|||||||
|
|
||||||
return testHelpers.withMockedRfqtQuotes(
|
return testHelpers.withMockedRfqtQuotes(
|
||||||
mockedRequests,
|
mockedRequests,
|
||||||
|
[],
|
||||||
RfqtQuoteEndpoint.Indicative,
|
RfqtQuoteEndpoint.Indicative,
|
||||||
async () => {
|
async () => {
|
||||||
const qr = new QuoteRequestor({
|
const qr = new QuoteRequestor({
|
||||||
@ -318,6 +388,7 @@ describe('QuoteRequestor', async () => {
|
|||||||
|
|
||||||
return testHelpers.withMockedRfqtQuotes(
|
return testHelpers.withMockedRfqtQuotes(
|
||||||
mockedRequests,
|
mockedRequests,
|
||||||
|
[],
|
||||||
RfqtQuoteEndpoint.Indicative,
|
RfqtQuoteEndpoint.Indicative,
|
||||||
async () => {
|
async () => {
|
||||||
const qr = new QuoteRequestor({ 'https://1337.0.0.1': [[makerToken, takerToken]] });
|
const qr = new QuoteRequestor({ 'https://1337.0.0.1': [[makerToken, takerToken]] });
|
||||||
@ -339,5 +410,241 @@ describe('QuoteRequestor', async () => {
|
|||||||
quoteRequestorHttpClient,
|
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 { BigNumber } from '@0x/utils';
|
||||||
import axios, { AxiosInstance } from 'axios';
|
import axios, { AxiosInstance } from 'axios';
|
||||||
import AxiosMockAdapter from 'axios-mock-adapter';
|
import AxiosMockAdapter from 'axios-mock-adapter';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import { InsufficientAssetLiquidityError } from '../../src/errors';
|
import { InsufficientAssetLiquidityError } from '../../src/errors';
|
||||||
import { MockedRfqtQuoteResponse } from '../../src/types';
|
import { AltMockedRfqtQuoteResponse, MockedRfqtQuoteResponse } from '../../src/types';
|
||||||
|
|
||||||
export enum RfqtQuoteEndpoint {
|
export enum RfqtQuoteEndpoint {
|
||||||
Indicative = 'price',
|
Indicative = 'price',
|
||||||
@ -36,21 +37,44 @@ export const testHelpers = {
|
|||||||
* requests to RFQ-t providers
|
* requests to RFQ-t providers
|
||||||
*/
|
*/
|
||||||
withMockedRfqtQuotes: async (
|
withMockedRfqtQuotes: async (
|
||||||
mockedResponses: MockedRfqtQuoteResponse[],
|
standardMockedResponses: MockedRfqtQuoteResponse[],
|
||||||
|
altMockedResponses: AltMockedRfqtQuoteResponse[],
|
||||||
quoteType: RfqtQuoteEndpoint,
|
quoteType: RfqtQuoteEndpoint,
|
||||||
afterResponseCallback: () => Promise<void>,
|
afterResponseCallback: () => Promise<void>,
|
||||||
axiosClient: AxiosInstance = axios,
|
axiosClient: AxiosInstance = axios,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const mockedAxios = new AxiosMockAdapter(axiosClient);
|
const mockedAxios = new AxiosMockAdapter(axiosClient, { onNoMatch: 'throwException' });
|
||||||
try {
|
try {
|
||||||
// Mock out RFQT responses
|
// Mock out Standard RFQT responses
|
||||||
for (const mockedResponse of mockedResponses) {
|
for (const mockedResponse of standardMockedResponses) {
|
||||||
const { endpoint, requestApiKey, requestParams, responseData, responseCode } = mockedResponse;
|
const { endpoint, requestApiKey, requestParams, responseData, responseCode } = mockedResponse;
|
||||||
const requestHeaders = { Accept: 'application/json, text/plain, */*', '0x-api-key': requestApiKey };
|
const requestHeaders = { Accept: 'application/json, text/plain, */*', '0x-api-key': requestApiKey };
|
||||||
mockedAxios
|
mockedAxios
|
||||||
.onGet(`${endpoint}/${quoteType}`, { params: requestParams }, requestHeaders)
|
.onGet(`${endpoint}/${quoteType}`, { params: requestParams }, requestHeaders)
|
||||||
.replyOnce(responseCode, responseData);
|
.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
|
// Perform the callback function, e.g. a test validation
|
||||||
await afterResponseCallback();
|
await afterResponseCallback();
|
||||||
} finally {
|
} finally {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user