import { parseAbi } from 'viem'; /** * The concentrated-liquidity venues Ranger can run on. Both expose the * Uniswap v3 periphery: the same NonfungiblePositionManager ABI for mint, * decreaseLiquidity and collect, the same factory getPool, the same pool * reads. What differs is what a venue record holds: the addresses and the * fee tiers the pair is deployed at. * * `LP_RANGE_VENUE` selects the venue for the agent's own capital (production * runs `uniswap-v3`; unset keeps the original PancakeSwap default so an old * checkout behaves as before). Managed mandates are bound to the position * manager their session policy names (`RANGER_POSITION_MANAGER` in * `packages/shared/src/managed-strategies.ts`): that is what the browser * grants, so a venue change there is a policy change, not an env var. */ export interface LpVenue { name: 'pancakeswap-v3' | 'uniswap-v3'; positionManager: `0x${string}`; factory: `0x${string}`; /** Fee tiers the WBNB/USDT pair is deployed at, deepest first at probe time. */ feeTiers: readonly number[]; } /** * One pool ABI for both venues. PancakeSwap declares slot0's feeProtocol as * uint32 and Uniswap as uint8, but ABI static types are word-padded, so the * seven return words decode identically either way; the declaration below * uses the Uniswap width and reads both. */ export const POOL_ABI = parseAbi([ 'function liquidity() view returns (uint128)', 'function tickSpacing() view returns (int24)', 'function token0() view returns (address)', 'function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)', 'function observe(uint32[] secondsAgos) view returns (int56[] tickCumulatives, uint160[] secondsPerLiquidityCumulativeX128)', ]); export const LP_VENUES: Record = { /* * Probed 2026-08-18 with tsx + viem readContract on https://bsc-rpc.publicnode.com: * NPM.factory() -> 0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865 (matches expected factory) * NPM.WETH9() -> 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c (WBNB, matches TOKENS_BSC) * Pool probes, same date and RPC, factory.getPool(WBNB, USDT, fee): * fee 500 -> 0x36696169C63e42cd08ce11f5deeBbCeBae652050 liquidity 1.19e24 tickSpacing 10 * fee 100 -> 0x172fcD41E0913e95784454622d1c3724f546f849 liquidity 8.96e24 tickSpacing 1 * fee 2500 -> 0x1401ff943D08a7E098328C1d3a9d388923B115D2 liquidity 2.00e22 tickSpacing 50 * All three report token0 = USDT, token1 = WBNB. */ 'pancakeswap-v3': { name: 'pancakeswap-v3', positionManager: '0x46A15B0b27311cedF172AB29E4f4766fbE7F4364', factory: '0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865', feeTiers: [500, 100, 2500], }, /* * Addresses from developers.uniswap.org, v3 BNB deployments, read 2026-09-12. * Probed 2026-09-12 with tsx + viem readContract on https://bsc-rpc.publicnode.com (block 121491250): * NPM.factory() -> 0xdB1d10011AD0Ff90774D0C6Bb92e5C5c8b4461F7 (matches the published factory) * NPM.WETH9() -> 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c (WBNB, matches TOKENS_BSC) * factory.getPool(WBNB, USDT, fee), all token0 = USDT, token1 = WBNB: * fee 500 -> 0x6fe9E9de56356F7eDBfcBB29FAB7cd69471a4869 liquidity 1.14e24 tickSpacing 10 * fee 100 -> 0x47a90A2d92A8367A91EfA1906bFc8c1E05bf10c4 liquidity 1.35e23 tickSpacing 1 * fee 3000 -> 0x7862D9B4bE2156B15d54F41ee4EDE2d5b0b455e4 liquidity 3.50e22 tickSpacing 60 * fee 10000 -> 0x4d170f8714367C44787AE98259CE8Adb72240067 liquidity 1.81e22 tickSpacing 200 * Live since 2026-09-13 00:45 UTC: position 2745250 minted in the 500 pool. */ 'uniswap-v3': { name: 'uniswap-v3', positionManager: '0x7b8A01B39D58278b5DE7e48c8449c9f4F5170613', factory: '0xdB1d10011AD0Ff90774D0C6Bb92e5C5c8b4461F7', feeTiers: [500, 100, 3000, 10000], }, }; /** The venue whose position manager is `address`, or undefined for a manager Ranger does not know. */ export function venueForPositionManager(address: string): LpVenue | undefined { return Object.values(LP_VENUES).find((venue) => venue.positionManager.toLowerCase() === address.toLowerCase()); } /** * The venue named by `LP_RANGE_VENUE`. An unknown name is reported, not * thrown: Ranger refuses to run on it, the other agents keep running. */ export function selectLpVenue(name = process.env.LP_RANGE_VENUE): { venue: LpVenue } | { error: string } { if (!name) return { venue: LP_VENUES['pancakeswap-v3'] }; const venue = LP_VENUES[name as LpVenue['name']]; return venue ? { venue } : { error: `LP_RANGE_VENUE=${name} is not one of ${Object.keys(LP_VENUES).join(', ')}` }; }