import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses'; import { OtcOrder } from '@0x/protocol-utils'; import { Signature, SignatureType } from '@0x/protocol-utils/lib/src/signature_utils'; import { TokenMetadata } from '@0x/token-metadata'; import { MarketOperation } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { AxiosInstance } from 'axios'; import { Integrator } from '../../src/config'; import { DEFAULT_MIN_EXPIRY_DURATION_MS, NULL_ADDRESS, ONE_SECOND_MS } from '../../src/core/constants'; import { RfqMaker } from '../../src/entities'; import { QuoteRequestor } from '../../src/quoteRequestor/QuoteRequestor'; import { FeeService } from '../../src/services/fee_service'; import { RfqtService } from '../../src/services/RfqtService'; import { RfqMakerBalanceCacheService } from '../../src/services/rfq_maker_balance_cache_service'; import { FirmQuoteContext, QuoteContext, QuoteContextVolumeRequired } from '../../src/services/types'; import { FeeWithDetails, IndicativeQuote, RfqtV2Price } from '../../src/core/types'; import { CacheClient } from '../../src/utils/cache_client'; import { ConfigManager } from '../../src/utils/config_manager'; import { GasStationAttendant } from '../../src/utils/GasStationAttendant'; import { QuoteServerClient } from '../../src/utils/quote_server_client'; import { RfqBalanceCheckUtils, RfqBlockchainUtils } from '../../src/utils/rfq_blockchain_utils'; import { RfqMakerDbUtils } from '../../src/utils/rfq_maker_db_utils'; import { RfqMakerAssetOfferings, RfqMakerManager } from '../../src/utils/rfq_maker_manager'; import { TokenMetadataManager } from '../../src/utils/TokenMetadataManager'; import { TokenPriceOracle } from '../../src/utils/TokenPriceOracle'; import { ZeroExApiClient } from '../../src/utils/ZeroExApiClient'; import { RfqDynamicBlacklist } from '../../src/utils/rfq_dynamic_blacklist'; jest.mock('../../src/utils/rfq_maker_manager', () => ({ RfqMakerManager: jest.fn().mockImplementation(() => { return { getRfqtV2MakersForPair: jest.fn(), }; }), })); jest.mock('../../src/quoteRequestor/QuoteRequestor', () => ({ QuoteRequestor: jest.fn().mockImplementation(() => { return { requestRfqtIndicativeQuotesAsync: jest.fn().mockResolvedValue([]), requestRfqtFirmQuotesAsync: jest.fn().mockResolvedValue([]), }; }), })); jest.mock('../../src/utils/quote_server_client', () => ({ QuoteServerClient: jest.fn().mockImplementation(() => { return { batchGetPriceV2Async: jest.fn().mockResolvedValue([]), }; }), })); jest.mock('../../src/services/rfq_maker_balance_cache_service', () => ({ RfqMakerBalanceCacheService: jest.fn().mockImplementation(() => { return { getERC20OwnerBalancesAsync: jest.fn().mockResolvedValue([]), }; }), })); // TODO (rhinodavid): Find a better way to initialize mocked classes const mockQuoteRequestor = jest.mocked(new QuoteRequestor({} as RfqMakerAssetOfferings, {} as AxiosInstance)); const mockRfqMakerManager = jest.mocked(new RfqMakerManager({} as ConfigManager, {} as RfqMakerDbUtils, 0)); const mockQuoteServerClient = jest.mocked(new QuoteServerClient({} as AxiosInstance)); const mockFeeService = jest.mocked( new FeeService( 1337, {} as TokenMetadata, {} as ConfigManager, {} as GasStationAttendant, {} as TokenPriceOracle, {} as ZeroExApiClient, DEFAULT_MIN_EXPIRY_DURATION_MS, ), ); const mockRfqBlockchainUtils = jest.mocked({} as RfqBlockchainUtils); const mockTokenMetadataManager = jest.mocked(new TokenMetadataManager(1337, {} as RfqBlockchainUtils)); const mockContractAddresses = getContractAddressesForChainOrThrow(1337); const mockRfqMakerBalanceCacheService = jest.mocked( new RfqMakerBalanceCacheService({} as CacheClient, {} as RfqBalanceCheckUtils), ); const mockCacheClient = jest.mocked({} as CacheClient); const mockTokenPriceOracle = jest.mocked({} as TokenPriceOracle); const mockRfqDynamicBlacklist = jest.mocked(new Set() as RfqDynamicBlacklist); describe('Rfqt Service', () => { beforeEach(() => { mockQuoteRequestor.requestRfqtFirmQuotesAsync.mockClear(); mockQuoteRequestor.requestRfqtIndicativeQuotesAsync.mockClear(); mockQuoteServerClient.batchGetPriceV2Async.mockClear(); mockRfqMakerManager.getRfqtV2MakersForPair.mockClear(); mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync.mockClear(); }); describe('v1', () => { describe('getV1PricesAsync', () => { it('passes through calls to QuoteRequestor::requestRfqtIndicativeQuotesAsync', async () => { const rfqtService = new RfqtService( 0, mockRfqMakerManager, mockQuoteRequestor, mockQuoteServerClient, DEFAULT_MIN_EXPIRY_DURATION_MS, mockRfqBlockchainUtils, mockTokenMetadataManager, mockContractAddresses, mockFeeService, 1, mockRfqMakerBalanceCacheService, mockCacheClient, mockTokenPriceOracle, mockRfqDynamicBlacklist, ); await rfqtService.getV1PricesAsync({ altRfqAssetOfferings: { 'alt-mm': [ { id: 'id', baseAsset: '0xbaseasset', quoteAsset: '0xquoteasset', baseAssetDecimals: 420, quoteAssetDecimals: 69, }, ], }, assetFillAmount: new BigNumber(111), comparisonPrice: new BigNumber(666), makerToken: '0xmakertoken', marketOperation: MarketOperation.Buy, takerAddress: '0xtakeraddress', takerToken: '0xtakertoken', intentOnFilling: false, integrator: { apiKeys: [], allowedChainIds: [], integratorId: 'uuid-integrator', rfqm: false, label: 'Scam Integrator 1', }, txOrigin: '0xtxorigin', }); const args = mockQuoteRequestor.requestRfqtIndicativeQuotesAsync.mock.calls[0]; expect(args).toMatchInlineSnapshot(` [ "0xmakertoken", "0xtakertoken", "111", "Buy", "666", { "altRfqAssetOfferings": { "alt-mm": [ { "baseAsset": "0xbaseasset", "baseAssetDecimals": 420, "id": "id", "quoteAsset": "0xquoteasset", "quoteAssetDecimals": 69, }, ], }, "integrator": { "allowedChainIds": [], "apiKeys": [], "integratorId": "uuid-integrator", "label": "Scam Integrator 1", "rfqm": false, }, "intentOnFilling": false, "isIndicative": true, "isLastLook": false, "makerEndpointMaxResponseTimeMs": 600, "takerAddress": "0xtakeraddress", "txOrigin": "0xtxorigin", }, ] `); }); }); describe('getV1QuotesAsync', () => { it('passes through calls to QuoteRequestor::requestRfqtFirmQuotesAsync', async () => { mockQuoteRequestor.requestRfqtFirmQuotesAsync.mockResolvedValue([]); const rfqtService = new RfqtService( 0, mockRfqMakerManager, mockQuoteRequestor, mockQuoteServerClient, DEFAULT_MIN_EXPIRY_DURATION_MS, mockRfqBlockchainUtils, mockTokenMetadataManager, mockContractAddresses, mockFeeService, 1, mockRfqMakerBalanceCacheService, mockCacheClient, mockTokenPriceOracle, mockRfqDynamicBlacklist, ); await rfqtService.getV1QuotesAsync({ altRfqAssetOfferings: { 'alt-mm': [ { id: 'id', baseAsset: '0xbaseasset', quoteAsset: '0xquoteasset', baseAssetDecimals: 420, quoteAssetDecimals: 69, }, ], }, assetFillAmount: new BigNumber(111), comparisonPrice: new BigNumber(666), makerToken: '0xmakertoken', marketOperation: MarketOperation.Buy, takerAddress: '0xtakeraddress', takerToken: '0xtakertoken', intentOnFilling: false, integrator: { allowedChainIds: [], apiKeys: [], integratorId: 'uuid-integrator', rfqm: false, label: 'Scam Integrator 1', }, txOrigin: '0xtxorigin', }); const args = mockQuoteRequestor.requestRfqtFirmQuotesAsync.mock.calls[0]; expect(args).toMatchInlineSnapshot(` [ "0xmakertoken", "0xtakertoken", "111", "Buy", "666", { "altRfqAssetOfferings": { "alt-mm": [ { "baseAsset": "0xbaseasset", "baseAssetDecimals": 420, "id": "id", "quoteAsset": "0xquoteasset", "quoteAssetDecimals": 69, }, ], }, "integrator": { "allowedChainIds": [], "apiKeys": [], "integratorId": "uuid-integrator", "label": "Scam Integrator 1", "rfqm": false, }, "intentOnFilling": false, "isIndicative": false, "isLastLook": false, "makerEndpointMaxResponseTimeMs": 600, "takerAddress": "0xtakeraddress", "txOrigin": "0xtxorigin", }, ] `); }); }); }); describe('v2', () => { const maker = new RfqMaker({ makerId: 'maker-id', chainId: 1337, updatedAt: new Date(), pairs: [['0x1', '0x2']], rfqmUri: null, rfqtUri: 'maker.uri', }); const maker2 = new RfqMaker({ makerId: 'maker2-id', chainId: 1337, updatedAt: new Date(), pairs: [['0x1', '0x2']], rfqmUri: null, rfqtUri: 'maker2.uri', }); const altonomy = new RfqMaker({ makerId: 'fc8468a7-8bc3-4df0-abce-2bbd04c24cb0', chainId: 1337, updatedAt: new Date(), pairs: [['0x1', '0x2']], rfqmUri: null, rfqtUri: 'altonomy.uri', }); const integrator: Integrator = { allowedChainIds: [1337], apiKeys: [], integratorId: 'integrator-id', label: 'test integrator', rfqm: false, }; describe('getV2PricesAsync', () => { it('transforms the API request into a quote server client request for buys', async () => { const quoteContext: QuoteContext = { isFirm: false, workflow: 'rfqt', isUnwrap: false, originalMakerToken: '0x1', takerTokenDecimals: 18, makerTokenDecimals: 18, feeModelVersion: 1, assetFillAmount: new BigNumber(1000), chainId: 1337, integrator, makerToken: '0x1', isSelling: false, takerAddress: '0x0', takerToken: '0x2', txOrigin: '0xtakeraddress', }; mockRfqMakerManager.getRfqtV2MakersForPair = jest.fn().mockReturnValue([maker]); mockFeeService.calculateFeeAsync = jest.fn().mockResolvedValue({ feeWithDetails: { token: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082', amount: new BigNumber(100), type: 'fixed', }, }); const rfqtService = new RfqtService( 1337, mockRfqMakerManager, mockQuoteRequestor, mockQuoteServerClient, DEFAULT_MIN_EXPIRY_DURATION_MS, mockRfqBlockchainUtils, mockTokenMetadataManager, mockContractAddresses, mockFeeService, 1, mockRfqMakerBalanceCacheService, mockCacheClient, mockTokenPriceOracle, mockRfqDynamicBlacklist, ); await rfqtService.getV2PricesAsync(quoteContext); expect(mockQuoteServerClient.batchGetPriceV2Async.mock.calls[0]).toMatchInlineSnapshot(` [ [ "maker.uri", ], { "allowedChainIds": [ 1337, ], "apiKeys": [], "integratorId": "integrator-id", "label": "test integrator", "rfqm": false, }, { "buyAmountBaseUnits": "1000", "buyTokenAddress": "0x1", "chainId": "1337", "feeAmount": "100", "feeToken": "0x0b1ba0af832d7c05fd64161e0db78e85978e8082", "integratorId": "integrator-id", "protocolVersion": "4", "sellTokenAddress": "0x2", "takerAddress": "0x0", "txOrigin": "0xtakeraddress", "workflow": "rfqt", }, [Function], "rfqt", ] `); }); it('[workflow: gasless-rfqt] transforms the API request into a quote server client request for buys', async () => { const quoteContext: QuoteContext = { isFirm: false, workflow: 'gasless-rfqt', isUnwrap: false, originalMakerToken: '0x1', takerTokenDecimals: 18, makerTokenDecimals: 18, feeModelVersion: 1, assetFillAmount: new BigNumber(1000), chainId: 1337, integrator, makerToken: '0x1', isSelling: false, takerAddress: '0x0', takerToken: '0x2', txOrigin: '0xtakeraddress', }; mockRfqMakerManager.getRfqtV2MakersForPair = jest.fn().mockReturnValue([altonomy]); mockFeeService.calculateFeeAsync = jest.fn().mockResolvedValue({ feeWithDetails: { token: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082', amount: new BigNumber(100), type: 'fixed', }, }); const rfqtService = new RfqtService( 1337, mockRfqMakerManager, mockQuoteRequestor, mockQuoteServerClient, DEFAULT_MIN_EXPIRY_DURATION_MS, mockRfqBlockchainUtils, mockTokenMetadataManager, mockContractAddresses, mockFeeService, 1, mockRfqMakerBalanceCacheService, mockCacheClient, mockTokenPriceOracle, mockRfqDynamicBlacklist, ); await rfqtService.getV2PricesAsync(quoteContext); expect(mockQuoteServerClient.batchGetPriceV2Async.mock.calls[0]).toMatchInlineSnapshot(` [ [ "altonomy.uri", ], { "allowedChainIds": [ 1337, ], "apiKeys": [], "integratorId": "integrator-id", "label": "test integrator", "rfqm": false, }, { "buyAmountBaseUnits": "1000", "buyTokenAddress": "0x1", "chainId": "1337", "feeAmount": "100", "feeToken": "0x0b1ba0af832d7c05fd64161e0db78e85978e8082", "integratorId": "integrator-id", "protocolVersion": "4", "sellTokenAddress": "0x2", "takerAddress": "0x0", "txOrigin": "0xtakeraddress", "workflow": "gasless-rfqt", }, [Function], "gasless-rfqt", ] `); }); it('transforms the API request into a quote server client request for sells', async () => { const quoteContext: QuoteContext = { isFirm: false, workflow: 'rfqt', isUnwrap: false, originalMakerToken: '0x1', takerTokenDecimals: 18, makerTokenDecimals: 18, feeModelVersion: 1, assetFillAmount: new BigNumber(1000), chainId: 1337, integrator, makerToken: '0x1', isSelling: true, takerAddress: '0x0', takerToken: '0x2', txOrigin: '0xtakeraddress', }; mockRfqMakerManager.getRfqtV2MakersForPair = jest.fn().mockReturnValue([maker]); mockFeeService.calculateFeeAsync = jest.fn().mockResolvedValue({ feeWithDetails: { token: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082', amount: new BigNumber(100), type: 'fixed', }, }); const rfqtService = new RfqtService( 1337, mockRfqMakerManager, mockQuoteRequestor, mockQuoteServerClient, DEFAULT_MIN_EXPIRY_DURATION_MS, mockRfqBlockchainUtils, mockTokenMetadataManager, mockContractAddresses, mockFeeService, 1, mockRfqMakerBalanceCacheService, mockCacheClient, mockTokenPriceOracle, mockRfqDynamicBlacklist, ); await rfqtService.getV2PricesAsync(quoteContext); expect(mockQuoteServerClient.batchGetPriceV2Async.mock.calls[0]).toMatchInlineSnapshot(` [ [ "maker.uri", ], { "allowedChainIds": [ 1337, ], "apiKeys": [], "integratorId": "integrator-id", "label": "test integrator", "rfqm": false, }, { "buyTokenAddress": "0x1", "chainId": "1337", "feeAmount": "100", "feeToken": "0x0b1ba0af832d7c05fd64161e0db78e85978e8082", "integratorId": "integrator-id", "protocolVersion": "4", "sellAmountBaseUnits": "1000", "sellTokenAddress": "0x2", "takerAddress": "0x0", "txOrigin": "0xtakeraddress", "workflow": "rfqt", }, [Function], "rfqt", ] `); }); it('gets prices', async () => { const quoteContext: QuoteContext = { isFirm: false, workflow: 'rfqt', isUnwrap: false, originalMakerToken: '0x1', takerTokenDecimals: 18, makerTokenDecimals: 18, feeModelVersion: 1, assetFillAmount: new BigNumber(1000), chainId: 1337, integrator, makerToken: '0x1', isSelling: true, takerAddress: '0x0', takerToken: '0x2', txOrigin: '0xtakeraddress', }; const price: IndicativeQuote = { // $eslint-fix-me https://github.com/rhinodavid/eslint-fix-me // eslint-disable-next-line @typescript-eslint/no-loss-of-precision expiry: new BigNumber(9999999999999999), maker: '0xmakeraddress', makerAmount: new BigNumber(1000), makerToken: '0x1', makerUri: 'maker.uri', takerAmount: new BigNumber(1001), takerToken: '0x2', }; mockRfqMakerManager.getRfqtV2MakersForPair = jest.fn().mockReturnValue([maker]); mockQuoteServerClient.batchGetPriceV2Async = jest.fn().mockResolvedValue([price]); mockFeeService.calculateFeeAsync = jest.fn().mockResolvedValue({ feeWithDetails: { token: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082', amount: new BigNumber(100), type: 'fixed', }, }); const rfqtService = new RfqtService( 1337, mockRfqMakerManager, mockQuoteRequestor, mockQuoteServerClient, DEFAULT_MIN_EXPIRY_DURATION_MS, mockRfqBlockchainUtils, mockTokenMetadataManager, mockContractAddresses, mockFeeService, 1, mockRfqMakerBalanceCacheService, mockCacheClient, mockTokenPriceOracle, mockRfqDynamicBlacklist, ); const result = await rfqtService.getV2PricesAsync(quoteContext); expect(result.length).toEqual(1); expect(result[0].makerId).toEqual('maker-id'); expect(result[0]).toMatchInlineSnapshot(` { "expiry": "10000000000000000", "makerAddress": "0xmakeraddress", "makerAmount": "1000", "makerId": "maker-id", "makerToken": "0x1", "makerUri": "maker.uri", "requestedSellAmount": "1000", "takerAmount": "1001", "takerToken": "0x2", } `); }); it('gets prices from whitelisted makers only', async () => { const quoteContext: QuoteContext = { isFirm: false, workflow: 'rfqt', isUnwrap: false, originalMakerToken: '0x1', takerTokenDecimals: 18, makerTokenDecimals: 18, feeModelVersion: 1, assetFillAmount: new BigNumber(1000), chainId: 1337, integrator: { ...integrator, whitelistMakerIds: ['maker1'], }, makerToken: '0x1', isSelling: true, takerAddress: '0x0', takerToken: '0x2', txOrigin: '0xtakeraddress', }; const price: IndicativeQuote = { // $eslint-fix-me https://github.com/rhinodavid/eslint-fix-me // eslint-disable-next-line @typescript-eslint/no-loss-of-precision expiry: new BigNumber(9999999999999999), maker: '0xmakeraddress', makerAmount: new BigNumber(1000), makerToken: '0x1', makerUri: 'maker.uri', takerAmount: new BigNumber(1001), takerToken: '0x2', }; mockRfqMakerManager.getRfqtV2MakersForPair = jest.fn().mockReturnValue([ { ...maker, makerId: 'maker1', }, { ...maker, makerId: 'maker2', }, ]); mockQuoteServerClient.batchGetPriceV2Async = jest.fn().mockResolvedValue([price]); mockFeeService.calculateFeeAsync = jest.fn().mockResolvedValue({ feeWithDetails: { token: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082', amount: new BigNumber(100), type: 'fixed', }, }); const rfqtService = new RfqtService( 1337, mockRfqMakerManager, mockQuoteRequestor, mockQuoteServerClient, DEFAULT_MIN_EXPIRY_DURATION_MS, mockRfqBlockchainUtils, mockTokenMetadataManager, mockContractAddresses, mockFeeService, 1, mockRfqMakerBalanceCacheService, mockCacheClient, mockTokenPriceOracle, mockRfqDynamicBlacklist, ); const result = await rfqtService.getV2PricesAsync(quoteContext); expect(result.length).toEqual(1); expect(result[0].makerId).toEqual('maker1'); expect(result[0]).toMatchInlineSnapshot(` { "expiry": "10000000000000000", "makerAddress": "0xmakeraddress", "makerAmount": "1000", "makerId": "maker1", "makerToken": "0x1", "makerUri": "maker.uri", "requestedSellAmount": "1000", "takerAmount": "1001", "takerToken": "0x2", } `); }); }); describe('getV2QuotesAsync', () => { const makerToken = '0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE'; const makerAddress = '0x79b7a69d90c82E014Bf0315e164208119B510FA0'; const maker2Address = '0xe1bA72a87fb38bd7323f61177f158Fbb2D4549f4'; const takerToken = '0x42d6622deCe394b54999Fbd73D108123806f6a18'; const takerAddress = '0xE06fFA8146bBdECcBaaF72B6043b29091071AEB8'; const fakeNow = new Date(1657069278103); const expiry = new BigNumber(fakeNow.getTime() + 1_000_000).dividedBy(ONE_SECOND_MS).decimalPlaces(0); mockRfqBlockchainUtils.isValidOrderSignerAsync = jest.fn().mockResolvedValue(true); it('filters out quotes with no signatures', async () => { const quoteContext: FirmQuoteContext = { isFirm: true, workflow: 'rfqt', isUnwrap: false, originalMakerToken: '0x1', takerTokenDecimals: 18, makerTokenDecimals: 18, feeModelVersion: 1, assetFillAmount: new BigNumber(1000), chainId: 1337, integrator, makerToken, isSelling: false, takerAddress, trader: takerAddress, takerToken, txOrigin: takerAddress, }; mockRfqMakerManager.getRfqtV2MakersForPair = jest.fn().mockReturnValue([maker]); mockQuoteServerClient.batchGetPriceV2Async = jest.fn().mockResolvedValue([ { maker: makerAddress, makerUri: maker.rfqtUri, makerToken, takerToken, makerAmount: new BigNumber(999), takerAmount: new BigNumber(1000), expiry, }, ]); mockQuoteServerClient.signV2Async = jest.fn().mockResolvedValue(undefined); mockFeeService.calculateFeeAsync = jest.fn().mockResolvedValue({ feeWithDetails: { token: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082', amount: new BigNumber(100), type: 'fixed', }, }); const rfqtService = new RfqtService( 1337, mockRfqMakerManager, mockQuoteRequestor, mockQuoteServerClient, DEFAULT_MIN_EXPIRY_DURATION_MS, mockRfqBlockchainUtils, mockTokenMetadataManager, mockContractAddresses, mockFeeService, 1, mockRfqMakerBalanceCacheService, mockCacheClient, mockTokenPriceOracle, mockRfqDynamicBlacklist, ); const result = await rfqtService.getV2QuotesAsync(quoteContext); expect(result.length).toEqual(0); }); it('filters out quotes with no signatures and still handles fillable amounts correctly', async () => { const quoteContext: FirmQuoteContext = { isFirm: true, workflow: 'rfqt', isUnwrap: false, originalMakerToken: '0x1', takerTokenDecimals: 18, makerTokenDecimals: 18, feeModelVersion: 1, assetFillAmount: new BigNumber(1000), chainId: 1337, integrator, makerToken, isSelling: false, takerAddress, trader: takerAddress, takerToken, txOrigin: takerAddress, }; mockRfqMakerManager.getRfqtV2MakersForPair = jest.fn().mockReturnValue([maker, maker2]); mockQuoteServerClient.batchGetPriceV2Async = jest.fn().mockResolvedValue([ { maker: makerAddress, makerUri: maker.rfqtUri, makerToken, takerToken, makerAmount: new BigNumber(999), takerAmount: new BigNumber(1000), expiry, }, { maker: maker2Address, makerUri: maker2.rfqtUri, makerToken, takerToken, makerAmount: new BigNumber(999), takerAmount: new BigNumber(1000), expiry, }, ]); // First maker has no signature mockQuoteServerClient.signV2Async = jest.fn().mockResolvedValueOnce(undefined).mockResolvedValueOnce({ v: 27, r: '0x123', s: '0x456', signatureType: SignatureType.EIP712, }); mockFeeService.calculateFeeAsync = jest.fn().mockResolvedValue({ feeWithDetails: { token: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082', amount: new BigNumber(100), type: 'fixed', }, }); // Second maker has a smaller fillable amount mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync = jest .fn() .mockResolvedValue([new BigNumber(1000), new BigNumber(100)]); const rfqtService = new RfqtService( 1337, mockRfqMakerManager, mockQuoteRequestor, mockQuoteServerClient, DEFAULT_MIN_EXPIRY_DURATION_MS, mockRfqBlockchainUtils, mockTokenMetadataManager, mockContractAddresses, mockFeeService, 1, mockRfqMakerBalanceCacheService, mockCacheClient, mockTokenPriceOracle, mockRfqDynamicBlacklist, ); const result = await rfqtService.getV2QuotesAsync(quoteContext, fakeNow); const order = new OtcOrder({ chainId: 1337, makerAmount: new BigNumber(999), takerAmount: new BigNumber(1000), taker: NULL_ADDRESS, makerToken, takerToken, maker: maker2Address, txOrigin: quoteContext.txOrigin, expiryAndNonce: new BigNumber( '10401598717691489530826623925864187439861993812812831231287826374367', ), verifyingContract: mockContractAddresses.exchangeProxy, }); expect(result.length).toEqual(1); expect(result).toEqual([ { fee: undefined, fillableMakerAmount: new BigNumber(100), fillableTakerAmount: new BigNumber(100), fillableTakerFeeAmount: new BigNumber(0), makerId: maker2.makerId, makerUri: maker2.rfqtUri, order, requestedBuyAmount: new BigNumber(1000), requestedSellAmount: undefined, signature: { v: 27, r: '0x0000000000000000000000000000000000000000000000000000000000000123', s: '0x0000000000000000000000000000000000000000000000000000000000000456', signatureType: SignatureType.EIP712, }, }, ]); }); it("doesn't blow up if a sign request fails", async () => { const quoteContext: FirmQuoteContext = { isFirm: true, workflow: 'rfqt', isUnwrap: false, originalMakerToken: '0x1', takerTokenDecimals: 18, makerTokenDecimals: 18, feeModelVersion: 1, assetFillAmount: new BigNumber(1000), chainId: 1337, integrator, makerToken, isSelling: false, takerAddress, trader: takerAddress, takerToken, txOrigin: takerAddress, }; mockRfqMakerManager.getRfqtV2MakersForPair = jest.fn().mockReturnValue([maker]); mockQuoteServerClient.batchGetPriceV2Async = jest.fn().mockResolvedValue([ { maker: makerAddress, makerUri: maker.rfqtUri, makerToken, takerToken, makerAmount: new BigNumber(999), takerAmount: new BigNumber(1000), expiry, }, ]); mockQuoteServerClient.signV2Async = jest.fn().mockRejectedValue(new Error('EXPLODE')); mockFeeService.calculateFeeAsync = jest.fn().mockResolvedValue({ feeWithDetails: { token: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082', amount: new BigNumber(100), type: 'fixed', }, }); const rfqtService = new RfqtService( 1337, mockRfqMakerManager, mockQuoteRequestor, mockQuoteServerClient, DEFAULT_MIN_EXPIRY_DURATION_MS, mockRfqBlockchainUtils, mockTokenMetadataManager, mockContractAddresses, mockFeeService, 1, mockRfqMakerBalanceCacheService, mockCacheClient, mockTokenPriceOracle, mockRfqDynamicBlacklist, ); const result = await rfqtService.getV2QuotesAsync(quoteContext); expect(result.length).toEqual(0); }); it('[workflow: rfqt] creates orders with unique nonces', async () => { const quoteContext: FirmQuoteContext = { isFirm: true, workflow: 'rfqt', isUnwrap: false, originalMakerToken: '0x1', takerTokenDecimals: 18, makerTokenDecimals: 18, feeModelVersion: 1, assetFillAmount: new BigNumber(1000), chainId: 1337, integrator, makerToken, isSelling: false, takerAddress, trader: takerAddress, takerToken, txOrigin: takerAddress, }; mockRfqMakerManager.getRfqtV2MakersForPair = jest.fn().mockReturnValue([maker]); mockQuoteServerClient.batchGetPriceV2Async = jest.fn().mockResolvedValue([ { maker: makerAddress, makerUri: maker.rfqtUri, makerToken, takerToken, makerAmount: new BigNumber(999), takerAmount: new BigNumber(1000), expiry, }, { maker: makerAddress, makerUri: maker.rfqtUri, makerToken, takerToken, makerAmount: new BigNumber(900), takerAmount: new BigNumber(1000), expiry, }, ]); mockFeeService.calculateFeeAsync = jest.fn().mockResolvedValue({ feeWithDetails: { token: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082', amount: new BigNumber(100), type: 'fixed', }, }); mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync = jest .fn() .mockResolvedValue([new BigNumber(10000), new BigNumber(10000)]); const signature: Signature = { r: 'r', v: 21, s: 's', signatureType: SignatureType.EIP712 }; mockQuoteServerClient.signV2Async = jest.fn().mockResolvedValue(signature); const rfqtService = new RfqtService( 1337, mockRfqMakerManager, mockQuoteRequestor, mockQuoteServerClient, DEFAULT_MIN_EXPIRY_DURATION_MS, mockRfqBlockchainUtils, mockTokenMetadataManager, mockContractAddresses, mockFeeService, 1, mockRfqMakerBalanceCacheService, mockCacheClient, mockTokenPriceOracle, mockRfqDynamicBlacklist, ); const result = await rfqtService.getV2QuotesAsync(quoteContext, fakeNow); const [{ nonce: nonce1 }, { nonce: nonce2 }] = [ OtcOrder.parseExpiryAndNonce(result[0].order.expiryAndNonce), OtcOrder.parseExpiryAndNonce(result[1].order.expiryAndNonce), ]; expect(nonce1.toString()).not.toEqual(nonce2.toString()); }); it('[workflow: gasless-rfqt] creates orders with unique buckets', async () => { const quoteContext: FirmQuoteContext = { isFirm: true, workflow: 'gasless-rfqt', isUnwrap: false, originalMakerToken: '0x1', takerTokenDecimals: 18, makerTokenDecimals: 18, feeModelVersion: 1, assetFillAmount: new BigNumber(1000), chainId: 1337, integrator, makerToken, isSelling: false, takerAddress, trader: takerAddress, takerToken, txOrigin: takerAddress, }; mockRfqMakerManager.getRfqtV2MakersForPair = jest.fn().mockReturnValue([altonomy]); mockQuoteServerClient.batchGetPriceV2Async = jest.fn().mockResolvedValue([ { maker: makerAddress, makerUri: altonomy.rfqtUri, makerToken, takerToken, makerAmount: new BigNumber(999), takerAmount: new BigNumber(1000), expiry, }, { maker: makerAddress, makerUri: altonomy.rfqtUri, makerToken, takerToken, makerAmount: new BigNumber(900), takerAmount: new BigNumber(1000), expiry, }, ]); mockFeeService.calculateFeeAsync = jest.fn().mockResolvedValue({ feeWithDetails: { token: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082', amount: new BigNumber(100), type: 'fixed', }, }); mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync = jest .fn() .mockResolvedValue([new BigNumber(10000), new BigNumber(10000)]); const signature: Signature = { r: 'r', v: 21, s: 's', signatureType: SignatureType.EIP712 }; mockQuoteServerClient.signV2Async = jest.fn().mockResolvedValue(signature); // 0 is a special return value since this will trigger a wrap around mockCacheClient.getNextNOtcOrderBucketsAsync = jest.fn().mockResolvedValue(0); const rfqtService = new RfqtService( 1337, mockRfqMakerManager, mockQuoteRequestor, mockQuoteServerClient, DEFAULT_MIN_EXPIRY_DURATION_MS, mockRfqBlockchainUtils, mockTokenMetadataManager, mockContractAddresses, mockFeeService, 1, mockRfqMakerBalanceCacheService, mockCacheClient, mockTokenPriceOracle, mockRfqDynamicBlacklist, ); const result = await rfqtService.getV2QuotesAsync(quoteContext, fakeNow); console.log('result', result); const [{ nonceBucket: bucket1 }, { nonceBucket: bucket2 }] = [ OtcOrder.parseExpiryAndNonce(result[0].order.expiryAndNonce), OtcOrder.parseExpiryAndNonce(result[1].order.expiryAndNonce), ]; expect(bucket1.toString()).not.toEqual(bucket2.toString()); // check that buckets are greater than zero (successfully wrapped around for negative numbers) expect(bucket1.toNumber()).toBeGreaterThanOrEqual(0); expect(bucket2.toNumber()).toBeGreaterThanOrEqual(0); }); it('gets a signed quote', async () => { const quoteContext: FirmQuoteContext = { isFirm: true, workflow: 'rfqt', isUnwrap: false, originalMakerToken: '0x1', takerTokenDecimals: 18, makerTokenDecimals: 18, feeModelVersion: 1, assetFillAmount: new BigNumber(1000), chainId: 1337, integrator, makerToken, isSelling: false, takerAddress: NULL_ADDRESS, trader: takerAddress, takerToken, txOrigin: takerAddress, }; mockRfqMakerManager.getRfqtV2MakersForPair = jest.fn().mockReturnValue([maker]); mockQuoteServerClient.batchGetPriceV2Async = jest.fn().mockResolvedValue([ { maker: makerAddress, makerUri: maker.rfqtUri, makerToken, takerToken, makerAmount: new BigNumber(999), takerAmount: new BigNumber(1000), expiry, }, ]); const signature: Signature = { r: 'r', v: 21, s: 's', signatureType: SignatureType.EIP712 }; mockQuoteServerClient.signV2Async = jest.fn().mockResolvedValue(signature); mockFeeService.calculateFeeAsync = jest.fn().mockResolvedValue({ feeWithDetails: { token: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082', amount: new BigNumber(100), type: 'fixed', }, }); mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync = jest .fn() .mockResolvedValue([new BigNumber(10000)]); const rfqtService = new RfqtService( 1337, mockRfqMakerManager, mockQuoteRequestor, mockQuoteServerClient, DEFAULT_MIN_EXPIRY_DURATION_MS, mockRfqBlockchainUtils, mockTokenMetadataManager, mockContractAddresses, mockFeeService, 1, mockRfqMakerBalanceCacheService, mockCacheClient, mockTokenPriceOracle, mockRfqDynamicBlacklist, ); const result = await rfqtService.getV2QuotesAsync(quoteContext, fakeNow); expect(result.length).toEqual(1); expect(result[0]).toMatchObject({ fillableMakerAmount: new BigNumber(999), fillableTakerAmount: new BigNumber(1000), fillableTakerFeeAmount: new BigNumber(0), makerId: maker.makerId, makerUri: maker.rfqtUri, signature, }); expect(result[0].order).toMatchInlineSnapshot(` OtcOrder { "chainId": 1337, "expiry": "1657070278", "expiryAndNonce": "10401598717691489530826623925864187439861993812812831231287826374366", "maker": "0x79b7a69d90c82E014Bf0315e164208119B510FA0", "makerAmount": "999", "makerToken": "0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE", "nonce": "1657069278", "nonceBucket": "0", "taker": "0x0000000000000000000000000000000000000000", "takerAmount": "1000", "takerToken": "0x42d6622deCe394b54999Fbd73D108123806f6a18", "txOrigin": "0xE06fFA8146bBdECcBaaF72B6043b29091071AEB8", "verifyingContract": "0x5315e44798395d4a952530d131249fe00f554565", } `); }); describe('test_big_order_sampling', () => { // public async _getV2SampledPricesInternalAsync( // TESTED static V2AssignFee(prices: RfqtV2Price[], fee: FeeWithDetails): RfqtV2Price[] { // ******* static composeBigSizePrices(fullSizePrices: RfqtV2Price[], smallSizePrices: RfqtV2Price[]): RfqtV2Price[] { // TESTED ******** add more cases static generateModifiedSizeOrderQuoteContext( // static removeFeeFromPrices(pricesWithFee: RfqtV2Price[]): RfqtV2Price[] { it('tests QuoteContext build', async () => { const quoteContext: FirmQuoteContext = { isFirm: true, trader: '0x000000', takerToken: '0x000000', makerToken: '0x000000', txOrigin: '0x000000', chainId: 1337, takerAddress: '', workflow: 'rfqt', originalMakerToken: '', takerTokenDecimals: 0, makerTokenDecimals: 0, integrator: { apiKeys: [], allowedChainIds: [], integratorId: 'uuid-integrator', rfqm: false, label: 'Scam Integrator 1', }, isUnwrap: false, isSelling: false, feeModelVersion: 0, takerAmount: new BigNumber(2000), makerAmount: new BigNumber(3000), // TODO one of these two must be undefined volumeUSD: new BigNumber(5000), assetFillAmount: new BigNumber(4000), }; quoteContext.makerAmount = undefined; quoteContext.takerAmount = new BigNumber(3000); const modifiedQuoteContext = RfqtService.generateModifiedSizeOrderQuoteContext( quoteContext as QuoteContextVolumeRequired, new BigNumber(1000), ); expect(modifiedQuoteContext.takerAmount).toEqual(new BigNumber(3000 / (5000 / 1000))); expect(modifiedQuoteContext.makerAmount).toEqual(undefined); expect(modifiedQuoteContext.assetFillAmount).toEqual(new BigNumber(4000 / (5000 / 1000))); expect(modifiedQuoteContext.volumeUSD).toEqual(new BigNumber(5000 / (5000 / 1000))); }); it('tests_assign_fee_remove_fee', async () => { const feeWithDetails: FeeWithDetails = { details: { kind: 'default', tradeSizeBps: 0, zeroExFeeAmount: new BigNumber(0), feeTokenBaseUnitPriceUsd: new BigNumber(0), takerTokenBaseUnitPriceUsd: new BigNumber(0), makerTokenBaseUnitPriceUsd: new BigNumber(0), feeModelVersion: 0, gasFeeAmount: new BigNumber(0), gasPrice: new BigNumber(0), }, breakdown: {}, conversionRates: { nativeTokenBaseUnitPriceUsd: null, feeTokenBaseUnitPriceUsd: null, takerTokenBaseUnitPriceUsd: null, makerTokenBaseUnitPriceUsd: null, }, token: '', amount: new BigNumber(123), type: 'fixed', }; const prices: RfqtV2Price[] = [ { expiry: new BigNumber(0), makerAddress: '', makerAmount: new BigNumber(0), makerId: '', makerToken: '', makerUri: '', takerAmount: new BigNumber(0), takerToken: '', fee: undefined, }, ]; RfqtService.V2AssignFee(prices, feeWithDetails); expect(prices[0].fee).toEqual(feeWithDetails); RfqtService.removeFeeFromPrices(prices); expect(prices[0].fee).toEqual(undefined); }); it('gets prices for bigger than threshold size', async () => { const quoteContext: QuoteContext = { isFirm: false, workflow: 'rfqt', isUnwrap: false, originalMakerToken: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', takerTokenDecimals: 18, makerTokenDecimals: 18, feeModelVersion: 1, assetFillAmount: new BigNumber(253), chainId: 1337, integrator, makerToken: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', isSelling: false, takerAddress: '0x0', takerToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', txOrigin: '0xtakeraddress', volumeUSD: new BigNumber(500000), makerAmount: new BigNumber(253), // forcing not integer result to see rounding works }; const fullSizePrice: IndicativeQuote = { // $eslint-fix-me https://github.com/rhinodavid/eslint-fix-me // eslint-disable-next-line @typescript-eslint/no-loss-of-precision expiry: new BigNumber(9999999999999999), maker: '0xmakeraddress', makerAmount: new BigNumber(253), makerToken: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', makerUri: 'maker.uri', takerAmount: new BigNumber(500000), takerToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', }; const smallSizePrice: IndicativeQuote = { // $eslint-fix-me https://github.com/rhinodavid/eslint-fix-me // eslint-disable-next-line @typescript-eslint/no-loss-of-precision expiry: new BigNumber(9999999999999999), maker: '0xmakeraddress', makerAmount: new BigNumber(50), makerToken: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', makerUri: 'maker.uri', takerAmount: new BigNumber(100000), takerToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', }; mockQuoteServerClient.batchGetPriceV2Async.mockClear(); mockRfqMakerManager.getRfqtV2MakersForPair = jest.fn().mockReturnValue([maker]); mockQuoteServerClient.batchGetPriceV2Async = jest .fn() .mockResolvedValueOnce([fullSizePrice]) .mockResolvedValueOnce([smallSizePrice]); mockFeeService.calculateFeeAsync = jest .fn() .mockResolvedValueOnce({ feeWithDetails: { token: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082', amount: new BigNumber(5), type: 'fixed', }, }) .mockResolvedValueOnce({ feeWithDetails: { token: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082', amount: new BigNumber(1), type: 'fixed', }, }) .mockResolvedValueOnce({ feeWithDetails: { token: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082', amount: new BigNumber(4), type: 'fixed', }, }); const rfqtService = new RfqtService( 1337, mockRfqMakerManager, mockQuoteRequestor, mockQuoteServerClient, DEFAULT_MIN_EXPIRY_DURATION_MS, mockRfqBlockchainUtils, mockTokenMetadataManager, mockContractAddresses, mockFeeService, 1, mockRfqMakerBalanceCacheService, mockCacheClient, mockTokenPriceOracle, mockRfqDynamicBlacklist, ); const result = await rfqtService.getV2PricesAsync(quoteContext); expect(result.length).toEqual(2); console.log(`result[0]`, result[0]); console.log(`result[1]`, result[1]); expect(result[0].makerId).toEqual('maker-id'); expect(result[0]).toMatchInlineSnapshot(` { "expiry": "10000000000000000", "makerAddress": "0xmakeraddress", "makerAmount": "50", "makerId": "maker-id", "makerToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "makerUri": "maker.uri", "requestedBuyAmount": "50", "takerAmount": "100000", "takerToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", } `); expect(result[1]).toMatchInlineSnapshot(` { "expiry": "10000000000000000", "makerAddress": "0xmakeraddress", "makerAmount": "203", "makerId": "maker-id", "makerToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "makerUri": "maker.uri", "takerAmount": "400000", "takerToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", } `); expect(mockQuoteServerClient.batchGetPriceV2Async.mock.calls.length).toEqual(2); expect(mockQuoteServerClient.batchGetPriceV2Async.mock.calls[0][2]).toMatchInlineSnapshot(` { "buyAmountBaseUnits": "253", "buyTokenAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "chainId": "1337", "feeAmount": "5", "feeToken": "0x0b1ba0af832d7c05fd64161e0db78e85978e8082", "integratorId": "integrator-id", "protocolVersion": "4", "sellTokenAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "takerAddress": "0x0", "txOrigin": "0xtakeraddress", "workflow": "rfqt", } `); expect(mockQuoteServerClient.batchGetPriceV2Async.mock.calls[1][2]).toMatchInlineSnapshot(` { "buyAmountBaseUnits": "50", "buyTokenAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "chainId": "1337", "feeAmount": "1", "feeToken": "0x0b1ba0af832d7c05fd64161e0db78e85978e8082", "integratorId": "integrator-id", "protocolVersion": "4", "sellTokenAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "takerAddress": "0x0", "txOrigin": "0xtakeraddress", "workflow": "rfqt", } `); }); it('gets prices', async () => { const quoteContext: QuoteContext = { isFirm: false, workflow: 'rfqt', isUnwrap: false, originalMakerToken: '0x1', takerTokenDecimals: 18, makerTokenDecimals: 18, feeModelVersion: 1, assetFillAmount: new BigNumber(1000), chainId: 1337, integrator, makerToken: '0x1', isSelling: true, takerAddress: '0x0', takerToken: '0x2', txOrigin: '0xtakeraddress', }; const price: IndicativeQuote = { // $eslint-fix-me https://github.com/rhinodavid/eslint-fix-me // eslint-disable-next-line @typescript-eslint/no-loss-of-precision expiry: new BigNumber(9999999999999999), maker: '0xmakeraddress', makerAmount: new BigNumber(1000), makerToken: '0x1', makerUri: 'maker.uri', takerAmount: new BigNumber(1001), takerToken: '0x2', }; mockRfqMakerManager.getRfqtV2MakersForPair = jest.fn().mockReturnValue([maker]); mockQuoteServerClient.batchGetPriceV2Async = jest.fn().mockResolvedValue([price]); mockFeeService.calculateFeeAsync = jest.fn().mockResolvedValue({ feeWithDetails: { token: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082', amount: new BigNumber(100), type: 'fixed', }, }); const rfqtService = new RfqtService( 1337, mockRfqMakerManager, mockQuoteRequestor, mockQuoteServerClient, DEFAULT_MIN_EXPIRY_DURATION_MS, mockRfqBlockchainUtils, mockTokenMetadataManager, mockContractAddresses, mockFeeService, 1, mockRfqMakerBalanceCacheService, mockCacheClient, mockTokenPriceOracle, mockRfqDynamicBlacklist, ); const result = await rfqtService.getV2PricesAsync(quoteContext); expect(result.length).toEqual(1); expect(result[0].makerId).toEqual('maker-id'); expect(result[0]).toMatchInlineSnapshot(` { "expiry": "10000000000000000", "makerAddress": "0xmakeraddress", "makerAmount": "1000", "makerId": "maker-id", "makerToken": "0x1", "makerUri": "maker.uri", "requestedSellAmount": "1000", "takerAmount": "1001", "takerToken": "0x2", } `); }); }); }); }); });