import type { Address, PublicClient } from "viem"; import { hookV2Abi } from "./abi"; import type { Caps, Extras, Pool, Rules } from "./pool"; /** * Quotes with the contracts' own maths (CaptainHook and its FeeMath), in whole ETH and whole tokens: * the same quotes the Captain Hook site shows. A quote is an estimate: the swap itself may still be * refused (a size cap reached by others this second, or the price moving), so send it with a * minimum out. */ export type Quote = { /** Tokens for a buy, after any Auto Burn; ETH for a sell, after the fee and tax. */ out: number; fee: number; tax: number; /** The fee as a share of what it was charged on (0.01 = 1%). */ rate: number; /** Tokens Auto Burn takes out of a buy. */ burned: number; /** How far this swap's own price is from the pool's price before it, fees aside (0.05 = 5%). */ priceImpact: number; /** v2's Exit Clock: the part of `tax` it charges on a sell, and its rate then (0.05 = 5%). */ exit?: number; exitRate?: number; /** * The size cap this swap breaks on its own, in tokens, the block that sets it, and seconds until it * ends. For v2's Wallet Cap, `cap` is what's left of this wallet's cap. */ over?: { cap: number; by: "antiSnipe" | "maxTx" | "walletCap"; endsIn: number }; }; /** What v2's hook knows of a wallet in a pool: what it bought under Wallet Cap, and its Exit Clock holding. */ export type WalletState = { bought: bigint; held: { amount: bigint; since: number } }; /** A wallet's Wallet Cap and Exit Clock state in a v2 pool; undefined on v1. */ export async function readWallet(client: PublicClient, pool: Pool, account: Address, hook: Address = pool.key.hooks): Promise { if (pool.version !== 2) return undefined; const [bought, [amount, since]] = await Promise.all([ client.readContract({ address: hook, abi: hookV2Abi, functionName: "walletBought", args: [pool.id, account] }), client.readContract({ address: hook, abi: hookV2Abi, functionName: "holdings", args: [pool.id, account] }), ]); return { bought, held: { amount, since: Number(since) } }; } /** * Exit Clock's rate on a sell, as a share (FeeMath.exitRate): the start rate falling in a straight * line to zero over the period, on the tokens the wallet bought in the pool, and the full start rate * on any beyond them. */ export function exitRate(e: Extras, sold: number, tracked: number, heldFor: number): number { if (!e.exitStartFee || sold <= 0) return 0; const aged = heldFor >= e.exitPeriod ? 0 : Math.floor((e.exitStartFee * (e.exitPeriod - heldFor)) / e.exitPeriod); return ((Math.min(tracked, sold) * aged + Math.max(0, sold - tracked) * e.exitStartFee) / sold) / PIPS; } const PIPS = 1e6; /** Seconds since the Allowlist Phase ended (0 while it lasts): Anti-Snipe's and Max Transaction's clock. */ export function windowClock(r: Rules, elapsed: number): number { return elapsed < r.allowlistWindow ? 0 : elapsed - r.allowlistWindow; } function feeRate(r: Rules, isBuy: boolean, elapsed: number, impact: number): number { const clock = windowClock(r, elapsed); let rate = r.dynamicFee ? Math.min(r.dynMax / PIPS, r.dynBase / PIPS + (impact * r.dynSensitivity) / 100) : r.baseFee / PIPS; if (isBuy && clock < r.snipeWindow) rate = Math.max(rate, ((r.snipeStartFee / PIPS) * (r.snipeWindow - clock)) / r.snipeWindow); return Math.min(rate, 0.99); } function capAt(r: Rules, caps: Caps, isBuy: boolean, elapsed: number): Quote["over"] { const clock = windowClock(r, elapsed); let best: Quote["over"]; if (isBuy && clock < r.snipeWindow) best = { cap: Number(caps.snipeMaxBuy) / 1e18, by: "antiSnipe", endsIn: r.allowlistWindow + r.snipeWindow - elapsed }; if (clock < r.maxTxWindow) { const cap = Number(caps.maxTxAmount) / 1e18; if (!best || cap < best.cap) best = { cap, by: "maxTx", endsIn: r.allowlistWindow + r.maxTxWindow - elapsed }; } return best; } /** A buy of `ethIn`, from the pool's parts. `elapsed` is seconds since launch; shares and fees in pips. */ export function quoteBuyWith( pool: { eth: number; tokens: number }, r: Rules, caps: Caps, ethIn: number, elapsed: number, burnShare = 0, protocolFee = 0, extras?: Extras, wallet?: WalletState, ): Quote { const tax = (ethIn * r.buyTax) / PIPS; const rest = ethIn - tax; const impact = r.dynamicFee ? (1 + rest / pool.eth) ** 2 - 1 : 0; const rate = feeRate(r, true, elapsed, impact); const fee = rest * rate; const net = (rest - fee) * (1 - protocolFee / PIPS); const gross = (pool.tokens * net) / (pool.eth + net); const burned = (gross * burnShare) / PIPS; let cap = capAt(r, caps, true, elapsed); // Wallet Cap: what this wallet may still buy while its window lasts. if (extras?.walletCapWindow && windowClock(r, elapsed) < extras.walletCapWindow) { const left = Math.max(0, Number(extras.walletCap - (wallet?.bought ?? 0n)) / 1e18); if (gross > left && (!cap || gross <= cap.cap || left < cap.cap)) cap = { cap: left, by: "walletCap", endsIn: r.allowlistWindow + extras.walletCapWindow - elapsed }; } return { out: gross - burned, fee, tax, rate, burned, priceImpact: net / (pool.eth + net), over: cap && gross > cap.cap ? cap : undefined }; } /** A sell of `tokensIn`, from the pool's parts. `heldFor` is how long the wallet's Exit Clock holding has been held, in seconds. */ export function quoteSellWith( pool: { eth: number; tokens: number }, r: Rules, caps: Caps, tokensIn: number, elapsed: number, protocolFee = 0, extras?: Extras, wallet?: WalletState, heldFor = 0, ): Quote { const traded = tokensIn * (1 - protocolFee / PIPS); const gross = (pool.eth * traded) / (pool.tokens + traded); const impact = r.dynamicFee ? 1 - (pool.tokens / (pool.tokens + tokensIn)) ** 2 : 0; const rate = feeRate(r, false, elapsed, impact); const fee = gross * rate; // Exit Clock is charged with the sell tax, on the same ETH. const exitShare = extras ? exitRate(extras, tokensIn, Number(wallet?.held.amount ?? 0n) / 1e18, heldFor) : 0; const exit = (gross - fee) * exitShare; const tax = ((gross - fee) * r.sellTax) / PIPS + exit; const cap = capAt(r, caps, false, elapsed); const q: Quote = { out: gross - fee - tax, fee, tax, rate, burned: 0, priceImpact: traded / (pool.tokens + traded), over: cap && tokensIn > cap.cap ? cap : undefined }; return exit > 0 ? { ...q, exit, exitRate: exitShare } : q; } /** * What a buy of `ethIn` ETH gets at `now` (unix seconds; defaults to this computer's clock). On v2, * pass the buyer's `wallet` (readWallet) to check what's left of their Wallet Cap. */ export function quoteBuy(pool: Pool, ethIn: number, now = Date.now() / 1000, wallet?: WalletState): Quote { return quoteBuyWith(pool.reserves, pool.rules, pool.caps, ethIn, Math.max(0, now - pool.launchTime), pool.payouts.burnShare, pool.protocolFee.buy, pool.extras, wallet); } /** * What a sell of `tokensIn` tokens gets at `now`. On v2 with Exit Clock, pass the seller's `wallet` * (readWallet): without it the quote charges the full starting fee, as for tokens bought elsewhere. */ export function quoteSell(pool: Pool, tokensIn: number, now = Date.now() / 1000, wallet?: WalletState): Quote { const heldFor = wallet ? now - wallet.held.since : 0; return quoteSellWith(pool.reserves, pool.rules, pool.caps, tokensIn, Math.max(0, now - pool.launchTime), pool.protocolFee.sell, pool.extras, wallet, heldFor); }