* Extended Quote report for indicative quote * feat: Only save 'full' quotes on quote report * Unify extended quote report
1149 lines
48 KiB
TypeScript
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,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
});
|