[MKR-3] Prepare Asset Swapper for RFQM (#187)
* Prepare QuoteRequestor for RFQM * Add unit tests for Quote Requestor changes * Fix lint errors
This commit is contained in:
parent
7bf009fbf6
commit
70ddab0231
@ -66,7 +66,7 @@
|
||||
"@0x/dev-utils": "^4.2.1",
|
||||
"@0x/json-schemas": "^5.4.1",
|
||||
"@0x/protocol-utils": "^1.3.1",
|
||||
"@0x/quote-server": "^4.0.1",
|
||||
"@0x/quote-server": "^5.0.0",
|
||||
"@0x/types": "^3.3.1",
|
||||
"@0x/typescript-typings": "^5.1.6",
|
||||
"@0x/utils": "^6.2.0",
|
||||
|
@ -358,6 +358,7 @@ export class SwapQuoter {
|
||||
if (calcOpts.rfqt !== undefined) {
|
||||
calcOpts.rfqt.quoteRequestor = new QuoteRequestor(
|
||||
rfqtOptions ? rfqtOptions.makerAssetOfferings || {} : {},
|
||||
{},
|
||||
this._quoteRequestorHttpClient,
|
||||
rfqtOptions ? rfqtOptions.altRfqCreds : undefined,
|
||||
rfqtOptions ? rfqtOptions.warningLogger : undefined,
|
||||
|
@ -240,6 +240,7 @@ export interface RfqRequestOpts {
|
||||
makerEndpointMaxResponseTimeMs?: number;
|
||||
nativeExclusivelyRFQ?: boolean;
|
||||
altRfqAssetOfferings?: AltRfqMakerAssetOfferings;
|
||||
isLastLook?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -257,7 +258,6 @@ export interface SwapQuoteRequestOpts extends GetMarketOrdersOpts {
|
||||
export interface RfqMakerAssetOfferings {
|
||||
[endpoint: string]: Array<[string, string]>;
|
||||
}
|
||||
|
||||
export interface AltOffering {
|
||||
id: string;
|
||||
baseAsset: string;
|
||||
|
@ -83,6 +83,7 @@ export class QuoteRequestor {
|
||||
sellTokenAddress: string, // taker token
|
||||
assetFillAmount: BigNumber,
|
||||
comparisonPrice?: BigNumber,
|
||||
isLastLook: boolean = false,
|
||||
): TakerRequestQueryParams {
|
||||
const { buyAmountBaseUnits, sellAmountBaseUnits } =
|
||||
marketOperation === MarketOperation.Buy
|
||||
@ -97,13 +98,20 @@ export class QuoteRequestor {
|
||||
|
||||
const requestParamsWithBigNumbers: Pick<
|
||||
TakerRequestQueryParams,
|
||||
'buyTokenAddress' | 'sellTokenAddress' | 'txOrigin' | 'comparisonPrice' | 'protocolVersion' | 'takerAddress'
|
||||
| 'txOrigin'
|
||||
| 'takerAddress'
|
||||
| 'buyTokenAddress'
|
||||
| 'sellTokenAddress'
|
||||
| 'comparisonPrice'
|
||||
| 'isLastLook'
|
||||
| 'protocolVersion'
|
||||
> = {
|
||||
txOrigin,
|
||||
takerAddress,
|
||||
comparisonPrice: comparisonPrice === undefined ? undefined : comparisonPrice.toString(),
|
||||
buyTokenAddress,
|
||||
sellTokenAddress,
|
||||
comparisonPrice: comparisonPrice === undefined ? undefined : comparisonPrice.toString(),
|
||||
isLastLook: isLastLook.toString(),
|
||||
protocolVersion: '4',
|
||||
};
|
||||
|
||||
@ -124,8 +132,38 @@ export class QuoteRequestor {
|
||||
}
|
||||
}
|
||||
|
||||
private static _makerSupportsPair(
|
||||
typedMakerUrl: TypedMakerUrl,
|
||||
makerToken: string,
|
||||
takerToken: string,
|
||||
altMakerAssetOfferings: AltRfqMakerAssetOfferings | undefined,
|
||||
assetOfferings: RfqMakerAssetOfferings | undefined,
|
||||
): boolean {
|
||||
if (typedMakerUrl.pairType === RfqPairType.Standard && assetOfferings) {
|
||||
for (const assetPair of assetOfferings[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;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _rfqtAssetOfferings: RfqMakerAssetOfferings,
|
||||
private readonly _rfqmAssetOfferings: RfqMakerAssetOfferings,
|
||||
private readonly _quoteRequestorHttpClient: AxiosInstance,
|
||||
private readonly _altRfqCreds?: { altRfqApiKey: string; altRfqProfile: string },
|
||||
private readonly _warningLogger: LogFunction = constants.DEFAULT_WARNING_LOGGER,
|
||||
@ -135,6 +173,31 @@ export class QuoteRequestor {
|
||||
rfqMakerBlacklist.infoLogger = this._infoLogger;
|
||||
}
|
||||
|
||||
public async requestRfqmFirmQuotesAsync(
|
||||
makerToken: string, // maker token
|
||||
takerToken: string, // taker token
|
||||
assetFillAmount: BigNumber,
|
||||
marketOperation: MarketOperation,
|
||||
comparisonPrice: BigNumber | undefined,
|
||||
options: RfqRequestOpts,
|
||||
): Promise<SignedNativeOrder[]> {
|
||||
const _opts: RfqRequestOpts = {
|
||||
...constants.DEFAULT_RFQT_REQUEST_OPTS,
|
||||
...options,
|
||||
isLastLook: true,
|
||||
};
|
||||
|
||||
return this._fetchAndValidateFirmQuotesAsync(
|
||||
makerToken,
|
||||
takerToken,
|
||||
assetFillAmount,
|
||||
marketOperation,
|
||||
comparisonPrice,
|
||||
_opts,
|
||||
this._rfqmAssetOfferings,
|
||||
);
|
||||
}
|
||||
|
||||
public async requestRfqtFirmQuotesAsync(
|
||||
makerToken: string, // maker token
|
||||
takerToken: string, // taker token
|
||||
@ -148,72 +211,40 @@ export class QuoteRequestor {
|
||||
throw new Error('RFQ-T firm quotes require the presence of a tx origin');
|
||||
}
|
||||
|
||||
const quotesRaw = await this._getQuotesAsync<V4RFQFirmQuote>(
|
||||
return this._fetchAndValidateFirmQuotesAsync(
|
||||
makerToken,
|
||||
takerToken,
|
||||
assetFillAmount,
|
||||
marketOperation,
|
||||
comparisonPrice,
|
||||
_opts,
|
||||
'firm',
|
||||
this._rfqtAssetOfferings,
|
||||
);
|
||||
const quotes = quotesRaw.map(result => ({ ...result, response: result.response.signedOrder }));
|
||||
}
|
||||
|
||||
// validate
|
||||
const validationFunction = (o: V4SignedRfqOrder) => {
|
||||
try {
|
||||
// Handle the validate throwing, i.e if it isn't an object or json response
|
||||
return this._schemaValidator.isValid(o, schemas.v4RfqSignedOrderSchema);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
public async requestRfqmIndicativeQuotesAsync(
|
||||
makerToken: string,
|
||||
takerToken: string,
|
||||
assetFillAmount: BigNumber,
|
||||
marketOperation: MarketOperation,
|
||||
comparisonPrice: BigNumber | undefined,
|
||||
options: RfqRequestOpts,
|
||||
): Promise<V4RFQIndicativeQuote[]> {
|
||||
const _opts: RfqRequestOpts = {
|
||||
...constants.DEFAULT_RFQT_REQUEST_OPTS,
|
||||
...options,
|
||||
isLastLook: true,
|
||||
};
|
||||
const validQuotes = quotes.filter(result => {
|
||||
const order = result.response;
|
||||
if (!validationFunction(order)) {
|
||||
this._warningLogger(result, 'Invalid RFQ-T firm quote received, filtering out');
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!hasExpectedAddresses([
|
||||
[makerToken, order.makerToken],
|
||||
[takerToken, order.takerToken],
|
||||
[_opts.takerAddress, order.taker],
|
||||
[_opts.txOrigin, order.txOrigin],
|
||||
])
|
||||
) {
|
||||
this._warningLogger(
|
||||
order,
|
||||
'Unexpected token, tx origin or taker address in RFQ-T order, filtering out',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (this._isExpirationTooSoon(new BigNumber(order.expiry))) {
|
||||
this._warningLogger(order, 'Expiry too soon in RFQ-T firm quote, filtering out');
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Save the maker URI for later and return just the order
|
||||
const rfqQuotes = validQuotes.map(result => {
|
||||
const { signature, ...rest } = result.response;
|
||||
const order: SignedNativeOrder = {
|
||||
order: {
|
||||
...rest,
|
||||
makerAmount: new BigNumber(result.response.makerAmount),
|
||||
takerAmount: new BigNumber(result.response.takerAmount),
|
||||
expiry: new BigNumber(result.response.expiry),
|
||||
salt: new BigNumber(result.response.salt),
|
||||
},
|
||||
type: FillQuoteTransformerOrderType.Rfq,
|
||||
signature,
|
||||
};
|
||||
this._orderSignatureToMakerUri[nativeDataToId(result.response)] = result.makerUri;
|
||||
return order;
|
||||
});
|
||||
return rfqQuotes;
|
||||
return this._fetchAndValidateIndicativeQuotesAsync(
|
||||
makerToken,
|
||||
takerToken,
|
||||
assetFillAmount,
|
||||
marketOperation,
|
||||
comparisonPrice,
|
||||
_opts,
|
||||
this._rfqmAssetOfferings,
|
||||
);
|
||||
}
|
||||
|
||||
public async requestRfqtIndicativeQuotesAsync(
|
||||
@ -236,42 +267,15 @@ export class QuoteRequestor {
|
||||
if (!_opts.txOrigin) {
|
||||
_opts.txOrigin = constants.NULL_ADDRESS;
|
||||
}
|
||||
const rawQuotes = await this._getQuotesAsync<V4RFQIndicativeQuote>(
|
||||
return this._fetchAndValidateIndicativeQuotesAsync(
|
||||
makerToken,
|
||||
takerToken,
|
||||
assetFillAmount,
|
||||
marketOperation,
|
||||
comparisonPrice,
|
||||
_opts,
|
||||
'indicative',
|
||||
this._rfqtAssetOfferings,
|
||||
);
|
||||
|
||||
// validate
|
||||
const validationFunction = (o: V4RFQIndicativeQuote) => this._isValidRfqtIndicativeQuoteResponse(o);
|
||||
const validQuotes = rawQuotes.filter(result => {
|
||||
const order = result.response;
|
||||
if (!validationFunction(order)) {
|
||||
this._warningLogger(result, 'Invalid RFQ-T indicative quote received, filtering out');
|
||||
return false;
|
||||
}
|
||||
if (!hasExpectedAddresses([[makerToken, order.makerToken], [takerToken, order.takerToken]])) {
|
||||
this._warningLogger(order, 'Unexpected token or taker address in RFQ-T order, filtering out');
|
||||
return false;
|
||||
}
|
||||
if (this._isExpirationTooSoon(new BigNumber(order.expiry))) {
|
||||
this._warningLogger(order, 'Expiry too soon in RFQ-T indicative quote, filtering out');
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
const quotes = validQuotes.map(r => r.response);
|
||||
quotes.forEach(q => {
|
||||
q.makerAmount = new BigNumber(q.makerAmount);
|
||||
q.takerAmount = new BigNumber(q.takerAmount);
|
||||
q.expiry = new BigNumber(q.expiry);
|
||||
});
|
||||
return quotes;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -313,34 +317,6 @@ export class QuoteRequestor {
|
||||
return true;
|
||||
}
|
||||
|
||||
private _makerSupportsPair(
|
||||
typedMakerUrl: TypedMakerUrl,
|
||||
makerToken: string,
|
||||
takerToken: string,
|
||||
altMakerAssetOfferings: AltRfqMakerAssetOfferings | 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;
|
||||
}
|
||||
|
||||
private _isExpirationTooSoon(expirationTimeSeconds: BigNumber): boolean {
|
||||
const expirationTimeMs = expirationTimeSeconds.times(constants.ONE_SECOND_MS);
|
||||
const currentTimeMs = new BigNumber(Date.now());
|
||||
@ -355,6 +331,7 @@ export class QuoteRequestor {
|
||||
comparisonPrice: BigNumber | undefined,
|
||||
options: RfqRequestOpts,
|
||||
quoteType: 'firm' | 'indicative',
|
||||
assetOfferings: RfqMakerAssetOfferings,
|
||||
): Promise<Array<RfqQuote<ResponseT>>> {
|
||||
const requestParams = QuoteRequestor.makeQueryParameters(
|
||||
options.txOrigin,
|
||||
@ -364,6 +341,7 @@ export class QuoteRequestor {
|
||||
takerToken,
|
||||
assetFillAmount,
|
||||
comparisonPrice,
|
||||
options.isLastLook,
|
||||
);
|
||||
|
||||
const quotePath = (() => {
|
||||
@ -377,7 +355,7 @@ export class QuoteRequestor {
|
||||
}
|
||||
})();
|
||||
|
||||
const standardUrls = Object.keys(this._rfqtAssetOfferings).map(
|
||||
const standardUrls = Object.keys(assetOfferings).map(
|
||||
(mm: string): TypedMakerUrl => {
|
||||
return { pairType: RfqPairType.Standard, url: mm };
|
||||
},
|
||||
@ -410,7 +388,15 @@ export class QuoteRequestor {
|
||||
if (isBlacklisted) {
|
||||
this._infoLogger({ rfqtMakerInteraction: { ...partialLogEntry } });
|
||||
return;
|
||||
} else if (!this._makerSupportsPair(typedMakerUrl, makerToken, takerToken, options.altRfqAssetOfferings)) {
|
||||
} else if (
|
||||
!QuoteRequestor._makerSupportsPair(
|
||||
typedMakerUrl,
|
||||
makerToken,
|
||||
takerToken,
|
||||
options.altRfqAssetOfferings,
|
||||
assetOfferings,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
} else {
|
||||
// make request to MM
|
||||
@ -509,4 +495,130 @@ export class QuoteRequestor {
|
||||
const results = (await Promise.all(quotePromises)).filter(x => x !== undefined);
|
||||
return results as Array<RfqQuote<ResponseT>>;
|
||||
}
|
||||
private async _fetchAndValidateFirmQuotesAsync(
|
||||
makerToken: string,
|
||||
takerToken: string,
|
||||
assetFillAmount: BigNumber,
|
||||
marketOperation: MarketOperation,
|
||||
comparisonPrice: BigNumber | undefined,
|
||||
options: RfqRequestOpts,
|
||||
assetOfferings: RfqMakerAssetOfferings,
|
||||
): Promise<SignedNativeOrder[]> {
|
||||
const quotesRaw = await this._getQuotesAsync<V4RFQFirmQuote>(
|
||||
makerToken,
|
||||
takerToken,
|
||||
assetFillAmount,
|
||||
marketOperation,
|
||||
comparisonPrice,
|
||||
options,
|
||||
'firm',
|
||||
assetOfferings,
|
||||
);
|
||||
const quotes = quotesRaw.map(result => ({ ...result, response: result.response.signedOrder }));
|
||||
|
||||
// validate
|
||||
const validationFunction = (o: V4SignedRfqOrder) => {
|
||||
try {
|
||||
// Handle the validate throwing, i.e if it isn't an object or json response
|
||||
return this._schemaValidator.isValid(o, schemas.v4RfqSignedOrderSchema);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const validQuotes = quotes.filter(result => {
|
||||
const order = result.response;
|
||||
if (!validationFunction(order)) {
|
||||
this._warningLogger(result, 'Invalid RFQ-T firm quote received, filtering out');
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!hasExpectedAddresses([
|
||||
[makerToken, order.makerToken],
|
||||
[takerToken, order.takerToken],
|
||||
[options.takerAddress, order.taker],
|
||||
[options.txOrigin, order.txOrigin],
|
||||
])
|
||||
) {
|
||||
this._warningLogger(
|
||||
order,
|
||||
'Unexpected token, tx origin or taker address in RFQ-T order, filtering out',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (this._isExpirationTooSoon(new BigNumber(order.expiry))) {
|
||||
this._warningLogger(order, 'Expiry too soon in RFQ-T firm quote, filtering out');
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Save the maker URI for later and return just the order
|
||||
const rfqQuotes = validQuotes.map(result => {
|
||||
const { signature, ...rest } = result.response;
|
||||
const order: SignedNativeOrder = {
|
||||
order: {
|
||||
...rest,
|
||||
makerAmount: new BigNumber(result.response.makerAmount),
|
||||
takerAmount: new BigNumber(result.response.takerAmount),
|
||||
expiry: new BigNumber(result.response.expiry),
|
||||
salt: new BigNumber(result.response.salt),
|
||||
},
|
||||
type: FillQuoteTransformerOrderType.Rfq,
|
||||
signature,
|
||||
};
|
||||
this._orderSignatureToMakerUri[nativeDataToId(result.response)] = result.makerUri;
|
||||
return order;
|
||||
});
|
||||
return rfqQuotes;
|
||||
}
|
||||
|
||||
private async _fetchAndValidateIndicativeQuotesAsync(
|
||||
makerToken: string,
|
||||
takerToken: string,
|
||||
assetFillAmount: BigNumber,
|
||||
marketOperation: MarketOperation,
|
||||
comparisonPrice: BigNumber | undefined,
|
||||
options: RfqRequestOpts,
|
||||
assetOfferings: RfqMakerAssetOfferings,
|
||||
): Promise<V4RFQIndicativeQuote[]> {
|
||||
// fetch quotes
|
||||
const rawQuotes = await this._getQuotesAsync<V4RFQIndicativeQuote>(
|
||||
makerToken,
|
||||
takerToken,
|
||||
assetFillAmount,
|
||||
marketOperation,
|
||||
comparisonPrice,
|
||||
options,
|
||||
'indicative',
|
||||
assetOfferings,
|
||||
);
|
||||
|
||||
// validate
|
||||
const validationFunction = (o: V4RFQIndicativeQuote) => this._isValidRfqtIndicativeQuoteResponse(o);
|
||||
const validQuotes = rawQuotes.filter(result => {
|
||||
const order = result.response;
|
||||
if (!validationFunction(order)) {
|
||||
this._warningLogger(result, 'Invalid RFQ indicative quote received, filtering out');
|
||||
return false;
|
||||
}
|
||||
if (!hasExpectedAddresses([[makerToken, order.makerToken], [takerToken, order.takerToken]])) {
|
||||
this._warningLogger(order, 'Unexpected token or taker address in RFQ order, filtering out');
|
||||
return false;
|
||||
}
|
||||
if (this._isExpirationTooSoon(new BigNumber(order.expiry))) {
|
||||
this._warningLogger(order, 'Expiry too soon in RFQ indicative quote, filtering out');
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
const quotes = validQuotes.map(r => r.response);
|
||||
quotes.forEach(q => {
|
||||
q.makerAmount = new BigNumber(q.makerAmount);
|
||||
q.takerAmount = new BigNumber(q.takerAmount);
|
||||
q.expiry = new BigNumber(q.expiry);
|
||||
});
|
||||
return quotes;
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ import { NULL_ADDRESS } from '../src/utils/market_operation_utils/constants';
|
||||
import { QuoteRequestor } from '../src/utils/quote_requestor';
|
||||
|
||||
import { chaiSetup } from './utils/chai_setup';
|
||||
import { RfqtQuoteEndpoint, testHelpers } from './utils/test_helpers';
|
||||
import { RfqQuoteEndpoint, testHelpers } from './utils/test_helpers';
|
||||
|
||||
const quoteRequestorHttpClient = Axios.create({
|
||||
httpAgent: new HttpAgent({ keepAlive: true, timeout: KEEP_ALIVE_TTL }),
|
||||
@ -64,6 +64,196 @@ describe('QuoteRequestor', async () => {
|
||||
],
|
||||
};
|
||||
|
||||
describe('requestRfqmFirmQuotesAsync for firm quotes', async () => {
|
||||
it('should return successful RFQM requests', async () => {
|
||||
const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
|
||||
const txOrigin = takerAddress;
|
||||
const apiKey = 'my-ko0l-api-key';
|
||||
|
||||
// Set up RFQM responses
|
||||
// tslint:disable-next-line:array-type
|
||||
const mockedRequests: MockedRfqQuoteResponse[] = [];
|
||||
const altMockedRequests: AltMockedRfqQuoteResponse[] = [];
|
||||
|
||||
const expectedParams: TakerRequestQueryParams = {
|
||||
sellTokenAddress: takerToken,
|
||||
buyTokenAddress: makerToken,
|
||||
sellAmountBaseUnits: '10000',
|
||||
comparisonPrice: undefined,
|
||||
takerAddress,
|
||||
txOrigin,
|
||||
isLastLook: 'true', // the major difference between RFQ-T and RFQ-M
|
||||
protocolVersion: '4',
|
||||
};
|
||||
const mockedDefaults = {
|
||||
requestApiKey: apiKey,
|
||||
requestParams: expectedParams,
|
||||
responseCode: StatusCodes.Success,
|
||||
};
|
||||
const validSignedOrder = {
|
||||
makerToken,
|
||||
takerToken,
|
||||
makerAmount: new BigNumber('1000'),
|
||||
takerAmount: new BigNumber('1000'),
|
||||
maker: takerAddress,
|
||||
taker: takerAddress,
|
||||
pool: '0x',
|
||||
salt: '0',
|
||||
chainId: 1,
|
||||
verifyingContract: takerAddress,
|
||||
txOrigin,
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
// [GOOD] Successful response
|
||||
mockedRequests.push({
|
||||
...mockedDefaults,
|
||||
endpoint: 'https://1337.0.0.1',
|
||||
responseData: {
|
||||
signedOrder: validSignedOrder,
|
||||
},
|
||||
});
|
||||
// [GOOD] Another Successful response
|
||||
mockedRequests.push({
|
||||
...mockedDefaults,
|
||||
endpoint: 'https://37.0.0.1',
|
||||
responseData: { signedOrder: validSignedOrder },
|
||||
});
|
||||
// [BAD] Test out a bad response code, ensure it doesnt cause throw
|
||||
mockedRequests.push({
|
||||
...mockedDefaults,
|
||||
endpoint: 'https://420.0.0.1',
|
||||
responseData: { error: 'bad request' },
|
||||
responseCode: StatusCodes.InternalError,
|
||||
});
|
||||
// [BAD] Test out a successful response code but a partial order
|
||||
mockedRequests.push({
|
||||
...mockedDefaults,
|
||||
endpoint: 'https://421.0.0.1',
|
||||
responseData: { signedOrder: { makerToken: '123' } },
|
||||
});
|
||||
// [BAD] A successful response code and invalid response data (encoding)
|
||||
mockedRequests.push({
|
||||
...mockedDefaults,
|
||||
endpoint: 'https://421.1.0.1',
|
||||
responseData: 'this is not JSON!',
|
||||
});
|
||||
// [BAD] A successful response code and valid order, but for wrong maker asset data
|
||||
mockedRequests.push({
|
||||
...mockedDefaults,
|
||||
endpoint: 'https://422.0.0.1',
|
||||
responseData: { signedOrder: { ...validSignedOrder, makerToken: '0x1234' } },
|
||||
});
|
||||
// [BAD] A successful response code and valid order, but for wrong taker asset data
|
||||
mockedRequests.push({
|
||||
...mockedDefaults,
|
||||
endpoint: 'https://423.0.0.1',
|
||||
responseData: { signedOrder: { ...validSignedOrder, takerToken: '0x1234' } },
|
||||
});
|
||||
// [BAD] A successful response code and good order but its unsigned
|
||||
mockedRequests.push({
|
||||
...mockedDefaults,
|
||||
endpoint: 'https://424.0.0.1',
|
||||
responseData: { signedOrder: _.omit(validSignedOrder, ['signature']) },
|
||||
});
|
||||
// [BAD] A successful response code and good order but for the wrong txOrigin
|
||||
mockedRequests.push({
|
||||
...mockedDefaults,
|
||||
endpoint: 'https://425.0.0.1',
|
||||
responseData: { signedOrder: { ...validSignedOrder, txOrigin: NULL_ADDRESS } },
|
||||
});
|
||||
// [GOOD] 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: CREATED_STATUS_CODE,
|
||||
requestData: altFirmRequestData,
|
||||
responseData: altFirmResponse,
|
||||
});
|
||||
|
||||
const normalizedSuccessfulOrder = {
|
||||
order: {
|
||||
..._.omit(validSignedOrder, ['signature']),
|
||||
makerAmount: new BigNumber(validSignedOrder.makerAmount),
|
||||
takerAmount: new BigNumber(validSignedOrder.takerAmount),
|
||||
expiry: new BigNumber(validSignedOrder.expiry),
|
||||
salt: new BigNumber(validSignedOrder.salt),
|
||||
},
|
||||
signature: validSignedOrder.signature,
|
||||
type: FillQuoteTransformerOrderType.Rfq,
|
||||
};
|
||||
|
||||
return testHelpers.withMockedRfqQuotes(
|
||||
mockedRequests,
|
||||
altMockedRequests,
|
||||
RfqQuoteEndpoint.Firm,
|
||||
async () => {
|
||||
const qr = new QuoteRequestor(
|
||||
{}, // No RFQ-T asset offerings
|
||||
{
|
||||
'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 provider when they don't support the requested asset pair. */,
|
||||
'https://37.0.0.1': [[makerToken, takerToken]],
|
||||
},
|
||||
quoteRequestorHttpClient,
|
||||
ALT_RFQ_CREDS,
|
||||
);
|
||||
const resp = await qr.requestRfqmFirmQuotesAsync(
|
||||
makerToken,
|
||||
takerToken,
|
||||
new BigNumber(10000),
|
||||
MarketOperation.Sell,
|
||||
undefined,
|
||||
{
|
||||
apiKey,
|
||||
takerAddress,
|
||||
txOrigin: takerAddress,
|
||||
intentOnFilling: true,
|
||||
altRfqAssetOfferings,
|
||||
},
|
||||
);
|
||||
expect(resp).to.deep.eq([
|
||||
normalizedSuccessfulOrder,
|
||||
normalizedSuccessfulOrder,
|
||||
normalizedSuccessfulOrder,
|
||||
]);
|
||||
},
|
||||
quoteRequestorHttpClient,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('requestRfqtFirmQuotesAsync for firm quotes', async () => {
|
||||
it('should return successful RFQT requests', async () => {
|
||||
const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
|
||||
@ -82,6 +272,7 @@ describe('QuoteRequestor', async () => {
|
||||
comparisonPrice: undefined,
|
||||
takerAddress,
|
||||
txOrigin,
|
||||
isLastLook: 'false',
|
||||
protocolVersion: '4',
|
||||
};
|
||||
const mockedDefaults = {
|
||||
@ -207,10 +398,10 @@ describe('QuoteRequestor', async () => {
|
||||
type: FillQuoteTransformerOrderType.Rfq,
|
||||
};
|
||||
|
||||
return testHelpers.withMockedRfqtQuotes(
|
||||
return testHelpers.withMockedRfqQuotes(
|
||||
mockedRequests,
|
||||
altMockedRequests,
|
||||
RfqtQuoteEndpoint.Firm,
|
||||
RfqQuoteEndpoint.Firm,
|
||||
async () => {
|
||||
const qr = new QuoteRequestor(
|
||||
{
|
||||
@ -225,6 +416,7 @@ describe('QuoteRequestor', async () => {
|
||||
'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]],
|
||||
},
|
||||
{},
|
||||
quoteRequestorHttpClient,
|
||||
ALT_RFQ_CREDS,
|
||||
);
|
||||
@ -252,6 +444,114 @@ describe('QuoteRequestor', async () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('requestRfqmIndicativeQuotesAsync for Indicative quotes', async () => {
|
||||
it('should return successful RFQM requests', async () => {
|
||||
const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
|
||||
const apiKey = 'my-ko0l-api-key';
|
||||
|
||||
// Set up RFQ responses
|
||||
// tslint:disable-next-line:array-type
|
||||
const mockedRequests: MockedRfqQuoteResponse[] = [];
|
||||
const expectedParams: TakerRequestQueryParams = {
|
||||
sellTokenAddress: takerToken,
|
||||
buyTokenAddress: makerToken,
|
||||
sellAmountBaseUnits: '10000',
|
||||
comparisonPrice: undefined,
|
||||
takerAddress,
|
||||
txOrigin: takerAddress,
|
||||
isLastLook: 'true', // the major difference between RFQ-T and RFQ-M
|
||||
protocolVersion: '4',
|
||||
};
|
||||
const mockedDefaults = {
|
||||
requestApiKey: apiKey,
|
||||
requestParams: expectedParams,
|
||||
responseCode: StatusCodes.Success,
|
||||
};
|
||||
|
||||
// [GOOD] Successful response
|
||||
const successfulQuote1 = {
|
||||
makerToken,
|
||||
takerToken,
|
||||
makerAmount: new BigNumber(expectedParams.sellAmountBaseUnits),
|
||||
takerAmount: new BigNumber(expectedParams.sellAmountBaseUnits),
|
||||
expiry: makeThreeMinuteExpiry(),
|
||||
};
|
||||
|
||||
mockedRequests.push({
|
||||
...mockedDefaults,
|
||||
endpoint: 'https://1337.0.0.1',
|
||||
responseData: successfulQuote1,
|
||||
});
|
||||
// [GOOD] Another Successful response
|
||||
mockedRequests.push({
|
||||
...mockedDefaults,
|
||||
endpoint: 'https://37.0.0.1',
|
||||
responseData: successfulQuote1,
|
||||
});
|
||||
|
||||
// [BAD] Test out a bad response code, ensure it doesnt cause throw
|
||||
mockedRequests.push({
|
||||
...mockedDefaults,
|
||||
endpoint: 'https://420.0.0.1',
|
||||
responseData: { error: 'bad request' },
|
||||
responseCode: StatusCodes.InternalError,
|
||||
});
|
||||
// [BAD] Test out a successful response code but an invalid order
|
||||
mockedRequests.push({
|
||||
...mockedDefaults,
|
||||
endpoint: 'https://421.0.0.1',
|
||||
responseData: { makerToken: '123' },
|
||||
});
|
||||
// [BAD] A successful response code and valid response data, but for wrong maker asset data
|
||||
mockedRequests.push({
|
||||
...mockedDefaults,
|
||||
endpoint: 'https://422.0.0.1',
|
||||
responseData: { ...successfulQuote1, makerToken: otherToken1 },
|
||||
});
|
||||
// [BAD] A successful response code and valid response data, but for wrong taker asset data
|
||||
mockedRequests.push({
|
||||
...mockedDefaults,
|
||||
endpoint: 'https://423.0.0.1',
|
||||
responseData: { ...successfulQuote1, takerToken: otherToken1 },
|
||||
});
|
||||
|
||||
return testHelpers.withMockedRfqQuotes(
|
||||
mockedRequests,
|
||||
[],
|
||||
RfqQuoteEndpoint.Indicative,
|
||||
async () => {
|
||||
const qr = new QuoteRequestor(
|
||||
{}, // No RFQ-T asset offerings
|
||||
{
|
||||
'https://1337.0.0.1': [[makerToken, takerToken]],
|
||||
'https://37.0.0.1': [[makerToken, takerToken]],
|
||||
'https://420.0.0.1': [[makerToken, takerToken]],
|
||||
'https://421.0.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]],
|
||||
},
|
||||
quoteRequestorHttpClient,
|
||||
);
|
||||
const resp = await qr.requestRfqmIndicativeQuotesAsync(
|
||||
makerToken,
|
||||
takerToken,
|
||||
new BigNumber(10000),
|
||||
MarketOperation.Sell,
|
||||
undefined,
|
||||
{
|
||||
apiKey,
|
||||
takerAddress,
|
||||
txOrigin: takerAddress,
|
||||
intentOnFilling: true,
|
||||
},
|
||||
);
|
||||
expect(resp.sort()).to.eql([successfulQuote1, successfulQuote1].sort());
|
||||
},
|
||||
quoteRequestorHttpClient,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('requestRfqtIndicativeQuotesAsync for Indicative quotes', async () => {
|
||||
it('should optionally accept a "comparisonPrice" parameter', async () => {
|
||||
const response = QuoteRequestor.makeQueryParameters(
|
||||
@ -279,6 +579,7 @@ describe('QuoteRequestor', async () => {
|
||||
comparisonPrice: undefined,
|
||||
takerAddress,
|
||||
txOrigin: takerAddress,
|
||||
isLastLook: 'false',
|
||||
protocolVersion: '4',
|
||||
};
|
||||
const mockedDefaults = {
|
||||
@ -333,10 +634,10 @@ describe('QuoteRequestor', async () => {
|
||||
responseData: successfulQuote1,
|
||||
});
|
||||
|
||||
return testHelpers.withMockedRfqtQuotes(
|
||||
return testHelpers.withMockedRfqQuotes(
|
||||
mockedRequests,
|
||||
[],
|
||||
RfqtQuoteEndpoint.Indicative,
|
||||
RfqQuoteEndpoint.Indicative,
|
||||
async () => {
|
||||
const qr = new QuoteRequestor(
|
||||
{
|
||||
@ -348,6 +649,7 @@ describe('QuoteRequestor', async () => {
|
||||
'https://424.0.0.1': [[makerToken, takerToken]],
|
||||
'https://37.0.0.1': [[makerToken, takerToken]],
|
||||
},
|
||||
{},
|
||||
quoteRequestorHttpClient,
|
||||
);
|
||||
const resp = await qr.requestRfqtIndicativeQuotesAsync(
|
||||
@ -386,6 +688,7 @@ describe('QuoteRequestor', async () => {
|
||||
takerAddress,
|
||||
txOrigin: takerAddress,
|
||||
protocolVersion: '4',
|
||||
isLastLook: 'false',
|
||||
};
|
||||
const mockedDefaults = {
|
||||
requestApiKey: apiKey,
|
||||
@ -424,16 +727,17 @@ describe('QuoteRequestor', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
return testHelpers.withMockedRfqtQuotes(
|
||||
return testHelpers.withMockedRfqQuotes(
|
||||
mockedRequests,
|
||||
[],
|
||||
RfqtQuoteEndpoint.Indicative,
|
||||
RfqQuoteEndpoint.Indicative,
|
||||
async () => {
|
||||
const qr = new QuoteRequestor(
|
||||
{
|
||||
'https://1337.0.0.1': [[makerToken, takerToken]],
|
||||
'https://420.0.0.1': [[makerToken, takerToken]],
|
||||
},
|
||||
{},
|
||||
quoteRequestorHttpClient,
|
||||
);
|
||||
const resp = await qr.requestRfqtIndicativeQuotesAsync(
|
||||
@ -469,6 +773,7 @@ describe('QuoteRequestor', async () => {
|
||||
comparisonPrice: undefined,
|
||||
takerAddress,
|
||||
txOrigin: takerAddress,
|
||||
isLastLook: 'false',
|
||||
protocolVersion: '4',
|
||||
};
|
||||
// Successful response
|
||||
@ -487,13 +792,14 @@ describe('QuoteRequestor', async () => {
|
||||
responseCode: StatusCodes.Success,
|
||||
});
|
||||
|
||||
return testHelpers.withMockedRfqtQuotes(
|
||||
return testHelpers.withMockedRfqQuotes(
|
||||
mockedRequests,
|
||||
[],
|
||||
RfqtQuoteEndpoint.Indicative,
|
||||
RfqQuoteEndpoint.Indicative,
|
||||
async () => {
|
||||
const qr = new QuoteRequestor(
|
||||
{ 'https://1337.0.0.1': [[makerToken, takerToken]] },
|
||||
{},
|
||||
quoteRequestorHttpClient,
|
||||
);
|
||||
const resp = await qr.requestRfqtIndicativeQuotesAsync(
|
||||
@ -722,12 +1028,12 @@ describe('QuoteRequestor', async () => {
|
||||
for (const altScenario of altScenarios) {
|
||||
logUtils.log(`Alt MM indicative scenario ${scenarioCounter}`);
|
||||
scenarioCounter += 1;
|
||||
await testHelpers.withMockedRfqtQuotes(
|
||||
await testHelpers.withMockedRfqQuotes(
|
||||
[],
|
||||
altMockedRequests,
|
||||
RfqtQuoteEndpoint.Indicative,
|
||||
RfqQuoteEndpoint.Indicative,
|
||||
async () => {
|
||||
const qr = new QuoteRequestor({}, quoteRequestorHttpClient, ALT_RFQ_CREDS);
|
||||
const qr = new QuoteRequestor({}, {}, quoteRequestorHttpClient, ALT_RFQ_CREDS);
|
||||
const resp = await qr.requestRfqtIndicativeQuotesAsync(
|
||||
altScenario.requestedMakerToken,
|
||||
altScenario.requestedTakerToken,
|
||||
|
@ -6,7 +6,7 @@ import * as _ from 'lodash';
|
||||
import { InsufficientAssetLiquidityError } from '../../src/errors';
|
||||
import { AltMockedRfqQuoteResponse, MockedRfqQuoteResponse } from '../../src/types';
|
||||
|
||||
export enum RfqtQuoteEndpoint {
|
||||
export enum RfqQuoteEndpoint {
|
||||
Indicative = 'price',
|
||||
Firm = 'quote',
|
||||
}
|
||||
@ -34,18 +34,18 @@ export const testHelpers = {
|
||||
},
|
||||
/**
|
||||
* A helper utility for testing which mocks out
|
||||
* requests to RFQ-t providers
|
||||
* requests to RFQ-T/M providers
|
||||
*/
|
||||
withMockedRfqtQuotes: async (
|
||||
withMockedRfqQuotes: async (
|
||||
standardMockedResponses: MockedRfqQuoteResponse[],
|
||||
altMockedResponses: AltMockedRfqQuoteResponse[],
|
||||
quoteType: RfqtQuoteEndpoint,
|
||||
quoteType: RfqQuoteEndpoint,
|
||||
afterResponseCallback: () => Promise<void>,
|
||||
axiosClient: AxiosInstance = axios,
|
||||
): Promise<void> => {
|
||||
const mockedAxios = new AxiosMockAdapter(axiosClient, { onNoMatch: 'throwException' });
|
||||
try {
|
||||
// Mock out Standard RFQT responses
|
||||
// Mock out Standard RFQ-T/M responses
|
||||
for (const mockedResponse of standardMockedResponses) {
|
||||
const { endpoint, requestApiKey, requestParams, responseData, responseCode } = mockedResponse;
|
||||
const requestHeaders = { Accept: 'application/json, text/plain, */*', '0x-api-key': requestApiKey };
|
||||
@ -59,7 +59,7 @@ export const testHelpers = {
|
||||
.replyOnce(responseCode, responseData);
|
||||
}
|
||||
}
|
||||
// Mock out Alt RFQT responses
|
||||
// Mock out Alt RFQ-T/M responses
|
||||
for (const mockedResponse of altMockedResponses) {
|
||||
const { endpoint, mmApiKey, requestData, responseData, responseCode } = mockedResponse;
|
||||
const requestHeaders = {
|
||||
|
@ -830,9 +830,10 @@
|
||||
typedoc "~0.16.11"
|
||||
yargs "^10.0.3"
|
||||
|
||||
"@0x/quote-server@^4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@0x/quote-server/-/quote-server-4.0.1.tgz#05947589bfa7905d274ac3c726cb9918b93b0f9e"
|
||||
"@0x/quote-server@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@0x/quote-server/-/quote-server-5.0.0.tgz#15554099bdfdf71e2910430860257d622f24f703"
|
||||
integrity sha512-U14C60RVnILL8n5DwuInG98MnhXbBbiEi8M2ymFGnHO+AjucGfm28BM6/GD59ftiqZmFSkOvBRU94QJ3mSsCQw==
|
||||
dependencies:
|
||||
"@0x/json-schemas" "^5.0.7"
|
||||
"@0x/order-utils" "^10.2.4"
|
||||
|
Loading…
x
Reference in New Issue
Block a user