Skip to content

Commit 2d2ef8f

Browse files
committed
add defi adapters
1 parent 93768fe commit 2d2ef8f

8 files changed

Lines changed: 399 additions & 0 deletions

File tree

server/src/defi/abis.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export const ERC20_ABI = [
2+
'function balanceOf(address) view returns (uint256)',
3+
'function decimals() view returns (uint8)',
4+
'function allowance(address owner, address spender) view returns (uint256)',
5+
'function approve(address spender, uint256 value) returns (bool)',
6+
];
7+
8+
export const UNISWAP_V3_FACTORY_ABI = [
9+
'function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool)',
10+
];
11+
12+
export const UNISWAP_V3_POOL_ABI = [
13+
'function slot0() external view returns (uint160 sqrtPriceX96,int24 tick,uint16 observationIndex,uint16 observationCardinality,uint16 observationCardinalityNext,uint8 feeProtocol,bool unlocked)',
14+
'function liquidity() external view returns (uint128)',
15+
];
16+
17+
export const UNISWAP_V3_QUOTER_V2_ABI = [
18+
'function quoteExactInputSingle((address tokenIn,address tokenOut,uint256 amountIn,uint24 fee,uint160 sqrtPriceLimitX96)) external returns (uint256 amountOut,uint160 sqrtPriceX96After,uint32 initializedTicksCrossed,uint256 gasEstimate)',
19+
];
20+
21+
export const UNISWAP_V3_ROUTER_ABI = [
22+
'function exactInputSingle((address tokenIn,address tokenOut,uint24 fee,address recipient,uint256 deadline,uint256 amountIn,uint256 amountOutMinimum,uint160 sqrtPriceLimitX96)) payable returns (uint256 amountOut)',
23+
];
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Injectable } from '@nestjs/common';
2+
import type { DexId } from './addresses';
3+
import type { DexAdapter } from './adapters/dex-adapter';
4+
import { UniswapV3Adapter } from './adapters/uniswapV3.adapter';
5+
import { PancakeV3Adapter } from './adapters/pancakeV3.adapter';
6+
7+
@Injectable()
8+
export class DexAdapterRegistry {
9+
private readonly adapters: Record<DexId, DexAdapter>;
10+
11+
constructor(
12+
private readonly uniV3: UniswapV3Adapter,
13+
private readonly cakeV3: PancakeV3Adapter,
14+
) {
15+
this.adapters = {
16+
uniswapV3: uniV3,
17+
pancakeV3: cakeV3,
18+
};
19+
}
20+
21+
get(id: DexId): DexAdapter {
22+
const a = this.adapters[id];
23+
if (!a) throw new Error(`No adapter for id ${id}`);
24+
return a;
25+
}
26+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { BigNumber, ethers } from 'ethers';
2+
import type { DexId } from '../addresses';
3+
4+
export type QuoteSingleParams = {
5+
tokenIn: string;
6+
tokenOut: string;
7+
fee: number; // v3 fee tier (500/3000/10000)
8+
amountIn: BigNumber;
9+
sqrtPriceLimitX96?: BigNumber | number;
10+
};
11+
12+
export type ExactInputSingleParams = {
13+
tokenIn: string;
14+
tokenOut: string;
15+
fee: number;
16+
recipient: string;
17+
deadline: number;
18+
amountIn: BigNumber;
19+
amountOutMinimum: BigNumber;
20+
sqrtPriceLimitX96?: BigNumber | number;
21+
};
22+
23+
export interface DexAdapter {
24+
readonly id: DexId;
25+
supportsChain(chainId: number): boolean;
26+
27+
getAddresses(chainId: number): {
28+
factory: string;
29+
router: string;
30+
quoterV2: string;
31+
weth: string;
32+
};
33+
34+
getPool(
35+
provider: ethers.providers.Provider,
36+
chainId: number,
37+
tokenIn: string,
38+
tokenOut: string,
39+
fee: number,
40+
): Promise<string>;
41+
42+
quoteExactInputSingle(
43+
provider: ethers.providers.Provider,
44+
chainId: number,
45+
p: QuoteSingleParams,
46+
): Promise<{ amountOut: BigNumber }>;
47+
48+
estimateGasExactInputSingle(
49+
signer: ethers.Signer,
50+
chainId: number,
51+
p: ExactInputSingleParams,
52+
): Promise<BigNumber>;
53+
54+
exactInputSingle(
55+
signer: ethers.Signer,
56+
chainId: number,
57+
p: ExactInputSingleParams,
58+
): Promise<ethers.providers.TransactionReceipt>;
59+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { BigNumber, ethers } from 'ethers';
3+
import { DEX_ADDRESSES } from '../addresses';
4+
import {
5+
DexAdapter,
6+
ExactInputSingleParams,
7+
QuoteSingleParams,
8+
} from './dex-adapter';
9+
import {
10+
UNISWAP_V3_FACTORY_ABI,
11+
UNISWAP_V3_QUOTER_V2_ABI,
12+
UNISWAP_V3_ROUTER_ABI,
13+
} from '../abis';
14+
15+
/**
16+
* Pancake v3 is a Uniswap v3 fork; ABIs are compatible. Addresses must be set in DEX_ADDRESSES.pancakeV3.
17+
*/
18+
@Injectable()
19+
export class PancakeV3Adapter implements DexAdapter {
20+
readonly id = 'pancakeV3' as const;
21+
22+
supportsChain(chainId: number): boolean {
23+
return !!DEX_ADDRESSES.pancakeV3[chainId];
24+
}
25+
26+
getAddresses(chainId: number) {
27+
const a = DEX_ADDRESSES.pancakeV3[chainId];
28+
if (!a) throw new Error(`PancakeV3 not configured for chain ${chainId}`);
29+
return a;
30+
}
31+
32+
async getPool(
33+
provider: ethers.providers.Provider,
34+
chainId: number,
35+
tokenIn: string,
36+
tokenOut: string,
37+
fee: number,
38+
) {
39+
const { factory } = this.getAddresses(chainId);
40+
const c = new ethers.Contract(factory, UNISWAP_V3_FACTORY_ABI, provider);
41+
return await c.getPool(tokenIn, tokenOut, fee);
42+
}
43+
44+
async quoteExactInputSingle(
45+
provider: ethers.providers.Provider,
46+
chainId: number,
47+
p: QuoteSingleParams,
48+
) {
49+
const { quoterV2 } = this.getAddresses(chainId);
50+
const q = new ethers.Contract(quoterV2, UNISWAP_V3_QUOTER_V2_ABI, provider);
51+
const res = await q.callStatic.quoteExactInputSingle({
52+
tokenIn: p.tokenIn,
53+
tokenOut: p.tokenOut,
54+
amountIn: p.amountIn,
55+
fee: p.fee,
56+
sqrtPriceLimitX96: p.sqrtPriceLimitX96 ?? 0,
57+
});
58+
return { amountOut: res[0] as BigNumber };
59+
}
60+
61+
async estimateGasExactInputSingle(
62+
signer: ethers.Signer,
63+
chainId: number,
64+
p: ExactInputSingleParams,
65+
) {
66+
const { router } = this.getAddresses(chainId);
67+
const r = new ethers.Contract(router, UNISWAP_V3_ROUTER_ABI, signer);
68+
return await r.estimateGas.exactInputSingle({
69+
tokenIn: p.tokenIn,
70+
tokenOut: p.tokenOut,
71+
fee: p.fee,
72+
recipient: p.recipient,
73+
deadline: p.deadline,
74+
amountIn: p.amountIn,
75+
amountOutMinimum: p.amountOutMinimum,
76+
sqrtPriceLimitX96: p.sqrtPriceLimitX96 ?? 0,
77+
});
78+
}
79+
80+
async exactInputSingle(
81+
signer: ethers.Signer,
82+
chainId: number,
83+
p: ExactInputSingleParams,
84+
) {
85+
const { router } = this.getAddresses(chainId);
86+
const r = new ethers.Contract(router, UNISWAP_V3_ROUTER_ABI, signer);
87+
const tx = await r.exactInputSingle({
88+
tokenIn: p.tokenIn,
89+
tokenOut: p.tokenOut,
90+
fee: p.fee,
91+
recipient: p.recipient,
92+
deadline: p.deadline,
93+
amountIn: p.amountIn,
94+
amountOutMinimum: p.amountOutMinimum,
95+
sqrtPriceLimitX96: p.sqrtPriceLimitX96 ?? 0,
96+
});
97+
const rcpt = await tx.wait();
98+
return rcpt;
99+
}
100+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { BigNumber, ethers } from 'ethers';
3+
import { DEX_ADDRESSES } from '../addresses';
4+
import {
5+
DexAdapter,
6+
ExactInputSingleParams,
7+
QuoteSingleParams,
8+
} from './dex-adapter';
9+
import {
10+
UNISWAP_V3_FACTORY_ABI,
11+
UNISWAP_V3_QUOTER_V2_ABI,
12+
UNISWAP_V3_ROUTER_ABI,
13+
} from '../abis';
14+
15+
@Injectable()
16+
export class UniswapV3Adapter implements DexAdapter {
17+
readonly id = 'uniswapV3' as const;
18+
19+
supportsChain(chainId: number): boolean {
20+
return !!DEX_ADDRESSES.uniswapV3[chainId];
21+
}
22+
23+
getAddresses(chainId: number) {
24+
const a = DEX_ADDRESSES.uniswapV3[chainId];
25+
if (!a) throw new Error(`UniswapV3 not configured for chain ${chainId}`);
26+
return a;
27+
}
28+
29+
async getPool(
30+
provider: ethers.providers.Provider,
31+
chainId: number,
32+
tokenIn: string,
33+
tokenOut: string,
34+
fee: number,
35+
) {
36+
const { factory } = this.getAddresses(chainId);
37+
const c = new ethers.Contract(factory, UNISWAP_V3_FACTORY_ABI, provider);
38+
return await c.getPool(tokenIn, tokenOut, fee);
39+
}
40+
41+
async quoteExactInputSingle(
42+
provider: ethers.providers.Provider,
43+
chainId: number,
44+
p: QuoteSingleParams,
45+
) {
46+
const { quoterV2 } = this.getAddresses(chainId);
47+
const q = new ethers.Contract(quoterV2, UNISWAP_V3_QUOTER_V2_ABI, provider);
48+
const res = await q.callStatic.quoteExactInputSingle({
49+
tokenIn: p.tokenIn,
50+
tokenOut: p.tokenOut,
51+
amountIn: p.amountIn,
52+
fee: p.fee,
53+
sqrtPriceLimitX96: p.sqrtPriceLimitX96 ?? 0,
54+
});
55+
return { amountOut: res[0] as BigNumber };
56+
}
57+
58+
async estimateGasExactInputSingle(
59+
signer: ethers.Signer,
60+
chainId: number,
61+
p: ExactInputSingleParams,
62+
) {
63+
const { router } = this.getAddresses(chainId);
64+
const r = new ethers.Contract(router, UNISWAP_V3_ROUTER_ABI, signer);
65+
return await r.estimateGas.exactInputSingle({
66+
tokenIn: p.tokenIn,
67+
tokenOut: p.tokenOut,
68+
fee: p.fee,
69+
recipient: p.recipient,
70+
deadline: p.deadline,
71+
amountIn: p.amountIn,
72+
amountOutMinimum: p.amountOutMinimum,
73+
sqrtPriceLimitX96: p.sqrtPriceLimitX96 ?? 0,
74+
});
75+
}
76+
77+
async exactInputSingle(
78+
signer: ethers.Signer,
79+
chainId: number,
80+
p: ExactInputSingleParams,
81+
) {
82+
const { router } = this.getAddresses(chainId);
83+
const r = new ethers.Contract(router, UNISWAP_V3_ROUTER_ABI, signer);
84+
const tx = await r.exactInputSingle({
85+
tokenIn: p.tokenIn,
86+
tokenOut: p.tokenOut,
87+
fee: p.fee,
88+
recipient: p.recipient,
89+
deadline: p.deadline,
90+
amountIn: p.amountIn,
91+
amountOutMinimum: p.amountOutMinimum,
92+
sqrtPriceLimitX96: p.sqrtPriceLimitX96 ?? 0,
93+
});
94+
const rcpt = await tx.wait();
95+
return rcpt;
96+
}
97+
}

server/src/defi/addresses.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
export type DexId = 'uniswapV3' | 'pancakeV3';
2+
3+
export type DexAddresses = {
4+
factory: string;
5+
router: string;
6+
quoterV2: string;
7+
weth: string; // canonical wrapped native for the chain
8+
};
9+
10+
/**
11+
* IMPORTANT: Only put addresses you are certain about.
12+
* Fill in Pancake v3 addresses
13+
*/
14+
export const DEX_ADDRESSES: Record<DexId, Record<number, DexAddresses>> = {
15+
uniswapV3: {
16+
// Ethereum mainnet
17+
1: {
18+
factory: '0x1F98431c8aD98523631AE4a59f267346ea31F984',
19+
router: '0xE592427A0AEce92De3Edee1F18E0157C05861564',
20+
quoterV2: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e',
21+
weth: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
22+
},
23+
// Arbitrum
24+
42161: {
25+
factory: '0x1F98431c8aD98523631AE4a59f267346ea31F984',
26+
router: '0xE592427A0AEce92De3Edee1F18E0157C05861564',
27+
quoterV2: '0x655C406EBFa14EE2006250925e54ec43AD184f8B',
28+
weth: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
29+
},
30+
// Polygon
31+
137: {
32+
factory: '0x1F98431c8aD98523631AE4a59f267346ea31F984',
33+
router: '0xE592427A0AEce92De3Edee1F18E0157C05861564',
34+
quoterV2: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e',
35+
weth: '0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619',
36+
},
37+
},
38+
pancakeV3: {
39+
56: {
40+
factory: '0xD7B6E04e3C8939A58A1d2641d3cA70E3fB1d6e48',
41+
router: '0x8F352E7bD04327e9DF20D4fE3259Dce0a1B0Fc75',
42+
quoterV2: '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6', // Same interface as Uniswap V3 Quoter
43+
weth: '0xBB4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c', // WBNB
44+
},
45+
},
46+
};

server/src/defi/utils/erc20.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { BigNumber, ethers } from 'ethers';
2+
import { ERC20_ABI } from '../abis';
3+
4+
export async function readDecimals(
5+
provider: ethers.providers.Provider,
6+
token: string,
7+
) {
8+
if (token === ethers.constants.AddressZero) return 18;
9+
const erc = new ethers.Contract(token, ERC20_ABI, provider);
10+
return await erc.decimals();
11+
}
12+
13+
export async function ensureAllowance(
14+
signer: ethers.Signer,
15+
token: string,
16+
owner: string,
17+
spender: string,
18+
needed: BigNumber,
19+
) {
20+
const erc = new ethers.Contract(token, ERC20_ABI, signer);
21+
const allowance: BigNumber = await erc.allowance(owner, spender);
22+
if (allowance.lt(needed)) {
23+
const tx = await erc.approve(spender, ethers.constants.MaxUint256);
24+
await tx.wait();
25+
}
26+
}

0 commit comments

Comments
 (0)