import { encodeAbiParameters, keccak256, zeroAddress, type Address, type Hex, type PublicClient } from "viem"; import { hookAbi, hookV2Abi, poolManagerAbi } from "./abi"; import { ADDRESSES, type Addresses } from "./chain"; export type PoolKey = { currency0: Address; currency1: Address; fee: number; tickSpacing: number; hooks: Address }; /** A pool's rules as the hook stores them (fees, taxes and shares in pips, 1e6 = 100%; windows in seconds). */ export type Rules = { baseFee: number; dynamicFee: boolean; dynBase: number; dynMax: number; dynSensitivity: number; snipeWindow: number; snipeStartFee: number; maxTxWindow: number; buyTax: number; sellTax: number; sandwichGuard: boolean; allowlistWindow: number; payouts: boolean; /** v2: whether the pool has Wallet Cap or Exit Clock. */ extras?: boolean; }; /** Anti-Snipe's per-second cap on what the pool sells, and Max Transaction's per-wallet one, in token units. */ export type Caps = { snipeMaxBuy: bigint; maxTxAmount: bigint }; /** * Revenue Split (v1) or Revenue Router (v2), Buyback & Burn and Auto Burn. On v2, `teamShare` is all * the payees' shares together and `team` the first payee; `Pool.payees` lists them all. */ export type Payouts = { team: Address; teamShare: number; buybackShare: number; buybackEvery: number; burnShare: number }; /** A v2 Revenue Router payee: its share is in pips of the creator's income. */ export type Payee = { wallet: Address; share: number }; /** v2's Wallet Cap (window in seconds, cap in token units) and Exit Clock (start fee in pips, period in seconds). */ export type Extras = { walletCapWindow: number; exitStartFee: number; exitPeriod: number; walletCap: bigint }; /** Everything a quote needs, read from the chain. */ export type Pool = { token: Address; /** The contracts it launched on. */ version: 1 | 2; key: PoolKey; id: Hex; creator: Address; /** Unix seconds. */ launchTime: number; rules: Rules; caps: Caps; payouts: Payouts; payees: Payee[]; /** v2's Wallet Cap and Exit Clock; undefined on v1. */ extras?: Extras; sqrtPriceX96: bigint; /** The pool's virtual reserves, in whole ETH and whole tokens: the constant-product pool it trades like. */ reserves: { eth: number; tokens: number }; /** Uniswap's protocol fee on the pool, in pips (0 unless its fee switch is on). */ protocolFee: { buy: number; sell: number }; }; /** Every Captain Hook pool: native ETH / token, no LP fee, tick spacing 60, Captain Hook's hook. */ export function poolKey(token: Address, hook: Address = ADDRESSES.hook): PoolKey { return { currency0: zeroAddress, currency1: token, fee: 0, tickSpacing: 60, hooks: hook }; } /** v4's pool id: keccak256 of the ABI-encoded key. */ export function poolId(key: PoolKey): Hex { return keccak256( encodeAbiParameters( [{ type: "address" }, { type: "address" }, { type: "uint24" }, { type: "int24" }, { type: "address" }], [key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks], ), ); } /** The pool's virtual reserves: L / √P ETH and L × √P tokens. */ export function reservesOf(sqrtPriceX96: bigint, liquidity: bigint): { eth: number; tokens: number } { const p = Number(sqrtPriceX96) / 2 ** 96; const l = Number(liquidity) / 1e18; return { eth: l / p, tokens: l * p }; } /** The pool's slot0 in the pool manager's storage (v4 StateLibrary: keccak256(poolId, 6)). */ const slot0Of = (client: PublicClient, addresses: Addresses, id: Hex) => client.readContract({ address: addresses.poolManager, abi: poolManagerAbi, functionName: "extsload", args: [keccak256(encodeAbiParameters([{ type: "bytes32" }, { type: "uint256" }], [id, 6n]))], }); /** * A Captain Hook token's pool, read from the chain, on whichever version it launched on, or * undefined if the token wasn't launched on Captain Hook. `client` must be on the chain the * addresses are for. */ export async function readPool(client: PublicClient, token: Address, addresses: Addresses = ADDRESSES): Promise { const v1 = await readPoolV1(client, token, addresses); return v1 ?? (addresses.v2 ? readPoolV2(client, token, addresses) : undefined); } async function readPoolV1(client: PublicClient, token: Address, addresses: Addresses): Promise { const key = poolKey(token, addresses.hook); const id = poolId(key); const [creator, launchTime, liquidity] = await client.readContract({ address: addresses.hook, abi: hookAbi, functionName: "pools", args: [id] }); if (creator === zeroAddress) return undefined; const [[rules, caps], payouts, slot0] = await Promise.all([ client.readContract({ address: addresses.hook, abi: hookAbi, functionName: "rulesOf", args: [id] }), client.readContract({ address: addresses.hook, abi: hookAbi, functionName: "payoutsOf", args: [id] }), slot0Of(client, addresses, id), ]); const p = payouts as Payouts; return pool(token, 1, key, id, creator, launchTime, liquidity, slot0 as Hex, { rules: rules as Rules, caps: caps as Caps, payouts: p, payees: p.teamShare > 0 ? [{ wallet: p.team, share: p.teamShare }] : [], }); } async function readPoolV2(client: PublicClient, token: Address, addresses: Addresses): Promise { const hook = addresses.v2!.hook; const key = poolKey(token, hook); const id = poolId(key); const [creator, launchTime, liquidity] = await client.readContract({ address: hook, abi: hookV2Abi, functionName: "pools", args: [id] }); if (creator === zeroAddress) return undefined; const [[rules, caps], paid, payees, extras, slot0] = await Promise.all([ client.readContract({ address: hook, abi: hookV2Abi, functionName: "rulesOf", args: [id] }), client.readContract({ address: hook, abi: hookV2Abi, functionName: "payoutsOf", args: [id] }), client.readContract({ address: hook, abi: hookV2Abi, functionName: "payeesOf", args: [id] }), client.readContract({ address: hook, abi: hookV2Abi, functionName: "extrasOf", args: [id] }), slot0Of(client, addresses, id), ]); const list = (payees as readonly Payee[]).map((p) => ({ wallet: p.wallet, share: Number(p.share) })); return pool(token, 2, key, id, creator, launchTime, liquidity, slot0 as Hex, { rules: rules as Rules, caps: caps as Caps, // In v1's shape: all the payees' shares together, the first payee as the team wallet. payouts: { team: list[0]?.wallet ?? zeroAddress, teamShare: paid.payeeShare, buybackShare: paid.buybackShare, buybackEvery: paid.buybackEvery, burnShare: paid.burnShare }, payees: list, extras: extras as Extras, }); } function pool( token: Address, version: 1 | 2, key: PoolKey, id: Hex, creator: Address, launchTime: bigint, liquidity: bigint, slot0: Hex, parts: Pick, ): Pool { const word = BigInt(slot0); const sqrtPriceX96 = word & ((1n << 160n) - 1n); const fee = Number((word >> 184n) & 0xffffffn); return { token, version, key, id, creator, launchTime: Number(launchTime), ...parts, sqrtPriceX96, reserves: reservesOf(sqrtPriceX96, liquidity), protocolFee: { buy: fee & 0xfff, sell: fee >> 12 }, }; }