Skip to content

Commit

Permalink
feat: TP-1841 batch requests (#1241)
Browse files Browse the repository at this point in the history
  • Loading branch information
keithbro-imx authored Dec 6, 2023
1 parent 3062180 commit 08ea90c
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const allTokens: Token[] = [
{ symbol: 'zkWLT', address: '0x8A5b0470ee48248bEb7D1E745c1EbA0DCA77215e' },
{ symbol: 'zkSRE', address: '0x43566cAB87CC147C95e2895E7b972E19993520e4' },
{ symbol: 'zkCORE', address: '0x4B96E7b7eA673A996F140d5De411a97b7eab934E' },
{ symbol: 'zkWAT', address: '0xaC953a0d7B67Fae17c87abf79f09D0f818AC66A2' },
];

const buildExchange = (secondaryFeeRecipient: string, secondaryFeePercentage: number) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { JsonRpcProvider } from '@ethersproject/providers';
import { JsonRpcProvider, JsonRpcBatchProvider } from '@ethersproject/providers';
import { Contract } from '@ethersproject/contracts';
import { BigNumber } from '@ethersproject/bignumber';
import { InvalidAddressError, InvalidMaxHopsError, InvalidSlippageError, NoRoutesAvailableError } from 'errors';
Expand Down Expand Up @@ -71,11 +71,14 @@ describe('getUnsignedSwapTxFromAmountIn', () => {
}));

(JsonRpcProvider as unknown as jest.Mock).mockImplementation(() => ({
connect: jest.fn().mockResolvedValue(erc20Contract),
})) as unknown as JsonRpcProvider;

(JsonRpcBatchProvider as unknown as jest.Mock).mockImplementation(() => ({
getFeeData: async () => ({
maxFeePerGas: null,
gasPrice: TEST_GAS_PRICE,
}),
connect: jest.fn().mockResolvedValue(erc20Contract),
})) as unknown as JsonRpcProvider;
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { JsonRpcProvider } from '@ethersproject/providers';
import { JsonRpcProvider, JsonRpcBatchProvider } from '@ethersproject/providers';
import { Contract } from '@ethersproject/contracts';
import { BigNumber } from '@ethersproject/bignumber';
import { ethers } from 'ethers';
Expand Down Expand Up @@ -67,7 +67,7 @@ describe('getUnsignedSwapTxFromAmountOut', () => {
paused: jest.fn().mockResolvedValue(false),
}));

