Files
protocol/apps-node/rfq-api/test/services/GaslessSwapService.test.ts
2023-04-07 11:13:18 -07:00

1866 lines
120 KiB
TypeScript

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<typeof getV1QuoteAsync>,
Parameters<typeof getV1QuoteAsync>
>;
const getMetaTransactionV2QuoteAsyncMock = getV2QuoteAsync as jest.Mock<
ReturnType<typeof getV2QuoteAsync>,
Parameters<typeof getV2QuoteAsync>
>;
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',
});
});
});
});
});