import { zeroAddress, type Address, type Hex, type PublicClient } from "viem"; import { factoryAbi, factoryV2Abi, hookAbi, hookV2Abi, routerAbi, routerV2Abi, tokenAbi } from "./abi"; import { ADDRESSES, versionOfHook } from "./chain"; import type { Payee, PoolKey } from "./pool"; /* * Ready-to-send calls: spread one into viem's writeContract (or simulateContract first, which is * how to get an exact quote): * * const { request } = await publicClient.simulateContract({ ...buy({ key, eth, minOut, to, deadline }), account }); * await walletClient.writeContract(request); * * Nothing here signs or sends anything. */ /** The least a swap should accept: a quote, less slippage (0.03 = 3%), in base units. */ export function minOut(quote: number, slippage = 0.03, decimals = 18): bigint { return toUnits(quote * (1 - slippage), decimals); } /** A float in base units, to the 15 significant digits a double holds, without float noise past them. */ export function toUnits(x: number, decimals = 18): bigint { if (!(x > 0)) return 0n; const [mantissa, e] = x.toExponential(14).split("e"); const digits = BigInt(mantissa.replace(".", "")); const exp = Number(e) - 14 + decimals; return exp >= 0 ? digits * 10n ** BigInt(exp) : digits / 10n ** BigInt(-exp); } /** A deadline `seconds` from now by the chain's own clock, which a local clock can disagree with. */ export async function deadline(client: PublicClient, seconds = 600): Promise { return (await client.getBlock()).timestamp + BigInt(seconds); } /** The router a pool trades through: v1's or v2's, by the pool's hook. */ export function routerFor(key: PoolKey): Address { return versionOfHook(key.hooks) === 2 ? ADDRESSES.v2!.router : ADDRESSES.router; } /** * Buy tokens with `eth` wei. On a v2 pool, `referrer` ties the buyer to whoever referred them, on * their first referred trade, for good; it pays the referrer a share of the pool fee out of Captain * Hook's part, not the buyer's. */ export function buy(o: { key: PoolKey; eth: bigint; minOut: bigint; to: Address; deadline: bigint; referrer?: Address; router?: Address }) { if (versionOfHook(o.key.hooks) === 2) return { address: o.router ?? routerFor(o.key), abi: routerV2Abi, functionName: "buy", args: [o.key, o.minOut, o.to, o.deadline, o.referrer ?? zeroAddress], value: o.eth, } as const; return { address: o.router ?? ADDRESSES.router, abi: routerAbi, functionName: "buy", args: [o.key, o.minOut, o.to, o.deadline], value: o.eth, } as const; } /** Sell `amount` token units. Approve the pool's router for them first (approveRouter). */ export function sell(o: { key: PoolKey; amount: bigint; minOut: bigint; to: Address; deadline: bigint; referrer?: Address; router?: Address }) { if (versionOfHook(o.key.hooks) === 2) return { address: o.router ?? routerFor(o.key), abi: routerV2Abi, functionName: "sell", args: [o.key, o.amount, o.minOut, o.to, o.deadline, o.referrer ?? zeroAddress], } as const; return { address: o.router ?? ADDRESSES.router, abi: routerAbi, functionName: "sell", args: [o.key, o.amount, o.minOut, o.to, o.deadline], } as const; } /** Let the router move `amount` of a token for a sell: v1's by default, or pass routerFor(key). */ export function approveRouter(token: Address, amount: bigint, router: Address = ADDRESSES.router) { return { address: token, abi: tokenAbi, functionName: "approve", args: [router, amount] } as const; } /** Let the pool's own router (v1's or v2's) move `amount` of its token for a sell. */ export function approveFor(key: PoolKey, amount: bigint) { return approveRouter(key.currency1, amount, routerFor(key)); } /** Claim a wallet's ETH earnings to `to`. Check `claimable(account)` on the hook first; each version's hook claims its own. */ export function claim(to: Address, hook: Address = ADDRESSES.hook) { return { address: hook, abi: hookAbi, functionName: "claim", args: [to] } as const; } /** * v2: share out the Revenue Router pots of `ids` (pools whose payee the wallet is), then claim * everything the wallet is owed on v2 to `to`. */ export function claimFrom(to: Address, ids: Hex[], hook: Address = ADDRESSES.v2!.hook) { return { address: hook, abi: hookV2Abi, functionName: "claimFrom", args: [to, ids] } as const; } /** v2: claim a referral code (3 to 20 of a-z, 0-9 and "-", one per wallet), as its bytes padded to 32. */ export function claimCode(code: Hex, router: Address = ADDRESSES.v2!.router) { return { address: router, abi: routerV2Abi, functionName: "claimCode", args: [code] } as const; } /** The launch factory's settings: fees, taxes and shares in pips (1e6 = 100%), windows in seconds. */ export type LaunchSettings = { baseFee: number; dynamicFee: boolean; dynBase: number; dynMax: number; dynSensitivity: number; snipeWindow: number; snipeStartFee: number; snipeMaxBuy: number; maxTxWindow: number; maxTxShare: number; buyTax: number; sellTax: number; sandwichGuard: boolean; allowlistWindow: number; team: Address; teamShare: number; buybackShare: number; buybackEvery: number; burnShare: number; vestShare: number; vestCliff: number; vestDuration: number; }; export type LaunchParams = { name: string; symbol: string; /** In token base units (18 decimals): 1e18 to 1e33. */ supply: bigint; /** The starting market cap, in wei. */ startMcap: bigint; settings: LaunchSettings; meta: { description: string; image: string; website: string; x: string; telegram: string }; /** Up to 200 wallets for the Allowlist Phase; the creator is always added. */ allowlist: Address[]; }; /** No blocks: a flat 1% pool fee. Spread it and set what you need. */ export const PLAIN: LaunchSettings = { baseFee: 10_000, dynamicFee: false, dynBase: 0, dynMax: 0, dynSensitivity: 0, snipeWindow: 0, snipeStartFee: 0, snipeMaxBuy: 0, maxTxWindow: 0, maxTxShare: 0, buyTax: 0, sellTax: 0, sandwichGuard: false, allowlistWindow: 0, team: "0x0000000000000000000000000000000000000000", teamShare: 0, buybackShare: 0, buybackEvery: 0, burnShare: 0, vestShare: 0, vestCliff: 0, vestDuration: 0, }; /** * Launch a token on v1. The sender is the creator, so send it from the creator's own wallet, never * through a forwarder or multicall contract (that contract would be the creator); `devBuy` wei is * their first buy, locked 7 days. * Check the settings first with the factory's toRules(settings, supply), which names the first one * out of range. */ export function launch(params: LaunchParams, devBuy = 0n, factory: Address = ADDRESSES.factory) { return { address: factory, abi: factoryAbi, functionName: "launch", args: [params], value: devBuy } as const; } /** * v2's launch settings: v1's without Revenue Split's wallet and share (v2's payees go beside the * settings), plus Wallet Cap, Exit Clock, Team Vesting's own wallet and the dev lock's length. */ export type LaunchSettingsV2 = Omit & { /** Seconds, 60 to 86,400; 0 turns Wallet Cap off. */ walletCapWindow: number; /** Share of the supply each wallet may buy in all, 1,000 to 50,000 pips. */ walletCapShare: number; /** Exit Clock's fee on an instant sell, up to 100,000 pips; 0 turns it off. */ exitStartFee: number; /** Seconds held until Exit Clock charges nothing: 1 hour to 30 days. */ exitPeriod: number; vestWallet: Address; /** 1 to 365, or 0 for the default of 7. */ devLockDays: number; }; export type LaunchParamsV2 = Omit & { settings: LaunchSettingsV2; /** Revenue Router: up to 8 wallets, each with its share of the creator's income in pips. */ payees: Payee[]; }; /** v2 with no blocks: a flat 1% pool fee. Spread it and set what you need. */ export const PLAIN_V2: LaunchSettingsV2 = { baseFee: 10_000, dynamicFee: false, dynBase: 0, dynMax: 0, dynSensitivity: 0, snipeWindow: 0, snipeStartFee: 0, snipeMaxBuy: 0, maxTxWindow: 0, maxTxShare: 0, walletCapWindow: 0, walletCapShare: 0, buyTax: 0, sellTax: 0, exitStartFee: 0, exitPeriod: 0, sandwichGuard: false, allowlistWindow: 0, buybackShare: 0, buybackEvery: 0, burnShare: 0, vestWallet: zeroAddress, vestShare: 0, vestCliff: 0, vestDuration: 0, devLockDays: 0, }; /** * Launch a token on v2. As on v1 the sender is the creator: send it from their own wallet. `devBuy` * wei is the creator's first buy, locked for `settings.devLockDays`. */ export function launchV2(params: LaunchParamsV2, devBuy = 0n, factory: Address = ADDRESSES.v2!.factory) { return { address: factory, abi: factoryV2Abi, functionName: "launch", args: [params], value: devBuy } as const; }