[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:
phil-ociraptor 2021-03-31 12:11:10 -05:00 committed by GitHub
parent 7bf009fbf6
commit 70ddab0231
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 560 additions and 140 deletions

View File

@ -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",

View File

@ -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,

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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 = {

View File

@ -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"