import { tokenUtils } from '@0x/dev-utils'; import { assetDataUtils } from '@0x/order-utils'; import { TakerRequestQueryParams } from '@0x/quote-server'; import { StatusCodes } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as chai from 'chai'; import 'mocha'; import { constants } from '../src/constants'; import { MarketOperation, MockedRfqtFirmQuoteResponse, MockedRfqtIndicativeQuoteResponse } from '../src/types'; import { QuoteRequestor, quoteRequestorHttpClient } from '../src/utils/quote_requestor'; import { rfqtMocker } from '../src/utils/rfqt_mocker'; import { chaiSetup } from './utils/chai_setup'; import { testOrderFactory } from './utils/test_order_factory'; chaiSetup.configure(); const expect = chai.expect; 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 makerAssetData = assetDataUtils.encodeERC20AssetData(makerToken); const takerAssetData = assetDataUtils.encodeERC20AssetData(takerToken); describe('requestRfqtFirmQuotesAsync for firm quotes', async () => { 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: MockedRfqtFirmQuoteResponse[] = []; const expectedParams: TakerRequestQueryParams = { sellTokenAddress: takerToken, buyTokenAddress: makerToken, sellAmountBaseUnits: '10000', comparisonPrice: undefined, takerAddress, }; // Successful response const successfulOrder1 = testOrderFactory.generateTestSignedOrder({ makerAssetData, takerAssetData, takerAddress, feeRecipientAddress: '0x0000000000000000000000000000000000000001', expirationTimeSeconds: makeThreeMinuteExpiry(), }); mockedRequests.push({ endpoint: 'https://1337.0.0.1', requestApiKey: apiKey, requestParams: expectedParams, responseData: { signedOrder: successfulOrder1 }, responseCode: StatusCodes.Success, }); // Test out a bad response code, ensure it doesnt cause throw mockedRequests.push({ endpoint: 'https://420.0.0.1', requestApiKey: apiKey, requestParams: expectedParams, responseData: { error: 'bad request' }, responseCode: StatusCodes.InternalError, }); // Test out a successful response code but an invalid order mockedRequests.push({ endpoint: 'https://421.0.0.1', requestApiKey: apiKey, requestParams: expectedParams, responseData: { makerAssetData: '123' }, responseCode: StatusCodes.Success, }); // ensure that a non-JSON response doesn't throw an error when trying to parse mockedRequests.push({ endpoint: 'https://421.1.0.1', requestApiKey: apiKey, requestParams: expectedParams, responseData: 'this is not JSON!', responseCode: StatusCodes.Success, }); // A successful response code and valid order, but for wrong maker asset data const wrongMakerAssetDataOrder = testOrderFactory.generateTestSignedOrder({ makerAssetData: assetDataUtils.encodeERC20AssetData(otherToken1), expirationTimeSeconds: makeThreeMinuteExpiry(), takerAssetData, }); mockedRequests.push({ endpoint: 'https://422.0.0.1', requestApiKey: apiKey, requestParams: expectedParams, responseData: { signedOrder: wrongMakerAssetDataOrder }, responseCode: StatusCodes.Success, }); // A successful response code and valid order, but for wrong taker asset data const wrongTakerAssetDataOrder = testOrderFactory.generateTestSignedOrder({ makerAssetData, expirationTimeSeconds: makeThreeMinuteExpiry(), takerAssetData: assetDataUtils.encodeERC20AssetData(otherToken1), }); mockedRequests.push({ endpoint: 'https://423.0.0.1', requestApiKey: apiKey, requestParams: expectedParams, responseData: { signedOrder: wrongTakerAssetDataOrder }, responseCode: StatusCodes.Success, }); // A successful response code and good order but its unsigned const unsignedOrder = testOrderFactory.generateTestSignedOrder({ makerAssetData, takerAssetData, expirationTimeSeconds: makeThreeMinuteExpiry(), feeRecipientAddress: '0x0000000000000000000000000000000000000002', }); delete unsignedOrder.signature; mockedRequests.push({ endpoint: 'https://424.0.0.1', requestApiKey: apiKey, requestParams: expectedParams, responseData: { signedOrder: unsignedOrder }, responseCode: StatusCodes.Success, }); // A successful response code and good order but for the wrong takerAddress const orderWithNullTaker = testOrderFactory.generateTestSignedOrder({ makerAssetData, takerAssetData, expirationTimeSeconds: makeThreeMinuteExpiry(), takerAddress: constants.NULL_ADDRESS, feeRecipientAddress: '0x0000000000000000000000000000000000000002', }); mockedRequests.push({ endpoint: 'https://425.0.0.1', requestApiKey: apiKey, requestParams: expectedParams, responseData: { signedOrder: orderWithNullTaker }, responseCode: StatusCodes.Success, }); // Another Successful response const successfulOrder2 = testOrderFactory.generateTestSignedOrder({ makerAssetData, takerAssetData, takerAddress, expirationTimeSeconds: makeThreeMinuteExpiry(), }); mockedRequests.push({ endpoint: 'https://37.0.0.1', requestApiKey: apiKey, requestParams: expectedParams, responseData: { signedOrder: successfulOrder2 }, responseCode: StatusCodes.Success, }); return rfqtMocker.withMockedRfqtFirmQuotes( mockedRequests, 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]], }); const resp = await qr.requestRfqtFirmQuotesAsync( makerAssetData, takerAssetData, new BigNumber(10000), MarketOperation.Sell, undefined, { apiKey, takerAddress, intentOnFilling: true, }, ); expect(resp.sort()).to.eql( [{ signedOrder: successfulOrder1 }, { signedOrder: successfulOrder2 }].sort(), ); }, quoteRequestorHttpClient, ); }); }); describe('requestRfqtIndicativeQuotesAsync for Indicative quotes', async () => { it('should optionally accept a "comparisonPrice" parameter', async () => { const response = QuoteRequestor.makeQueryParameters( otherToken1, MarketOperation.Sell, makerAssetData, takerAssetData, 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: MockedRfqtIndicativeQuoteResponse[] = []; const expectedParams: TakerRequestQueryParams = { sellTokenAddress: takerToken, buyTokenAddress: makerToken, sellAmountBaseUnits: '10000', comparisonPrice: undefined, takerAddress, }; // Successful response const successfulQuote1 = { makerAssetData, takerAssetData, makerAssetAmount: new BigNumber(expectedParams.sellAmountBaseUnits), takerAssetAmount: new BigNumber(expectedParams.sellAmountBaseUnits), expirationTimeSeconds: makeThreeMinuteExpiry(), }; mockedRequests.push({ endpoint: 'https://1337.0.0.1', requestApiKey: apiKey, requestParams: expectedParams, responseData: successfulQuote1, responseCode: StatusCodes.Success, }); // Test out a bad response code, ensure it doesnt cause throw mockedRequests.push({ endpoint: 'https://420.0.0.1', requestApiKey: apiKey, requestParams: expectedParams, responseData: { error: 'bad request' }, responseCode: StatusCodes.InternalError, }); // Test out a successful response code but an invalid order mockedRequests.push({ endpoint: 'https://421.0.0.1', requestApiKey: apiKey, requestParams: expectedParams, responseData: { makerAssetData: '123' }, responseCode: StatusCodes.Success, }); // A successful response code and valid response data, but for wrong maker asset data mockedRequests.push({ endpoint: 'https://422.0.0.1', requestApiKey: apiKey, requestParams: expectedParams, responseData: { ...successfulQuote1, makerAssetData: assetDataUtils.encodeERC20AssetData(otherToken1) }, responseCode: StatusCodes.Success, }); // A successful response code and valid response data, but for wrong taker asset data mockedRequests.push({ endpoint: 'https://423.0.0.1', requestApiKey: apiKey, requestParams: expectedParams, responseData: { ...successfulQuote1, takerAssetData: assetDataUtils.encodeERC20AssetData(otherToken1) }, responseCode: StatusCodes.Success, }); // Another Successful response mockedRequests.push({ endpoint: 'https://37.0.0.1', requestApiKey: apiKey, requestParams: expectedParams, responseData: successfulQuote1, responseCode: StatusCodes.Success, }); return rfqtMocker.withMockedRfqtIndicativeQuotes( mockedRequests, 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://422.0.0.1': [[makerToken, takerToken]], 'https://423.0.0.1': [[makerToken, takerToken]], 'https://424.0.0.1': [[makerToken, takerToken]], 'https://37.0.0.1': [[makerToken, takerToken]], }); const resp = await qr.requestRfqtIndicativeQuotesAsync( makerAssetData, takerAssetData, new BigNumber(10000), MarketOperation.Sell, undefined, { apiKey, takerAddress, intentOnFilling: true, }, ); expect(resp.sort()).to.eql([successfulQuote1, successfulQuote1].sort()); }, quoteRequestorHttpClient, ); }); it('should return successful RFQT indicative quote requests', async () => { const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a'; const apiKey = 'my-ko0l-api-key'; // Set up RFQT responses // tslint:disable-next-line:array-type const mockedRequests: MockedRfqtIndicativeQuoteResponse[] = []; const expectedParams: TakerRequestQueryParams = { sellTokenAddress: takerToken, buyTokenAddress: makerToken, buyAmountBaseUnits: '10000', comparisonPrice: undefined, takerAddress, }; // Successful response const successfulQuote1 = { makerAssetData, takerAssetData, makerAssetAmount: new BigNumber(expectedParams.buyAmountBaseUnits), takerAssetAmount: new BigNumber(expectedParams.buyAmountBaseUnits), expirationTimeSeconds: makeThreeMinuteExpiry(), }; mockedRequests.push({ endpoint: 'https://1337.0.0.1', requestApiKey: apiKey, requestParams: expectedParams, responseData: successfulQuote1, responseCode: StatusCodes.Success, }); return rfqtMocker.withMockedRfqtIndicativeQuotes( mockedRequests, async () => { const qr = new QuoteRequestor({ 'https://1337.0.0.1': [[makerToken, takerToken]] }); const resp = await qr.requestRfqtIndicativeQuotesAsync( makerAssetData, takerAssetData, new BigNumber(10000), MarketOperation.Buy, undefined, { apiKey, takerAddress, intentOnFilling: true, }, ); expect(resp.sort()).to.eql([successfulQuote1].sort()); }, quoteRequestorHttpClient, ); }); }); });