import { ValidationError, ValidationErrorCodes } from '@0x/api-utils'; import { ethSignHashWithKey, MetaTransaction, MetaTransactionV2, OtcOrder, SignatureType } from '@0x/protocol-utils'; import { BigNumber } from '@0x/utils'; import { AxiosInstance } from 'axios'; import { providers } from 'ethersv5'; import Redis from 'ioredis'; import { Producer } from 'sqs-producer'; import { Connection } from 'typeorm'; import { Integrator } from '../../src/config'; import { DEFAULT_MIN_EXPIRY_DURATION_MS, ZERO } from '../../src/core/constants'; import { MetaTransactionJobEntity } from '../../src/entities'; import { RfqmJobStatus } from '../../src/entities/types'; import { GaslessSwapService } from '../../src/services/GaslessSwapService'; import { FeeService } from '../../src/services/fee_service'; import { RfqmService } from '../../src/services/rfqm_service'; import { RfqMakerBalanceCacheService } from '../../src/services/rfq_maker_balance_cache_service'; import { ApprovalResponse, FetchIndicativeQuoteResponse, LiquiditySource, MetaTransactionV1QuoteResponse, MetaTransactionV2QuoteResponse, OtcOrderRfqmQuoteResponse, } from '../../src/services/types'; import { BalanceChecker } from '../../src/utils/balance_checker'; import { CacheClient } from '../../src/utils/cache_client'; import { getV1QuoteAsync, getV2QuoteAsync } from '../../src/utils/MetaTransactionClient'; import { QuoteServerClient } from '../../src/utils/quote_server_client'; import { RfqmDbUtils } from '../../src/utils/rfqm_db_utils'; import { RfqBlockchainUtils } from '../../src/utils/rfq_blockchain_utils'; import { RfqMakerManager } from '../../src/utils/rfq_maker_manager'; import { TokenMetadataManager } from '../../src/utils/TokenMetadataManager'; import { GaslessSwapServiceTypes, GaslessTypes } from '../../src/core/types'; import { Fees } from '../../src/core/types/meta_transaction_fees'; import { ContractAddresses } from '@0x/contract-addresses'; import { SupportedProvider } from 'ethereum-types'; import { MOCK_EXECUTE_META_TRANSACTION_APPROVAL, MOCK_META_TRANSACTION_TRADE } from '../constants'; jest.mock('../../src/services/rfqm_service', () => { return { RfqmService: jest.fn().mockImplementation(() => { return { fetchFirmQuoteAsync: jest.fn(), fetchIndicativeQuoteAsync: jest.fn(), getGaslessApprovalResponseAsync: jest.fn(), }; }), }; }); jest.mock('../../src/utils/MetaTransactionClient', () => { return { getV1QuoteAsync: jest.fn(), getV2QuoteAsync: jest.fn(), }; }); jest.mock('../../src/utils/rfq_blockchain_utils', () => { return { RfqBlockchainUtils: jest.fn().mockImplementation(() => { return { getTokenBalancesAsync: jest.fn(), getMinOfBalancesAndAllowancesAsync: jest.fn(), getExchangeProxyAddress: jest.fn(), computeEip712Hash: jest.fn(), }; }), }; }); jest.mock('../../src/utils/rfqm_db_utils', () => { return { RfqmDbUtils: jest.fn().mockImplementation(() => { return { findMetaTransactionJobsWithStatusesAsync: jest.fn().mockResolvedValue([]), writeMetaTransactionJobAsync: jest.fn(), }; }), }; }); jest.mock('ioredis', () => { return { default: jest.fn().mockImplementation(() => { return { set: jest.fn(), get: jest.fn(), }; }), }; }); jest.mock('sqs-producer', () => { return { Producer: jest.fn().mockImplementation(() => { return { send: jest.fn(), }; }), }; }); const getMetaTransactionV1QuoteAsyncMock = getV1QuoteAsync as jest.Mock< ReturnType, Parameters >; const getMetaTransactionV2QuoteAsyncMock = getV2QuoteAsync as jest.Mock< ReturnType, Parameters >; const mockSqsProducer = jest.mocked(new Producer({})); const mockDbUtils = jest.mocked(new RfqmDbUtils({} as Connection)); const mockBlockchainUtils = jest.mocked( new RfqBlockchainUtils( {} as SupportedProvider, '0xdefi', '0xpermitAndCall', {} as BalanceChecker, {} as providers.JsonRpcProvider, ), ); const mockRfqmService = jest.mocked( new RfqmService( 0, {} as FeeService, 0, {} as ContractAddresses, '0x0', {} as RfqBlockchainUtils, {} as RfqmDbUtils, {} as Producer, {} as QuoteServerClient, DEFAULT_MIN_EXPIRY_DURATION_MS, {} as CacheClient, {} as RfqMakerBalanceCacheService, {} as RfqMakerManager, {} as TokenMetadataManager, 0, ), ); const mockRedis = jest.mocked(new Redis()); const gaslessSwapService = new GaslessSwapService( /* chainId */ 1337, mockRfqmService, new URL('https://hokiesports.com/quote'), {} as AxiosInstance, mockRedis, mockDbUtils, mockBlockchainUtils, mockSqsProducer, ); describe('GaslessSwapService', () => { const takerPrivateKey = '0xd2c2349e10170e4219d9febd1c663ea5c7334f79c38d25f4f52c85af796c7c05'; const integratorAddress = '0x4ea754349ace5303c82f0d1d491041e042f2ad22'; const zeroExAddress = '0x4ea754349ace5303c82f0d1d491041e042f2ad22'; const metaTransactionV1 = new MetaTransaction({ callData: '0x415565b00000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000000000017b9e2a304f00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000860000000000000000000000000000000000000000000000000000000000000086000000000000000000000000000000000000000000000000000000000000007c000000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000003400000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000052000000000000000000000000000000002517569636b5377617000000000000000000000000000000000000000000000000000000000000008570b55cfac18858000000000000000000000000000000000000000000000000000000039d0b9efd1000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000a5e0829caced8ffdd4de3c43696c57f7d7a678ff000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000002517569636b53776170000000000000000000000000000000000000000000000000000000000000042b85aae7d60c42c00000000000000000000000000000000000000000000000000000001c94ebec37000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000a5e0829caced8ffdd4de3c43696c57f7d7a678ff000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000030000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000000d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000000000000b446f646f5632000000000000000000000000000000000000000000000000000000000000000000042b85aae7d60c42c00000000000000000000000000000000000000000000000000000001db5156c13000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000400000000000000000000000005333eb1e32522f1893b7c9fea3c263807a02d561000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000012556e69737761705633000000000000000000000000000000000000000000000000000000000000190522016f044a05b0000000000000000000000000000000000000000000000000000000b08217af9400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000060000000000000000000000000e592427a0aece92de3edee1f18e0157c058615640000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012556e697377617056330000000000000000000000000000000000000000000000000000000000000c829100b78224ef50000000000000000000000000000000000000000000000000000000570157389f000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000427ceb23fd6bc0add59e62ac25578270cff1b9f6190001f41bfd67037b42cf73acf2047067bd4f2c47d9bfd6000bb82791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f619000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000869584cd0000000000000000000000008c611defbd838a13de3a5923693c58a7c1807c6300000000000000000000000000000000000000000000005b89d96b4863067a6b', chainId: 137, verifyingContract: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', expirationTimeSeconds: new BigNumber('9990868679'), feeAmount: new BigNumber(0), feeToken: '0x0000000000000000000000000000000000000000', maxGasPrice: new BigNumber(4294967296), minGasPrice: new BigNumber(1), // $eslint-fix-me https://github.com/rhinodavid/eslint-fix-me // eslint-disable-next-line @typescript-eslint/no-loss-of-precision salt: new BigNumber(32606650794224189614795510724011106220035660490560169776986607186708081701146), sender: '0x0000000000000000000000000000000000000000', signer: '0x4c42a706410f1190f97d26fe3c999c90070aa40f', value: new BigNumber(0), }); const metaTransactionV2 = new MetaTransactionV2({ callData: '0x415565b00000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000000000017b9e2a304f00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000860000000000000000000000000000000000000000000000000000000000000086000000000000000000000000000000000000000000000000000000000000007c000000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000003400000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000052000000000000000000000000000000002517569636b5377617000000000000000000000000000000000000000000000000000000000000008570b55cfac18858000000000000000000000000000000000000000000000000000000039d0b9efd1000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000a5e0829caced8ffdd4de3c43696c57f7d7a678ff000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000002517569636b53776170000000000000000000000000000000000000000000000000000000000000042b85aae7d60c42c00000000000000000000000000000000000000000000000000000001c94ebec37000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000a5e0829caced8ffdd4de3c43696c57f7d7a678ff000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000030000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000000d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000000000000b446f646f5632000000000000000000000000000000000000000000000000000000000000000000042b85aae7d60c42c00000000000000000000000000000000000000000000000000000001db5156c13000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000400000000000000000000000005333eb1e32522f1893b7c9fea3c263807a02d561000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000012556e69737761705633000000000000000000000000000000000000000000000000000000000000190522016f044a05b0000000000000000000000000000000000000000000000000000000b08217af9400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000060000000000000000000000000e592427a0aece92de3edee1f18e0157c058615640000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012556e697377617056330000000000000000000000000000000000000000000000000000000000000c829100b78224ef50000000000000000000000000000000000000000000000000000000570157389f000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000427ceb23fd6bc0add59e62ac25578270cff1b9f6190001f41bfd67037b42cf73acf2047067bd4f2c47d9bfd6000bb82791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f619000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000869584cd0000000000000000000000008c611defbd838a13de3a5923693c58a7c1807c6300000000000000000000000000000000000000000000005b89d96b4863067a6b', chainId: 137, verifyingContract: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', expirationTimeSeconds: new BigNumber('9990868679'), feeToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', fees: [ { recipient: integratorAddress, amount: new BigNumber(1000000000000000000), }, { recipient: zeroExAddress, amount: new BigNumber(1000000000000000), }, ], salt: new BigNumber(12), sender: '0x0000000000000000000000000000000000000000', signer: '0x4c42a706410f1190f97d26fe3c999c90070aa40f', }); const metaTransactionV1EndpointPrice: FetchIndicativeQuoteResponse = { allowanceTarget: '0x12345', buyAmount: new BigNumber(1800054805473), sellAmount: new BigNumber(1000000000000000000000), buyTokenAddress: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', sellTokenAddress: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', gas: new BigNumber(1043459), price: new BigNumber(1800.054805), }; const metaTransactionV2EndpointPrice: FetchIndicativeQuoteResponse = { allowanceTarget: '0x12345', buyAmount: new BigNumber(1800054805473), sellAmount: new BigNumber(1000000000000000000000), buyTokenAddress: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', sellTokenAddress: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', price: new BigNumber(1800.054805), estimatedPriceImpact: new BigNumber(10), }; const sources: LiquiditySource[] = [ { name: 'QuickSwap', proportion: new BigNumber('0.2308'), }, { name: 'DODO_V2', proportion: new BigNumber('0.07692'), }, { name: 'Uniswap_V3', proportion: new BigNumber('0.6923'), }, ]; const fees: Fees = { integratorFee: { type: 'volume', feeToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', feeAmount: new BigNumber(1000000000000000000), feeRecipient: integratorAddress, billingType: 'on-chain', volumePercentage: new BigNumber(0.1), }, zeroExFee: { type: 'integrator_share', feeToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', feeAmount: new BigNumber(1000000000000000), feeRecipient: zeroExAddress, billingType: 'on-chain', integratorSharePercentage: new BigNumber(0.1), }, gasFee: { type: 'gas', gasPrice: new BigNumber(115200000000), feeToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', feeAmount: new BigNumber(10000000), feeRecipient: null, billingType: 'off-chain', estimatedGas: new BigNumber(1043459), feeTokenAmountPerWei: new BigNumber(0.001), }, }; const expiry = new BigNumber('9999999999999999'); const otcOrder = new OtcOrder({ txOrigin: '0x0000000000000000000000000000000000000000', taker: '0x1111111111111111111111111111111111111111', maker: '0x2222222222222222222222222222222222222222', makerToken: '0x3333333333333333333333333333333333333333', takerToken: '0x4444444444444444444444444444444444444444', expiryAndNonce: OtcOrder.encodeExpiryAndNonce(expiry, ZERO, expiry), chainId: 1337, verifyingContract: '0x0000000000000000000000000000000000000000', }); const otcQuote: OtcOrderRfqmQuoteResponse = { allowanceTarget: '0x12345', buyAmount: new BigNumber(1800054805473), sellAmount: new BigNumber(1000000000000000000000), buyTokenAddress: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', sellTokenAddress: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', gas: new BigNumber(1043459), price: new BigNumber(1800.054805), type: GaslessTypes.OtcOrder, order: otcOrder, orderHash: otcOrder.getHash(), }; beforeEach(() => { mockBlockchainUtils.getExchangeProxyAddress.mockReturnValue('0x12345'); jest.clearAllMocks(); }); describe('fetchPriceAsync', () => { describe('zero-g', () => { it('gets an RFQ price if available', async () => { mockRfqmService.fetchIndicativeQuoteAsync.mockResolvedValueOnce(metaTransactionV1EndpointPrice); const result = (await gaslessSwapService.fetchPriceAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.OtcOrder], }, GaslessSwapServiceTypes.ZeroG, )) as FetchIndicativeQuoteResponse & { liquiditySource: 'rfq' | 'amm' }; expect(result?.liquiditySource).toEqual('rfq'); expect(result).toMatchInlineSnapshot(` { "allowanceTarget": "0x12345", "buyAmount": "1800054805473", "buyTokenAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "gas": "1043459", "liquiditySource": "rfq", "price": "1800.054805", "sellAmount": "1000000000000000000000", "sellTokenAddress": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", } `); expect(getMetaTransactionV1QuoteAsyncMock).not.toBeCalled(); }); it('gets an AMM price if no RFQ liquidity is available', async () => { getMetaTransactionV1QuoteAsyncMock.mockResolvedValueOnce({ trade: { kind: GaslessTypes.MetaTransaction, hash: metaTransactionV1.getHash(), metaTransaction: metaTransactionV1, }, price: metaTransactionV1EndpointPrice, }); mockRfqmService.fetchIndicativeQuoteAsync.mockResolvedValueOnce(null); const result = (await gaslessSwapService.fetchPriceAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.OtcOrder], }, GaslessSwapServiceTypes.ZeroG, )) as FetchIndicativeQuoteResponse & { liquiditySource: 'rfq' | 'amm' }; expect(result?.liquiditySource).toEqual('amm'); expect(result).toMatchInlineSnapshot(` { "allowanceTarget": "0x12345", "buyAmount": "1800054805473", "buyTokenAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "gas": "1043459", "liquiditySource": "amm", "price": "1800.054805", "sellAmount": "1000000000000000000000", "sellTokenAddress": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", } `); }); it('gets an AMM price if RFQ request throws', async () => { getMetaTransactionV1QuoteAsyncMock.mockResolvedValueOnce({ trade: { kind: GaslessTypes.MetaTransaction, hash: metaTransactionV1.getHash(), metaTransaction: metaTransactionV1, }, price: metaTransactionV1EndpointPrice, }); mockRfqmService.fetchIndicativeQuoteAsync.mockImplementationOnce(() => { throw new Error('rfqm quote threw up'); }); const result = (await gaslessSwapService.fetchPriceAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.OtcOrder], }, GaslessSwapServiceTypes.ZeroG, )) as FetchIndicativeQuoteResponse & { liquiditySource: 'rfq' | 'amm' }; expect(result?.liquiditySource).toEqual('amm'); expect(result).toMatchInlineSnapshot(` { "allowanceTarget": "0x12345", "buyAmount": "1800054805473", "buyTokenAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "gas": "1043459", "liquiditySource": "amm", "price": "1800.054805", "sellAmount": "1000000000000000000000", "sellTokenAddress": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", } `); }); it('returns `null` if no liquidity is available', async () => { getMetaTransactionV1QuoteAsyncMock.mockResolvedValueOnce(null); mockRfqmService.fetchIndicativeQuoteAsync.mockResolvedValueOnce(null); const result = await gaslessSwapService.fetchPriceAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.OtcOrder], }, GaslessSwapServiceTypes.ZeroG, ); expect(result).toBeNull(); }); it('throws if AMM request throws and RFQ has no liquidity / request throws', async () => { mockRfqmService.fetchIndicativeQuoteAsync.mockImplementationOnce(() => { throw new Error('rfqm price threw up'); }); getMetaTransactionV1QuoteAsyncMock.mockImplementationOnce(() => { throw new Error('amm price threw up'); }); await expect(() => gaslessSwapService.fetchPriceAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.OtcOrder], }, GaslessSwapServiceTypes.ZeroG, ), ).rejects.toThrow('Error fetching price'); }); it('throws validation error if AMM quote throws validation error', async () => { getMetaTransactionV1QuoteAsyncMock.mockImplementation(() => { throw new ValidationError([ { field: 'sellAmount', code: ValidationErrorCodes.FieldInvalid, reason: 'sellAmount too small', }, ]); }); mockRfqmService.fetchFirmQuoteAsync.mockResolvedValue({ quote: null, quoteReportId: null }); await expect(() => gaslessSwapService.fetchPriceAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.OtcOrder], }, GaslessSwapServiceTypes.ZeroG, ), ).rejects.toThrow(ValidationError); }); }); describe('tx relay', () => { it('gets a meta-transaction price when requesting meta-transaction v1 back', async () => { getMetaTransactionV2QuoteAsyncMock.mockResolvedValueOnce({ trade: { kind: GaslessTypes.MetaTransaction, hash: metaTransactionV1.getHash(), metaTransaction: metaTransactionV1, }, price: metaTransactionV2EndpointPrice, sources, fees, }); mockRfqmService.fetchIndicativeQuoteAsync.mockResolvedValueOnce(null); const result = (await gaslessSwapService.fetchPriceAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, feeType: 'volume', feeRecipient: integratorAddress, feeSellTokenPercentage: new BigNumber(0.1), acceptedTypes: [GaslessTypes.MetaTransaction], }, GaslessSwapServiceTypes.TxRelay, )) as FetchIndicativeQuoteResponse & { sources: LiquiditySource[]; fees?: Fees }; expect(getMetaTransactionV2QuoteAsyncMock.mock.calls[0][2].metaTransactionVersion).toEqual('v1'); expect(result).toMatchInlineSnapshot(` { "allowanceTarget": "0x12345", "buyAmount": "1800054805473", "buyTokenAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "estimatedPriceImpact": "10", "fees": { "gasFee": { "feeAmount": "10000000", "feeToken": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "feeType": "gas", }, "integratorFee": { "feeAmount": "1000000000000000000", "feeToken": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "feeType": "volume", }, "zeroExFee": { "feeAmount": "1000000000000000", "feeToken": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "feeType": "integrator_share", }, }, "price": "1800.054805", "sellAmount": "1000000000000000000000", "sellTokenAddress": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "sources": [ { "name": "QuickSwap", "proportion": "0.2308", }, { "name": "DODO_V2", "proportion": "0.07692", }, { "name": "Uniswap_V3", "proportion": "0.6923", }, ], } `); }); it('gets a meta-transaction price when requesting meta-transaction v2 back', async () => { getMetaTransactionV2QuoteAsyncMock.mockResolvedValueOnce({ trade: { kind: GaslessTypes.MetaTransactionV2, hash: metaTransactionV2.getHash(), metaTransaction: metaTransactionV2, }, price: metaTransactionV2EndpointPrice, sources, fees, }); mockRfqmService.fetchIndicativeQuoteAsync.mockResolvedValueOnce(null); const result = (await gaslessSwapService.fetchPriceAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, feeType: 'volume', feeRecipient: integratorAddress, feeSellTokenPercentage: new BigNumber(0.1), acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.MetaTransactionV2], }, GaslessSwapServiceTypes.TxRelay, )) as FetchIndicativeQuoteResponse & { sources: LiquiditySource[]; fees?: Fees }; expect(getMetaTransactionV2QuoteAsyncMock.mock.calls[0][2].metaTransactionVersion).toEqual('v2'); expect(result).toMatchInlineSnapshot(` { "allowanceTarget": "0x12345", "buyAmount": "1800054805473", "buyTokenAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "estimatedPriceImpact": "10", "fees": { "gasFee": { "feeAmount": "10000000", "feeToken": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "feeType": "gas", }, "integratorFee": { "feeAmount": "1000000000000000000", "feeToken": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "feeType": "volume", }, "zeroExFee": { "feeAmount": "1000000000000000", "feeToken": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "feeType": "integrator_share", }, }, "price": "1800.054805", "sellAmount": "1000000000000000000000", "sellTokenAddress": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "sources": [ { "name": "QuickSwap", "proportion": "0.2308", }, { "name": "DODO_V2", "proportion": "0.07692", }, { "name": "Uniswap_V3", "proportion": "0.6923", }, ], } `); }); it('returns `null` if no liquidity is available', async () => { getMetaTransactionV2QuoteAsyncMock.mockResolvedValueOnce(null); const result = await gaslessSwapService.fetchPriceAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, feeType: 'volume', feeRecipient: integratorAddress, feeSellTokenPercentage: new BigNumber(0.1), acceptedTypes: [GaslessTypes.MetaTransaction], }, GaslessSwapServiceTypes.TxRelay, ); expect(getMetaTransactionV2QuoteAsyncMock.mock.calls[0][2].metaTransactionVersion).toEqual('v1'); expect(result).toBeNull(); }); it('throws if meta-transaction request throws', async () => { getMetaTransactionV2QuoteAsyncMock.mockImplementationOnce(() => { throw new Error('meta-transaction price throws'); }); await expect(() => gaslessSwapService.fetchPriceAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, feeType: 'volume', feeRecipient: integratorAddress, feeSellTokenPercentage: new BigNumber(0.1), acceptedTypes: [GaslessTypes.MetaTransactionV2], }, GaslessSwapServiceTypes.TxRelay, ), ).rejects.toThrow('Error fetching price'); }); it('throws validation error if meta-transaction v2 quote throws validation error', async () => { getMetaTransactionV2QuoteAsyncMock.mockImplementation(() => { throw new ValidationError([ { field: 'sellAmount', code: ValidationErrorCodes.FieldInvalid, reason: 'sellAmount too small', }, ]); }); await expect(() => gaslessSwapService.fetchPriceAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', feeType: 'volume', feeRecipient: integratorAddress, feeSellTokenPercentage: new BigNumber(0.1), acceptedTypes: [GaslessTypes.MetaTransaction], }, GaslessSwapServiceTypes.TxRelay, ), ).rejects.toThrow(ValidationError); }); }); }); describe('fetchQuoteAsync', () => { describe('zero-g', () => { it('gets an RFQ quote if available', async () => { mockRfqmService.fetchFirmQuoteAsync.mockResolvedValueOnce({ quote: otcQuote, quoteReportId: null }); const result = (await gaslessSwapService.fetchQuoteAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: false, acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.OtcOrder], }, GaslessSwapServiceTypes.ZeroG, )) as OtcOrderRfqmQuoteResponse & { liquiditySource: 'rfq' | 'amm' }; expect(result).not.toBeNull(); expect(result?.type).toEqual(GaslessTypes.OtcOrder); expect(result).toMatchInlineSnapshot(` { "allowanceTarget": "0x12345", "buyAmount": "1800054805473", "buyTokenAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "gas": "1043459", "liquiditySource": "rfq", "order": OtcOrder { "chainId": 1337, "expiry": "9999999999999999", "expiryAndNonce": "62771017353866801361256158845395900325234131236973929026614555535965487103", "maker": "0x2222222222222222222222222222222222222222", "makerAmount": "0", "makerToken": "0x3333333333333333333333333333333333333333", "nonce": "9999999999999999", "nonceBucket": "0", "taker": "0x1111111111111111111111111111111111111111", "takerAmount": "0", "takerToken": "0x4444444444444444444444444444444444444444", "txOrigin": "0x0000000000000000000000000000000000000000", "verifyingContract": "0x0000000000000000000000000000000000000000", }, "orderHash": "0xec6ca6e8c744adf7c3e61f670ca6b5153efc8dc74efec40094ab98b49094e9f3", "price": "1800.054805", "sellAmount": "1000000000000000000000", "sellTokenAddress": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "type": "otc", } `); expect(getMetaTransactionV1QuoteAsyncMock).not.toBeCalled(); }); it('gets an AMM quote if no RFQ liquidity is available', async () => { getMetaTransactionV1QuoteAsyncMock.mockResolvedValueOnce({ trade: { kind: GaslessTypes.MetaTransaction, hash: metaTransactionV1.getHash(), metaTransaction: metaTransactionV1, }, price: metaTransactionV1EndpointPrice, }); mockRfqmService.fetchFirmQuoteAsync.mockResolvedValueOnce({ quote: null, quoteReportId: null }); const result = (await gaslessSwapService.fetchQuoteAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: false, acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.OtcOrder], }, GaslessSwapServiceTypes.ZeroG, )) as MetaTransactionV1QuoteResponse & { liquiditySource: 'rfq' | 'amm' }; expect(result).not.toBeNull(); expect(result?.type).toEqual(GaslessTypes.MetaTransaction); if (result?.type !== GaslessTypes.MetaTransaction) { // Refine type for further assertions throw new Error('Result should be a meta transaction'); } expect(result.metaTransaction.getHash()).toEqual(metaTransactionV1.getHash()); expect(result).toMatchInlineSnapshot(` { "allowanceTarget": "0x12345", "approval": undefined, "buyAmount": "1800054805473", "buyTokenAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "gas": "1043459", "liquiditySource": "amm", "metaTransaction": MetaTransaction { "callData": "0x415565b00000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000000000017b9e2a304f00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000860000000000000000000000000000000000000000000000000000000000000086000000000000000000000000000000000000000000000000000000000000007c000000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000003400000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000052000000000000000000000000000000002517569636b5377617000000000000000000000000000000000000000000000000000000000000008570b55cfac18858000000000000000000000000000000000000000000000000000000039d0b9efd1000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000a5e0829caced8ffdd4de3c43696c57f7d7a678ff000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000002517569636b53776170000000000000000000000000000000000000000000000000000000000000042b85aae7d60c42c00000000000000000000000000000000000000000000000000000001c94ebec37000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000a5e0829caced8ffdd4de3c43696c57f7d7a678ff000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000030000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000000d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000000000000b446f646f5632000000000000000000000000000000000000000000000000000000000000000000042b85aae7d60c42c00000000000000000000000000000000000000000000000000000001db5156c13000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000400000000000000000000000005333eb1e32522f1893b7c9fea3c263807a02d561000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000012556e69737761705633000000000000000000000000000000000000000000000000000000000000190522016f044a05b0000000000000000000000000000000000000000000000000000000b08217af9400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000060000000000000000000000000e592427a0aece92de3edee1f18e0157c058615640000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012556e697377617056330000000000000000000000000000000000000000000000000000000000000c829100b78224ef50000000000000000000000000000000000000000000000000000000570157389f000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000427ceb23fd6bc0add59e62ac25578270cff1b9f6190001f41bfd67037b42cf73acf2047067bd4f2c47d9bfd6000bb82791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f619000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000869584cd0000000000000000000000008c611defbd838a13de3a5923693c58a7c1807c6300000000000000000000000000000000000000000000005b89d96b4863067a6b", "chainId": 137, "expirationTimeSeconds": "9990868679", "feeAmount": "0", "feeToken": "0x0000000000000000000000000000000000000000", "maxGasPrice": "4294967296", "minGasPrice": "1", "salt": "32606650794224190000000000000000000000000000000000000000000000000000000000000", "sender": "0x0000000000000000000000000000000000000000", "signer": "0x4c42a706410f1190f97d26fe3c999c90070aa40f", "value": "0", "verifyingContract": "0xdef1c0ded9bec7f1a1670819833240f027b25eff", }, "metaTransactionHash": "0xde5a11983edd012047dd3107532f007a73ae488bfb354f35b8a40580e2a775a1", "price": "1800.054805", "sellAmount": "1000000000000000000000", "sellTokenAddress": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "type": "metatransaction", } `); }); it('throws validation error if AMM quote throws validation error', async () => { getMetaTransactionV1QuoteAsyncMock.mockImplementation(() => { throw new ValidationError([ { field: 'sellAmount', code: ValidationErrorCodes.FieldInvalid, reason: 'sellAmount too small', }, ]); }); mockRfqmService.fetchFirmQuoteAsync.mockResolvedValue({ quote: null, quoteReportId: null }); await expect(() => gaslessSwapService.fetchQuoteAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: false, acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.OtcOrder], }, GaslessSwapServiceTypes.ZeroG, ), ).rejects.toThrow(ValidationError); }); it('adds an affiliate address if one is included in the integrator configuration but not in the quote request', async () => { getMetaTransactionV1QuoteAsyncMock.mockResolvedValueOnce({ trade: { kind: GaslessTypes.MetaTransaction, hash: metaTransactionV1.getHash(), metaTransaction: metaTransactionV1, }, price: metaTransactionV1EndpointPrice, }); mockRfqmService.fetchFirmQuoteAsync.mockResolvedValueOnce({ quote: null, quoteReportId: null }); await gaslessSwapService.fetchQuoteAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: { affiliateAddress: '0xaffiliateAddress' } as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: false, acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.OtcOrder], }, GaslessSwapServiceTypes.ZeroG, ); expect(getMetaTransactionV1QuoteAsyncMock.mock.calls[0][/* params */ 2]['affiliateAddress']).toEqual( '0xaffiliateAddress', ); }); it('uses the affiliate address in the quote request even if one is present in integrator configuration', async () => { getMetaTransactionV1QuoteAsyncMock.mockResolvedValueOnce({ trade: { kind: GaslessTypes.MetaTransaction, hash: metaTransactionV1.getHash(), metaTransaction: metaTransactionV1, }, price: metaTransactionV1EndpointPrice, }); mockRfqmService.fetchFirmQuoteAsync.mockResolvedValueOnce({ quote: null, quoteReportId: null }); await gaslessSwapService.fetchQuoteAsync( { affiliateAddress: '0xaffiliateAddressShouldUse', buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: { affiliateAddress: '0xaffiliateAddressShouldntUse' } as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: false, acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.OtcOrder], }, GaslessSwapServiceTypes.ZeroG, ); expect(getMetaTransactionV1QuoteAsyncMock.mock.calls[0][/* params */ 2]['affiliateAddress']).toEqual( '0xaffiliateAddressShouldUse', ); }); it('returns `null` if no liquidity is available', async () => { mockRfqmService.fetchFirmQuoteAsync.mockResolvedValueOnce({ quote: null, quoteReportId: null }); getMetaTransactionV1QuoteAsyncMock.mockResolvedValueOnce(null); const result = await gaslessSwapService.fetchQuoteAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: false, acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.OtcOrder], }, GaslessSwapServiceTypes.ZeroG, ); expect(result).toBeNull(); }); it('throws if AMM request throws and RFQ has no liquidity / request throws', async () => { mockRfqmService.fetchFirmQuoteAsync.mockImplementationOnce(() => { throw new Error('rfqm price threw up'); }); getMetaTransactionV1QuoteAsyncMock.mockImplementationOnce(() => { throw new Error('amm price threw up'); }); await expect(() => gaslessSwapService.fetchQuoteAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: false, acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.OtcOrder], }, GaslessSwapServiceTypes.ZeroG, ), ).rejects.toThrow('Error fetching quote'); }); it('stores a metatransaction hash', async () => { getMetaTransactionV1QuoteAsyncMock.mockResolvedValueOnce({ trade: { kind: GaslessTypes.MetaTransaction, hash: metaTransactionV1.getHash(), metaTransaction: metaTransactionV1, }, price: metaTransactionV1EndpointPrice, }); mockRfqmService.fetchFirmQuoteAsync.mockResolvedValueOnce({ quote: null, quoteReportId: null }); await gaslessSwapService.fetchQuoteAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: false, acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.OtcOrder], }, GaslessSwapServiceTypes.ZeroG, ); expect(mockRedis.set).toBeCalledWith( `metaTransactionHash.${metaTransactionV1.getHash()}`, 0, 'EX', 900, ); }); it('gets the approval object', async () => { const approval: ApprovalResponse = { isRequired: true, isGaslessAvailable: true, type: MOCK_EXECUTE_META_TRANSACTION_APPROVAL.kind, eip712: MOCK_EXECUTE_META_TRANSACTION_APPROVAL.eip712, }; getMetaTransactionV1QuoteAsyncMock.mockResolvedValueOnce({ trade: { kind: GaslessTypes.MetaTransaction, hash: metaTransactionV1.getHash(), metaTransaction: metaTransactionV1, }, price: metaTransactionV1EndpointPrice, }); mockRfqmService.fetchFirmQuoteAsync.mockResolvedValueOnce({ quote: null, quoteReportId: null }); mockRfqmService.getGaslessApprovalResponseAsync.mockResolvedValueOnce(approval); const result = await gaslessSwapService.fetchQuoteAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: true, acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.OtcOrder], }, GaslessSwapServiceTypes.ZeroG, ); expect(Object.keys(result?.approval?.eip712?.domain ?? {})).toEqual([ 'name', 'version', 'verifyingContract', 'salt', ]); expect(result?.approval).toEqual(approval); }); }); describe('tx relay', () => { it('gets a meta-transaction quote when requesting meta-transaction v1 back', async () => { mockBlockchainUtils.computeEip712Hash.mockReturnValueOnce( '0xde5a11983edd012047dd3107532f007a73ae488bfb354f35b8a40580e2a775a1', ); getMetaTransactionV2QuoteAsyncMock.mockResolvedValueOnce({ trade: { kind: GaslessTypes.MetaTransaction, hash: metaTransactionV1.getHash(), metaTransaction: metaTransactionV1, }, price: metaTransactionV2EndpointPrice, sources, fees, }); const result = (await gaslessSwapService.fetchQuoteAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: false, feeType: 'volume', feeRecipient: integratorAddress, feeSellTokenPercentage: new BigNumber(0.1), acceptedTypes: [GaslessTypes.MetaTransaction], }, GaslessSwapServiceTypes.TxRelay, )) as MetaTransactionV2QuoteResponse; expect(getMetaTransactionV2QuoteAsyncMock.mock.calls[0][2].metaTransactionVersion).toEqual('v1'); expect(result).not.toBeNull(); expect(result?.trade.type).toEqual(GaslessTypes.MetaTransaction); expect(result?.trade.hash).toEqual(metaTransactionV1.getHash()); // Make sure the domaim has the correct field order. // `toMatchInlineSnapshot` would sort fields in object in alphabetical order. expect(Object.keys(result?.trade.eip712.domain)).toEqual([ 'name', 'version', 'chainId', 'verifyingContract', ]); expect(result).toMatchInlineSnapshot(` { "allowanceTarget": "0x12345", "approval": undefined, "buyAmount": "1800054805473", "buyTokenAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "estimatedPriceImpact": "10", "fees": { "gasFee": { "feeAmount": "10000000", "feeToken": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "feeType": "gas", }, "integratorFee": { "feeAmount": "1000000000000000000", "feeToken": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "feeType": "volume", }, "zeroExFee": { "feeAmount": "1000000000000000", "feeToken": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "feeType": "integrator_share", }, }, "price": "1800.054805", "sellAmount": "1000000000000000000000", "sellTokenAddress": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "sources": [ { "name": "QuickSwap", "proportion": "0.2308", }, { "name": "DODO_V2", "proportion": "0.07692", }, { "name": "Uniswap_V3", "proportion": "0.6923", }, ], "trade": { "eip712": { "domain": { "chainId": 1337, "name": "ZeroEx", "verifyingContract": "0x5315e44798395d4a952530d131249fe00f554565", "version": "1.0.0", }, "message": { "callData": "0x415565b00000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000000000017b9e2a304f00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000860000000000000000000000000000000000000000000000000000000000000086000000000000000000000000000000000000000000000000000000000000007c000000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000003400000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000052000000000000000000000000000000002517569636b5377617000000000000000000000000000000000000000000000000000000000000008570b55cfac18858000000000000000000000000000000000000000000000000000000039d0b9efd1000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000a5e0829caced8ffdd4de3c43696c57f7d7a678ff000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000002517569636b53776170000000000000000000000000000000000000000000000000000000000000042b85aae7d60c42c00000000000000000000000000000000000000000000000000000001c94ebec37000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000a5e0829caced8ffdd4de3c43696c57f7d7a678ff000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000030000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000000d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000000000000b446f646f5632000000000000000000000000000000000000000000000000000000000000000000042b85aae7d60c42c00000000000000000000000000000000000000000000000000000001db5156c13000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000400000000000000000000000005333eb1e32522f1893b7c9fea3c263807a02d561000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000012556e69737761705633000000000000000000000000000000000000000000000000000000000000190522016f044a05b0000000000000000000000000000000000000000000000000000000b08217af9400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000060000000000000000000000000e592427a0aece92de3edee1f18e0157c058615640000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012556e697377617056330000000000000000000000000000000000000000000000000000000000000c829100b78224ef50000000000000000000000000000000000000000000000000000000570157389f000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000427ceb23fd6bc0add59e62ac25578270cff1b9f6190001f41bfd67037b42cf73acf2047067bd4f2c47d9bfd6000bb82791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f619000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000869584cd0000000000000000000000008c611defbd838a13de3a5923693c58a7c1807c6300000000000000000000000000000000000000000000005b89d96b4863067a6b", "expirationTimeSeconds": "9990868679", "feeAmount": "0", "feeToken": "0x0000000000000000000000000000000000000000", "maxGasPrice": "4294967296", "minGasPrice": "1", "salt": "32606650794224190000000000000000000000000000000000000000000000000000000000000", "sender": "0x0000000000000000000000000000000000000000", "signer": "0x4c42a706410f1190f97d26fe3c999c90070aa40f", "value": "0", }, "primaryType": "MetaTransactionData", "types": { "EIP712Domain": [ { "name": "name", "type": "string", }, { "name": "version", "type": "string", }, { "name": "chainId", "type": "uint256", }, { "name": "verifyingContract", "type": "address", }, ], "MetaTransactionData": [ { "name": "signer", "type": "address", }, { "name": "sender", "type": "address", }, { "name": "minGasPrice", "type": "uint256", }, { "name": "maxGasPrice", "type": "uint256", }, { "name": "expirationTimeSeconds", "type": "uint256", }, { "name": "salt", "type": "uint256", }, { "name": "callData", "type": "bytes", }, { "name": "value", "type": "uint256", }, { "name": "feeToken", "type": "address", }, { "name": "feeAmount", "type": "uint256", }, ], }, }, "hash": "0xde5a11983edd012047dd3107532f007a73ae488bfb354f35b8a40580e2a775a1", "type": "metatransaction", }, } `); }); it('gets a meta-transaction quote when requesting meta-transaction v2 back', async () => { mockBlockchainUtils.computeEip712Hash.mockReturnValueOnce( '0xde5a11983edd012047dd3107532f007a73ae488bfb354f35b8a40580e2a775a1', ); getMetaTransactionV2QuoteAsyncMock.mockResolvedValueOnce({ trade: { kind: GaslessTypes.MetaTransactionV2, hash: metaTransactionV2.getHash(), metaTransaction: metaTransactionV2, }, price: metaTransactionV2EndpointPrice, sources, fees, }); const result = (await gaslessSwapService.fetchQuoteAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: false, feeType: 'volume', feeRecipient: integratorAddress, feeSellTokenPercentage: new BigNumber(0.1), acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.MetaTransactionV2], }, GaslessSwapServiceTypes.TxRelay, )) as MetaTransactionV2QuoteResponse; expect(getMetaTransactionV2QuoteAsyncMock.mock.calls[0][2].metaTransactionVersion).toEqual('v2'); expect(result).not.toBeNull(); expect(result?.trade.type).toEqual(GaslessTypes.MetaTransactionV2); expect(result?.trade.hash).toEqual(metaTransactionV2.getHash()); // Make sure the domaim has the correct field order. // `toMatchInlineSnapshot` would sort fields in object in alphabetical order. expect(Object.keys(result?.trade.eip712.domain)).toEqual([ 'name', 'version', 'chainId', 'verifyingContract', ]); expect(result).toMatchInlineSnapshot(` { "allowanceTarget": "0x12345", "approval": undefined, "buyAmount": "1800054805473", "buyTokenAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "estimatedPriceImpact": "10", "fees": { "gasFee": { "feeAmount": "10000000", "feeToken": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "feeType": "gas", }, "integratorFee": { "feeAmount": "1000000000000000000", "feeToken": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "feeType": "volume", }, "zeroExFee": { "feeAmount": "1000000000000000", "feeToken": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "feeType": "integrator_share", }, }, "price": "1800.054805", "sellAmount": "1000000000000000000000", "sellTokenAddress": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "sources": [ { "name": "QuickSwap", "proportion": "0.2308", }, { "name": "DODO_V2", "proportion": "0.07692", }, { "name": "Uniswap_V3", "proportion": "0.6923", }, ], "trade": { "eip712": { "domain": { "chainId": 1337, "name": "ZeroEx", "verifyingContract": "0x5315e44798395d4a952530d131249fe00f554565", "version": "1.0.0", }, "message": { "callData": "0x415565b00000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000000000017b9e2a304f00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000860000000000000000000000000000000000000000000000000000000000000086000000000000000000000000000000000000000000000000000000000000007c000000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000003400000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000052000000000000000000000000000000002517569636b5377617000000000000000000000000000000000000000000000000000000000000008570b55cfac18858000000000000000000000000000000000000000000000000000000039d0b9efd1000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000a5e0829caced8ffdd4de3c43696c57f7d7a678ff000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000002517569636b53776170000000000000000000000000000000000000000000000000000000000000042b85aae7d60c42c00000000000000000000000000000000000000000000000000000001c94ebec37000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000a5e0829caced8ffdd4de3c43696c57f7d7a678ff000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000030000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000000d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000000000000b446f646f5632000000000000000000000000000000000000000000000000000000000000000000042b85aae7d60c42c00000000000000000000000000000000000000000000000000000001db5156c13000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000400000000000000000000000005333eb1e32522f1893b7c9fea3c263807a02d561000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000012556e69737761705633000000000000000000000000000000000000000000000000000000000000190522016f044a05b0000000000000000000000000000000000000000000000000000000b08217af9400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000060000000000000000000000000e592427a0aece92de3edee1f18e0157c058615640000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012556e697377617056330000000000000000000000000000000000000000000000000000000000000c829100b78224ef50000000000000000000000000000000000000000000000000000000570157389f000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000427ceb23fd6bc0add59e62ac25578270cff1b9f6190001f41bfd67037b42cf73acf2047067bd4f2c47d9bfd6000bb82791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f619000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000869584cd0000000000000000000000008c611defbd838a13de3a5923693c58a7c1807c6300000000000000000000000000000000000000000000005b89d96b4863067a6b", "expirationTimeSeconds": "9990868679", "feeToken": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "fees": [ { "amount": "1000000000000000000", "recipient": "0x4ea754349ace5303c82f0d1d491041e042f2ad22", }, { "amount": "1000000000000000", "recipient": "0x4ea754349ace5303c82f0d1d491041e042f2ad22", }, ], "salt": "12", "sender": "0x0000000000000000000000000000000000000000", "signer": "0x4c42a706410f1190f97d26fe3c999c90070aa40f", }, "primaryType": "MetaTransactionDataV2", "types": { "EIP712Domain": [ { "name": "name", "type": "string", }, { "name": "version", "type": "string", }, { "name": "chainId", "type": "uint256", }, { "name": "verifyingContract", "type": "address", }, ], "MetaTransactionDataV2": [ { "name": "signer", "type": "address", }, { "name": "sender", "type": "address", }, { "name": "expirationTimeSeconds", "type": "uint256", }, { "name": "salt", "type": "uint256", }, { "name": "callData", "type": "bytes", }, { "name": "feeToken", "type": "address", }, { "name": "fees", "type": "MetaTransactionFeeData[]", }, ], "MetaTransactionFeeData": [ { "name": "recipient", "type": "address", }, { "name": "amount", "type": "uint256", }, ], }, }, "hash": "0x064362764b6a640d4a1c56acf599363f038891a41b9c1e1b9d5c22eb9c98953f", "type": "metatransaction_v2", }, } `); }); it('throws validation error if meta-transaction throws validation error', async () => { getMetaTransactionV2QuoteAsyncMock.mockImplementation(() => { throw new ValidationError([ { field: 'sellAmount', code: ValidationErrorCodes.FieldInvalid, reason: 'sellAmount too small', }, ]); }); await expect(() => gaslessSwapService.fetchQuoteAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: false, feeType: 'volume', feeRecipient: integratorAddress, feeSellTokenPercentage: new BigNumber(0.1), acceptedTypes: [GaslessTypes.MetaTransaction], }, GaslessSwapServiceTypes.TxRelay, ), ).rejects.toThrow(ValidationError); }); it('adds an affiliate address if one is included in the integrator configuration but not in the quote request', async () => { mockBlockchainUtils.computeEip712Hash.mockReturnValueOnce( '0xde5a11983edd012047dd3107532f007a73ae488bfb354f35b8a40580e2a775a1', ); getMetaTransactionV2QuoteAsyncMock.mockResolvedValueOnce({ trade: { kind: GaslessTypes.MetaTransaction, hash: metaTransactionV1.getHash(), metaTransaction: metaTransactionV1, }, price: metaTransactionV2EndpointPrice, sources, fees, }); await gaslessSwapService.fetchQuoteAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: { affiliateAddress: '0xaffiliateAddress' } as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: false, feeType: 'volume', feeRecipient: integratorAddress, feeSellTokenPercentage: new BigNumber(0.1), acceptedTypes: [GaslessTypes.MetaTransaction], }, GaslessSwapServiceTypes.TxRelay, ); expect(getMetaTransactionV2QuoteAsyncMock.mock.calls[0][2].metaTransactionVersion).toEqual('v1'); expect(getMetaTransactionV2QuoteAsyncMock.mock.calls[0][/* params */ 2].affiliateAddress).toEqual( '0xaffiliateAddress', ); }); it('uses the affiliate address in the quote request even if one is present in integrator configuration', async () => { mockBlockchainUtils.computeEip712Hash.mockReturnValueOnce( '0xde5a11983edd012047dd3107532f007a73ae488bfb354f35b8a40580e2a775a1', ); getMetaTransactionV2QuoteAsyncMock.mockResolvedValueOnce({ trade: { kind: GaslessTypes.MetaTransaction, hash: metaTransactionV1.getHash(), metaTransaction: metaTransactionV1, }, price: metaTransactionV2EndpointPrice, sources, fees, }); await gaslessSwapService.fetchQuoteAsync( { affiliateAddress: '0xaffiliateAddressShouldUse', buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: { affiliateAddress: '0xaffiliateAddressShouldntUse' } as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: false, feeType: 'volume', feeRecipient: integratorAddress, feeSellTokenPercentage: new BigNumber(0.1), acceptedTypes: [GaslessTypes.MetaTransaction], }, GaslessSwapServiceTypes.TxRelay, ); expect(getMetaTransactionV2QuoteAsyncMock.mock.calls[0][2].metaTransactionVersion).toEqual('v1'); expect(getMetaTransactionV2QuoteAsyncMock.mock.calls[0][/* params */ 2].affiliateAddress).toEqual( '0xaffiliateAddressShouldUse', ); }); it('returns `null` if no liquidity is available', async () => { getMetaTransactionV2QuoteAsyncMock.mockResolvedValueOnce(null); const result = await gaslessSwapService.fetchQuoteAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: false, feeType: 'volume', feeRecipient: integratorAddress, feeSellTokenPercentage: new BigNumber(0.1), acceptedTypes: [GaslessTypes.MetaTransaction, GaslessTypes.MetaTransactionV2], }, GaslessSwapServiceTypes.TxRelay, ); expect(getMetaTransactionV2QuoteAsyncMock.mock.calls[0][2].metaTransactionVersion).toEqual('v2'); expect(result).toBeNull(); }); it('throws if meta-transaction request throws', async () => { getMetaTransactionV2QuoteAsyncMock.mockImplementationOnce(() => { throw new Error('meta-transaction request throws'); }); await expect(() => gaslessSwapService.fetchQuoteAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: false, feeType: 'volume', feeRecipient: integratorAddress, feeSellTokenPercentage: new BigNumber(0.1), acceptedTypes: [GaslessTypes.MetaTransaction], }, GaslessSwapServiceTypes.TxRelay, ), ).rejects.toThrow('Error fetching quote'); }); it('stores a metatransaction hash', async () => { mockBlockchainUtils.computeEip712Hash.mockReturnValueOnce( '0xde5a11983edd012047dd3107532f007a73ae488bfb354f35b8a40580e2a775a1', ); getMetaTransactionV2QuoteAsyncMock.mockResolvedValueOnce({ trade: { kind: GaslessTypes.MetaTransaction, hash: metaTransactionV1.getHash(), metaTransaction: metaTransactionV1, }, price: metaTransactionV2EndpointPrice, sources, fees, }); await gaslessSwapService.fetchQuoteAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: false, feeType: 'volume', feeRecipient: integratorAddress, feeSellTokenPercentage: new BigNumber(0.1), acceptedTypes: [GaslessTypes.MetaTransaction], }, GaslessSwapServiceTypes.TxRelay, ); expect(getMetaTransactionV2QuoteAsyncMock.mock.calls[0][2].metaTransactionVersion).toEqual('v1'); expect(mockRedis.set).toBeCalledWith( `metaTransactionHash.${metaTransactionV1.getHash()}`, 0, 'EX', 900, ); }); it('gets the approval object', async () => { mockBlockchainUtils.computeEip712Hash.mockReturnValueOnce( '0xde5a11983edd012047dd3107532f007a73ae488bfb354f35b8a40580e2a775a1', ); const approval: ApprovalResponse = { isRequired: true, isGaslessAvailable: true, type: MOCK_EXECUTE_META_TRANSACTION_APPROVAL.kind, eip712: MOCK_EXECUTE_META_TRANSACTION_APPROVAL.eip712, }; getMetaTransactionV2QuoteAsyncMock.mockResolvedValueOnce({ trade: { kind: GaslessTypes.MetaTransaction, hash: metaTransactionV1.getHash(), metaTransaction: metaTransactionV1, }, price: metaTransactionV2EndpointPrice, sources, fees, }); mockRfqmService.getGaslessApprovalResponseAsync.mockResolvedValueOnce(approval); const result = await gaslessSwapService.fetchQuoteAsync( { buyAmount: new BigNumber(1800054805473), buyToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', buyTokenDecimals: 6, integrator: {} as Integrator, sellToken: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', sellTokenDecimals: 18, takerAddress: '0xtaker', checkApproval: true, feeType: 'volume', feeRecipient: integratorAddress, feeSellTokenPercentage: new BigNumber(0.1), acceptedTypes: [GaslessTypes.MetaTransactionV2], }, GaslessSwapServiceTypes.TxRelay, ); expect(getMetaTransactionV2QuoteAsyncMock.mock.calls[0][2].metaTransactionVersion).toEqual('v2'); expect(Object.keys(result?.approval?.eip712?.domain ?? {})).toEqual([ 'name', 'version', 'verifyingContract', 'salt', ]); expect(result?.approval).toEqual(approval); }); }); }); describe('processSubmitAsync', () => { describe('zero-g', () => { it('fails if the metatransaction is expired', async () => { const expiredMetaTransaction = new MetaTransaction({ callData: '0x415565b00000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000000000017b9e2a304f00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000860000000000000000000000000000000000000000000000000000000000000086000000000000000000000000000000000000000000000000000000000000007c000000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000003400000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000052000000000000000000000000000000002517569636b5377617000000000000000000000000000000000000000000000000000000000000008570b55cfac18858000000000000000000000000000000000000000000000000000000039d0b9efd1000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000a5e0829caced8ffdd4de3c43696c57f7d7a678ff000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000002517569636b53776170000000000000000000000000000000000000000000000000000000000000042b85aae7d60c42c00000000000000000000000000000000000000000000000000000001c94ebec37000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000a5e0829caced8ffdd4de3c43696c57f7d7a678ff000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000030000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000000d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000000000000b446f646f5632000000000000000000000000000000000000000000000000000000000000000000042b85aae7d60c42c00000000000000000000000000000000000000000000000000000001db5156c13000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000400000000000000000000000005333eb1e32522f1893b7c9fea3c263807a02d561000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000012556e69737761705633000000000000000000000000000000000000000000000000000000000000190522016f044a05b0000000000000000000000000000000000000000000000000000000b08217af9400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000060000000000000000000000000e592427a0aece92de3edee1f18e0157c058615640000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012556e697377617056330000000000000000000000000000000000000000000000000000000000000c829100b78224ef50000000000000000000000000000000000000000000000000000000570157389f000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000427ceb23fd6bc0add59e62ac25578270cff1b9f6190001f41bfd67037b42cf73acf2047067bd4f2c47d9bfd6000bb82791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f619000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000869584cd0000000000000000000000008c611defbd838a13de3a5923693c58a7c1807c6300000000000000000000000000000000000000000000005b89d96b4863067a6b', chainId: 137, verifyingContract: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', expirationTimeSeconds: new BigNumber('420'), feeAmount: new BigNumber(0), feeToken: '0x0000000000000000000000000000000000000000', maxGasPrice: new BigNumber(4294967296), minGasPrice: new BigNumber(1), // $eslint-fix-me https://github.com/rhinodavid/eslint-fix-me // eslint-disable-next-line @typescript-eslint/no-loss-of-precision salt: new BigNumber(32606650794224189614795510724011106220035660490560169776986607186708081701146), sender: '0x0000000000000000000000000000000000000000', signer: '0x4C42a706410F1190f97D26Fe3c999c90070aa40F', value: new BigNumber(0), }); await expect(() => gaslessSwapService.processSubmitAsync( { kind: GaslessTypes.MetaTransaction, trade: { metaTransaction: expiredMetaTransaction, type: GaslessTypes.MetaTransaction, signature: { r: '', s: '', signatureType: SignatureType.EthSign, v: 1, }, }, }, 'integratorId', ), ).rejects.toThrowError(ValidationError); }); it("fails if the metatransaction hash doesn't exist in the redis store", async () => { mockRedis.get = jest.fn().mockResolvedValueOnce(null); await expect(() => gaslessSwapService.processSubmitAsync( { kind: GaslessTypes.MetaTransaction, trade: { metaTransaction: metaTransactionV1, type: GaslessTypes.MetaTransaction, signature: { r: '', s: '', signatureType: SignatureType.EthSign, v: 1, }, }, }, 'integratorId', ), ).rejects.toThrowError('MetaTransaction hash not found'); expect(mockRedis.get).toBeCalledWith(`metaTransactionHash.${metaTransactionV1.getHash()}`); }); it('fails if there is already a pending transaction for the taker/taker token', async () => { mockRedis.get = jest.fn().mockResolvedValueOnce({}); mockDbUtils.findMetaTransactionJobsWithStatusesAsync.mockResolvedValueOnce([ new MetaTransactionJobEntity({ chainId: 1337, expiry: metaTransactionV1.expirationTimeSeconds, fee: { amount: metaTransactionV1.feeAmount, token: metaTransactionV1.feeToken, type: 'fixed', }, inputToken: metaTransactionV1EndpointPrice.sellTokenAddress, inputTokenAmount: metaTransactionV1EndpointPrice.sellAmount, integratorId: 'integrator-id', metaTransaction: metaTransactionV1, metaTransactionHash: '0xotherhash', minOutputTokenAmount: new BigNumber(0), outputToken: metaTransactionV1EndpointPrice.buyTokenAddress, status: RfqmJobStatus.PendingProcessing, takerAddress: metaTransactionV1.signer, takerSignature: { r: '', s: '', signatureType: SignatureType.EthSign, v: 1, }, }), ]); await expect(() => gaslessSwapService.processSubmitAsync( { kind: GaslessTypes.MetaTransaction, trade: { metaTransaction: metaTransactionV1, type: GaslessTypes.MetaTransaction, signature: ethSignHashWithKey(metaTransactionV1.getHash(), takerPrivateKey), }, }, 'integratorId', ), ).rejects.toThrowError('pending trade'); }); it('fails if the signature is invalid', async () => { const otherPrivateKey = '0xae4536e2cdee8f32adc77ebe86977a01c6526a32eee7c4c2ccfb1d5ddcddaaa2'; mockRedis.get = jest.fn().mockResolvedValueOnce({}); mockDbUtils.findMetaTransactionJobsWithStatusesAsync.mockResolvedValueOnce([]); await expect(() => gaslessSwapService.processSubmitAsync( { kind: GaslessTypes.MetaTransaction, trade: { metaTransaction: metaTransactionV1, type: GaslessTypes.MetaTransaction, signature: ethSignHashWithKey(metaTransactionV1.getHash(), otherPrivateKey), }, }, 'integratorId', ), ).rejects.toThrow(ValidationError); }); it('fails if taker balance is too low', async () => { mockRedis.get = jest.fn().mockResolvedValueOnce({}); mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync.mockResolvedValueOnce([new BigNumber(21)]); await expect(() => gaslessSwapService.processSubmitAsync( { kind: GaslessTypes.MetaTransaction, trade: { metaTransaction: metaTransactionV1, type: GaslessTypes.MetaTransaction, signature: ethSignHashWithKey(metaTransactionV1.getHash(), takerPrivateKey), }, }, 'integratorId', ), ).rejects.toThrow(ValidationError); }); it('creates a metatransaction job', async () => { mockRedis.get = jest.fn().mockResolvedValueOnce({}); mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync = jest .fn() .mockResolvedValueOnce([metaTransactionV1EndpointPrice.sellAmount]); mockDbUtils.writeMetaTransactionJobAsync.mockResolvedValueOnce({ id: 'id', } as MetaTransactionJobEntity); const result = await gaslessSwapService.processSubmitAsync( { kind: GaslessTypes.MetaTransaction, trade: { metaTransaction: metaTransactionV1, type: GaslessTypes.MetaTransaction, signature: ethSignHashWithKey(metaTransactionV1.getHash(), takerPrivateKey), }, }, 'integratorId', ); expect(result.metaTransactionHash).toEqual(metaTransactionV1.getHash()); expect(result.type).toEqual(GaslessTypes.MetaTransaction); expect(mockSqsProducer.send).toHaveBeenCalledWith({ body: '{"id":"id","type":"metatransaction"}', deduplicationId: 'id', groupId: 'id', id: 'id', }); }); it('creates a metatransaction v2 job', async () => { mockRedis.get = jest.fn().mockResolvedValueOnce({}); mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync = jest .fn() .mockResolvedValueOnce([metaTransactionV1EndpointPrice.sellAmount]); mockDbUtils.writeMetaTransactionJobAsync.mockResolvedValueOnce({ id: 'id', } as MetaTransactionJobEntity); const result = await gaslessSwapService.processSubmitAsync( { kind: GaslessTypes.MetaTransactionV2, trade: { type: GaslessTypes.MetaTransactionV2, trade: MOCK_META_TRANSACTION_TRADE.trade, signature: ethSignHashWithKey(MOCK_META_TRANSACTION_TRADE.trade.getHash(), takerPrivateKey), }, }, 'integratorId', ); expect(result.tradeHash).toEqual(MOCK_META_TRANSACTION_TRADE.trade.getHash()); expect(result.type).toEqual(GaslessTypes.MetaTransactionV2); expect(mockSqsProducer.send).toHaveBeenCalledWith({ body: '{"id":"id","type":"metatransaction"}', deduplicationId: 'id', groupId: 'id', id: 'id', }); }); }); }); });