(JsonRpcProvider as unknown as jest.Mock).mockImplementation(() => ({
(JsonRpcBatchProvider as unknown as jest.Mock).mockImplementation(() => ({
getFeeData: async () => ({
maxFeePerGas: null,
gasPrice: TEST_GAS_PRICE,
Expand Down
61 changes: 37 additions & 24 deletions packages/internal/dex/sdk/src/exchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { fetchGasPrice } from 'lib/transactionUtils/gas';
import { getApproval, prepareApproval } from 'lib/transactionUtils/approval';
import { getOurQuoteReqAmount, prepareUserQuote } from 'lib/transactionUtils/getQuote';
import { Fees } from 'lib/fees';
import { SecondaryFee__factory } from 'contracts/types';
import { Multicall__factory, SecondaryFee__factory } from 'contracts/types';
import { NativeTokenService } from 'lib/nativeTokenService';
import { DEFAULT_MAX_HOPS, DEFAULT_SLIPPAGE, MAX_MAX_HOPS, MIN_MAX_HOPS } from './constants';
import { Router } from './lib/router';
Expand Down Expand Up @@ -47,7 +47,9 @@ const toPublicQuote = (
});

export class Exchange {
private provider: ethers.providers.JsonRpcProvider;
private provider: ethers.providers.StaticJsonRpcProvider;

private batchProvider: ethers.providers.JsonRpcBatchProvider;

private router: Router;

Expand Down Expand Up @@ -76,13 +78,24 @@ export class Exchange {
this.routerContractAddress = config.chain.contracts.peripheryRouter;
this.secondaryFeeContractAddress = config.chain.contracts.secondaryFee;

this.provider = new ethers.providers.JsonRpcProvider(config.chain.rpcUrl);
this.provider = new ethers.providers.StaticJsonRpcProvider({
url: config.chain.rpcUrl,
skipFetchSetup: true,
}, config.chain.chainId);

this.batchProvider = new ethers.providers.JsonRpcBatchProvider({
url: config.chain.rpcUrl,
skipFetchSetup: true,
}, config.chain.chainId);

this.router = new Router(this.provider, config.chain.commonRoutingTokens, {
multicallAddress: config.chain.contracts.multicall,
factoryAddress: config.chain.contracts.coreFactory,
quoterAddress: config.chain.contracts.quoterV2,
});
const multicallContract = Multicall__factory.connect(config.chain.contracts.multicall, this.provider);

this.router = new Router(
this.batchProvider,
multicallContract,
config.chain.commonRoutingTokens,
config.chain.contracts,
);
}

private static validate(
Expand All @@ -102,12 +115,12 @@ export class Exchange {
assert(slippagePercent >= 0, new InvalidSlippageError('slippage percent must be greater than or equal to 0'));
}

private async getSecondaryFees() {
private async getSecondaryFees(provider: ethers.providers.JsonRpcBatchProvider) {
if (this.secondaryFees.length === 0) {
return [];
}

const secondaryFeeContract = SecondaryFee__factory.connect(this.secondaryFeeContractAddress, this.provider);
const secondaryFeeContract = SecondaryFee__factory.connect(this.secondaryFeeContractAddress, provider);

if (await secondaryFeeContract.paused()) {
// Do not use secondary fees if the contract is paused
Expand Down Expand Up @@ -143,11 +156,14 @@ export class Exchange {
Exchange.validate(tokenInLiteral, tokenOutLiteral, maxHops, slippagePercent, fromAddress);

// get the decimals of the tokens that will be swapped
const [tokenInDecimals, tokenOutDecimals, secondaryFees] = await Promise.all([
getTokenDecimals(tokenInLiteral, this.provider, this.nativeToken),
getTokenDecimals(tokenOutLiteral, this.provider, this.nativeToken),
this.getSecondaryFees(),
]);
const promises = [
getTokenDecimals(tokenInLiteral, this.batchProvider, this.nativeToken),
getTokenDecimals(tokenOutLiteral, this.batchProvider, this.nativeToken),
this.getSecondaryFees(this.batchProvider),
fetchGasPrice(this.batchProvider, this.nativeToken),
] as const;

const [tokenInDecimals, tokenOutDecimals, secondaryFees, gasPrice] = await Promise.all(promises);

const tokenIn = this.parseTokenLiteral(tokenInLiteral, tokenInDecimals);
const tokenOut = this.parseTokenLiteral(tokenOutLiteral, tokenOutDecimals);
Expand All @@ -163,15 +179,12 @@ export class Exchange {
const ourQuoteReqAmount = getOurQuoteReqAmount(amountSpecified, fees, tradeType, this.nativeTokenService);

// Quotes will always use ERC20s. If the user-specified token is Native, we use the Wrapped Native Token pool
const [ourQuote, gasPrice] = await Promise.all([
this.router.findOptimalRoute(
ourQuoteReqAmount,
this.nativeTokenService.maybeWrapToken(otherToken),
tradeType,
maxHops,
),
fetchGasPrice(this.provider, this.nativeToken),
]);
const ourQuote = await this.router.findOptimalRoute(
ourQuoteReqAmount,
this.nativeTokenService.maybeWrapToken(otherToken),
tradeType,
maxHops,
);

const adjustedQuote = adjustQuoteWithFees(ourQuote, amountSpecified, fees, this.nativeTokenService);

Expand Down
99 changes: 53 additions & 46 deletions packages/internal/dex/sdk/src/lib/getQuotesForRoutes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
import { TradeType } from '@uniswap/sdk-core';
import { BigNumber, utils } from 'ethers';
import { ProviderCallError } from 'errors';
import { getQuotesForRoutes } from './getQuotesForRoutes';
import { getQuotesForRoutes, Provider } from './getQuotesForRoutes';
import {
IMX_TEST_TOKEN,
TEST_QUOTER_ADDRESS,
Expand All @@ -13,7 +13,6 @@ import {
newAmountFromString,
} from '../test/utils';
import { erc20ToUniswapToken, newAmount } from './utils';
import { Multicall } from './multicall';

const UNISWAP_IMX = erc20ToUniswapToken(IMX_TEST_TOKEN);
const UNISWAP_WETH = erc20ToUniswapToken(WETH_TEST_TOKEN);
Expand All @@ -36,10 +35,10 @@ const types = [
'uint256', // gasEstimate
];

const buildMulticallContract = (multicall: jest.Mock): Multicall => ({ callStatic: { multicall } });
const buildProvider = (send: jest.Mock): Provider => ({ send });

describe('getQuotesForRoutes', () => {
it('uses a suitable gas limit', async () => {
it('makes an eth_call against the provider', async () => {
const expectedAmountOut = utils.parseEther('1000');
const expectedGasEstimate = '100000';

Expand All @@ -50,48 +49,71 @@ describe('getQuotesForRoutes', () => {
expectedGasEstimate,
]);

const multicallContract = buildMulticallContract(
jest.fn().mockResolvedValue({
returnData: [
{
returnData,
},
],
}),
);
const provider = buildProvider(jest.fn().mockResolvedValueOnce(returnData));

const quoteResults = await getQuotesForRoutes(
multicallContract,
provider,
TEST_QUOTER_ADDRESS,
[route],
newAmountFromString('1', WETH_TEST_TOKEN),
TradeType.EXACT_INPUT,
);

expect(quoteResults).toHaveLength(1);
expect(multicallContract.callStatic.multicall).toHaveBeenCalledWith([{
expect(provider.send).toHaveBeenCalledWith('eth_call', [{
// eslint-disable-next-line max-len
callData: expect.any(String),
gasLimit: 2000000,
target: TEST_QUOTER_ADDRESS,
}]);
data: '0xc6a5026a0000000000000000000000004f062a3eaec3730560ab89b5ce5ac0ab2c5517ae00000000000000000000000072958b06abdf2701ace6ceb3ce0b8b1ce11e08510000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000027100000000000000000000000000000000000000000000000000000000000000000',
to: '0x9B323E56215aAdcD4f45a6Be660f287DE154AFC5',
}, 'latest']);
});

describe('when multicall fails', () => {
it('should throw ProviderCallError', async () => {
const mockedMulticallContract = buildMulticallContract(
describe('when all calls in the batch fail', () => {
it('returns no quote results', async () => {
const provider = buildProvider(
jest.fn().mockRejectedValue(new ProviderCallError('an rpc error message')),
);

const amount = newAmount(BigNumber.from('123123'), WETH_TEST_TOKEN);

await expect(getQuotesForRoutes(
mockedMulticallContract,
const quoteResults = await getQuotesForRoutes(
provider,
TEST_QUOTER_ADDRESS,
[route],
amount,
TradeType.EXACT_INPUT,
)).rejects.toThrow(new ProviderCallError('failed multicall: an rpc error message'));
);
expect(quoteResults).toHaveLength(0);
});
});

describe('when one call of two in the batch fail', () => {
it('returns one quote results', async () => {
const expectedAmountOut = utils.parseEther('1000');
const expectedGasEstimate = '100000';

const returnData = utils.defaultAbiCoder.encode(types, [
expectedAmountOut,
'100',
'1',
expectedGasEstimate,
]);

const provider = buildProvider(
jest.fn()
.mockRejectedValueOnce(new ProviderCallError('an rpc error message'))
.mockResolvedValueOnce(returnData),
);

const amount = newAmount(BigNumber.from('123123'), WETH_TEST_TOKEN);

const quoteResults = await getQuotesForRoutes(
provider,
TEST_QUOTER_ADDRESS,
[route, route],
amount,
TradeType.EXACT_INPUT,
);
expect(quoteResults).toHaveLength(1);
});
});

Expand All @@ -107,19 +129,13 @@ describe('getQuotesForRoutes', () => {
expectedGasEstimate,
]);

const multicallContract = buildMulticallContract(
jest.fn().mockResolvedValue({
returnData: [
{
returnData,
},
],
}),
const provider = buildProvider(
jest.fn().mockResolvedValue(returnData),
);

const amount = newAmount(BigNumber.from('123123'), WETH_TEST_TOKEN);
const amountOutReceived = await getQuotesForRoutes(
multicallContract,
provider,
TEST_QUOTER_ADDRESS,
[route],
amount,
Expand Down Expand Up @@ -152,22 +168,13 @@ describe('getQuotesForRoutes', () => {
expectedGasEstimate2,
]);

const multicallContract = buildMulticallContract(
jest.fn().mockResolvedValueOnce({
returnData: [
{
returnData: returnData1,
},
{
returnData: returnData2,
},
],
}),
const provider = buildProvider(
jest.fn().mockResolvedValueOnce(returnData1).mockResolvedValueOnce(returnData2),
);

const amount = newAmount(BigNumber.from('123123'), WETH_TEST_TOKEN);
const amountOutReceived = await getQuotesForRoutes(
multicallContract,
provider,
TEST_QUOTER_ADDRESS,
[route, route],
amount,
Expand Down
Loading

0 comments on commit 08ea90c

Please sign in to comment.