import { ethers } from 'ethers';
// === 配置(主网)===
const RPC_URL = 'https://evmrpc.kasplex.org';
const V3_FACTORY = '0x0dfb1Bb755d872EA1fa4d95E4ad0c2E6317Ce9B9';
const V3_POSITION_MANAGER = '0x343b244bEDF133D57C61b241557bF29AA32ea4F9';
// === ABIs ===
const ERC20_ABI = [
'function approve(address,uint256) returns (bool)',
'function allowance(address,address) view returns (uint256)',
'function decimals() view returns (uint8)',
'function symbol() view returns (string)',
];
const FACTORY_ABI = [
'function getPool(address,address,uint24) view returns (address)',
];
const POOL_ABI = [
'function slot0() view returns (uint160, int24, uint16, uint16, uint16, uint8, bool)',
'function tickSpacing() view returns (int24)',
'function token0() view returns (address)',
'function token1() view returns (address)',
'function liquidity() view returns (uint128)',
];
const PM_ABI = [
'function mint(tuple(address token0, address token1, uint24 fee, int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired, uint256 amount0Min, uint256 amount1Min, address recipient, uint256 deadline)) payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)',
'function positions(uint256 tokenId) view returns (uint96, address, address, address, uint24, int24, int24, uint128, uint256, uint256, uint128, uint128)',
];
// === 辅助函数 ===
function nearestUsableTick(tick: number, tickSpacing: number): number {
return Math.round(tick / tickSpacing) * tickSpacing;
}
function sortTokens(a: string, b: string): [string, string] {
return a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a];
}
/**
* 创建一个 V3 流动性头寸。
*
* @param signer - 已连接的 ethers Signer
* @param tokenA - 第一个代币地址
* @param tokenB - 第二个代币地址
* @param fee - 手续费等级(100、500、3000 或 10000)
* @param amount0 - 期望的 token0 数量(原始单位)
* @param amount1 - 期望的 token1 数量(原始单位)
* @param rangePercent - 价格范围百分比(例如 10 表示 ±10%)
*/
async function addLiquidityV3(
signer: ethers.Signer,
tokenA: string,
tokenB: string,
fee: number,
amount0: bigint,
amount1: bigint,
rangePercent: number = 10
) {
const provider = signer.provider!;
const userAddress = await signer.getAddress();
const deadline = Math.floor(Date.now() / 1000) + 1200;
// 排序代币(token0 地址 < token1 地址)
const [token0, token1] = sortTokens(tokenA, tokenB);
const isSwapped = token0.toLowerCase() !== tokenA.toLowerCase();
if (isSwapped) [amount0, amount1] = [amount1, amount0];
// --- 获取池状态 ---
const factory = new ethers.Contract(V3_FACTORY, FACTORY_ABI, provider);
const poolAddress = await factory.getPool(token0, token1, fee);
if (poolAddress === ethers.ZeroAddress) {
throw new Error(`此交易对在手续费等级 ${fee} 下不存在池`);
}
const pool = new ethers.Contract(poolAddress, POOL_ABI, provider);
const [sqrtPriceX96, currentTick] = await pool.slot0();
const tickSpacing = Number(await pool.tickSpacing());
console.log(`池地址:${poolAddress}`);
console.log(`当前 tick:${currentTick}`);
console.log(`Tick 间距:${tickSpacing}`);
// --- 计算 tick 范围 ---
// 将百分比转换为近似 tick 偏移量
// 1% 的价格变化 ≈ 100 个 tick(因为 1.0001^100 ≈ 1.01)
const tickOffset = Math.floor(rangePercent * 100);
const tickLower = nearestUsableTick(Number(currentTick) - tickOffset, tickSpacing);
const tickUpper = nearestUsableTick(Number(currentTick) + tickOffset, tickSpacing);
console.log(`范围:tick [${tickLower}, ${tickUpper}]`);
// --- 授权代币 ---
console.log('正在授权代币...');
const token0Contract = new ethers.Contract(token0, ERC20_ABI, signer);
const token1Contract = new ethers.Contract(token1, ERC20_ABI, signer);
const allow0 = await token0Contract.allowance(userAddress, V3_POSITION_MANAGER);
const allow1 = await token1Contract.allowance(userAddress, V3_POSITION_MANAGER);
if (allow0 < amount0) {
await (await token0Contract.approve(V3_POSITION_MANAGER, ethers.MaxUint256)).wait();
}
if (allow1 < amount1) {
await (await token1Contract.approve(V3_POSITION_MANAGER, ethers.MaxUint256)).wait();
}
// --- 铸造头寸 ---
console.log('正在铸造头寸...');
const pm = new ethers.Contract(V3_POSITION_MANAGER, PM_ABI, signer);
const mintParams = {
token0,
token1,
fee,
tickLower,
tickUpper,
amount0Desired: amount0,
amount1Desired: amount1,
amount0Min: 0n, // 为简化设为 0;生产环境请使用滑点保护
amount1Min: 0n,
recipient: userAddress,
deadline,
};
const tx = await pm.mint(mintParams);
const receipt = await tx.wait();
console.log(`头寸已铸造。交易哈希:${receipt!.hash}`);
// 从事件中解析 tokenId(简化版)
for (const log of receipt!.logs) {
try {
const parsed = pm.interface.parseLog({ topics: [...log.topics], data: log.data });
if (parsed && parsed.name === 'IncreaseLiquidity') {
console.log(`Token ID:${parsed.args.tokenId}`);
console.log(`流动性:${parsed.args.liquidity}`);
console.log(`Amount0:${parsed.args.amount0}`);
console.log(`Amount1:${parsed.args.amount1}`);
}
} catch {
// 非 PM 事件,跳过
}
}
}
// === 使用示例 ===
async function main() {
const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet('YOUR_PRIVATE_KEY', provider);
const TOKEN_A = '0xB190a6A7fC2873f1Abf145279eD664348d5Ef630';
const TOKEN_B = '0x3Ac3B30b7f18AEFD4590D7FE4d9C5944aaeB7220';
await addLiquidityV3(
signer,
TOKEN_A,
TOKEN_B,
3000, // 0.3% 手续费等级
ethers.parseEther('10'), // 10 个 token0
ethers.parseEther('10'), // 10 个 token1
10 // ±10% 范围
);
}
main().catch(console.error);