protocol/packages/asset-swapper/test/quote_requestor_test.ts
Jorge Pérez b7adc5a889
feat: Extended Quote Report
* Extended Quote report for indicative quote

* feat: Only save 'full' quotes on quote report

* Unify extended quote report
2021-11-09 13:05:01 -06:00

1149 lines
48 KiB
TypeScript

import { tokenUtils } from '@0x/dev-utils';
import { ETH_TOKEN_ADDRESS, FillQuoteTransformerOrderType, SignatureType } from '@0x/protocol-utils';
import { TakerRequestQueryParamsUnnested, V4RFQIndicativeQuote } from '@0x/quote-server';
import { StatusCodes } from '@0x/types';
import { BigNumber, logUtils } from '@0x/utils';
import Axios from 'axios';
import * as chai from 'chai';
import { Agent as HttpAgent } from 'http';
import { Agent as HttpsAgent } from 'https';
import _ = require('lodash');
import 'mocha';
import { constants, KEEP_ALIVE_TTL } from '../src/constants';
import {
AltMockedRfqQuoteResponse,
AltQuoteModel,
AltQuoteRequestData,
AltQuoteSide,
AltRfqMakerAssetOfferings,
MarketOperation,
MockedRfqQuoteResponse,
} from '../src/types';
import { NULL_ADDRESS } from '../src/utils/market_operation_utils/constants';
import { QuoteRequestor } from '../src/utils/quote_requestor';
import { chaiSetup } from './utils/chai_setup';
import { RfqQuoteEndpoint, testHelpers } from './utils/test_helpers';
const quoteRequestorHttpClient = Axios.create({
httpAgent: new HttpAgent({ keepAlive: true, timeout: KEEP_ALIVE_TTL }),
httpsAgent: new HttpsAgent({ keepAlive: true, timeout: KEEP_ALIVE_TTL }),
});
chaiSetup.configure();
const expect = chai.expect;
const ALT_MM_API_KEY = 'averysecurekey';
const ALT_PROFILE = 'acoolprofile';
const ALT_RFQ_CREDS = {
altRfqApiKey: ALT_MM_API_KEY,
altRfqProfile: ALT_PROFILE,
};
const CREATED_STATUS_CODE = 201;
function makeThreeMinuteExpiry(): BigNumber {
const expiry = new Date(Date.now());
expiry.setMinutes(expiry.getMinutes() + 3);
return new BigNumber(Math.round(expiry.valueOf() / constants.ONE_SECOND_MS));
}
describe('QuoteRequestor', async () => {
const [makerToken, takerToken, otherToken1] = tokenUtils.getDummyERC20TokenAddresses();
const validSignature = { v: 28, r: '0x', s: '0x', signatureType: SignatureType.EthSign };
const altRfqAssetOfferings: AltRfqMakerAssetOfferings = {
'https://132.0.0.1': [
{
id: 'XYZ-123',
baseAsset: makerToken,
quoteAsset: takerToken,
baseAssetDecimals: 2,
quoteAssetDecimals: 3,
},
],
};
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: TakerRequestQueryParamsUnnested = {
sellTokenAddress: takerToken,
buyTokenAddress: makerToken,
sellAmountBaseUnits: '10000',
comparisonPrice: undefined,
takerAddress,
txOrigin,
isLastLook: 'true', // the major difference between RFQ-T and RFQ-M
protocolVersion: '4',
feeAmount: '1000000000',
feeToken: ETH_TOKEN_ADDRESS,
feeType: 'fixed',
};
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,
{
integrator: {
integratorId: apiKey,
label: 'foo',
},
takerAddress,
txOrigin: takerAddress,
intentOnFilling: true,
altRfqAssetOfferings,
isLastLook: true,
fee: {
amount: new BigNumber('1000000000'),
token: ETH_TOKEN_ADDRESS,
type: 'fixed',
},
},
);
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';
const txOrigin = takerAddress;
const apiKey = 'my-ko0l-api-key';
// Set up RFQT responses
// tslint:disable-next-line:array-type
const mockedRequests: MockedRfqQuoteResponse[] = [];
const altMockedRequests: AltMockedRfqQuoteResponse[] = [];
const expectedParams: TakerRequestQueryParamsUnnested = {
sellTokenAddress: takerToken,
buyTokenAddress: makerToken,
sellAmountBaseUnits: '10000',
comparisonPrice: undefined,
takerAddress,
txOrigin,
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,
},
};
// Successful response
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://1337.0.0.1',
responseData: {
signedOrder: validSignedOrder,
},
});
// Another Successful response
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://37.0.0.1',
responseData: { signedOrder: validSignedOrder },
});
// 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,
});
// Test out a successful response code but a partial order
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://421.0.0.1',
responseData: { signedOrder: { makerToken: '123' } },
});
// A successful response code and invalid response data (encoding)
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://421.1.0.1',
responseData: 'this is not JSON!',
});
// 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' } },
});
// 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' } },
});
// A successful response code and good order but its unsigned
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://424.0.0.1',
responseData: { signedOrder: _.omit(validSignedOrder, ['signature']) },
});
// 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 } },
});
// 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(
{
'https://1337.0.0.1': [[makerToken, takerToken]],
'https://420.0.0.1': [[makerToken, takerToken]],
'https://421.0.0.1': [[makerToken, takerToken]],
'https://421.1.0.1': [[makerToken, takerToken]],
'https://422.0.0.1': [[makerToken, takerToken]],
'https://423.0.0.1': [[makerToken, takerToken]],
'https://424.0.0.1': [[makerToken, takerToken]],
'https://425.0.0.1': [[makerToken, takerToken]],
'https://426.0.0.1': [] /* Shouldn't ping an RFQ-T provider when they don't support the requested asset pair. */,
'https://37.0.0.1': [[makerToken, takerToken]],
},
{},
quoteRequestorHttpClient,
ALT_RFQ_CREDS,
);
const resp = await qr.requestRfqtFirmQuotesAsync(
makerToken,
takerToken,
new BigNumber(10000),
MarketOperation.Sell,
undefined,
{
integrator: {
integratorId: apiKey,
label: 'foo',
},
takerAddress,
txOrigin: takerAddress,
intentOnFilling: true,
altRfqAssetOfferings,
},
);
expect(resp).to.deep.eq([
normalizedSuccessfulOrder,
normalizedSuccessfulOrder,
normalizedSuccessfulOrder,
]);
},
quoteRequestorHttpClient,
);
});
});
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: TakerRequestQueryParamsUnnested = {
sellTokenAddress: takerToken,
buyTokenAddress: makerToken,
sellAmountBaseUnits: '10000',
comparisonPrice: undefined,
takerAddress,
txOrigin: takerAddress,
isLastLook: 'true', // the major difference between RFQ-T and RFQ-M
protocolVersion: '4',
feeAmount: '1000000000',
feeToken: ETH_TOKEN_ADDRESS,
feeType: 'fixed',
};
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(),
};
const goodMMUri1 = 'https://1337.0.0.1';
const goodMMUri2 = 'https://37.0.0.1';
mockedRequests.push({
...mockedDefaults,
endpoint: goodMMUri1,
responseData: successfulQuote1,
});
// [GOOD] Another Successful response
mockedRequests.push({
...mockedDefaults,
endpoint: goodMMUri2,
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 },
});
const assetOfferings: { [k: string]: [[string, string]] } = {
'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]],
};
assetOfferings[goodMMUri1] = [[makerToken, takerToken]];
assetOfferings[goodMMUri2] = [[makerToken, takerToken]];
return testHelpers.withMockedRfqQuotes(
mockedRequests,
[],
RfqQuoteEndpoint.Indicative,
async () => {
const qr = new QuoteRequestor(
{}, // No RFQ-T asset offerings
assetOfferings,
quoteRequestorHttpClient,
);
const resp = await qr.requestRfqmIndicativeQuotesAsync(
makerToken,
takerToken,
new BigNumber(10000),
MarketOperation.Sell,
undefined,
{
integrator: {
integratorId: apiKey,
label: 'foo',
},
takerAddress,
txOrigin: takerAddress,
intentOnFilling: true,
isLastLook: true,
fee: {
type: 'fixed',
token: ETH_TOKEN_ADDRESS,
amount: new BigNumber('1000000000'),
},
},
);
expect(resp.sort()).to.eql(
[
{ ...successfulQuote1, makerUri: goodMMUri1 },
{ ...successfulQuote1, makerUri: goodMMUri2 },
].sort(),
);
},
quoteRequestorHttpClient,
);
});
});
describe('requestRfqtIndicativeQuotesAsync for Indicative quotes', async () => {
it('should optionally accept a "comparisonPrice" parameter', async () => {
const response = QuoteRequestor.makeQueryParameters(
otherToken1, // tx origin
otherToken1, // taker
MarketOperation.Sell,
makerToken,
takerToken,
new BigNumber(1000),
new BigNumber(300.2),
);
expect(response.comparisonPrice).to.eql('300.2');
});
it('should return successful RFQT requests', async () => {
const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
const apiKey = 'my-ko0l-api-key';
// Set up RFQT responses
// tslint:disable-next-line:array-type
const mockedRequests: MockedRfqQuoteResponse[] = [];
const expectedParams: TakerRequestQueryParamsUnnested = {
sellTokenAddress: takerToken,
buyTokenAddress: makerToken,
sellAmountBaseUnits: '10000',
comparisonPrice: undefined,
takerAddress,
txOrigin: takerAddress,
protocolVersion: '4',
};
const mockedDefaults = {
requestApiKey: apiKey,
requestParams: expectedParams,
responseCode: StatusCodes.Success,
};
// Successful response
const successfulQuote1 = {
makerToken,
takerToken,
makerAmount: new BigNumber(expectedParams.sellAmountBaseUnits),
takerAmount: new BigNumber(expectedParams.sellAmountBaseUnits),
expiry: makeThreeMinuteExpiry(),
};
const goodMMUri1 = 'https://1337.0.0.1';
const goodMMUri2 = 'https://37.0.0.1';
mockedRequests.push({
...mockedDefaults,
endpoint: goodMMUri1,
responseData: successfulQuote1,
});
// 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,
});
// Test out a successful response code but an invalid order
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://421.0.0.1',
responseData: { makerToken: '123' },
});
// 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 },
});
// 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 },
});
// Another Successful response
mockedRequests.push({
...mockedDefaults,
endpoint: goodMMUri2,
responseData: successfulQuote1,
});
const assetOfferings: { [k: string]: [[string, string]] } = {
'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]],
};
assetOfferings[goodMMUri1] = [[makerToken, takerToken]];
assetOfferings[goodMMUri2] = [[makerToken, takerToken]];
return testHelpers.withMockedRfqQuotes(
mockedRequests,
[],
RfqQuoteEndpoint.Indicative,
async () => {
const qr = new QuoteRequestor(assetOfferings, {}, quoteRequestorHttpClient);
const resp = await qr.requestRfqtIndicativeQuotesAsync(
makerToken,
takerToken,
new BigNumber(10000),
MarketOperation.Sell,
undefined,
{
integrator: {
integratorId: apiKey,
label: 'foo',
},
takerAddress,
txOrigin: takerAddress,
intentOnFilling: true,
},
);
expect(resp.sort()).to.eql(
[
{ ...successfulQuote1, makerUri: goodMMUri1 },
{ ...successfulQuote1, makerUri: goodMMUri2 },
].sort(),
);
},
quoteRequestorHttpClient,
);
});
it('should only return RFQT requests that meet the timeout', async () => {
const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
const apiKey = 'my-ko0l-api-key';
const maxTimeoutMs = 10;
// tslint:disable-next-line:custom-no-magic-numbers
const exceedTimeoutMs = maxTimeoutMs + 50;
// Set up RFQT responses
// tslint:disable-next-line:array-type
const mockedRequests: MockedRfqQuoteResponse[] = [];
const expectedParams: TakerRequestQueryParamsUnnested = {
sellTokenAddress: takerToken,
buyTokenAddress: makerToken,
sellAmountBaseUnits: '10000',
comparisonPrice: undefined,
takerAddress,
txOrigin: takerAddress,
protocolVersion: '4',
};
const mockedDefaults = {
requestApiKey: apiKey,
requestParams: expectedParams,
responseCode: StatusCodes.Success,
};
// Successful response
const successfulQuote1 = {
makerToken,
takerToken,
makerAmount: new BigNumber(expectedParams.sellAmountBaseUnits),
takerAmount: new BigNumber(expectedParams.sellAmountBaseUnits),
expiry: makeThreeMinuteExpiry(),
};
// One good request
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://1337.0.0.1',
responseData: successfulQuote1,
});
// One request that will timeout
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://420.0.0.1',
responseData: successfulQuote1,
callback: async () => {
// tslint:disable-next-line:no-inferred-empty-object-type
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([StatusCodes.Success, successfulQuote1]);
}, exceedTimeoutMs);
});
},
});
return testHelpers.withMockedRfqQuotes(
mockedRequests,
[],
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(
makerToken,
takerToken,
new BigNumber(10000),
MarketOperation.Sell,
undefined,
{
integrator: {
integratorId: apiKey,
label: 'foo',
},
takerAddress,
txOrigin: takerAddress,
intentOnFilling: true,
makerEndpointMaxResponseTimeMs: maxTimeoutMs,
},
);
expect(resp.sort()).to.eql([{ ...successfulQuote1, makerUri: 'https://1337.0.0.1' }].sort()); // notice only one result, despite two requests made
},
quoteRequestorHttpClient,
);
});
it('should return successful RFQT indicative quote requests (Buy)', async () => {
const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
const apiKey = 'my-ko0l-api-key';
// Set up RFQT responses
// tslint:disable-next-line:array-type
const mockedRequests: MockedRfqQuoteResponse[] = [];
const expectedParams: TakerRequestQueryParamsUnnested = {
sellTokenAddress: takerToken,
buyTokenAddress: makerToken,
buyAmountBaseUnits: '10000',
comparisonPrice: undefined,
takerAddress,
txOrigin: takerAddress,
protocolVersion: '4',
};
// Successful response
const successfulQuote1 = {
makerToken,
takerToken,
makerAmount: new BigNumber(expectedParams.buyAmountBaseUnits),
takerAmount: new BigNumber(expectedParams.buyAmountBaseUnits),
expiry: makeThreeMinuteExpiry(),
};
mockedRequests.push({
endpoint: 'https://1337.0.0.1',
requestApiKey: apiKey,
requestParams: expectedParams,
responseData: successfulQuote1,
responseCode: StatusCodes.Success,
});
return testHelpers.withMockedRfqQuotes(
mockedRequests,
[],
RfqQuoteEndpoint.Indicative,
async () => {
const qr = new QuoteRequestor(
{ 'https://1337.0.0.1': [[makerToken, takerToken]] },
{},
quoteRequestorHttpClient,
);
const resp = await qr.requestRfqtIndicativeQuotesAsync(
makerToken,
takerToken,
new BigNumber(10000),
MarketOperation.Buy,
undefined,
{
integrator: {
integratorId: apiKey,
label: 'foo',
},
takerAddress,
txOrigin: takerAddress,
intentOnFilling: true,
},
);
expect(resp.sort()).to.eql([{ ...successfulQuote1, makerUri: 'https://1337.0.0.1' }].sort());
},
quoteRequestorHttpClient,
);
});
it('should be able to handle and filter RFQ offerings', () => {
const tests: Array<[string[] | undefined, string[]]> = [
[['https://top.maker'], []],
[undefined, ['https://foo.bar/', 'https://lorem.ipsum/']],
[['https://lorem.ipsum/'], ['https://lorem.ipsum/']],
];
for (const test of tests) {
const [apiKeyWhitelist, results] = test;
const response = QuoteRequestor.getTypedMakerUrlsAndWhitelist(
{
integrator: {
integratorId: 'foo',
label: 'bar',
whitelistIntegratorUrls: apiKeyWhitelist,
},
altRfqAssetOfferings: {},
},
{
'https://foo.bar/': [
[
'0xA6cD4cb8c62aCDe44739E3Ed0F1d13E0e31f2d94',
'0xF45107c0200a04A8aB9C600cc52A3C89AE5D0489',
],
],
'https://lorem.ipsum/': [
[
'0xA6cD4cb8c62aCDe44739E3Ed0F1d13E0e31f2d94',
'0xF45107c0200a04A8aB9C600cc52A3C89AE5D0489',
],
],
},
);
const typedUrls = response.map(typed => typed.url);
expect(typedUrls).to.eql(results);
}
});
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: AltMockedRfqQuoteResponse[] = [];
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: CREATED_STATUS_CODE,
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: CREATED_STATUS_CODE,
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: CREATED_STATUS_CODE,
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: CREATED_STATUS_CODE,
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.withMockedRfqQuotes(
[],
altMockedRequests,
RfqQuoteEndpoint.Indicative,
async () => {
const qr = new QuoteRequestor({}, {}, quoteRequestorHttpClient, ALT_RFQ_CREDS);
const resp = await qr.requestRfqtIndicativeQuotesAsync(
altScenario.requestedMakerToken,
altScenario.requestedTakerToken,
altScenario.requestedAmount,
altScenario.requestedOperation,
undefined,
{
integrator: {
integratorId: apiKey,
label: 'foo',
},
takerAddress,
txOrigin,
intentOnFilling: true,
altRfqAssetOfferings,
},
);
// 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,
);
}
});
});
});