From 664598bf0ff06eb50d448880ee4daac55b7e72bc Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Wed, 25 Feb 2026 16:14:33 -0500 Subject: [PATCH 01/21] initial commit --- .gitignore | 6 + .gitmodules | 7 + aggregator-hooks/.env.example | 23 + aggregator-hooks/README.md | 186 +++ aggregator-hooks/historical/FluidDexLite.ts | 157 +++ aggregator-hooks/historical/FluidDexT1.ts | 248 ++++ aggregator-hooks/historical/StableSwapNG.ts | 232 ++++ aggregator-hooks/package-lock.json | 712 ++++++++++ aggregator-hooks/package.json | 22 + aggregator-hooks/src/cli.ts | 77 ++ aggregator-hooks/src/createPools.ts | 1294 +++++++++++++++++++ aggregator-hooks/tsconfig.json | 24 + foundry.lock | 21 + lib/v4-core | 1 + lib/v4-hooks-public | 1 + mine_hook.sh | 136 ++ remappings.txt | 9 +- script/SelfCreateHook.s.sol | 141 ++ 18 files changed, 3296 insertions(+), 1 deletion(-) create mode 100644 aggregator-hooks/.env.example create mode 100644 aggregator-hooks/README.md create mode 100644 aggregator-hooks/historical/FluidDexLite.ts create mode 100644 aggregator-hooks/historical/FluidDexT1.ts create mode 100644 aggregator-hooks/historical/StableSwapNG.ts create mode 100644 aggregator-hooks/package-lock.json create mode 100644 aggregator-hooks/package.json create mode 100644 aggregator-hooks/src/cli.ts create mode 100644 aggregator-hooks/src/createPools.ts create mode 100644 aggregator-hooks/tsconfig.json create mode 160000 lib/v4-core create mode 160000 lib/v4-hooks-public create mode 100644 mine_hook.sh create mode 100644 script/SelfCreateHook.s.sol diff --git a/.gitignore b/.gitignore index 964ba404..161774a7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,12 @@ deployments/**/31337.* broadcast/**/dry-run/** script/deploy/tasks/1337/** script/deploy/tasks/31337/** +aggregator-hooks/.env +aggregator-hooks/checkpoints/ +aggregator-hooks/detected/ +aggregator-hooks/node_modules/ +deployed-pools/ + debug/ Cargo.lock diff --git a/.gitmodules b/.gitmodules index 2e1c76fa..756c598d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -61,3 +61,10 @@ [submodule "src/pkgs/universal-router-2_0"] path = src/pkgs/universal-router-2_0 url = https://github.com/Uniswap/universal-router +[submodule "lib/v4-hooks-public"] + path = lib/v4-hooks-public + url = https://github.com/Uniswap/v4-hooks-public + branch = aggregator-hooks +[submodule "lib/v4-core"] + path = lib/v4-core + url = https://github.com/Uniswap/v4-core \ No newline at end of file diff --git a/aggregator-hooks/.env.example b/aggregator-hooks/.env.example new file mode 100644 index 00000000..87d3d8eb --- /dev/null +++ b/aggregator-hooks/.env.example @@ -0,0 +1,23 @@ +# Environment variables for discovery scripts. +# All chain-specific vars must use VAR_ (e.g. RPC_URL_1, RPC_URL_8453). + +# createPools.ts +# RPC_URL_ required when using --chain-id +# PRIVATE_KEY= required for forge script (even with --dry-run) + +# Optional: RPS, CONCURRENCY (for stableswapng, fluiddext1 historical) +# RPS_1= +# CONCURRENCY_1= + +# Chain ID +#RPC_URL_= +#POOL_MANAGER_= # createPools self-deploy +#FLUID_LIQUIDITY_= # createPools self-deploy (fluiddext1 only) +#DEX_LITE_ADDRESS_= # discovery + createPools self-deploy (fluiddexlite) +#DEX_LITE_RESOLVER_ADDRESS_= # discovery + createPools self-deploy (fluiddexlite) +#FLUID_DEX_FACTORY_= +#FLUID_DEX_RESOLVER_= +#FACTORY_ADDRESS_= + + + diff --git a/aggregator-hooks/README.md b/aggregator-hooks/README.md new file mode 100644 index 00000000..e42291e4 --- /dev/null +++ b/aggregator-hooks/README.md @@ -0,0 +1,186 @@ +# Aggregator Hook Scripts + +Aggregator hook pool discovery and creation utilities. Merged from agg-hook-tests. + +## Factories + +Aggregator Hook factories should be deployed before running any of these scripts, although not strictly necessary. If desired you can deploy and initialize directly from an EOA. If Factories are not deployed, you must use --self-deploy when creating pools. + +## Scripts + +### Historical discovery (one-time full scrape) + +| Script | Description | +|--------|-------------| +| `historical/FluidDexLite.ts` | Scrape LogInitialize events from FluidDexLite | +| `historical/FluidDexT1.ts` | Scrape LogDexDeployed events from FluidDexFactory | +| `historical/StableSwapNG.ts` | Enumerate pool_count + pool_list from Curve StableSwap-NG factory | + +### Polling discovery (checkpoint-based, for cron) + +| Script | Description | +|--------|-------------| +| `polling/FluidDexLite.ts` | Poll LogInitialize since checkpoint | +| `polling/FluidDexT1.ts` | Poll LogDexDeployed since checkpoint | +| `polling/StableSwapNG.ts` | Poll PlainPoolDeployed/MetaPoolDeployed events since checkpoint; fetch last N pools by index | + +### Pool creation + +| Script | Description | +|--------|-------------| +| `src/createPools.ts` | Create Uniswap v4 hooks from discovered pool configs | + +--- + +## Environment variables + +All discovery scripts use chain-ID-suffixed env vars. Use `VAR_` (e.g. `RPC_URL_1`, `RPC_URL_8453`) or `VAR` for single-chain usage. + +### By script + +| Script | Required | Optional | +|--------|----------|----------| +| **fluiddexlite** (hist) | `RPC_URL` | `DEX_LITE_RESOLVER_ADDRESS` (default mainnet resolver) | +| **fluiddexlite** (poll) | `RPC_URL`, `DEX_LITE_ADDRESS` | — | +| **fluiddext1** (poll + hist) | `RPC_URL`, `FLUID_DEX_RESOLVER` | `FLUID_DEX_FACTORY`, `FACTORY_ADDRESS`, `RPS`, `CONCURRENCY` | +| **stableswapng** (poll + hist) | `RPC_URL` | `FACTORY_ADDRESS`, `RPS`, `CONCURRENCY`, `FINALITY_BLOCKS`, `LOOKBACK_BLOCKS` (poll only) | + +### Polling-only env (optional) + +| Env | Default | Description | +|-----|---------|-------------| +| `FINALITY_BLOCKS` | 10 | Subtract from latest; checkpoint = last scanned block | +| `LOOKBACK_BLOCKS` | 200000 | Used when checkpoint missing and no `--start-block` | + +--- + +## CLI arguments + +All discovery scripts require `--chain-id `. + +### Common args + +| Arg | Default (poll) | Default (hist) | Description | +|-----|----------------|----------------|-------------| +| `--chain-id` | (required) | (required) | Chain ID; selects env vars | +| `--output-dir` | `detected` | `detected` | Output base dir; files go to `output-dir/chain-id/.json` | +| `--checkpoint-dir` | `checkpoints` | — | Checkpoint dir (polling only) | +| `--chunk-blocks` | 10000 | 100000 | Block chunk size for getLogs | +| `--start-block` | — | — | Start scan from this block. Override checkpoint for polling. | + +### Historical-only args + +| Arg | Default | Description | +|-----|---------|-------------| +| `--end-block` | latest | End block for event scan | + +### Script-specific args + +| Script | Arg | Default | Description | +|--------|-----|---------|-------------| +| fluiddext1 (hist) | `--mode` | enumerate | `logs` \| `enumerate` \| `both` | +| stableswapng (poll) | `--chunk-blocks` | 10000 | Block chunk size for event scan | +| stableswapng (hist) | `--chunk` | 500 | pool_list batch size | +| stableswapng (hist) | `--start-index` | 0 | Start pool_list index | + +--- + +## Output paths + +- **Output**: `{OUTPUT_DIR}/{CHAIN_ID}/{OUTPUT_FILE}.json` +- **Checkpoint** (polling): `{CHECKPOINT_DIR}/{CHAIN_ID}/{CHECKPOINT_FILE}.json` + +| Script | Output file | Checkpoint file | +|--------|-------------|-----------------| +| fluiddexlite | fluiddexlite-pools.json | dexlite_checkpoint.json | +| fluiddext1 | fluiddext1-pools.json | fluiddext1_checkpoint.json | +| stableswapng | stableswapng-pools.json | stableswapng_checkpoint.json | + +--- + +## Example invocations + +```bash +# From contracts/aggregator-hooks/ directory: +cd aggregator-hooks && npm install + +# Historical fluiddexlite on mainnet +npx tsx historical/FluidDexLite.ts --chain-id 1 + +# Historical fluiddext1 on Base +npx tsx historical/FluidDexT1.ts --chain-id 8453 --output-dir output + +# Polling fluiddexlite on mainnet +npx tsx polling/FluidDexLite.ts --chain-id 1 + +# Historical stableswapng with custom chunk +npx tsx historical/StableSwapNG.ts --chain-id 1 --chunk 200 + +# createPools with chain-id (uses RPC_URL_1 from env) +npx tsx src/createPools.ts detected/1/fluiddexlite-pools-curated.json 0xFactoryAddr --chain-id 1 +``` + +--- + +## createPools + +### Arguments + +| Arg | Required | Default | Description | +|-----|----------|---------|-------------| +| `jsonFile` | yes | — | Path to JSON file with pool configs (each must have `poolType`) | +| `factoryAddress` | yes* | — | Factory contract address (*required when not using `--self-deploy`) | +| `--self-deploy` | no | — | Deploy hooks from wallet instead of via factory | +| `--chain-id ` | no | — | Chain ID; selects `RPC_URL_` from env | +| `--registry-dir ` | no | — | Append deployed pools to `deployed-.json` in this dir | +| `--dry-run` | no | — | Simulate forge scripts without broadcasting | +| `--verbose`, `-v` | no | — | Run forge scripts with `-vvvv` | +| `--start-at ` | no | 1 | Start at 1-based pool index (skip earlier pools). Use to resume. | +| `--jobs `, `-j ` | no | 1 | Parallel salt mining workers (1–16). Speeds up mining. | + +**Modes:** + +- **Factory mode:** `createPools.ts pools.json 0xFactoryAddr [--chain-id 1] [--registry-dir ./deployed-pools]` +- **Self-deploy:** `createPools.ts pools.json --self-deploy [--chain-id 1] [--registry-dir ./deployed-pools]` + +### Environment variables + +| Env | Description | +|-----|-------------| +| `RPC_URL` or `RPC_URL_` | RPC endpoint (use `RPC_URL_1` etc. when `--chain-id` is set) | +| `PRIVATE_KEY` | Signing key for transactions (required even with `--dry-run`) | + +### Dry run and verbose + +Use `--dry-run` to simulate without broadcasting: + +```bash +npx tsx src/createPools.ts your-pools.json --self-deploy --chain-id 1 --dry-run +``` + +Use `--verbose` or `-v` to run forge scripts with maximum verbosity (`-vvvv`) and log full forge output on errors. + +--- + +## Setup + +### TypeScript scripts + +```bash +cd aggregator-hooks +npm install +cp .env.example .env +# Edit .env with your values +``` + +### Solidity (mine_hook, createPools) + +The `createPools` script and `mine_hook.sh` run from the **contracts/** directory (project root). They require: + +1. **v4-hooks-public** (aggregator-hooks branch): Already added as submodule. Ensure it's on the `aggregator-hooks` branch: + ```bash + cd lib/v4-hooks-public && git fetch origin aggregator-hooks && git checkout aggregator-hooks + git submodule update --init --recursive + ``` + +2. **Foundry**: `forge` must be available. The scripts use `script/SelfCreateHook.s.sol` and `lib/v4-hooks-public/script/MineAggregatorHook.s.sol`. diff --git a/aggregator-hooks/historical/FluidDexLite.ts b/aggregator-hooks/historical/FluidDexLite.ts new file mode 100644 index 00000000..d2922335 --- /dev/null +++ b/aggregator-hooks/historical/FluidDexLite.ts @@ -0,0 +1,157 @@ +/** + * Fluid Dex Lite historical pool discovery. + * Enumerates all pools via FluidDexLiteResolver.getAllDexes(). + * + * FluidDexLite does NOT emit LogInitialize events; pool discovery uses the + * resolver's getAllDexes() view (see Fluid DEX Lite integration docs). + * + * Usage: + * npx tsx historical/fluiddexlite.ts --chain-id 1 + * + * Options: + * --chain-id (required) Chain ID; loads RPC_URL_, DEX_LITE_RESOLVER_ADDRESS_ + * --output-dir output directory (default: detected); writes to output-dir/chain-id/fluiddexlite-pools.json + * + * Env vars (use VAR_ or VAR for single chain): + * RPC_URL (required) + * DEX_LITE_RESOLVER_ADDRESS (optional) FluidDexLiteResolver; default mainnet: 0x26b696D0dfDAB6c894Aa9a6575fCD07BB25BbD2C + * DEX_LITE_ADDRESS (optional, legacy) kept for backward compat; resolver is used for discovery + * + * Output: JSON array in createPools.ts FluidDexLitePoolConfig format. + * Fees are fetched via getDexState() and converted from Fluid 1e4 to Uniswap v4 1e6 format. + */ + +import "dotenv/config"; +import fs from "node:fs"; +import path from "node:path"; +import { ethers } from "ethers"; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; + +const OUTPUT_FILE = "fluiddexlite-pools.json"; +const DEFAULT_RESOLVER = "0x26b696D0dfDAB6c894Aa9a6575fCD07BB25BbD2C"; + +/** Fluid native token; map to address(0) for Uniswap v4 pool init */ +const FLUID_NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + +function toUniswapV4Currency(addr: string): string { + return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() ? ZERO_ADDRESS : ethers.getAddress(addr); +} + +/** Same shape as createPools.ts FluidDexLitePoolConfig */ +type CreatePoolsFluidLiteConfig = { + poolType: "fluiddexlite"; + dexSalt: string; + currency0: string; + currency1: string; + fee: number | null; + tickSpacing: number | null; + sqrtPriceX96: string | null; +}; + +const RESOLVER_ABI = [ + "function getAllDexes() external view returns (tuple(address token0, address token1, bytes32 salt)[] memory)", + "function getDexState(tuple(address token0, address token1, bytes32 salt) dexKey) external view returns (tuple(tuple(uint256 fee, uint256 revenueCut, uint256 rebalancingStatus, bool isCenterPriceShiftActive, uint256 centerPrice, address centerPriceAddress, bool isRangePercentShiftActive, uint256 upperRangePercent, uint256 lowerRangePercent, bool isThresholdPercentShiftActive, uint256 upperShiftThresholdPercent, uint256 lowerShiftThresholdPercent, uint256 token0Decimals, uint256 token1Decimals, uint256 totalToken0AdjustedAmount, uint256 totalToken1AdjustedAmount) dexVariables, tuple(uint256 lastInteractionTimestamp, uint256 rebalancingShiftingTime, uint256 maxCenterPrice, uint256 minCenterPrice, uint256 shiftPercentage, uint256 centerPriceShiftingTime, uint256 startTimestamp) centerPriceShift, tuple(uint256 oldUpperRangePercent, uint256 oldLowerRangePercent, uint256 shiftingTime, uint256 startTimestamp) rangeShift, tuple(uint256 oldUpperThresholdPercent, uint256 oldLowerThresholdPercent, uint256 shiftingTime, uint256 startTimestamp) thresholdShift) dexState)", +] as const; + +/** Fluid fee uses 1e4 basis (10000 = 100%). Uniswap v4 uses 1e6 (10000 = 1%). */ +function fluidFeeToUniswapV4(fluidFee: bigint | number): number { + const MAX_U24 = 16_777_215; + const converted = Number(fluidFee) * 100; // fluidFee * 1e6 / 1e4 + return Math.min(Math.max(0, Math.floor(converted)), MAX_U24); +} + +function ensureDirForFile(filePath: string) { + const dir = path.dirname(path.resolve(filePath)); + fs.mkdirSync(dir, { recursive: true }); +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + const chainIdRaw = args["chain-id"]; + if (chainIdRaw == null) { + console.error("Missing required --chain-id "); + process.exit(1); + } + const chainId = toInt(chainIdRaw, 0); + if (chainId <= 0) { + console.error("--chain-id must be a positive integer"); + process.exit(1); + } + + const rpcUrl = getEnvForChain("RPC_URL", chainId); + const resolverRaw = getEnvForChain("DEX_LITE_RESOLVER_ADDRESS", chainId) ?? DEFAULT_RESOLVER; + if (!rpcUrl) { + console.error("Missing env: RPC_URL (or RPC_URL_)"); + process.exit(1); + } + + const resolver = ethers.getAddress(resolverRaw); + const outputDir = (args["output-dir"] as string) ?? "detected"; + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const resolverContract = new ethers.Contract(resolver, RESOLVER_ABI as unknown as string[], provider); + + console.error(`[enum] FluidDexLiteResolver at ${resolver}`); + const dexKeys = (await resolverContract.getAllDexes()) as Array<{ + token0: string; + token1: string; + salt: string; + }>; + + const configs: CreatePoolsFluidLiteConfig[] = []; + for (const dk of dexKeys) { + const mapped = [toUniswapV4Currency(dk.token0), toUniswapV4Currency(dk.token1)].sort((a, b) => + a.toLowerCase().localeCompare(b.toLowerCase()), + ); + const [currency0, currency1] = [mapped[0], mapped[1]]; + + let fee: number | null = null; + try { + const dexKey = { token0: dk.token0, token1: dk.token1, salt: dk.salt }; + const dexState = (await resolverContract.getDexState(dexKey)) as { + dexVariables: { fee: bigint | number }; + }; + const fluidFee = dexState.dexVariables.fee; + if (fluidFee && Number(fluidFee) > 0) { + fee = fluidFeeToUniswapV4(fluidFee); + } + } catch { + // getDexState reverted; keep fee null + } + + configs.push({ + poolType: "fluiddexlite", + dexSalt: dk.salt as string, + currency0, + currency1, + fee, + tickSpacing: null, + sqrtPriceX96: null, + }); + } + + const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); + ensureDirForFile(outPath); + fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + "\n"); + + console.log( + JSON.stringify( + { + ok: true, + chainId, + resolver, + poolsFound: configs.length, + outFile: outPath, + }, + null, + 2, + ), + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/aggregator-hooks/historical/FluidDexT1.ts b/aggregator-hooks/historical/FluidDexT1.ts new file mode 100644 index 00000000..bd8e7e26 --- /dev/null +++ b/aggregator-hooks/historical/FluidDexT1.ts @@ -0,0 +1,248 @@ +/** + * Fluid Dex T1 historical pool discovery. + * Scrapes LogDexDeployed events from FluidDexFactory and fetches tokens via resolver. + * + * Usage: + * npx tsx historical/fluiddext1.ts --chain-id 1 + * + * Options: + * --chain-id (required) Chain ID; loads RPC_URL_, FLUID_DEX_RESOLVER_, etc. + * --output-dir output directory (default: output); writes to output-dir/chain-id/fluiddext1-pools.json + * --chunk-blocks block chunk size for getLogs (default: 100000) + * --start-block start block for log scan (default: 0) + * --end-block end block for log scan (default: latest) + * --mode logs|enumerate|both (default: logs) + * + * Env vars (use VAR_ or VAR for single chain): + * RPC_URL (required) + * FLUID_DEX_RESOLVER (required) + * FLUID_DEX_FACTORY (optional, default mainnet) + * RPS (optional, default 80) max RPC requests per second + * CONCURRENCY (optional, default 8) max concurrent RPC calls + * + * Output: JSON array in createPools.ts FluidDexT1PoolConfig format. + */ + +import "dotenv/config"; +import fs from "node:fs"; +import path from "node:path"; +import { JsonRpcProvider, Contract, Interface, getAddress } from "ethers"; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; + +const OUTPUT_FILE = "fluiddext1-pools.json"; +const DEFAULT_FACTORY = "0x91716c4eDA1fB55e84Bf8b4c7085f84285c19085"; + +/** Same shape as createPools.ts FluidDexT1PoolConfig */ +type CreatePoolsFluidDexT1Config = { + poolType: "fluiddext1"; + fluidPool: string; + currency0: string; + currency1: string; + fee: number | null; + tickSpacing: number | null; + sqrtPriceX96: string | null; +}; + +const FACTORY_ABI = [ + "event LogDexDeployed(address indexed dex, uint256 indexed dexId)", + "function totalDexes() external view returns (uint256)", + "function getDexAddress(uint256 dexId) public view returns (address)", + "function isDex(address dex) public view returns (bool)", +] as const; + +const RESOLVER_ABI = [ + "function getPoolTokens(address pool) external view returns (address token0, address token1)", +] as const; + +/** Fluid native token; map to address(0) for Uniswap v4 pool init */ +const FLUID_NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + +function toUniswapV4Currency(addr: string): string { + return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() ? ZERO_ADDRESS : getAddress(addr); +} + +function pRateLimit(rps: number): () => Promise { + if (rps <= 0) return async () => {}; + const minGapMs = 1000 / rps; + let nextAllowed = 0; + return async function acquire() { + const now = Date.now(); + if (now < nextAllowed) { + await new Promise((r) => setTimeout(r, nextAllowed - now)); + } + nextAllowed = Math.max(now, nextAllowed) + minGapMs; + }; +} + +function pLimit(concurrency: number) { + let active = 0; + const queue: Array<() => void> = []; + + const next = () => { + active--; + const fn = queue.shift(); + if (fn) fn(); + }; + + return async function limit(fn: () => Promise): Promise { + if (active >= concurrency) { + await new Promise((resolve) => queue.push(resolve)); + } + active++; + try { + return await fn(); + } finally { + next(); + } + }; +} + +/** Return [currency0, currency1] with currency0 < currency1 (lexicographic on mapped addrs) */ +function orderCurrencies(token0: string, token1: string): [string, string] { + const mapped = [toUniswapV4Currency(token0), toUniswapV4Currency(token1)].sort((a, b) => + a.toLowerCase().localeCompare(b.toLowerCase()), + ); + return [mapped[0], mapped[1]]; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + const chainIdRaw = args["chain-id"]; + if (chainIdRaw == null) { + console.error("Missing required --chain-id "); + process.exit(1); + } + const chainId = toInt(chainIdRaw, 0); + if (chainId <= 0) { + console.error("--chain-id must be a positive integer"); + process.exit(1); + } + + const rpcUrl = getEnvForChain("RPC_URL", chainId); + const reservesResolverAddr = + getEnvForChain("FLUID_DEX_RESERVES_RESOLVER", chainId) ?? getEnvForChain("FLUID_DEX_RESOLVER", chainId); + const factoryAddrRaw = + getEnvForChain("FLUID_DEX_FACTORY", chainId) ?? getEnvForChain("FACTORY_ADDRESS", chainId) ?? DEFAULT_FACTORY; + + if (!rpcUrl || !reservesResolverAddr) { + console.error("Missing env: RPC_URL and FLUID_DEX_RESOLVER"); + process.exit(1); + } + + const factoryAddr = getAddress(factoryAddrRaw.toLowerCase()); + const outputDir = (args["output-dir"] as string) ?? "detected"; + const chunkSize = BigInt(Math.max(1, toInt(args["chunk-blocks"], 100_000))); + const startBlock = BigInt(Math.max(0, toInt(args["start-block"], 0))); + const endBlockRaw = args["end-block"]; + const mode = ((args["mode"] as string | undefined) ?? "enumerate").toLowerCase(); + const rps = toInt(args["rps"] ?? getEnvForChain("RPS", chainId), 80); + const concurrency = Math.max(1, toInt(getEnvForChain("CONCURRENCY", chainId), 8)); + + const rateLimitAcquire = pRateLimit(rps); + const limit = pLimit(concurrency); + + const provider = new JsonRpcProvider(rpcUrl); + const factory = new Contract(factoryAddr, FACTORY_ABI, provider); + const resolver = new Contract(getAddress(reservesResolverAddr.toLowerCase()), RESOLVER_ABI, provider); + + const latest = BigInt(await provider.getBlockNumber()); + const endBlock = endBlockRaw ? BigInt(String(endBlockRaw)) : latest; + + if (startBlock < 0n) throw new Error("start-block must be >= 0"); + if (endBlock < startBlock) throw new Error("end-block must be >= start-block"); + + const byDexAddr = new Map(); + + if (mode === "logs" || mode === "both") { + const iface = new Interface(FACTORY_ABI as unknown as string[]); + const topic0 = iface.getEvent("LogDexDeployed")!.topicHash; + + for (let from = startBlock; from <= endBlock; from += chunkSize) { + const to = from + chunkSize - 1n > endBlock ? endBlock : from + chunkSize - 1n; + + const logs = await provider.getLogs({ + address: factoryAddr, + fromBlock: from, + toBlock: to, + topics: [topic0], + }); + + for (const log of logs) { + const parsed = iface.parseLog(log); + const dex = getAddress(parsed!.args.dex); + const dexId = parsed!.args.dexId.toString(); + if (!byDexAddr.has(dex)) byDexAddr.set(dex, dexId); + } + + if ((to - startBlock) % (chunkSize * 10n) === 0n || to === endBlock) { + console.error(`[logs] scanned ${from}..${to} (${logs.length} logs)`); + } + } + } + + if (mode === "enumerate" || mode === "both") { + const total = await factory.totalDexes(); + console.error(`[enumerate] totalDexes() = ${total}`); + + for (let i = 1n; i <= total; i++) { + const dex = getAddress(await factory.getDexAddress(i)); + const ok = await factory.isDex(dex); + if (!ok) continue; + + if (!byDexAddr.has(dex)) byDexAddr.set(dex, i.toString()); + + if (i % 50n === 0n || i === total) { + console.error(`[enumerate] ${i}/${total}`); + } + } + } + + const uniqueDexes = Array.from(byDexAddr.keys()); + console.log(`Found ${uniqueDexes.length} unique Fluid Dex T1 pools. Fetching tokens via resolver...`); + + const configs: CreatePoolsFluidDexT1Config[] = []; + let skipped = 0; + + for (const fluidPool of uniqueDexes) { + const config = await limit(async () => { + await rateLimitAcquire(); + let token0: string; + let token1: string; + try { + [token0, token1] = await resolver.getPoolTokens(fluidPool); + } catch { + return null; + } + const [currency0, currency1] = orderCurrencies(token0, token1); + + return { + poolType: "fluiddext1" as const, + fluidPool, + currency0, + currency1, + fee: null, + tickSpacing: null, + sqrtPriceX96: null, + } satisfies CreatePoolsFluidDexT1Config; + }); + + if (config) configs.push(config); + else skipped++; + } + + if (skipped > 0) { + console.error(`Skipped ${skipped} pools (getPoolTokens reverted - may be VaultT1 or deprecated)`); + } + + const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, JSON.stringify(configs, null, 2)); + console.log(`Wrote ${outPath} (${configs.length} pools in createPools.ts format)`); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/aggregator-hooks/historical/StableSwapNG.ts b/aggregator-hooks/historical/StableSwapNG.ts new file mode 100644 index 00000000..ab4636d1 --- /dev/null +++ b/aggregator-hooks/historical/StableSwapNG.ts @@ -0,0 +1,232 @@ +/** + * StableSwap-NG historical pool discovery. + * Enumerates all pools from Curve StableSwap-NG Factory via pool_count + pool_list. + * + * Usage: + * npx tsx historical/stableswapng.ts --chain-id 1 + * + * Options: + * --chain-id (required) Chain ID; loads RPC_URL_, FACTORY_ADDRESS_ + * --output-dir output directory (default: output); writes to output-dir/chain-id/stableswapng-pools.json + * --chunk chunk size for pool_list reads (default: 500) + * --start-index start at pool_list index n (default: 0) + * + * Env vars (use VAR_ or VAR for single chain): + * RPC_URL (required) + * FACTORY_ADDRESS (optional, default mainnet Curve StableSwap-NG factory) + * RPS (optional, default 80) max RPC requests per second + * CONCURRENCY (optional, default 8) max concurrent RPC calls + * + * Output: JSON array in createPools.ts StableSwapPoolConfig format. + */ + +import "dotenv/config"; +import fs from "node:fs"; +import path from "node:path"; +import { JsonRpcProvider, Contract, getAddress, ZeroAddress } from "ethers"; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; + +const OUTPUT_FILE = "stableswapng-pools.json"; +const DEFAULT_FACTORY = "0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf"; + +type PoolMeta = { + pool: string; + kind: "plain" | "meta"; + nCoins?: number; + coins?: string[]; + basePool?: string; +}; + +/** Same shape as createPools.ts StableSwapPoolConfig for stableswapng pool type */ +type CreatePoolsStableSwapConfig = { + poolType: "stableswapng"; + curvePool: string; + tokens: string[]; + fee: number | null; + tickSpacing: number | null; + sqrtPriceX96: string | null; +}; + +const CREATE_POOLS_DEFAULTS = { + fee: null, + tickSpacing: null, + sqrtPriceX96: null, +} as const; + +const FACTORY_ABI = [ + "function pool_count() view returns (uint256)", + "function pool_list(uint256) view returns (address)", + "function get_n_coins(address) view returns (uint256)", + "function get_coins(address) view returns (address[])", + "function get_base_pool(address) view returns (address)", +]; + +function saveJson(filePath: string, data: unknown) { + fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); +} + +function pRateLimit(rps: number): () => Promise { + if (rps <= 0) return async () => {}; + const minGapMs = 1000 / rps; + let nextAllowed = 0; + return async function acquire() { + const now = Date.now(); + if (now < nextAllowed) { + await new Promise((r) => setTimeout(r, nextAllowed - now)); + } + nextAllowed = Math.max(now, nextAllowed) + minGapMs; + }; +} + +function pLimit(concurrency: number) { + let active = 0; + const queue: Array<() => void> = []; + + const next = () => { + active--; + const fn = queue.shift(); + if (fn) fn(); + }; + + return async function limit(fn: () => Promise): Promise { + if (active >= concurrency) { + await new Promise((resolve) => queue.push(resolve)); + } + active++; + try { + return await fn(); + } finally { + next(); + } + }; +} + +function uniqAddresses(addrs: string[]): string[] { + const s = new Set(); + for (const a of addrs) { + if (!a) continue; + const norm = a.toLowerCase(); + if (norm === ZeroAddress.toLowerCase()) continue; + s.add(getAddress(a)); + } + return [...s]; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + const chainIdRaw = args["chain-id"]; + if (chainIdRaw == null) { + console.error("Missing required --chain-id "); + process.exit(1); + } + const chainId = toInt(chainIdRaw, 0); + if (chainId <= 0) { + console.error("--chain-id must be a positive integer"); + process.exit(1); + } + + const rpcUrl = getEnvForChain("RPC_URL", chainId); + const factoryAddrRaw = getEnvForChain("FACTORY_ADDRESS", chainId) ?? DEFAULT_FACTORY; + + if (!rpcUrl) { + console.error("Missing env: RPC_URL (or RPC_URL_)"); + process.exit(1); + } + + const factoryAddress = getAddress(factoryAddrRaw); + const outputDir = (args["output-dir"] as string) ?? "detected"; + const chunkSize = toInt(args["chunk"], 500); + const concurrency = Math.max(1, toInt(getEnvForChain("CONCURRENCY", chainId), 8)); + const rps = toInt(getEnvForChain("RPS", chainId), 80); + const rateLimitAcquire = rps > 0 ? pRateLimit(rps) : async () => {}; + const startIndex = toInt(args["start-index"], 0); + + const provider = new JsonRpcProvider(rpcUrl); + const factory = new Contract(factoryAddress, FACTORY_ABI, provider); + const limit = pLimit(concurrency); + + const poolCountBn: bigint = await factory.pool_count(); + const poolCount = Number(poolCountBn); + if (!Number.isFinite(poolCount) || poolCount < 0) { + throw new Error(`Unexpected pool_count: ${poolCountBn.toString()}`); + } + + console.log(`Factory: ${factoryAddress}`); + console.log(`pool_count: ${poolCount}`); + console.log(`Starting at index: ${startIndex}`); + console.log(`chunkSize=${chunkSize} concurrency=${concurrency} rps=${rps > 0 ? rps : "unlimited"}`); + + const pools: string[] = []; + const metas: PoolMeta[] = []; + const metaByPool = new Map(); + + for (let i = startIndex; i < poolCount; i += chunkSize) { + const end = Math.min(poolCount, i + chunkSize); + + const addrs = await Promise.all( + Array.from({ length: end - i }, (_, k) => + limit(async () => { + await rateLimitAcquire(); + const addr: string = await factory.pool_list(i + k); + return getAddress(addr); + }), + ), + ); + + pools.push(...addrs); + + const chunkMetas = await Promise.all( + addrs.map((pool) => + limit(async () => { + await rateLimitAcquire(); + const nCoinsBn = (await factory.get_n_coins(pool)) as bigint; + await rateLimitAcquire(); + const coinsRaw = (await factory.get_coins(pool)) as string[]; + await rateLimitAcquire(); + const basePoolRaw = await factory.get_base_pool(pool); + + const basePool = getAddress(basePoolRaw); + const coins = uniqAddresses(coinsRaw); + + return { + pool, + kind: basePool.toLowerCase() === ZeroAddress.toLowerCase() ? "plain" : "meta", + nCoins: Number(nCoinsBn), + coins, + basePool: basePool.toLowerCase() === ZeroAddress.toLowerCase() ? undefined : basePool, + } satisfies PoolMeta; + }), + ), + ); + + metas.push(...chunkMetas); + for (let j = 0; j < addrs.length; j++) metaByPool.set(addrs[j], chunkMetas[j]); + console.log(`Fetched [${i}, ${end}) / ${poolCount}`); + } + + const uniquePools = uniqAddresses(pools); + + const createPoolsConfigs: CreatePoolsStableSwapConfig[] = uniquePools.map((curvePool) => { + const meta = metaByPool.get(curvePool); + const tokens = meta?.coins ?? []; + return { + poolType: "stableswapng" as const, + curvePool, + tokens, + fee: CREATE_POOLS_DEFAULTS.fee, + tickSpacing: CREATE_POOLS_DEFAULTS.tickSpacing, + sqrtPriceX96: CREATE_POOLS_DEFAULTS.sqrtPriceX96, + }; + }); + + const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + saveJson(outPath, createPoolsConfigs); + console.log(`Wrote ${outPath} (${createPoolsConfigs.length} pools in createPools.ts format)`); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/aggregator-hooks/package-lock.json b/aggregator-hooks/package-lock.json new file mode 100644 index 00000000..fe004297 --- /dev/null +++ b/aggregator-hooks/package-lock.json @@ -0,0 +1,712 @@ +{ + "name": "agg-hook-scripts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agg-hook-scripts", + "version": "1.0.0", + "dependencies": { + "dotenv": "^17.3.1", + "ethers": "^6.13.0" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "tsx": "^4.19.2", + "typescript": "^5.7.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/aggregator-hooks/package.json b/aggregator-hooks/package.json new file mode 100644 index 00000000..ecd95f28 --- /dev/null +++ b/aggregator-hooks/package.json @@ -0,0 +1,22 @@ +{ + "name": "agg-hook-scripts", + "version": "1.0.0", + "description": "Aggregator hook pool discovery and creation scripts", + "type": "module", + "scripts": { + "build": "tsc", + "stableswapng": "tsx historical/StableSwapNG.ts", + "fluiddext1": "tsx historical/FluidDexT1.ts", + "fluiddexlite": "tsx historical/FluidDexLite.ts", + "create-pools": "tsx src/createPools.ts" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "tsx": "^4.19.2", + "typescript": "^5.7.0" + }, + "dependencies": { + "dotenv": "^17.3.1", + "ethers": "^6.13.0" + } +} diff --git a/aggregator-hooks/src/cli.ts b/aggregator-hooks/src/cli.ts new file mode 100644 index 00000000..d0b6347e --- /dev/null +++ b/aggregator-hooks/src/cli.ts @@ -0,0 +1,77 @@ +/** + * Shared CLI and env utilities for discovery scripts. + * All scripts use chain-ID-suffixed env vars and consistent CLI args. + */ +import path from "node:path"; + +/** Parse generic --key value args. Keys with no value get true. */ +export function parseArgs(argv: string[]): Record { + const out: Record = {}; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (!a.startsWith("--")) continue; + const key = a.slice(2); + const next = argv[i + 1]; + if (!next || next.startsWith("--")) { + out[key] = true; + } else { + out[key] = next; + i++; + } + } + return out; +} + +/** Get env var: VAR_${chainId} first, fallback to VAR for single-chain usage. */ +export function getEnvForChain(name: string, chainId: number): string | undefined { + const v = process.env[`${name}_${chainId}`]; + if (v != null && String(v).trim()) return String(v).trim(); + const fallback = process.env[name]; + if (fallback != null && String(fallback).trim()) return String(fallback).trim(); + return undefined; +} + +/** Require env var; throws if missing. */ +export function mustEnvForChain(name: string, chainId: number): string { + const v = getEnvForChain(name, chainId); + if (!v) throw new Error(`Missing required env var: ${name}_${chainId} or ${name}`); + return v; +} + +/** Parse int from string or number; return default if invalid. */ +export function toInt(v: unknown, def: number): number { + if (typeof v === "number") return Number.isFinite(v) ? Math.floor(v) : def; + if (typeof v === "string") { + const n = Number(v); + return Number.isFinite(n) ? Math.floor(n) : def; + } + return def; +} + +/** Resolve output path: outputDir/chainId/filename */ +export function resolveOutputPath(outputDir: string, chainId: number, filename: string): string { + return path.resolve(outputDir, String(chainId), filename); +} + +/** Resolve checkpoint path: checkpointDir/chainId/filename */ +export function resolveCheckpointPath(checkpointDir: string, chainId: number, filename: string): string { + return path.resolve(checkpointDir, String(chainId), filename); +} + +/** Common args for all scripts */ +export interface CommonArgs { + chainId: number; + outputDir: string; + chunkBlocks: number; +} + +/** Common args for polling scripts (add checkpoint dir) */ +export interface PollingArgs extends CommonArgs { + checkpointDir: string; +} + +/** Common args for historical scripts */ +export interface HistoricalArgs extends CommonArgs { + startBlock: number; + endBlock: number | null; +} diff --git a/aggregator-hooks/src/createPools.ts b/aggregator-hooks/src/createPools.ts new file mode 100644 index 00000000..d8f15a23 --- /dev/null +++ b/aggregator-hooks/src/createPools.ts @@ -0,0 +1,1294 @@ +#!/usr/bin/env node + +import "dotenv/config"; +import { ethers } from "ethers"; +import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"; +import { execSync, spawn } from "child_process"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +// projectRoot = contracts/ (where foundry.toml, script/, mine_hook.sh live) +const projectRoot = join(__dirname, "..", ".."); +import { getEnvForChain, mustEnvForChain, toInt } from "@src/cli"; + +/** PoolKey shape for registry output (matches Uniswap v4 PoolKey) */ +interface PoolKeyRecord { + currency0: string; + currency1: string; + fee: number; + tickSpacing: number; + hooks: string; +} + +/** Entry appended to pool-deployed registry */ +interface PoolDeployedEntry { + poolKeys: PoolKeyRecord[]; + metadata: { + externalPool: string; + hookAddress: string; + txHash?: string; + blockNumber?: number; + [key: string]: unknown; + }; +} + +// Foundry's default CREATE2 deployer - used when deploying via forge script with new X{salt} +const CREATE2_DEPLOYER = "0x4e59b44847b379578588920cA78FbF26c0B4956C"; + +// Protocol IDs from MineAggregatorHook.s.sol +const PROTOCOL_IDS = { + STABLESWAP: 0xc1, + STABLESWAPNG: 0xc2, + FLUIDDEXT1: 0xf1, + FLUIDDEXLITE: 0xf3, +} as const; + +type PoolType = "stableswap" | "stableswapng" | "fluiddext1" | "fluiddexlite"; + +interface StableSwapPoolConfig { + poolType: "stableswap" | "stableswapng"; + curvePool: string; + tokens: string[]; + fee: number | null; // Uniswap v4 fee (in basis points). null uses default: 0 + tickSpacing: number | null; // Uniswap v4 tick spacing. null uses default: 60 + sqrtPriceX96: string | null; // Uniswap v4 sqrt price. null uses default: 1:1 (2^96) +} + +interface FluidDexT1PoolConfig { + poolType: "fluiddext1"; + fluidPool: string; + currency0: string; + currency1: string; + fee: number | null; // Uniswap v4 fee (in basis points). null uses default: 0 + tickSpacing: number | null; // Uniswap v4 tick spacing. null uses default: 60 + sqrtPriceX96: string | null; // Uniswap v4 sqrt price. null uses default: 1:1 (2^96) +} + +interface FluidDexLitePoolConfig { + poolType: "fluiddexlite"; + dexSalt: string; + currency0: string; + currency1: string; + fee: number | null; // Uniswap v4 fee (in basis points). null uses default: 0 + tickSpacing: number | null; // Uniswap v4 tick spacing. null uses default: 60 + sqrtPriceX96: string | null; // Uniswap v4 sqrt price. null uses default: 1:1 (2^96) +} + +// Immutables for self-deploy (read from env) or factory (read from contract) +interface SelfDeployImmutables { + poolManager: string; + fluidDexReservesResolver?: string; + fluidLiquidity?: string; + fluidDexLite?: string; + fluidDexLiteResolver?: string; +} + +type PoolConfig = StableSwapPoolConfig | FluidDexT1PoolConfig | FluidDexLitePoolConfig; + +// Factory ABIs - minimal interfaces for reading immutables and calling createPool +const STABLE_SWAP_FACTORY_ABI = [ + "function POOL_MANAGER() external view returns (address)", + "function createPool(bytes32 salt, address curvePool, address[] calldata tokens, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)", +]; + +const STABLE_SWAP_NG_FACTORY_ABI = [ + "function POOL_MANAGER() external view returns (address)", + "function createPool(bytes32 salt, address curvePool, address[] calldata tokens, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)", +]; + +const FLUID_DEX_T1_FACTORY_ABI = [ + "function POOL_MANAGER() external view returns (address)", + "function fluidDexReservesResolver() external view returns (address)", + "function FLUID_LIQUIDITY() external view returns (address)", + "function createPool(bytes32 salt, address fluidPool, address currency0, address currency1, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)", +]; + +const FLUID_DEX_LITE_FACTORY_ABI = [ + "function POOL_MANAGER() external view returns (address)", + "function FLUID_DEX_LITE() external view returns (address)", + "function FLUID_DEX_LITE_RESOLVER() external view returns (address)", + "function createPool(bytes32 salt, bytes32 dexSalt, address currency0, address currency1, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)", +]; + +interface FactoryImmutables { + poolManager: string; + [key: string]: string; // For additional immutables +} + +interface ParsedArgs { + jsonFile: string; + factoryAddress: string | null; + selfDeploy: boolean; + rpcUrl: string; + /** Chain ID for env vars (required for self-deploy) */ + chainId: number | null; + /** When set, append deployed pools to registry files in this dir (e.g. --registry-dir ./deployed-pools) */ + registryDir: string | null; + /** When set, simulate forge scripts without broadcasting transactions */ + dryRun: boolean; + /** When set, run forge scripts verbosely (-vvvv) and log full output on errors */ + verbose: boolean; + /** 1-based index to start at (skip earlier pools). e.g. --start-at 3 starts with 3rd pool. */ + startAt: number; + /** Number of parallel mining workers (default 1) */ + jobs: number; +} + +/** + * Parse command line arguments + */ +function parseArgs(): ParsedArgs { + const args = process.argv.slice(2); + + // Check for --self-deploy flag + const selfDeployIndex = args.indexOf("--self-deploy"); + const selfDeploy = selfDeployIndex !== -1; + + // Extract positional args (exclude known flags and their values) + const flagNames = [ + "--self-deploy", + "--chain-id", + "--registry-dir", + "--dry-run", + "--verbose", + "-v", + "--start-at", + "--jobs", + "-j", + ]; + const positionalArgs: string[] = []; + for (let i = 0; i < args.length; i++) { + const a = args[i]; + if (flagNames.includes(a)) { + if (a === "--chain-id" || a === "--registry-dir" || a === "--start-at" || a === "--jobs" || a === "-j") i++; // skip value + continue; + } + positionalArgs.push(a); + } + + const minArgs = 1; + if (positionalArgs.length < minArgs) { + console.error("Usage: ts-node createPools.ts [factoryAddress] [--self-deploy] [--chain-id ]"); + console.error( + " jsonFile: Path to JSON file containing pool configurations (each config must have a 'poolType' field)", + ); + console.error(" factoryAddress: Factory contract address (required unless --self-deploy)"); + console.error(" --self-deploy: Deploy hooks directly from wallet instead of via factory"); + console.error(" --chain-id : Chain ID; selects RPC_URL_ from env (e.g. RPC_URL_1 for mainnet)"); + console.error(""); + console.error("Modes:"); + console.error( + " Factory mode: ts-node createPools.ts pools.json 0xFactoryAddr [--chain-id 1] [--registry-dir ./deployed-pools]", + ); + console.error( + " Self-deploy: ts-node createPools.ts pools.json --self-deploy [--chain-id 1] [--registry-dir ./deployed-pools]", + ); + console.error(" --dry-run: Simulate without broadcasting (forge script runs but no txs sent)"); + console.error(" --verbose, -v: Run forge scripts verbosely and log full output on errors"); + console.error( + " --start-at : Start at 1-based pool index (skip earlier pools). e.g. --start-at 3 to resume from pool 3.", + ); + console.error(" --jobs , -j : Run N parallel salt mining workers (default 1). Speeds up mining."); + console.error(""); + console.error("Environment variables:"); + console.error(" RPC_URL_: RPC endpoint (required when --chain-id set)"); + console.error(" PRIVATE_KEY: Private key for signing transactions (required)"); + process.exit(1); + } + + const jsonFile = positionalArgs[0]; + const factoryAddress = selfDeploy ? null : positionalArgs[1]; + + // Validate mutual exclusivity (factory address looks like 0x...) + if (selfDeploy && positionalArgs.length >= 2 && positionalArgs[1].startsWith("0x")) { + console.error("Error: --self-deploy and factoryAddress are mutually exclusive"); + process.exit(1); + } + + if (!selfDeploy && (!factoryAddress || positionalArgs.length < 2)) { + console.error("Error: In factory mode, factoryAddress is required (e.g. createPools.ts pools.json 0xFactoryAddr)"); + process.exit(1); + } + + const chainIdIndex = args.indexOf("--chain-id"); + const chainIdRaw = chainIdIndex !== -1 && args[chainIdIndex + 1] ? args[chainIdIndex + 1] : null; + const chainId = chainIdRaw != null ? toInt(String(chainIdRaw), 0) : null; + + const rpcUrl = + chainId != null && chainId > 0 + ? getEnvForChain("RPC_URL", chainId) + : (process.env.RPC_URL ?? "").trim() || undefined; + if (!rpcUrl) { + console.error( + chainId != null && chainId > 0 + ? `Error: RPC_URL_${chainId} environment variable is required when using --chain-id ${chainId}` + : "Error: RPC_URL environment variable is required", + ); + process.exit(1); + } + + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + console.error("Error: PRIVATE_KEY environment variable is required"); + process.exit(1); + } + + const registryDirIndex = args.indexOf("--registry-dir"); + const registryDir = registryDirIndex !== -1 && args[registryDirIndex + 1] ? args[registryDirIndex + 1] : null; + + const dryRun = args.includes("--dry-run"); + const verbose = args.includes("--verbose") || args.includes("-v"); + + const startAtIndex = args.indexOf("--start-at"); + const startAtRaw = startAtIndex !== -1 && args[startAtIndex + 1] ? args[startAtIndex + 1] : null; + const startAt = startAtRaw != null ? toInt(String(startAtRaw), 1) : 1; + if (startAt < 1) { + console.error("Error: --start-at must be >= 1"); + process.exit(1); + } + + const jobsIndex = args.indexOf("--jobs"); + const jobsIndexShort = args.indexOf("-j"); + const jobsIdx = jobsIndex !== -1 ? jobsIndex : jobsIndexShort; + const jobsRaw = jobsIdx !== -1 && args[jobsIdx + 1] ? args[jobsIdx + 1] : null; + const jobs = jobsRaw != null ? toInt(String(jobsRaw), 1) : 1; + if (jobs < 1 || jobs > 16) { + console.error("Error: --jobs must be between 1 and 16"); + process.exit(1); + } + + return { + jsonFile, + factoryAddress, + selfDeploy, + rpcUrl, + chainId, + registryDir, + dryRun, + verbose, + startAt, + jobs, + }; +} + +const POOL_TYPES: PoolType[] = ["stableswap", "stableswapng", "fluiddext1", "fluiddexlite"]; + +function isPoolType(s: unknown): s is PoolType { + return typeof s === "string" && POOL_TYPES.includes(s as PoolType); +} + +/** Default values for Uniswap v4 hook parameters when not specified */ +const DEFAULT_HOOK_PARAMS = { + fee: 0, + tickSpacing: 60, + sqrtPriceX96: "79228162514264337593543950336", // 1:1 (2^96) +} as const; + +/** + * Get resolved hook parameters with defaults applied + */ +function getHookParams(config: StableSwapPoolConfig | FluidDexT1PoolConfig | FluidDexLitePoolConfig) { + return { + fee: config.fee ?? DEFAULT_HOOK_PARAMS.fee, + tickSpacing: config.tickSpacing ?? DEFAULT_HOOK_PARAMS.tickSpacing, + sqrtPriceX96: config.sqrtPriceX96 ?? DEFAULT_HOOK_PARAMS.sqrtPriceX96, + }; +} + +/** + * Build PoolKey records for registry. For stableswap/stableswapng: all token pairs. + * For fluiddext1/fluiddexlite: single pair. currency0 < currency1 for v4 ordering. + */ +function buildPoolKeys(poolConfig: PoolConfig, poolType: PoolType, hookAddress: string): PoolKeyRecord[] { + const params = getHookParams(poolConfig); + + const orderPair = (a: string, b: string): [string, string] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); + + switch (poolType) { + case "stableswap": + case "stableswapng": { + const config = poolConfig as StableSwapPoolConfig; + const tokens = config.tokens; + const keys: PoolKeyRecord[] = []; + for (let i = 0; i < tokens.length; i++) { + for (let j = i + 1; j < tokens.length; j++) { + const [c0, c1] = orderPair(tokens[i], tokens[j]); + keys.push({ + currency0: c0, + currency1: c1, + fee: params.fee, + tickSpacing: params.tickSpacing, + hooks: hookAddress, + }); + } + } + return keys; + } + case "fluiddext1": + case "fluiddexlite": { + const config = poolConfig as FluidDexT1PoolConfig | FluidDexLitePoolConfig; + const [c0, c1] = orderPair(config.currency0, config.currency1); + return [ + { + currency0: c0, + currency1: c1, + fee: params.fee, + tickSpacing: params.tickSpacing, + hooks: hookAddress, + }, + ]; + } + } +} + +/** + * Get external pool address/identifier for metadata + */ +function getExternalPool(poolConfig: PoolConfig, poolType: PoolType): string { + switch (poolType) { + case "stableswap": + case "stableswapng": + return (poolConfig as StableSwapPoolConfig).curvePool; + case "fluiddext1": + return (poolConfig as FluidDexT1PoolConfig).fluidPool; + case "fluiddexlite": + return (poolConfig as FluidDexLitePoolConfig).dexSalt; + } +} + +/** + * Append a deployed pool entry to the registry file for the given pool type. + * File is an array of PoolDeployedEntry. Creates file if it doesn't exist. + */ +function appendToRegistryFile(registryDir: string, poolType: PoolType, entry: PoolDeployedEntry): void { + const fileName = `deployed-${poolType}.json`; + const filePath = join(registryDir, fileName); + + let poolsDeployed: PoolDeployedEntry[]; + if (existsSync(filePath)) { + const content = readFileSync(filePath, "utf-8"); + poolsDeployed = JSON.parse(content) as PoolDeployedEntry[]; + } else { + poolsDeployed = []; + } + + poolsDeployed.push(entry); + + if (!existsSync(registryDir)) { + mkdirSync(registryDir, { recursive: true }); + } + writeFileSync(filePath, JSON.stringify(poolsDeployed, null, 2)); + console.log(`Appended to registry: ${filePath}`); +} + +/** + * Load and parse JSON file for factory mode. + * Each config must have a "poolType" field; all configs must have the same poolType. + */ +function loadJsonFile(filePath: string): PoolConfig[] { + try { + const content = readFileSync(filePath, "utf-8"); + const pools = JSON.parse(content); + + if (!Array.isArray(pools)) { + throw new Error("JSON file must contain an array of pool configurations"); + } + + if (pools.length === 0) { + throw new Error("JSON file must contain at least one pool configuration"); + } + + for (let i = 0; i < pools.length; i++) { + const p = pools[i]; + if (!isPoolType(p?.poolType)) { + throw new Error(`Pool ${i + 1} missing or invalid 'poolType'. Must be one of: ${POOL_TYPES.join(", ")}`); + } + } + + const firstType = pools[0].poolType as PoolType; + for (let i = 1; i < pools.length; i++) { + if (pools[i].poolType !== firstType) { + throw new Error( + `Pool ${i + 1} has poolType "${pools[i].poolType}" but all configs must have the same poolType (first is "${firstType}")`, + ); + } + } + + return pools; + } catch (error) { + if (error instanceof Error) { + console.error(`Error loading JSON file: ${error.message}`); + } else { + console.error("Unknown error loading JSON file"); + } + process.exit(1); + } +} + +/** + * Read immutables from env vars for self-deploy mode. + * Uses VAR or VAR_ (e.g. POOL_MANAGER_1, DEX_LITE_ADDRESS_1). + */ +function getImmutablesFromEnv(chainId: number, poolType: PoolType): SelfDeployImmutables { + const immutables: SelfDeployImmutables = { + poolManager: mustEnvForChain("POOL_MANAGER", chainId), + }; + + if (poolType === "fluiddext1") { + immutables.fluidDexReservesResolver = mustEnvForChain("FLUID_DEX_RESOLVER", chainId); + immutables.fluidLiquidity = mustEnvForChain("FLUID_LIQUIDITY", chainId); + } else if (poolType === "fluiddexlite") { + const dexLite = getEnvForChain("FLUID_DEX_LITE", chainId) ?? getEnvForChain("DEX_LITE_ADDRESS", chainId); + const dexLiteResolver = + getEnvForChain("FLUID_DEX_LITE_RESOLVER", chainId) ?? getEnvForChain("DEX_LITE_RESOLVER_ADDRESS", chainId); + if (!dexLite) + throw new Error(`FLUID_DEX_LITE_${chainId} or DEX_LITE_ADDRESS_${chainId} required for fluiddexlite self-deploy`); + if (!dexLiteResolver) + throw new Error( + `FLUID_DEX_LITE_RESOLVER_${chainId} or DEX_LITE_RESOLVER_ADDRESS_${chainId} required for fluiddexlite self-deploy`, + ); + immutables.fluidDexLite = dexLite; + immutables.fluidDexLiteResolver = dexLiteResolver; + } + + return immutables; +} + +/** + * Convert SelfDeployImmutables to FactoryImmutables format + */ +function immutablesToFactoryFormat(immutables: SelfDeployImmutables): FactoryImmutables { + const result: FactoryImmutables = { + poolManager: immutables.poolManager, + }; + + if (immutables.fluidDexReservesResolver) { + result.fluidDexReservesResolver = immutables.fluidDexReservesResolver; + } + if (immutables.fluidLiquidity) { + result.fluidLiquidity = immutables.fluidLiquidity; + } + if (immutables.fluidDexLite) { + result.fluidDexLite = immutables.fluidDexLite; + } + if (immutables.fluidDexLiteResolver) { + result.fluidDexLiteResolver = immutables.fluidDexLiteResolver; + } + + return result; +} + +/** + * Read factory contract immutables + */ +async function readFactoryImmutables( + provider: ethers.Provider, + factoryAddress: string, + poolType: PoolType, +): Promise { + let abi: string[]; + + switch (poolType) { + case "stableswap": + abi = STABLE_SWAP_FACTORY_ABI; + break; + case "stableswapng": + abi = STABLE_SWAP_NG_FACTORY_ABI; + break; + case "fluiddext1": + abi = FLUID_DEX_T1_FACTORY_ABI; + break; + case "fluiddexlite": + abi = FLUID_DEX_LITE_FACTORY_ABI; + break; + } + + const factory = new ethers.Contract(factoryAddress, abi, provider); + const immutables: FactoryImmutables = { poolManager: "" }; + + // Read POOL_MANAGER (always present) + immutables.poolManager = await factory.POOL_MANAGER(); + + // Read additional immutables based on pool type + if (poolType === "fluiddext1") { + immutables.fluidDexReservesResolver = await factory.FLUID_DEX_RESOLVER(); + immutables.fluidLiquidity = await factory.FLUID_LIQUIDITY(); + } else if (poolType === "fluiddexlite") { + immutables.fluidDexLite = await factory.FLUID_DEX_LITE(); + immutables.fluidDexLiteResolver = await factory.FLUID_DEX_LITE_RESOLVER(); + } + + return immutables; +} + +/** + * Encode constructor arguments for salt mining + */ +function encodeConstructorArgs( + poolConfig: PoolConfig, + poolType: PoolType, + factoryImmutables: FactoryImmutables, +): string { + let encoded: string; + + switch (poolType) { + case "stableswap": + case "stableswapng": { + const config = poolConfig as StableSwapPoolConfig; + encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address"], + [factoryImmutables.poolManager, config.curvePool], + ); + break; + } + case "fluiddext1": { + const config = poolConfig as FluidDexT1PoolConfig; + encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "address", "address"], + [ + factoryImmutables.poolManager, + config.fluidPool, + factoryImmutables.fluidDexReservesResolver, + factoryImmutables.fluidLiquidity, + ], + ); + break; + } + case "fluiddexlite": { + const config = poolConfig as FluidDexLitePoolConfig; + encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "address", "bytes32"], + [ + factoryImmutables.poolManager, + factoryImmutables.fluidDexLite, + factoryImmutables.fluidDexLiteResolver, + config.dexSalt, + ], + ); + break; + } + } + + // Ensure 0x prefix + return encoded.startsWith("0x") ? encoded : `0x${encoded}`; +} + +/** + * Get protocol ID for pool type + */ +function getProtocolId(poolType: PoolType): number { + switch (poolType) { + case "stableswap": + return PROTOCOL_IDS.STABLESWAP; + case "stableswapng": + return PROTOCOL_IDS.STABLESWAPNG; + case "fluiddext1": + return PROTOCOL_IDS.FLUIDDEXT1; + case "fluiddexlite": + return PROTOCOL_IDS.FLUIDDEXLITE; + } +} + +/** PoolManager Initialize event for verification */ +const INITIALIZE_TOPIC = "0xdd466e674ea557f56295e2d0218a125ea4b4f0f6f3307b95f85e6110838d6438"; + +/** + * When forge script fails, verify whether deployment actually succeeded (false positive). + * Checks: 1) Hook has code at address parsed from output; 2) Initialize events for that hook. + */ +async function verifyDeploymentOnForgeFailure( + provider: ethers.Provider, + poolManagerAddress: string, + poolConfig: PoolConfig, + poolType: PoolType, + forgeOutput: string, +): Promise<{ + hookAddress: string; + hookDeployed: boolean; + poolsInitialized: number; +} | null> { + const hookMatch = forgeOutput.match(/Hook Address:\s*(0x[a-fA-F0-9]{40})/); + if (!hookMatch) return null; + + const hookAddress = ethers.getAddress(hookMatch[1]); + const code = await provider.getCode(hookAddress); + const hookDeployed = !!code && code !== "0x" && code.length > 2; + + if (!hookDeployed) return { hookAddress, hookDeployed: false, poolsInitialized: 0 }; + + const poolKeys = buildPoolKeys(poolConfig, poolType, hookAddress); + let poolsInitialized = 0; + + try { + const blockNumber = await provider.getBlockNumber(); + const fromBlock = Math.max(0, blockNumber - 500); + const logs = await provider.getLogs({ + address: poolManagerAddress, + topics: [INITIALIZE_TOPIC], + fromBlock, + toBlock: blockNumber, + }); + + for (const log of logs) { + if (log.topics.length < 4) continue; + const currency0 = "0x" + log.topics[2].slice(26); + const currency1 = "0x" + log.topics[3].slice(26); + const data = ethers.AbiCoder.defaultAbiCoder().decode( + ["uint24", "int24", "address", "uint160", "int24"], + log.data, + ); + const hooks = data[2]; + if (hooks?.toLowerCase() !== hookAddress.toLowerCase()) continue; + if ( + poolKeys.some( + (k) => + k.currency0.toLowerCase() === currency0.toLowerCase() && + k.currency1.toLowerCase() === currency1.toLowerCase(), + ) + ) { + poolsInitialized++; + } + } + } catch { + // Ignore log query errors + } + + return { hookAddress, hookDeployed, poolsInitialized }; +} + +/** + * Run a single mine_hook.sh worker. Resolves with salt on success, rejects on failure. + */ +function runMineHookWorker( + scriptPath: string, + args: string[], + execEnv: NodeJS.ProcessEnv, + projectRoot: string, + streamOutput: boolean, +): Promise<{ salt: string; output: string }> { + return new Promise((resolve, reject) => { + const child = spawn("bash", args, { + cwd: projectRoot, + env: execEnv, + stdio: ["inherit", "pipe", streamOutput ? "inherit" : "pipe"], + }); + + let output = ""; + child.stdout!.on("data", (chunk) => { + const str = chunk.toString(); + output += str; + if (streamOutput) process.stdout.write(chunk); + }); + + child.on("close", (code) => { + if (code === 0) { + const saltMatch = output.match(/Salt \(bytes32\):\s*(0x[a-fA-F0-9]{64})/); + if (saltMatch) resolve({ salt: saltMatch[1], output }); + else reject(new Error("Could not parse salt from mine_hook.sh output")); + } else { + const err = new Error(`mine_hook.sh exited with code ${code}`) as Error & { stdout?: string }; + err.stdout = output; + reject(err); + } + }); + child.on("error", reject); + }); +} + +/** + * Mine salt using mine_hook.sh script + * @param constructorArgs - Hex-encoded constructor arguments + * @param protocolId - Protocol identifier + * @param deployerAddress - Optional deployer address (factory address or wallet address) + * @param verbose - When true, run forge verbosely (passed via FORGE_VERBOSE env) + * @param jobs - Number of parallel workers (default 1). Each searches a different random region. + */ +async function mineSalt( + constructorArgs: string, + protocolId: number, + deployerAddress?: string, + verbose = false, + jobs = 1, +): Promise { + const scriptPath = join(projectRoot, "mine_hook.sh"); + const protocolIdHex = `0x${protocolId.toString(16).toUpperCase()}`; + + console.log(`Mining salt for protocol ${protocolIdHex}...`); + console.log(`Constructor args: ${constructorArgs.substring(0, 66)}...`); + if (deployerAddress) { + console.log(`Deployer address: ${deployerAddress}`); + } + if (jobs > 1) { + console.log(`Running ${jobs} parallel mining workers...`); + } + + const baseArgs = [scriptPath, constructorArgs, protocolIdHex]; + if (deployerAddress) { + baseArgs.push("500", deployerAddress); + } + const execEnv = { + ...process.env, + ...(verbose && { FORGE_VERBOSE: "1" }), + }; + + try { + if (jobs === 1) { + const result = await runMineHookWorker(scriptPath, baseArgs, execEnv, projectRoot, true); + console.log(`✓ Found salt: ${result.salt}`); + return result.salt; + } + + // Parallel: run N workers, first to find a valid salt wins + const children: ReturnType[] = []; + const workerPromises: Promise<{ salt: string } | null>[] = []; + + for (let i = 0; i < jobs; i++) { + const child = spawn("bash", baseArgs, { + cwd: projectRoot, + env: execEnv, + stdio: ["inherit", "pipe", "pipe"], + }); + children.push(child); + + let output = ""; + child.stdout!.on("data", (chunk) => { + output += chunk.toString(); + }); + + const promise = new Promise<{ salt: string } | null>((resolve) => { + child.on("close", (code) => { + if (code === 0) { + const saltMatch = output.match(/Salt \(bytes32\):\s*(0x[a-fA-F0-9]{64})/); + if (saltMatch) resolve({ salt: saltMatch[1] }); + else resolve(null); + } else { + resolve(null); + } + }); + child.on("error", () => resolve(null)); + }); + workerPromises.push(promise); + } + + const killAll = () => { + children.forEach((c) => { + try { + c.kill("SIGTERM"); + } catch { + /* ignore */ + } + }); + }; + + // Race: first success wins; reject only when all fail + const salt = await new Promise((resolve, reject) => { + let failedCount = 0; + workerPromises.forEach((p) => { + p.then((result) => { + if (result) { + killAll(); + resolve(result.salt); + } else { + failedCount++; + if (failedCount === jobs) { + reject(new Error("All mining workers failed")); + } + } + }); + }); + }); + + console.log(`✓ Found salt: ${salt}`); + return salt; + } catch (error) { + console.error("Error mining salt:"); + if (error instanceof Error) { + console.error(error.message); + const execErr = error as { stdout?: string; stderr?: string }; + if (execErr.stdout) { + console.error("\n--- Forge/mine_hook stdout ---\n", execErr.stdout); + } + if (execErr.stderr) { + console.error("\n--- Forge/mine_hook stderr ---\n", execErr.stderr); + } + } + throw error; + } +} + +/** + * Self-deploy a hook using SelfCreateHook.s.sol forge script + */ +function selfDeployPool( + poolConfig: PoolConfig, + poolType: PoolType, + immutables: SelfDeployImmutables, + salt: string, + rpcUrl: string, + dryRun: boolean, + verbose = false, +): string { + const protocolId = getProtocolId(poolType); + + // Build environment variables for the forge script + // Ensure PRIVATE_KEY has 0x prefix (vm.envUint requires it) + const rawKey = (process.env.PRIVATE_KEY ?? "").trim(); + const privateKey = rawKey.startsWith("0x") ? rawKey : `0x${rawKey}`; + + const envVars: Record = { + PROTOCOL_ID: protocolId.toString(), + SALT: salt, + POOL_MANAGER: immutables.poolManager, + PRIVATE_KEY: privateKey, + }; + + // Add pool-specific environment variables + switch (poolType) { + case "stableswap": + case "stableswapng": { + const config = poolConfig as StableSwapPoolConfig; + const params = getHookParams(config); + envVars.CURVE_POOL = config.curvePool; + // Pass all tokens; forge script will init one Uniswap pool per pair + envVars.TOKENS = config.tokens.join(","); + envVars.FEE = params.fee.toString(); + envVars.TICK_SPACING = params.tickSpacing.toString(); + envVars.SQRT_PRICE_X96 = params.sqrtPriceX96; + break; + } + case "fluiddext1": { + const config = poolConfig as FluidDexT1PoolConfig; + const params = getHookParams(config); + envVars.FLUID_POOL = config.fluidPool; + envVars.FLUID_DEX_RESOLVER = immutables.fluidDexReservesResolver!; + envVars.FLUID_LIQUIDITY = immutables.fluidLiquidity!; + envVars.TOKENS = [config.currency0, config.currency1].join(","); + envVars.FEE = params.fee.toString(); + envVars.TICK_SPACING = params.tickSpacing.toString(); + envVars.SQRT_PRICE_X96 = params.sqrtPriceX96; + break; + } + case "fluiddexlite": { + const config = poolConfig as FluidDexLitePoolConfig; + const params = getHookParams(config); + envVars.FLUID_DEX_LITE = immutables.fluidDexLite!; + envVars.FLUID_DEX_LITE_RESOLVER = immutables.fluidDexLiteResolver!; + envVars.DEX_SALT = config.dexSalt; + envVars.TOKENS = [config.currency0, config.currency1].join(","); + envVars.FEE = params.fee.toString(); + envVars.TICK_SPACING = params.tickSpacing.toString(); + envVars.SQRT_PRICE_X96 = params.sqrtPriceX96; + break; + } + } + + // Build env string for command + const envString = Object.entries(envVars) + .map(([key, value]) => `${key}="${value}"`) + .join(" "); + + console.log(`Self-deploying hook via SelfCreateHook.s.sol...`); + if (dryRun) console.log("(dry run - no broadcast)"); + console.log(`Protocol: ${poolType}, Salt: ${salt.substring(0, 18)}...`); + + try { + const broadcastFlag = dryRun ? "" : " --broadcast"; + const verboseFlag = verbose ? " -vvvv" : ""; + const command = `${envString} forge script script/SelfCreateHook.s.sol:SelfCreateHookScript --via-ir --rpc-url "${rpcUrl}"${broadcastFlag}${verboseFlag}`; + const output = execSync(command, { + encoding: "utf-8", + cwd: projectRoot, + env: { ...process.env, ...envVars }, + }); + + if (verbose) { + console.log("\n--- Forge script output ---\n", output); + } + + // Parse hook address from output + const hookMatch = output.match(/Hook Address:\s*(0x[a-fA-F0-9]{40})/); + if (hookMatch) { + const hookAddress = hookMatch[1]; + console.log(`✓ Hook deployed at: ${hookAddress}`); + return hookAddress; + } + + // If we can't parse the address, just indicate success + console.log(`✓ Self-deploy completed`); + return "deployed"; + } catch (error) { + console.error("Error in self-deploy:"); + if (error instanceof Error) { + console.error(error.message); + const execErr = error as { stdout?: string; stderr?: string }; + if (execErr.stdout) { + console.error("\n--- Forge stdout ---\n", execErr.stdout); + } + if (execErr.stderr) { + console.error("\n--- Forge stderr ---\n", execErr.stderr); + } + } + throw error; + } +} + +/** + * Create pool via factory contract + */ +async function createPool( + signer: ethers.Signer, + factoryAddress: string, + poolConfig: PoolConfig, + poolType: PoolType, + salt: string, +): Promise<{ hookAddress: string; blockNumber: number; txHash: string }> { + let abi: string[]; + let args: any[]; + + switch (poolType) { + case "stableswap": { + abi = STABLE_SWAP_FACTORY_ABI; + const config = poolConfig as StableSwapPoolConfig; + const params = getHookParams(config); + // Currency is type alias for address, so pass addresses directly + args = [ + salt, + config.curvePool, + config.tokens, // Currency[] is address[] in ABI + params.fee, + params.tickSpacing, + BigInt(params.sqrtPriceX96), + ]; + break; + } + case "stableswapng": { + abi = STABLE_SWAP_NG_FACTORY_ABI; + const config = poolConfig as StableSwapPoolConfig; + const params = getHookParams(config); + // Currency is type alias for address, so pass addresses directly + args = [ + salt, + config.curvePool, + config.tokens, // Currency[] is address[] in ABI + params.fee, + params.tickSpacing, + BigInt(params.sqrtPriceX96), + ]; + break; + } + case "fluiddext1": { + abi = FLUID_DEX_T1_FACTORY_ABI; + const config = poolConfig as FluidDexT1PoolConfig; + const params = getHookParams(config); + // Currency is type alias for address, so pass addresses directly + args = [ + salt, + config.fluidPool, + config.currency0, // Currency is address in ABI + config.currency1, // Currency is address in ABI + params.fee, + params.tickSpacing, + BigInt(params.sqrtPriceX96), + ]; + break; + } + case "fluiddexlite": { + abi = FLUID_DEX_LITE_FACTORY_ABI; + const config = poolConfig as FluidDexLitePoolConfig; + const params = getHookParams(config); + // Currency is type alias for address, so pass addresses directly + args = [ + salt, + config.dexSalt, + config.currency0, // Currency is address in ABI + config.currency1, // Currency is address in ABI + params.fee, + params.tickSpacing, + BigInt(params.sqrtPriceX96), + ]; + break; + } + } + + const factoryWithAbi = new ethers.Contract(factoryAddress, abi, signer); + + console.log(`Calling createPool on factory ${factoryAddress}...`); + console.log(`Args:`, args.map((a, i) => `${i}: ${a.toString().substring(0, 66)}...`).join(", ")); + + try { + const tx = await factoryWithAbi.createPool(...args); + console.log(`✓ Transaction sent: ${tx.hash}`); + + const receipt = await tx.wait(); + console.log(`✓ Transaction confirmed in block ${receipt.blockNumber}`); + + // Extract hook address from events + const hookDeployedEvent = receipt.logs.find((log: any) => { + try { + const parsed = factoryWithAbi.interface.parseLog(log); + return parsed?.name === "HookDeployed"; + } catch { + return false; + } + }); + + if (hookDeployedEvent) { + const parsed = factoryWithAbi.interface.parseLog(hookDeployedEvent); + const hookAddress = (parsed?.args.hook || parsed?.args[0]) as string; + console.log(`✓ Hook deployed at: ${hookAddress}`); + return { + hookAddress, + blockNumber: Number(receipt!.blockNumber), + txHash: tx.hash, + }; + } + + return { + hookAddress: "", + blockNumber: Number(receipt!.blockNumber), + txHash: tx.hash, + }; + } catch (error) { + console.error("Error creating pool:"); + if (error instanceof Error) { + console.error(error.message); + // Check for revert reason in error data + if ("data" in error && error.data) { + console.error("Error data:", error.data); + } + if ("reason" in error && error.reason) { + console.error("Revert reason:", error.reason); + } + } + throw error; + } +} + +/** + * Main execution function + */ +async function main() { + const { + jsonFile, + factoryAddress, + selfDeploy, + rpcUrl, + chainId: parsedChainId, + registryDir, + dryRun, + verbose, + startAt, + jobs, + } = parseArgs(); + + console.log("=== Pool Creation Script ==="); + console.log(`JSON File: ${jsonFile}`); + console.log(`Mode: ${selfDeploy ? "Self-Deploy" : "Factory"}`); + if (factoryAddress) { + console.log(`Factory Address: ${factoryAddress}`); + } + console.log(`RPC URL: ${rpcUrl}`); + if (registryDir) { + console.log(`Registry dir: ${registryDir}`); + } + if (dryRun) { + console.log("DRY RUN: forge scripts will simulate without broadcasting"); + } + if (verbose) { + console.log("VERBOSE: forge scripts will run with -vvvv"); + } + if (startAt > 1) { + console.log(`Starting at pool index: ${startAt} (skipping first ${startAt - 1} pool(s))`); + } + if (jobs > 1) { + console.log(`Salt mining: ${jobs} parallel workers`); + } + console.log(""); + + // Setup provider and signer + const provider = new ethers.JsonRpcProvider(rpcUrl); + const privateKey = process.env.PRIVATE_KEY!; + const signer = new ethers.Wallet(privateKey, provider); + console.log(`Using signer: ${signer.address}`); + console.log(""); + + if (selfDeploy) { + // Self-deploy mode: load configs (same format as factory), immutables from env + const allPools = loadJsonFile(jsonFile); + const pools = startAt > 1 ? allPools.slice(startAt - 1) : allPools; + if (startAt > 1 && pools.length === 0) { + console.error(`Error: --start-at ${startAt} exceeds pool count (${allPools.length})`); + process.exit(1); + } + console.log( + `Loaded ${allPools.length} pool configuration(s)${startAt > 1 ? `, processing from index ${startAt} (${pools.length} remaining)` : ""}`, + ); + console.log(""); + + const chainId = parsedChainId ?? Number((await provider.getNetwork()).chainId); + + for (let j = 0; j < pools.length; j++) { + const i = startAt - 1 + j; + const poolConfig = pools[j]; + const poolType = poolConfig.poolType; + const immutables = getImmutablesFromEnv(chainId, poolType); + const protocolId = getProtocolId(poolType); + console.log(`\n--- Processing Pool ${i + 1}/${allPools.length} (${poolType}) ---`); + + try { + // Convert immutables to factory format for constructor encoding + const factoryImmutables = immutablesToFactoryFormat(immutables); + + // Encode constructor arguments + const constructorArgs = encodeConstructorArgs(poolConfig, poolType, factoryImmutables); + + // Mine salt using CREATE2 deployer (Foundry routes new X{salt} through 0x4e59...) + const salt = await mineSalt(constructorArgs, protocolId, CREATE2_DEPLOYER, verbose, jobs); + + // Self-deploy via forge script + const hookAddress = selfDeployPool(poolConfig, poolType, immutables, salt, rpcUrl, dryRun, verbose); + + if (registryDir && hookAddress && hookAddress !== "deployed") { + const poolKeys = buildPoolKeys(poolConfig, poolType, hookAddress); + if (poolKeys.length > 0) { + const blockNumber = Number(await provider.getBlockNumber()); + appendToRegistryFile(registryDir, poolType, { + poolKeys, + metadata: { + externalPool: getExternalPool(poolConfig, poolType), + hookAddress, + blockNumber, + }, + }); + } + } + console.log(`✓ Successfully created pool ${i + 1}`); + } catch (error) { + console.error(`✗ Failed to create pool ${i + 1}:`); + if (error instanceof Error) { + console.error(error.message); + const execErr = error as { stdout?: string; stderr?: string }; + if (execErr.stdout) { + console.error("\n--- Forge stdout ---\n", execErr.stdout); + } + if (execErr.stderr) { + console.error("\n--- Forge stderr ---\n", execErr.stderr); + } + const forgeOutput = [execErr.stdout, execErr.stderr].filter(Boolean).join("\n"); + if (forgeOutput) { + const verification = await verifyDeploymentOnForgeFailure( + provider, + immutables.poolManager, + poolConfig, + poolType, + forgeOutput, + ); + if (verification) { + if (verification.hookDeployed) { + console.error(""); + console.error("Verification (possible false positive):"); + console.error(` Hook has code at ${verification.hookAddress} → deployment likely succeeded`); + if (verification.poolsInitialized > 0) { + console.error( + ` Found ${verification.poolsInitialized} Initialize event(s) for this hook → pool(s) initialized on-chain`, + ); + } + console.error(" Check block explorer to confirm."); + } + } + } + } + // Continue with next pool + continue; + } + } + } else { + // Factory mode: read immutables from factory contract + const allPools = loadJsonFile(jsonFile); + const pools = startAt > 1 ? allPools.slice(startAt - 1) : allPools; + if (startAt > 1 && pools.length === 0) { + console.error(`Error: --start-at ${startAt} exceeds pool count (${allPools.length})`); + process.exit(1); + } + const poolType = pools[0].poolType; + console.log( + `Loaded ${allPools.length} pool configuration(s) (poolType: ${poolType})${startAt > 1 ? `, processing from index ${startAt} (${pools.length} remaining)` : ""}`, + ); + console.log(""); + + // Read factory immutables once + console.log("Reading factory immutables..."); + const factoryImmutables = await readFactoryImmutables(provider, factoryAddress!, poolType); + console.log(`POOL_MANAGER: ${factoryImmutables.poolManager}`); + if (factoryImmutables.fluidDexReservesResolver) { + console.log(`FLUID_DEX_RESOLVER: ${factoryImmutables.fluidDexReservesResolver}`); + } + if (factoryImmutables.fluidLiquidity) { + console.log(`FLUID_LIQUIDITY: ${factoryImmutables.fluidLiquidity}`); + } + if (factoryImmutables.fluidDexLite) { + console.log(`FLUID_DEX_LITE: ${factoryImmutables.fluidDexLite}`); + } + if (factoryImmutables.fluidDexLiteResolver) { + console.log(`FLUID_DEX_LITE_RESOLVER: ${factoryImmutables.fluidDexLiteResolver}`); + } + console.log(""); + + const protocolId = getProtocolId(poolType); + for (let j = 0; j < pools.length; j++) { + const i = startAt - 1 + j; + const poolConfig = pools[j]; + console.log(`\n--- Processing Pool ${i + 1}/${allPools.length} ---`); + + try { + // Encode constructor arguments + const constructorArgs = encodeConstructorArgs(poolConfig, poolType, factoryImmutables); + + // Mine salt using factory address as deployer + const salt = await mineSalt(constructorArgs, protocolId, factoryAddress!, false, jobs); + + // Create pool via factory + const result = await createPool(signer, factoryAddress!, poolConfig, poolType, salt); + + if (registryDir && result.hookAddress && result.hookAddress.length > 0) { + const poolKeys = buildPoolKeys(poolConfig, poolType, result.hookAddress); + if (poolKeys.length > 0) { + appendToRegistryFile(registryDir, poolType, { + poolKeys, + metadata: { + externalPool: getExternalPool(poolConfig, poolType), + hookAddress: result.hookAddress, + txHash: result.txHash, + blockNumber: result.blockNumber, + }, + }); + } + } + console.log(`✓ Successfully created pool ${i + 1}`); + } catch (error) { + console.error(`✗ Failed to create pool ${i + 1}:`); + if (error instanceof Error) { + console.error(error.message); + const execErr = error as { stdout?: string; stderr?: string }; + if (execErr.stdout) { + console.error("\n--- Forge stdout ---\n", execErr.stdout); + } + if (execErr.stderr) { + console.error("\n--- Forge stderr ---\n", execErr.stderr); + } + } + // Continue with next pool + continue; + } + } + } + + console.log("\n=== Done ==="); +} + +// Run main function +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); diff --git a/aggregator-hooks/tsconfig.json b/aggregator-hooks/tsconfig.json new file mode 100644 index 00000000..c4a8f8ad --- /dev/null +++ b/aggregator-hooks/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "baseUrl": ".", + "paths": { + "@src/cli": ["src/cli.ts"] + }, + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*.ts", "historical/**/*.ts", "polling/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/foundry.lock b/foundry.lock index a7e21326..c3c6c931 100644 --- a/foundry.lock +++ b/foundry.lock @@ -17,6 +17,24 @@ "lib/solidity-lib": { "rev": "3fcc8ee6d5c7dea3283416cbcee601d89504a243" }, + "lib/v4-core": { + "tag": { + "name": "v4.0.0", + "rev": "e50237c43811bd9b526eff40f26772152a42daba" + } + }, + "lib/v4-hooks-public": { + "branch": { + "name": "aggregator-hooks", + "rev": "bae291c271be69882e084baa5993a033cb09362b" + } + }, + "src/pkgs/calibur": { + "rev": "69d5eb61498ffac7740530310b270459f2ae2a20" + }, + "src/pkgs/mixed-quoter": { + "rev": "d576527bff2e7c9db5434bb2b3806fd184610865" + }, "src/pkgs/permit2": { "rev": "cc56ad0f3439c502c246fc5cfcc3db92bb8b7219" }, @@ -29,6 +47,9 @@ "src/pkgs/universal-router": { "rev": "3663f6db6e2fe121753cd2d899699c2dc75dca86" }, + "src/pkgs/universal-router-2_0": { + "rev": "3663f6db6e2fe121753cd2d899699c2dc75dca86" + }, "src/pkgs/util-contracts": { "rev": "0e84aa540ecafacb22f9e61a5e684c5c32a21f56" }, diff --git a/lib/v4-core b/lib/v4-core new file mode 160000 index 00000000..d153b048 --- /dev/null +++ b/lib/v4-core @@ -0,0 +1 @@ +Subproject commit d153b048868a60c2403a3ef5b2301bb247884d46 diff --git a/lib/v4-hooks-public b/lib/v4-hooks-public new file mode 160000 index 00000000..9a79f86b --- /dev/null +++ b/lib/v4-hooks-public @@ -0,0 +1 @@ +Subproject commit 9a79f86b4ad0f2389e25125892a69400aaaa43ab diff --git a/mine_hook.sh b/mine_hook.sh new file mode 100644 index 00000000..f0ad9bf8 --- /dev/null +++ b/mine_hook.sh @@ -0,0 +1,136 @@ +#!/bin/bash + +# Mine an aggregator hook address by searching with incrementing salt offsets + +show_help() { + echo "Mine an aggregator hook address by searching with incrementing salt offsets" + echo "" + echo "Usage: $0 [max_attempts] [deployer_address]" + echo "" + echo "Arguments:" + echo " constructor_args Hex-encoded constructor arguments (e.g., 0x000000000000000000000000...)" + echo " protocol_id Protocol identifier for the hook type:" + echo " 0xC1 - StableSwap" + echo " 0xC2 - StableSwap-NG" + echo " 0xF1 - FluidDexT1" + echo " 0xF2 - FluidDexV2 (not yet implemented)" + echo " 0xF3 - FluidDexLite" + echo " max_attempts Optional. Maximum mining attempts (default: 500)" + echo " deployer_address Optional. Address that will deploy the hook." + echo " Use factory address for factory deploys, wallet address for self-deploys." + echo " (default: CREATE2_DEPLOYER 0x4e59b44847b379578588920cA78FbF26c0B4956C)" + echo "" + echo "Examples:" + echo " # Mine with default CREATE2_DEPLOYER" + echo " $0 0x00000000... 0xF3" + echo "" + echo " # Mine with factory as deployer" + echo " $0 0x00000000... 0xF3 500 0xYourFactoryAddress" + echo "" + echo " # Mine with wallet as deployer (for self-deploy)" + echo " $0 0x00000000... 0xF3 500 0xYourWalletAddress" + echo "" + echo "Options:" + echo " -h, --help Show this help message and exit" +} + +# Check for help flag +if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then + show_help + exit 0 +fi + +if [ $# -lt 2 ]; then + echo "Error: Missing required arguments" + echo "" + show_help + exit 1 +fi + +CONSTRUCTOR_ARGS=$1 +PROTOCOL_ID=$2 +MAX_ATTEMPTS=${3:-500} # Default to 500 attempts +DEPLOYER_ADDRESS=${4:-0x4e59b44847b379578588920cA78FbF26c0B4956C} # Default to CREATE2_DEPLOYER +SALT_INCREMENT=160444 # Must match MAX_LOOP in AggregatorHookMiner.sol + +# Random base offset (0 to 1 quadrillion) so each run searches a different salt region. +# Avoids createCollision when redeploying pools with the same constructor args. +# Use 4 bytes (not 8): bash uses signed 64-bit; od -tu8 can overflow to negative, breaking Solidity's uint256 env parsing and causing the same salts to be searched repeatedly. +RANDOM_BASE=$(($(LC_ALL=C od -An -N4 -tu4 /dev/urandom 2>/dev/null || echo $RANDOM) % 1000000000000000)) +[ -z "$RANDOM_BASE" ] && RANDOM_BASE=0 + +echo "Starting aggregator hook mining..." +echo "Random base offset: $RANDOM_BASE (avoids collisions on redeploy)" +echo "Constructor args: $CONSTRUCTOR_ARGS" +echo "Protocol ID: $PROTOCOL_ID" +echo "Deployer address: $DEPLOYER_ADDRESS" +echo "Max attempts: $MAX_ATTEMPTS" +echo "Salt increment per attempt: $SALT_INCREMENT" +echo "" + +# Run from contracts/ directory (where this script lives) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +if [ ! -f "lib/v4-hooks-public/script/MineAggregatorHook.s.sol" ]; then + echo "Error: MineAggregatorHook.s.sol not found at lib/v4-hooks-public/script/MineAggregatorHook.s.sol" + echo "Add v4-hooks-public and checkout aggregator-hooks branch:" + echo " git submodule add https://github.com/Uniswap/v4-hooks-public lib/v4-hooks-public" + echo " cd lib/v4-hooks-public && git fetch origin aggregator-hooks && git checkout aggregator-hooks" + echo " git submodule update --init --recursive" + exit 1 +fi + +for ((i=0; i "$FORGE_OUTPUT" 2>&1 & + FORGE_PID=$! + + # Heartbeat: update same line every 15s so user knows it's still running + ELAPSED=0 + while kill -0 $FORGE_PID 2>/dev/null; do + printf "\r ... still searching (Attempt %d/%d, %ds elapsed) " $((i + 1)) $MAX_ATTEMPTS $ELAPSED + sleep 15 + ELAPSED=$((ELAPSED + 15)) + done + printf "\r%-70s\r" "" # clear the heartbeat line + wait $FORGE_PID 2>/dev/null + FORGE_EXIT=$? + DURATION=$(( $(date +%s) - START_SEC )) + OUTPUT=$(cat "$FORGE_OUTPUT") + rm -f "$FORGE_OUTPUT" + + # Check if we found a valid salt (look for "Hook Address" in output) + if echo "$OUTPUT" | grep -q "Hook Address:"; then + echo "" + echo "SUCCESS! Found valid salt." + echo "" + echo "$OUTPUT" | grep -A 10 "=== Aggregator Hook Mining Results ===" + exit 0 + fi + + # Check if it was a "could not find salt" error (expected, continue searching) + if echo "$OUTPUT" | grep -q "could not find salt"; then + echo " No match found in this range (forge exit=$FORGE_EXIT, ${DURATION}s), continuing..." + continue + fi + + # Some other error occurred + echo " Unexpected error (forge exit=$FORGE_EXIT, ${DURATION}s):" + echo "$OUTPUT" + exit 1 +done + +echo "" +echo "FAILED: Could not find valid salt after $MAX_ATTEMPTS attempts" +echo "Total salts searched: $((MAX_ATTEMPTS * SALT_INCREMENT))" +exit 1 diff --git a/remappings.txt b/remappings.txt index 931e8a96..75d79da7 100644 --- a/remappings.txt +++ b/remappings.txt @@ -55,4 +55,11 @@ src/pkgs/mixed-quoter:@uniswap/swap-router-contracts=src/pkgs/swap-router-contra @uniswap/lib=lib/solidity-lib @uniswap/v2-core=src/pkgs/v2-core @uniswap/v3-core=src/pkgs/v3-core -@uniswap/v4-core=src/pkgs/v4-core \ No newline at end of file +@uniswap/v4-core=src/pkgs/v4-core +@aggregator-hooks/=lib/v4-hooks-public/src/aggregator-hooks/ +script/:@uniswap/v4-core/=lib/v4-hooks-public/lib/v4-core/ +lib/v4-hooks-public:@openzeppelin/contracts=lib/v4-hooks-public/lib/openzeppelin-contracts/contracts +lib/v4-hooks-public:@uniswap/v4-periphery/=lib/v4-hooks-public/lib/v4-periphery/ +lib/v4-hooks-public:@uniswap/v4-core/=lib/v4-hooks-public/lib/v4-core/ +lib/v4-hooks-public:@protocol-fees/=lib/v4-hooks-public/lib/protocol-fees/src/ +lib/v4-hooks-public/lib/protocol-fees:v4-core/=lib/v4-hooks-public/lib/v4-core/src/ \ No newline at end of file diff --git a/script/SelfCreateHook.s.sol b/script/SelfCreateHook.s.sol new file mode 100644 index 00000000..90698b5d --- /dev/null +++ b/script/SelfCreateHook.s.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Script.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; + +import {StableSwapAggregator} from "@aggregator-hooks/implementations/StableSwap/StableSwapAggregator.sol"; +import {StableSwapNGAggregator} from "@aggregator-hooks/implementations/StableSwapNG/StableSwapNGAggregator.sol"; +import {FluidDexT1Aggregator} from "@aggregator-hooks/implementations/FluidDexT1/FluidDexT1Aggregator.sol"; +import {FluidDexLiteAggregator} from "@aggregator-hooks/implementations/FluidDexLite/FluidDexLiteAggregator.sol"; + +import {ICurveStableSwap} from "@aggregator-hooks/implementations/StableSwap/interfaces/IStableSwap.sol"; +import {ICurveStableSwapNG} from "@aggregator-hooks/implementations/StableSwapNG/interfaces/IStableSwapNG.sol"; +import {IFluidDexT1} from "@aggregator-hooks/implementations/FluidDexT1/interfaces/IFluidDexT1.sol"; +import {IFluidDexReservesResolver} from "@aggregator-hooks/implementations/FluidDexT1/interfaces/IFluidDexReservesResolver.sol"; +import {IFluidDexLite} from "@aggregator-hooks/implementations/FluidDexLite/interfaces/IFluidDexLite.sol"; +import {IFluidDexLiteResolver} from "@aggregator-hooks/implementations/FluidDexLite/interfaces/IFluidDexLiteResolver.sol"; + +/// @notice Self-deploys an aggregator hook and initializes the pool without using a factory +/// @dev Broadcasts from PRIVATE_KEY and deploys using CREATE2 with the provided salt +contract SelfCreateHookScript is Script { + uint8 constant ID_STABLESWAP = 0xC1; + uint8 constant ID_STABLESWAPNG = 0xC2; + uint8 constant ID_FLUIDDEXT1 = 0xF1; + uint8 constant ID_FLUIDDEXLITE = 0xF3; + + function run() public { + // Load private key for broadcasting + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + + // Common parameters + uint8 protocolId = uint8(vm.envUint("PROTOCOL_ID")); + bytes32 salt = vm.envBytes32("SALT"); + address poolManager = vm.envAddress("POOL_MANAGER"); + + uint24 fee = uint24(vm.envUint("FEE")); + int24 tickSpacing = int24(int256(vm.envUint("TICK_SPACING"))); + uint160 sqrtPriceX96 = uint160(vm.envUint("SQRT_PRICE_X96")); + + address hookAddress; + + vm.startBroadcast(deployerPrivateKey); + + if (protocolId == ID_STABLESWAP) { + hookAddress = _deployStableSwap(salt, poolManager); + } else if (protocolId == ID_STABLESWAPNG) { + hookAddress = _deployStableSwapNG(salt, poolManager); + } else if (protocolId == ID_FLUIDDEXT1) { + hookAddress = _deployFluidDexT1(salt, poolManager); + } else if (protocolId == ID_FLUIDDEXLITE) { + hookAddress = _deployFluidDexLite(salt, poolManager); + } else { + revert("Invalid protocol ID"); + } + + // Initialize one Uniswap pool per token pair. TOKENS is comma-separated (2+ for fluid, 2+ for stableswap). + address[] memory tokens = vm.envAddress("TOKENS", ","); + require(tokens.length >= 2, "TOKENS must have at least 2 addresses"); + for (uint256 i = 0; i < tokens.length; i++) { + for (uint256 j = i + 1; j < tokens.length; j++) { + (address c0, address c1) = + tokens[i] < tokens[j] ? (tokens[i], tokens[j]) : (tokens[j], tokens[i]); + PoolKey memory poolKey = PoolKey({ + currency0: Currency.wrap(c0), + currency1: Currency.wrap(c1), + fee: fee, + tickSpacing: tickSpacing, + hooks: IHooks(hookAddress) + }); + IPoolManager(poolManager).initialize(poolKey, sqrtPriceX96); + console.log("Initialized pool:", c0, c1); + } + } + + vm.stopBroadcast(); + + // Output results + console.log("=== Self-Deploy Hook Results ==="); + console.log("Hook Address:", hookAddress); + console.log("Salt:", vm.toString(salt)); + console.log("Protocol ID:", protocolId); + console.log("Pool Manager:", poolManager); + console.log("Tokens:"); + console.log("Tokens length:", tokens.length); + for (uint256 i = 0; i < tokens.length; i++) { + console.log("Token:", tokens[i]); + } + console.log("Fee:", fee); + console.log("Tick Spacing:", uint24(tickSpacing)); + console.log("Sqrt Price X96:", sqrtPriceX96); + console.log("================================"); + } + + function _deployStableSwap(bytes32 salt, address poolManager) internal returns (address) { + address curvePool = vm.envAddress("CURVE_POOL"); + + StableSwapAggregator hook = + new StableSwapAggregator{salt: salt}(IPoolManager(poolManager), ICurveStableSwap(curvePool)); + + return address(hook); + } + + function _deployStableSwapNG(bytes32 salt, address poolManager) internal returns (address) { + address curvePool = vm.envAddress("CURVE_POOL"); + + StableSwapNGAggregator hook = + new StableSwapNGAggregator{salt: salt}(IPoolManager(poolManager), ICurveStableSwapNG(curvePool)); + + return address(hook); + } + + function _deployFluidDexT1(bytes32 salt, address poolManager) internal returns (address) { + address fluidPool = vm.envAddress("FLUID_POOL"); + address fluidDexReservesResolver = vm.envAddress("FLUID_DEX_RESOLVER"); + address fluidLiquidity = vm.envAddress("FLUID_LIQUIDITY"); + + FluidDexT1Aggregator hook = new FluidDexT1Aggregator{salt: salt}( + IPoolManager(poolManager), + IFluidDexT1(fluidPool), + IFluidDexReservesResolver(fluidDexReservesResolver), + fluidLiquidity + ); + + return address(hook); + } + + function _deployFluidDexLite(bytes32 salt, address poolManager) internal returns (address) { + address fluidDexLite = vm.envAddress("FLUID_DEX_LITE"); + address fluidDexLiteResolver = vm.envAddress("FLUID_DEX_LITE_RESOLVER"); + bytes32 dexSalt = vm.envBytes32("DEX_SALT"); + + FluidDexLiteAggregator hook = new FluidDexLiteAggregator{salt: salt}( + IPoolManager(poolManager), IFluidDexLite(fluidDexLite), IFluidDexLiteResolver(fluidDexLiteResolver), dexSalt + ); + + return address(hook); + } +} From 6a679fea0abfd16ad33bbd7a2a314f27ba1406e6 Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Thu, 26 Feb 2026 10:40:42 -0500 Subject: [PATCH 02/21] make agg hook protocol support modular --- aggregator-hooks/README.md | 69 +- .../creation-modules/FluidDexLite.ts | 124 +++ .../creation-modules/FluidDexT1.ts | 115 +++ .../creation-modules/StableSwap.ts | 103 +++ .../creation-modules/StableSwapNG.ts | 103 +++ aggregator-hooks/creation-modules/index.ts | 39 + aggregator-hooks/creation-modules/types.ts | 90 ++ aggregator-hooks/historical/FluidDexLite.ts | 29 +- aggregator-hooks/historical/FluidDexT1.ts | 42 +- aggregator-hooks/historical/StableSwapNG.ts | 45 +- aggregator-hooks/src/createPools.ts | 858 ++++-------------- aggregator-hooks/tsconfig.json | 2 +- mine_hook.sh | 2 +- 13 files changed, 841 insertions(+), 780 deletions(-) create mode 100644 aggregator-hooks/creation-modules/FluidDexLite.ts create mode 100644 aggregator-hooks/creation-modules/FluidDexT1.ts create mode 100644 aggregator-hooks/creation-modules/StableSwap.ts create mode 100644 aggregator-hooks/creation-modules/StableSwapNG.ts create mode 100644 aggregator-hooks/creation-modules/index.ts create mode 100644 aggregator-hooks/creation-modules/types.ts diff --git a/aggregator-hooks/README.md b/aggregator-hooks/README.md index e42291e4..ada9e645 100644 --- a/aggregator-hooks/README.md +++ b/aggregator-hooks/README.md @@ -16,14 +16,6 @@ Aggregator Hook factories should be deployed before running any of these scripts | `historical/FluidDexT1.ts` | Scrape LogDexDeployed events from FluidDexFactory | | `historical/StableSwapNG.ts` | Enumerate pool_count + pool_list from Curve StableSwap-NG factory | -### Polling discovery (checkpoint-based, for cron) - -| Script | Description | -|--------|-------------| -| `polling/FluidDexLite.ts` | Poll LogInitialize since checkpoint | -| `polling/FluidDexT1.ts` | Poll LogDexDeployed since checkpoint | -| `polling/StableSwapNG.ts` | Poll PlainPoolDeployed/MetaPoolDeployed events since checkpoint; fetch last N pools by index | - ### Pool creation | Script | Description | @@ -40,17 +32,9 @@ All discovery scripts use chain-ID-suffixed env vars. Use `VAR_` (e.g. | Script | Required | Optional | |--------|----------|----------| -| **fluiddexlite** (hist) | `RPC_URL` | `DEX_LITE_RESOLVER_ADDRESS` (default mainnet resolver) | -| **fluiddexlite** (poll) | `RPC_URL`, `DEX_LITE_ADDRESS` | — | -| **fluiddext1** (poll + hist) | `RPC_URL`, `FLUID_DEX_RESOLVER` | `FLUID_DEX_FACTORY`, `FACTORY_ADDRESS`, `RPS`, `CONCURRENCY` | -| **stableswapng** (poll + hist) | `RPC_URL` | `FACTORY_ADDRESS`, `RPS`, `CONCURRENCY`, `FINALITY_BLOCKS`, `LOOKBACK_BLOCKS` (poll only) | - -### Polling-only env (optional) - -| Env | Default | Description | -|-----|---------|-------------| -| `FINALITY_BLOCKS` | 10 | Subtract from latest; checkpoint = last scanned block | -| `LOOKBACK_BLOCKS` | 200000 | Used when checkpoint missing and no `--start-block` | +| **fluiddexlite** | `RPC_URL` | `DEX_LITE_RESOLVER_ADDRESS` (default mainnet resolver) | +| **fluiddext1** | `RPC_URL`, `FLUID_DEX_RESOLVER` | `FLUID_DEX_FACTORY`, `FACTORY_ADDRESS`, `RPS`, `CONCURRENCY` | +| **stableswapng** | `RPC_URL` | `FACTORY_ADDRESS`, `RPS`, `CONCURRENCY` | --- @@ -60,15 +44,14 @@ All discovery scripts require `--chain-id `. ### Common args -| Arg | Default (poll) | Default (hist) | Description | -|-----|----------------|----------------|-------------| -| `--chain-id` | (required) | (required) | Chain ID; selects env vars | -| `--output-dir` | `detected` | `detected` | Output base dir; files go to `output-dir/chain-id/.json` | -| `--checkpoint-dir` | `checkpoints` | — | Checkpoint dir (polling only) | -| `--chunk-blocks` | 10000 | 100000 | Block chunk size for getLogs | -| `--start-block` | — | — | Start scan from this block. Override checkpoint for polling. | +| Arg | Default | Description | +|-----|---------|-------------| +| `--chain-id` | (required) | Chain ID; selects env vars | +| `--output-dir` | `detected` | Output base dir; files go to `output-dir/chain-id/.json` | +| `--chunk-blocks` | 100000 | Block chunk size for getLogs | +| `--start-block` | — | Start scan from this block | -### Historical-only args +### Discovery args | Arg | Default | Description | |-----|---------|-------------| @@ -78,31 +61,33 @@ All discovery scripts require `--chain-id `. | Script | Arg | Default | Description | |--------|-----|---------|-------------| -| fluiddext1 (hist) | `--mode` | enumerate | `logs` \| `enumerate` \| `both` | -| stableswapng (poll) | `--chunk-blocks` | 10000 | Block chunk size for event scan | -| stableswapng (hist) | `--chunk` | 500 | pool_list batch size | -| stableswapng (hist) | `--start-index` | 0 | Start pool_list index | +| fluiddext1 | `--mode` | enumerate | `logs` \| `enumerate` \| `both` | +| stableswapng | `--chunk` | 500 | pool_list batch size | +| stableswapng | `--start-index` | 0 | Start pool_list index | --- ## Output paths - **Output**: `{OUTPUT_DIR}/{CHAIN_ID}/{OUTPUT_FILE}.json` -- **Checkpoint** (polling): `{CHECKPOINT_DIR}/{CHAIN_ID}/{CHECKPOINT_FILE}.json` -| Script | Output file | Checkpoint file | -|--------|-------------|-----------------| -| fluiddexlite | fluiddexlite-pools.json | dexlite_checkpoint.json | -| fluiddext1 | fluiddext1-pools.json | fluiddext1_checkpoint.json | -| stableswapng | stableswapng-pools.json | stableswapng_checkpoint.json | +| Script | Output file | +|--------|-------------| +| fluiddexlite | fluiddexlite-pools.json | +| fluiddext1 | fluiddext1-pools.json | +| stableswapng | stableswapng-pools.json | --- ## Example invocations +**Important:** Run all commands from the `aggregator-hooks/` directory so the scripts load `.env` from that folder. + ```bash # From contracts/aggregator-hooks/ directory: cd aggregator-hooks && npm install +cp .env.example .env +# Edit .env with your values, then run: # Historical fluiddexlite on mainnet npx tsx historical/FluidDexLite.ts --chain-id 1 @@ -110,9 +95,6 @@ npx tsx historical/FluidDexLite.ts --chain-id 1 # Historical fluiddext1 on Base npx tsx historical/FluidDexT1.ts --chain-id 8453 --output-dir output -# Polling fluiddexlite on mainnet -npx tsx polling/FluidDexLite.ts --chain-id 1 - # Historical stableswapng with custom chunk npx tsx historical/StableSwapNG.ts --chain-id 1 --chunk 200 @@ -132,11 +114,12 @@ npx tsx src/createPools.ts detected/1/fluiddexlite-pools-curated.json 0xFactoryA | `factoryAddress` | yes* | — | Factory contract address (*required when not using `--self-deploy`) | | `--self-deploy` | no | — | Deploy hooks from wallet instead of via factory | | `--chain-id ` | no | — | Chain ID; selects `RPC_URL_` from env | -| `--registry-dir ` | no | — | Append deployed pools to `deployed-.json` in this dir | +| `--registry-dir ` | no | `created-pools` | Append deployed pools to `deployed-.json` in this dir | | `--dry-run` | no | — | Simulate forge scripts without broadcasting | | `--verbose`, `-v` | no | — | Run forge scripts with `-vvvv` | | `--start-at ` | no | 1 | Start at 1-based pool index (skip earlier pools). Use to resume. | | `--jobs `, `-j ` | no | 1 | Parallel salt mining workers (1–16). Speeds up mining. | +| `--priority-gas-price ` | no | RPC default | Max priority fee per gas for EIP1559 (e.g. `3gwei`). Speeds up tx inclusion. | **Modes:** @@ -173,10 +156,14 @@ cp .env.example .env # Edit .env with your values ``` +**Run all discovery and createPools commands from the `aggregator-hooks/` directory** so they load the `.env` file in that folder. + ### Solidity (mine_hook, createPools) The `createPools` script and `mine_hook.sh` run from the **contracts/** directory (project root). They require: +> **Note:** When running `createPools` via `npx tsx src/createPools.ts`, run it from **aggregator-hooks/** so it loads `aggregator-hooks/.env`. The forge scripts invoked by createPools run from contracts/ but inherit env vars from the parent process. + 1. **v4-hooks-public** (aggregator-hooks branch): Already added as submodule. Ensure it's on the `aggregator-hooks` branch: ```bash cd lib/v4-hooks-public && git fetch origin aggregator-hooks && git checkout aggregator-hooks diff --git a/aggregator-hooks/creation-modules/FluidDexLite.ts b/aggregator-hooks/creation-modules/FluidDexLite.ts new file mode 100644 index 00000000..0e8e1978 --- /dev/null +++ b/aggregator-hooks/creation-modules/FluidDexLite.ts @@ -0,0 +1,124 @@ +/** + * FluidDex Lite aggregator hook deployment module. + */ +import { ethers } from "ethers"; +import { getEnvForChain, mustEnvForChain } from "../src/cli.js"; +import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables } from "./types.js"; + +export interface FluidDexLitePoolConfig { + poolType: "fluiddexlite"; + dexSalt: string; + currency0: Address; + currency1: Address; + fee: number | null; + tickSpacing: number | null; + sqrtPriceX96: bigint | null; +} + +const FACTORY_ABI = [ + "function POOL_MANAGER() external view returns (address)", + "function FLUID_DEX_LITE() external view returns (address)", + "function FLUID_DEX_LITE_RESOLVER() external view returns (address)", + "function createPool(bytes32 salt, bytes32 dexSalt, address currency0, address currency1, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)", +]; + +const PROTOCOL_ID = 0xf3; + +const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); + +export const fluiddexliteModule: CreationModule = { + poolType: "fluiddexlite", + protocolId: PROTOCOL_ID, + factoryAbi: FACTORY_ABI, + + getHookParams(config) { + return { + fee: config.fee ?? 0, + tickSpacing: config.tickSpacing ?? 60, + sqrtPriceX96: config.sqrtPriceX96 ?? DEFAULT_SQRT_PRICE_X96, + }; + }, + + buildPoolKeys(config, hookAddress) { + const params = this.getHookParams(config); + const [c0, c1] = orderPair(config.currency0, config.currency1); + return [ + { + currency0: c0, + currency1: c1, + fee: params.fee, + tickSpacing: params.tickSpacing, + hooks: hookAddress, + }, + ]; + }, + + getExternalPool(config) { + return config.dexSalt; + }, + + getImmutablesFromEnv(chainId: number): FactoryImmutables { + const dexLite = getEnvForChain("FLUID_DEX_LITE", chainId) ?? getEnvForChain("DEX_LITE_ADDRESS", chainId); + const dexLiteResolver = + getEnvForChain("FLUID_DEX_LITE_RESOLVER", chainId) ?? getEnvForChain("DEX_LITE_RESOLVER_ADDRESS", chainId); + if (!dexLite) + throw new Error(`FLUID_DEX_LITE_${chainId} or DEX_LITE_ADDRESS_${chainId} required for fluiddexlite self-deploy`); + if (!dexLiteResolver) + throw new Error( + `FLUID_DEX_LITE_RESOLVER_${chainId} or DEX_LITE_RESOLVER_ADDRESS_${chainId} required for fluiddexlite self-deploy`, + ); + return { + poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, + fluidDexLite: dexLite as Address, + fluidDexLiteResolver: dexLiteResolver as Address, + }; + }, + + async readFactoryImmutables(provider, factoryAddress) { + const factory = new ethers.Contract(factoryAddress, FACTORY_ABI, provider); + const [poolManager, fluidDexLite, fluidDexLiteResolver] = await Promise.all([ + factory.POOL_MANAGER(), + factory.FLUID_DEX_LITE(), + factory.FLUID_DEX_LITE_RESOLVER(), + ]); + return { + poolManager: poolManager as Address, + fluidDexLite: fluidDexLite as Address, + fluidDexLiteResolver: fluidDexLiteResolver as Address, + }; + }, + + encodeConstructorArgs(config, immutables) { + const encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "address", "bytes32"], + [immutables.poolManager, immutables.fluidDexLite, immutables.fluidDexLiteResolver, config.dexSalt], + ); + return encoded.startsWith("0x") ? encoded : `0x${encoded}`; + }, + + buildSelfDeployEnvVars(config, immutables) { + const params = this.getHookParams(config); + return { + FLUID_DEX_LITE: immutables.fluidDexLite!, + FLUID_DEX_LITE_RESOLVER: immutables.fluidDexLiteResolver!, + DEX_SALT: config.dexSalt, + TOKENS: [config.currency0, config.currency1].join(","), + FEE: params.fee.toString(), + TICK_SPACING: params.tickSpacing.toString(), + SQRT_PRICE_X96: params.sqrtPriceX96.toString(), + }; + }, + + buildCreatePoolArgs(config, salt) { + const params = this.getHookParams(config); + return [ + salt, + config.dexSalt, + config.currency0, + config.currency1, + params.fee, + params.tickSpacing, + params.sqrtPriceX96, + ]; + }, +}; diff --git a/aggregator-hooks/creation-modules/FluidDexT1.ts b/aggregator-hooks/creation-modules/FluidDexT1.ts new file mode 100644 index 00000000..ff3fe0d7 --- /dev/null +++ b/aggregator-hooks/creation-modules/FluidDexT1.ts @@ -0,0 +1,115 @@ +/** + * FluidDex T1 aggregator hook deployment module. + */ +import { ethers } from "ethers"; +import { mustEnvForChain } from "../src/cli.js"; +import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables } from "./types.js"; + +export interface FluidDexT1PoolConfig { + poolType: "fluiddext1"; + fluidPool: Address; + currency0: Address; + currency1: Address; + fee: number | null; + tickSpacing: number | null; + sqrtPriceX96: bigint | null; +} + +const FACTORY_ABI = [ + "function POOL_MANAGER() external view returns (address)", + "function fluidDexReservesResolver() external view returns (address)", + "function FLUID_LIQUIDITY() external view returns (address)", + "function createPool(bytes32 salt, address fluidPool, address currency0, address currency1, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)", +]; + +const PROTOCOL_ID = 0xf1; + +const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); + +export const fluiddext1Module: CreationModule = { + poolType: "fluiddext1", + protocolId: PROTOCOL_ID, + factoryAbi: FACTORY_ABI, + + getHookParams(config) { + return { + fee: config.fee ?? 0, + tickSpacing: config.tickSpacing ?? 60, + sqrtPriceX96: config.sqrtPriceX96 ?? DEFAULT_SQRT_PRICE_X96, + }; + }, + + buildPoolKeys(config, hookAddress) { + const params = this.getHookParams(config); + const [c0, c1] = orderPair(config.currency0, config.currency1); + return [ + { + currency0: c0, + currency1: c1, + fee: params.fee, + tickSpacing: params.tickSpacing, + hooks: hookAddress, + }, + ]; + }, + + getExternalPool(config) { + return config.fluidPool; + }, + + getImmutablesFromEnv(chainId: number): FactoryImmutables { + return { + poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, + fluidDexReservesResolver: mustEnvForChain("FLUID_DEX_RESOLVER", chainId) as Address, + fluidLiquidity: mustEnvForChain("FLUID_LIQUIDITY", chainId) as Address, + }; + }, + + async readFactoryImmutables(provider, factoryAddress) { + const factory = new ethers.Contract(factoryAddress, FACTORY_ABI, provider); + const [poolManager, fluidDexReservesResolver, fluidLiquidity] = await Promise.all([ + factory.POOL_MANAGER(), + factory.fluidDexReservesResolver(), + factory.FLUID_LIQUIDITY(), + ]); + return { + poolManager: poolManager as Address, + fluidDexReservesResolver: fluidDexReservesResolver as Address, + fluidLiquidity: fluidLiquidity as Address, + }; + }, + + encodeConstructorArgs(config, immutables) { + const encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "address", "address"], + [immutables.poolManager, config.fluidPool, immutables.fluidDexReservesResolver, immutables.fluidLiquidity], + ); + return encoded.startsWith("0x") ? encoded : `0x${encoded}`; + }, + + buildSelfDeployEnvVars(config, immutables) { + const params = this.getHookParams(config); + return { + FLUID_POOL: config.fluidPool, + FLUID_DEX_RESOLVER: immutables.fluidDexReservesResolver!, + FLUID_LIQUIDITY: immutables.fluidLiquidity!, + TOKENS: [config.currency0, config.currency1].join(","), + FEE: params.fee.toString(), + TICK_SPACING: params.tickSpacing.toString(), + SQRT_PRICE_X96: params.sqrtPriceX96.toString(), + }; + }, + + buildCreatePoolArgs(config, salt) { + const params = this.getHookParams(config); + return [ + salt, + config.fluidPool, + config.currency0, + config.currency1, + params.fee, + params.tickSpacing, + params.sqrtPriceX96, + ]; + }, +}; diff --git a/aggregator-hooks/creation-modules/StableSwap.ts b/aggregator-hooks/creation-modules/StableSwap.ts new file mode 100644 index 00000000..f0205df2 --- /dev/null +++ b/aggregator-hooks/creation-modules/StableSwap.ts @@ -0,0 +1,103 @@ +/** + * StableSwap (Curve) aggregator hook deployment module. + */ +import { ethers } from "ethers"; +import { mustEnvForChain } from "../src/cli.js"; +import { + DEFAULT_SQRT_PRICE_X96, + type Address, + type CreationModule, + type FactoryImmutables, + type PoolKeyRecord, +} from "./types.js"; + +export interface StableSwapPoolConfig { + poolType: "stableswap"; + curvePool: Address; + tokens: Address[]; + fee: number | null; + tickSpacing: number | null; + sqrtPriceX96: bigint | null; +} + +const FACTORY_ABI = [ + "function POOL_MANAGER() external view returns (address)", + "function createPool(bytes32 salt, address curvePool, address[] calldata tokens, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)", +]; + +const PROTOCOL_ID = 0xc1; + +const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); + +export const stableswapModule: CreationModule = { + poolType: "stableswap", + protocolId: PROTOCOL_ID, + factoryAbi: FACTORY_ABI, + + getHookParams(config) { + return { + fee: config.fee ?? 0, + tickSpacing: config.tickSpacing ?? 60, + sqrtPriceX96: config.sqrtPriceX96 ?? DEFAULT_SQRT_PRICE_X96, + }; + }, + + buildPoolKeys(config, hookAddress) { + const params = this.getHookParams(config); + const tokens = config.tokens; + const keys: PoolKeyRecord[] = []; + for (let i = 0; i < tokens.length; i++) { + for (let j = i + 1; j < tokens.length; j++) { + const [c0, c1] = orderPair(tokens[i], tokens[j]); + keys.push({ + currency0: c0, + currency1: c1, + fee: params.fee, + tickSpacing: params.tickSpacing, + hooks: hookAddress, + }); + } + } + return keys; + }, + + getExternalPool(config) { + return config.curvePool; + }, + + getImmutablesFromEnv(chainId: number): FactoryImmutables { + return { + poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, + }; + }, + + async readFactoryImmutables(provider, factoryAddress) { + const factory = new ethers.Contract(factoryAddress, FACTORY_ABI, provider); + const poolManager = await factory.POOL_MANAGER(); + return { poolManager: poolManager as Address }; + }, + + encodeConstructorArgs(config, immutables) { + const encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address"], + [immutables.poolManager, config.curvePool], + ); + return encoded.startsWith("0x") ? encoded : `0x${encoded}`; + }, + + buildSelfDeployEnvVars(config, immutables) { + const params = this.getHookParams(config); + return { + CURVE_POOL: config.curvePool, + TOKENS: config.tokens.join(","), + FEE: params.fee.toString(), + TICK_SPACING: params.tickSpacing.toString(), + SQRT_PRICE_X96: params.sqrtPriceX96.toString(), + }; + }, + + buildCreatePoolArgs(config, salt) { + const params = this.getHookParams(config); + return [salt, config.curvePool, config.tokens, params.fee, params.tickSpacing, BigInt(params.sqrtPriceX96)]; + }, +}; diff --git a/aggregator-hooks/creation-modules/StableSwapNG.ts b/aggregator-hooks/creation-modules/StableSwapNG.ts new file mode 100644 index 00000000..4451d7cd --- /dev/null +++ b/aggregator-hooks/creation-modules/StableSwapNG.ts @@ -0,0 +1,103 @@ +/** + * StableSwap-NG (Curve) aggregator hook deployment module. + */ +import { ethers } from "ethers"; +import { mustEnvForChain } from "../src/cli.js"; +import { + DEFAULT_SQRT_PRICE_X96, + type Address, + type CreationModule, + type FactoryImmutables, + type PoolKeyRecord, +} from "./types.js"; + +export interface StableSwapNGPoolConfig { + poolType: "stableswapng"; + curvePool: Address; + tokens: Address[]; + fee: number | null; + tickSpacing: number | null; + sqrtPriceX96: bigint | null; +} + +const FACTORY_ABI = [ + "function POOL_MANAGER() external view returns (address)", + "function createPool(bytes32 salt, address curvePool, address[] calldata tokens, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)", +]; + +const PROTOCOL_ID = 0xc2; + +const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); + +export const stableswapngModule: CreationModule = { + poolType: "stableswapng", + protocolId: PROTOCOL_ID, + factoryAbi: FACTORY_ABI, + + getHookParams(config) { + return { + fee: config.fee ?? 0, + tickSpacing: config.tickSpacing ?? 60, + sqrtPriceX96: config.sqrtPriceX96 ?? DEFAULT_SQRT_PRICE_X96, + }; + }, + + buildPoolKeys(config, hookAddress) { + const params = this.getHookParams(config); + const tokens = config.tokens; + const keys: PoolKeyRecord[] = []; + for (let i = 0; i < tokens.length; i++) { + for (let j = i + 1; j < tokens.length; j++) { + const [c0, c1] = orderPair(tokens[i], tokens[j]); + keys.push({ + currency0: c0, + currency1: c1, + fee: params.fee, + tickSpacing: params.tickSpacing, + hooks: hookAddress, + }); + } + } + return keys; + }, + + getExternalPool(config) { + return config.curvePool; + }, + + getImmutablesFromEnv(chainId: number): FactoryImmutables { + return { + poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, + }; + }, + + async readFactoryImmutables(provider, factoryAddress) { + const factory = new ethers.Contract(factoryAddress, FACTORY_ABI, provider); + const poolManager = await factory.POOL_MANAGER(); + return { poolManager: poolManager as Address }; + }, + + encodeConstructorArgs(config, immutables) { + const encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address"], + [immutables.poolManager, config.curvePool], + ); + return encoded.startsWith("0x") ? encoded : `0x${encoded}`; + }, + + buildSelfDeployEnvVars(config, immutables) { + const params = this.getHookParams(config); + return { + CURVE_POOL: config.curvePool, + TOKENS: config.tokens.join(","), + FEE: params.fee.toString(), + TICK_SPACING: params.tickSpacing.toString(), + SQRT_PRICE_X96: params.sqrtPriceX96.toString(), + }; + }, + + buildCreatePoolArgs(config, salt) { + const params = this.getHookParams(config); + return [salt, config.curvePool, config.tokens, params.fee, params.tickSpacing, params.sqrtPriceX96]; + }, +}; diff --git a/aggregator-hooks/creation-modules/index.ts b/aggregator-hooks/creation-modules/index.ts new file mode 100644 index 00000000..56703504 --- /dev/null +++ b/aggregator-hooks/creation-modules/index.ts @@ -0,0 +1,39 @@ +/** + * Creation modules registry. Import this to get all supported pool types. + */ +import { stableswapModule } from "./StableSwap.js"; +import { stableswapngModule } from "./StableSwapNG.js"; +import { fluiddext1Module } from "./FluidDexT1.js"; +import { fluiddexliteModule } from "./FluidDexLite.js"; +import type { CreationModule } from "./types.js"; + +export type { + Address, + CreationModule, + PoolKeyRecord, + PoolDeployedEntry, + FactoryImmutables, + HookParams, +} from "./types.js"; +export type { StableSwapPoolConfig } from "./StableSwap.js"; +export type { StableSwapNGPoolConfig } from "./StableSwapNG.js"; +export type { FluidDexT1PoolConfig } from "./FluidDexT1.js"; +export type { FluidDexLitePoolConfig } from "./FluidDexLite.js"; + +export { stableswapModule, stableswapngModule, fluiddext1Module, fluiddexliteModule }; + +export type PoolConfig = + | import("./StableSwap.js").StableSwapPoolConfig + | import("./StableSwapNG.js").StableSwapNGPoolConfig + | import("./FluidDexT1.js").FluidDexT1PoolConfig + | import("./FluidDexLite.js").FluidDexLitePoolConfig; + +/** Registry of all creation modules by pool type */ +export const CREATION_MODULES: Record = { + stableswap: stableswapModule, + stableswapng: stableswapngModule, + fluiddext1: fluiddext1Module, + fluiddexlite: fluiddexliteModule, +}; + +export const POOL_TYPES = Object.keys(CREATION_MODULES) as string[]; diff --git a/aggregator-hooks/creation-modules/types.ts b/aggregator-hooks/creation-modules/types.ts new file mode 100644 index 00000000..5d57de0e --- /dev/null +++ b/aggregator-hooks/creation-modules/types.ts @@ -0,0 +1,90 @@ +/** + * Shared types and CreationModule interface for pool deployment. + * Each protocol (stableswap, stableswapng, fluiddext1, fluiddexlite) implements this interface. + */ +import type { Provider } from "ethers"; + +/** Ethereum address: 0x-prefixed hex string */ +export type Address = `0x${string}`; + +/** PoolKey shape for registry output (matches Uniswap v4 PoolKey) */ +export interface PoolKeyRecord { + currency0: Address; + currency1: Address; + fee: number; + tickSpacing: number; + hooks: Address; +} + +/** Entry appended to pool-deployed registry */ +export interface PoolDeployedEntry { + poolKeys: PoolKeyRecord[]; + metadata: { + externalPool: string; + hookAddress: Address; + txHash?: string; + blockNumber?: number; + [key: string]: unknown; + }; +} + +/** Immutables for self-deploy (from env) or factory (from contract) */ +export interface FactoryImmutables { + poolManager: Address; + [key: string]: string | Address; +} + +/** Default sqrtPriceX96 for 1:1 (2^96) */ +export const DEFAULT_SQRT_PRICE_X96 = 79228162514264337593543950336n; + +/** Default hook parameters when not specified in config */ +export const DEFAULT_HOOK_PARAMS = { + fee: 0, + tickSpacing: 60, + sqrtPriceX96: DEFAULT_SQRT_PRICE_X96, +} as const; + +export interface HookParams { + fee: number; + tickSpacing: number; + sqrtPriceX96: bigint; +} + +/** + * Creation module interface. Each pool type implements this to provide + * type-specific deployment logic. + */ +export interface CreationModule { + /** Pool type identifier (e.g. "stableswap", "fluiddext1") */ + poolType: string; + + /** Protocol ID for salt mining (matches MineAggregatorHook.s.sol) */ + protocolId: number; + + /** Factory contract ABI for createPool and reading immutables */ + factoryAbi: string[]; + + /** Resolve hook params with defaults */ + getHookParams(config: TConfig): HookParams; + + /** Build PoolKey records for registry */ + buildPoolKeys(config: TConfig, hookAddress: Address): PoolKeyRecord[]; + + /** Get external pool address/identifier for metadata */ + getExternalPool(config: TConfig): string; + + /** Read immutables from env for self-deploy */ + getImmutablesFromEnv(chainId: number): FactoryImmutables; + + /** Read immutables from factory contract */ + readFactoryImmutables(provider: Provider, factoryAddress: Address): Promise; + + /** Encode constructor args for salt mining */ + encodeConstructorArgs(config: TConfig, immutables: FactoryImmutables): string; + + /** Build env vars for SelfCreateHook.s.sol self-deploy */ + buildSelfDeployEnvVars(config: TConfig, immutables: FactoryImmutables): Record; + + /** Build createPool call args for factory contract */ + buildCreatePoolArgs(config: TConfig, salt: string): unknown[]; +} diff --git a/aggregator-hooks/historical/FluidDexLite.ts b/aggregator-hooks/historical/FluidDexLite.ts index d2922335..1b0247b6 100644 --- a/aggregator-hooks/historical/FluidDexLite.ts +++ b/aggregator-hooks/historical/FluidDexLite.ts @@ -26,27 +26,28 @@ import fs from "node:fs"; import path from "node:path"; import { ethers } from "ethers"; import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; +import type { Address } from "../creation-modules/types.js"; const OUTPUT_FILE = "fluiddexlite-pools.json"; -const DEFAULT_RESOLVER = "0x26b696D0dfDAB6c894Aa9a6575fCD07BB25BbD2C"; +const DEFAULT_RESOLVER: Address = "0x26b696D0dfDAB6c894Aa9a6575fCD07BB25BbD2C"; /** Fluid native token; map to address(0) for Uniswap v4 pool init */ -const FLUID_NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; -const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +const FLUID_NATIVE: Address = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; +const ZERO_ADDRESS: Address = "0x0000000000000000000000000000000000000000"; -function toUniswapV4Currency(addr: string): string { - return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() ? ZERO_ADDRESS : ethers.getAddress(addr); +function toUniswapV4Currency(addr: string): Address { + return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() ? ZERO_ADDRESS : (ethers.getAddress(addr) as Address); } /** Same shape as createPools.ts FluidDexLitePoolConfig */ type CreatePoolsFluidLiteConfig = { poolType: "fluiddexlite"; dexSalt: string; - currency0: string; - currency1: string; + currency0: Address; + currency1: Address; fee: number | null; tickSpacing: number | null; - sqrtPriceX96: string | null; + sqrtPriceX96: bigint | null; }; const RESOLVER_ABI = [ @@ -54,14 +55,20 @@ const RESOLVER_ABI = [ "function getDexState(tuple(address token0, address token1, bytes32 salt) dexKey) external view returns (tuple(tuple(uint256 fee, uint256 revenueCut, uint256 rebalancingStatus, bool isCenterPriceShiftActive, uint256 centerPrice, address centerPriceAddress, bool isRangePercentShiftActive, uint256 upperRangePercent, uint256 lowerRangePercent, bool isThresholdPercentShiftActive, uint256 upperShiftThresholdPercent, uint256 lowerShiftThresholdPercent, uint256 token0Decimals, uint256 token1Decimals, uint256 totalToken0AdjustedAmount, uint256 totalToken1AdjustedAmount) dexVariables, tuple(uint256 lastInteractionTimestamp, uint256 rebalancingShiftingTime, uint256 maxCenterPrice, uint256 minCenterPrice, uint256 shiftPercentage, uint256 centerPriceShiftingTime, uint256 startTimestamp) centerPriceShift, tuple(uint256 oldUpperRangePercent, uint256 oldLowerRangePercent, uint256 shiftingTime, uint256 startTimestamp) rangeShift, tuple(uint256 oldUpperThresholdPercent, uint256 oldLowerThresholdPercent, uint256 shiftingTime, uint256 startTimestamp) thresholdShift) dexState)", ] as const; -/** Fluid fee uses 1e4 basis (10000 = 100%). Uniswap v4 uses 1e6 (10000 = 1%). */ +/** + * Converts Fluid fee (1e4 basis) to Uniswap v4 fee (1e6 basis). + * Fluid: 10000 = 100%. Uniswap v4: 10000 = 1%. + * + * @param fluidFee - Fluid fee in 1e4 basis (bigint or number) + * @returns Uniswap v4 fee as uint24 (max 16_777_215) + */ function fluidFeeToUniswapV4(fluidFee: bigint | number): number { const MAX_U24 = 16_777_215; const converted = Number(fluidFee) * 100; // fluidFee * 1e6 / 1e4 return Math.min(Math.max(0, Math.floor(converted)), MAX_U24); } -function ensureDirForFile(filePath: string) { +function ensureDirForFile(filePath: string): void { const dir = path.dirname(path.resolve(filePath)); fs.mkdirSync(dir, { recursive: true }); } @@ -87,7 +94,7 @@ async function main() { process.exit(1); } - const resolver = ethers.getAddress(resolverRaw); + const resolver = ethers.getAddress(resolverRaw) as Address; const outputDir = (args["output-dir"] as string) ?? "detected"; const provider = new ethers.JsonRpcProvider(rpcUrl); diff --git a/aggregator-hooks/historical/FluidDexT1.ts b/aggregator-hooks/historical/FluidDexT1.ts index bd8e7e26..54a0d645 100644 --- a/aggregator-hooks/historical/FluidDexT1.ts +++ b/aggregator-hooks/historical/FluidDexT1.ts @@ -28,19 +28,20 @@ import fs from "node:fs"; import path from "node:path"; import { JsonRpcProvider, Contract, Interface, getAddress } from "ethers"; import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; +import type { Address } from "../creation-modules/types.js"; const OUTPUT_FILE = "fluiddext1-pools.json"; -const DEFAULT_FACTORY = "0x91716c4eDA1fB55e84Bf8b4c7085f84285c19085"; +const DEFAULT_FACTORY: Address = "0x91716c4eDA1fB55e84Bf8b4c7085f84285c19085"; /** Same shape as createPools.ts FluidDexT1PoolConfig */ type CreatePoolsFluidDexT1Config = { poolType: "fluiddext1"; - fluidPool: string; - currency0: string; - currency1: string; + fluidPool: Address; + currency0: Address; + currency1: Address; fee: number | null; tickSpacing: number | null; - sqrtPriceX96: string | null; + sqrtPriceX96: bigint | null; }; const FACTORY_ABI = [ @@ -55,21 +56,21 @@ const RESOLVER_ABI = [ ] as const; /** Fluid native token; map to address(0) for Uniswap v4 pool init */ -const FLUID_NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; -const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +const FLUID_NATIVE: Address = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; +const ZERO_ADDRESS: Address = "0x0000000000000000000000000000000000000000"; -function toUniswapV4Currency(addr: string): string { - return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() ? ZERO_ADDRESS : getAddress(addr); +function toUniswapV4Currency(addr: string): Address { + return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() ? ZERO_ADDRESS : (getAddress(addr) as Address); } function pRateLimit(rps: number): () => Promise { if (rps <= 0) return async () => {}; const minGapMs = 1000 / rps; let nextAllowed = 0; - return async function acquire() { + return async function acquire(): Promise { const now = Date.now(); if (now < nextAllowed) { - await new Promise((r) => setTimeout(r, nextAllowed - now)); + await new Promise((r) => setTimeout(r, nextAllowed - now)); } nextAllowed = Math.max(now, nextAllowed) + minGapMs; }; @@ -79,7 +80,7 @@ function pLimit(concurrency: number) { let active = 0; const queue: Array<() => void> = []; - const next = () => { + const next = (): void => { active--; const fn = queue.shift(); if (fn) fn(); @@ -98,8 +99,7 @@ function pLimit(concurrency: number) { }; } -/** Return [currency0, currency1] with currency0 < currency1 (lexicographic on mapped addrs) */ -function orderCurrencies(token0: string, token1: string): [string, string] { +function orderCurrencies(token0: Address, token1: Address): [Address, Address] { const mapped = [toUniswapV4Currency(token0), toUniswapV4Currency(token1)].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()), ); @@ -131,7 +131,7 @@ async function main() { process.exit(1); } - const factoryAddr = getAddress(factoryAddrRaw.toLowerCase()); + const factoryAddr = getAddress(factoryAddrRaw.toLowerCase()) as Address; const outputDir = (args["output-dir"] as string) ?? "detected"; const chunkSize = BigInt(Math.max(1, toInt(args["chunk-blocks"], 100_000))); const startBlock = BigInt(Math.max(0, toInt(args["start-block"], 0))); @@ -153,7 +153,7 @@ async function main() { if (startBlock < 0n) throw new Error("start-block must be >= 0"); if (endBlock < startBlock) throw new Error("end-block must be >= start-block"); - const byDexAddr = new Map(); + const byDexAddr = new Map(); if (mode === "logs" || mode === "both") { const iface = new Interface(FACTORY_ABI as unknown as string[]); @@ -171,7 +171,7 @@ async function main() { for (const log of logs) { const parsed = iface.parseLog(log); - const dex = getAddress(parsed!.args.dex); + const dex = getAddress(parsed!.args.dex) as Address; const dexId = parsed!.args.dexId.toString(); if (!byDexAddr.has(dex)) byDexAddr.set(dex, dexId); } @@ -187,7 +187,7 @@ async function main() { console.error(`[enumerate] totalDexes() = ${total}`); for (let i = 1n; i <= total; i++) { - const dex = getAddress(await factory.getDexAddress(i)); + const dex = getAddress(await factory.getDexAddress(i)) as Address; const ok = await factory.isDex(dex); if (!ok) continue; @@ -208,10 +208,10 @@ async function main() { for (const fluidPool of uniqueDexes) { const config = await limit(async () => { await rateLimitAcquire(); - let token0: string; - let token1: string; + let token0: Address; + let token1: Address; try { - [token0, token1] = await resolver.getPoolTokens(fluidPool); + [token0, token1] = (await resolver.getPoolTokens(fluidPool)) as [Address, Address]; } catch { return null; } diff --git a/aggregator-hooks/historical/StableSwapNG.ts b/aggregator-hooks/historical/StableSwapNG.ts index ab4636d1..35cabcca 100644 --- a/aggregator-hooks/historical/StableSwapNG.ts +++ b/aggregator-hooks/historical/StableSwapNG.ts @@ -25,32 +25,33 @@ import fs from "node:fs"; import path from "node:path"; import { JsonRpcProvider, Contract, getAddress, ZeroAddress } from "ethers"; import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; +import type { Address } from "../creation-modules/types.js"; const OUTPUT_FILE = "stableswapng-pools.json"; -const DEFAULT_FACTORY = "0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf"; +const DEFAULT_FACTORY: Address = "0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf"; type PoolMeta = { - pool: string; + pool: Address; kind: "plain" | "meta"; nCoins?: number; - coins?: string[]; - basePool?: string; + coins?: Address[]; + basePool?: Address; }; /** Same shape as createPools.ts StableSwapPoolConfig for stableswapng pool type */ type CreatePoolsStableSwapConfig = { poolType: "stableswapng"; - curvePool: string; - tokens: string[]; + curvePool: Address; + tokens: Address[]; fee: number | null; tickSpacing: number | null; - sqrtPriceX96: string | null; + sqrtPriceX96: bigint | null; }; const CREATE_POOLS_DEFAULTS = { - fee: null, - tickSpacing: null, - sqrtPriceX96: null, + fee: null as number | null, + tickSpacing: null as number | null, + sqrtPriceX96: null as bigint | null, } as const; const FACTORY_ABI = [ @@ -61,7 +62,7 @@ const FACTORY_ABI = [ "function get_base_pool(address) view returns (address)", ]; -function saveJson(filePath: string, data: unknown) { +function saveJson(filePath: string, data: unknown): void { fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); } @@ -69,10 +70,10 @@ function pRateLimit(rps: number): () => Promise { if (rps <= 0) return async () => {}; const minGapMs = 1000 / rps; let nextAllowed = 0; - return async function acquire() { + return async function acquire(): Promise { const now = Date.now(); if (now < nextAllowed) { - await new Promise((r) => setTimeout(r, nextAllowed - now)); + await new Promise((r) => setTimeout(r, nextAllowed - now)); } nextAllowed = Math.max(now, nextAllowed) + minGapMs; }; @@ -82,7 +83,7 @@ function pLimit(concurrency: number) { let active = 0; const queue: Array<() => void> = []; - const next = () => { + const next = (): void => { active--; const fn = queue.shift(); if (fn) fn(); @@ -101,13 +102,13 @@ function pLimit(concurrency: number) { }; } -function uniqAddresses(addrs: string[]): string[] { - const s = new Set(); +function uniqAddresses(addrs: string[]): Address[] { + const s = new Set
(); for (const a of addrs) { if (!a) continue; const norm = a.toLowerCase(); if (norm === ZeroAddress.toLowerCase()) continue; - s.add(getAddress(a)); + s.add(getAddress(a) as Address); } return [...s]; } @@ -134,7 +135,7 @@ async function main() { process.exit(1); } - const factoryAddress = getAddress(factoryAddrRaw); + const factoryAddress = getAddress(factoryAddrRaw) as Address; const outputDir = (args["output-dir"] as string) ?? "detected"; const chunkSize = toInt(args["chunk"], 500); const concurrency = Math.max(1, toInt(getEnvForChain("CONCURRENCY", chainId), 8)); @@ -157,9 +158,9 @@ async function main() { console.log(`Starting at index: ${startIndex}`); console.log(`chunkSize=${chunkSize} concurrency=${concurrency} rps=${rps > 0 ? rps : "unlimited"}`); - const pools: string[] = []; + const pools: Address[] = []; const metas: PoolMeta[] = []; - const metaByPool = new Map(); + const metaByPool = new Map(); for (let i = startIndex; i < poolCount; i += chunkSize) { const end = Math.min(poolCount, i + chunkSize); @@ -169,7 +170,7 @@ async function main() { limit(async () => { await rateLimitAcquire(); const addr: string = await factory.pool_list(i + k); - return getAddress(addr); + return getAddress(addr) as Address; }), ), ); @@ -186,7 +187,7 @@ async function main() { await rateLimitAcquire(); const basePoolRaw = await factory.get_base_pool(pool); - const basePool = getAddress(basePoolRaw); + const basePool = getAddress(basePoolRaw) as Address; const coins = uniqAddresses(coinsRaw); return { diff --git a/aggregator-hooks/src/createPools.ts b/aggregator-hooks/src/createPools.ts index d8f15a23..e6d48a82 100644 --- a/aggregator-hooks/src/createPools.ts +++ b/aggregator-hooks/src/createPools.ts @@ -7,145 +7,45 @@ import { execSync, spawn } from "child_process"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; +import { getEnvForChain, mustEnvForChain, toInt } from "./cli.js"; +import { + CREATION_MODULES, + POOL_TYPES, + type Address, + type PoolConfig, + type PoolDeployedEntry, + type FactoryImmutables, +} from "../creation-modules/index.js"; + const __dirname = dirname(fileURLToPath(import.meta.url)); -// projectRoot = contracts/ (where foundry.toml, script/, mine_hook.sh live) const projectRoot = join(__dirname, "..", ".."); -import { getEnvForChain, mustEnvForChain, toInt } from "@src/cli"; - -/** PoolKey shape for registry output (matches Uniswap v4 PoolKey) */ -interface PoolKeyRecord { - currency0: string; - currency1: string; - fee: number; - tickSpacing: number; - hooks: string; -} - -/** Entry appended to pool-deployed registry */ -interface PoolDeployedEntry { - poolKeys: PoolKeyRecord[]; - metadata: { - externalPool: string; - hookAddress: string; - txHash?: string; - blockNumber?: number; - [key: string]: unknown; - }; -} -// Foundry's default CREATE2 deployer - used when deploying via forge script with new X{salt} +// Foundry's default CREATE2 deployer const CREATE2_DEPLOYER = "0x4e59b44847b379578588920cA78FbF26c0B4956C"; -// Protocol IDs from MineAggregatorHook.s.sol -const PROTOCOL_IDS = { - STABLESWAP: 0xc1, - STABLESWAPNG: 0xc2, - FLUIDDEXT1: 0xf1, - FLUIDDEXLITE: 0xf3, -} as const; - -type PoolType = "stableswap" | "stableswapng" | "fluiddext1" | "fluiddexlite"; - -interface StableSwapPoolConfig { - poolType: "stableswap" | "stableswapng"; - curvePool: string; - tokens: string[]; - fee: number | null; // Uniswap v4 fee (in basis points). null uses default: 0 - tickSpacing: number | null; // Uniswap v4 tick spacing. null uses default: 60 - sqrtPriceX96: string | null; // Uniswap v4 sqrt price. null uses default: 1:1 (2^96) -} - -interface FluidDexT1PoolConfig { - poolType: "fluiddext1"; - fluidPool: string; - currency0: string; - currency1: string; - fee: number | null; // Uniswap v4 fee (in basis points). null uses default: 0 - tickSpacing: number | null; // Uniswap v4 tick spacing. null uses default: 60 - sqrtPriceX96: string | null; // Uniswap v4 sqrt price. null uses default: 1:1 (2^96) -} - -interface FluidDexLitePoolConfig { - poolType: "fluiddexlite"; - dexSalt: string; - currency0: string; - currency1: string; - fee: number | null; // Uniswap v4 fee (in basis points). null uses default: 0 - tickSpacing: number | null; // Uniswap v4 tick spacing. null uses default: 60 - sqrtPriceX96: string | null; // Uniswap v4 sqrt price. null uses default: 1:1 (2^96) -} - -// Immutables for self-deploy (read from env) or factory (read from contract) -interface SelfDeployImmutables { - poolManager: string; - fluidDexReservesResolver?: string; - fluidLiquidity?: string; - fluidDexLite?: string; - fluidDexLiteResolver?: string; -} - -type PoolConfig = StableSwapPoolConfig | FluidDexT1PoolConfig | FluidDexLitePoolConfig; - -// Factory ABIs - minimal interfaces for reading immutables and calling createPool -const STABLE_SWAP_FACTORY_ABI = [ - "function POOL_MANAGER() external view returns (address)", - "function createPool(bytes32 salt, address curvePool, address[] calldata tokens, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)", -]; - -const STABLE_SWAP_NG_FACTORY_ABI = [ - "function POOL_MANAGER() external view returns (address)", - "function createPool(bytes32 salt, address curvePool, address[] calldata tokens, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)", -]; - -const FLUID_DEX_T1_FACTORY_ABI = [ - "function POOL_MANAGER() external view returns (address)", - "function fluidDexReservesResolver() external view returns (address)", - "function FLUID_LIQUIDITY() external view returns (address)", - "function createPool(bytes32 salt, address fluidPool, address currency0, address currency1, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)", -]; - -const FLUID_DEX_LITE_FACTORY_ABI = [ - "function POOL_MANAGER() external view returns (address)", - "function FLUID_DEX_LITE() external view returns (address)", - "function FLUID_DEX_LITE_RESOLVER() external view returns (address)", - "function createPool(bytes32 salt, bytes32 dexSalt, address currency0, address currency1, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)", -]; - -interface FactoryImmutables { - poolManager: string; - [key: string]: string; // For additional immutables -} - interface ParsedArgs { jsonFile: string; - factoryAddress: string | null; + factoryAddress: Address | null; selfDeploy: boolean; rpcUrl: string; - /** Chain ID for env vars (required for self-deploy) */ chainId: number | null; - /** When set, append deployed pools to registry files in this dir (e.g. --registry-dir ./deployed-pools) */ - registryDir: string | null; - /** When set, simulate forge scripts without broadcasting transactions */ + registryDir: string; dryRun: boolean; - /** When set, run forge scripts verbosely (-vvvv) and log full output on errors */ verbose: boolean; - /** 1-based index to start at (skip earlier pools). e.g. --start-at 3 starts with 3rd pool. */ startAt: number; - /** Number of parallel mining workers (default 1) */ jobs: number; + priorityGasPrice: string | null; +} + +function isPoolType(s: unknown): s is string { + return typeof s === "string" && POOL_TYPES.includes(s); } -/** - * Parse command line arguments - */ function parseArgs(): ParsedArgs { const args = process.argv.slice(2); - - // Check for --self-deploy flag const selfDeployIndex = args.indexOf("--self-deploy"); const selfDeploy = selfDeployIndex !== -1; - // Extract positional args (exclude known flags and their values) const flagNames = [ "--self-deploy", "--chain-id", @@ -156,12 +56,21 @@ function parseArgs(): ParsedArgs { "--start-at", "--jobs", "-j", + "--priority-gas-price", ]; const positionalArgs: string[] = []; for (let i = 0; i < args.length; i++) { const a = args[i]; if (flagNames.includes(a)) { - if (a === "--chain-id" || a === "--registry-dir" || a === "--start-at" || a === "--jobs" || a === "-j") i++; // skip value + if ( + a === "--chain-id" || + a === "--registry-dir" || + a === "--start-at" || + a === "--jobs" || + a === "-j" || + a === "--priority-gas-price" + ) + i++; continue; } positionalArgs.push(a); @@ -190,6 +99,9 @@ function parseArgs(): ParsedArgs { " --start-at : Start at 1-based pool index (skip earlier pools). e.g. --start-at 3 to resume from pool 3.", ); console.error(" --jobs , -j : Run N parallel salt mining workers (default 1). Speeds up mining."); + console.error( + " --priority-gas-price : Max priority fee per gas for EIP1559 (e.g. 3gwei). Speeds up tx inclusion.", + ); console.error(""); console.error("Environment variables:"); console.error(" RPC_URL_: RPC endpoint (required when --chain-id set)"); @@ -198,9 +110,12 @@ function parseArgs(): ParsedArgs { } const jsonFile = positionalArgs[0]; - const factoryAddress = selfDeploy ? null : positionalArgs[1]; + const factoryAddress: Address | null = selfDeploy + ? null + : positionalArgs[1] + ? (ethers.getAddress(positionalArgs[1]) as Address) + : null; - // Validate mutual exclusivity (factory address looks like 0x...) if (selfDeploy && positionalArgs.length >= 2 && positionalArgs[1].startsWith("0x")) { console.error("Error: --self-deploy and factoryAddress are mutually exclusive"); process.exit(1); @@ -228,14 +143,14 @@ function parseArgs(): ParsedArgs { process.exit(1); } - const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) { + if (!process.env.PRIVATE_KEY) { console.error("Error: PRIVATE_KEY environment variable is required"); process.exit(1); } const registryDirIndex = args.indexOf("--registry-dir"); - const registryDir = registryDirIndex !== -1 && args[registryDirIndex + 1] ? args[registryDirIndex + 1] : null; + const registryDir = + registryDirIndex !== -1 && args[registryDirIndex + 1] ? args[registryDirIndex + 1] : "created-pools"; const dryRun = args.includes("--dry-run"); const verbose = args.includes("--verbose") || args.includes("-v"); @@ -258,6 +173,11 @@ function parseArgs(): ParsedArgs { process.exit(1); } + const priorityGasPriceIndex = args.indexOf("--priority-gas-price"); + const priorityGasPriceRaw = + priorityGasPriceIndex !== -1 && args[priorityGasPriceIndex + 1] ? args[priorityGasPriceIndex + 1] : null; + const priorityGasPrice = priorityGasPriceRaw?.trim() || null; + return { jsonFile, factoryAddress, @@ -269,99 +189,11 @@ function parseArgs(): ParsedArgs { verbose, startAt, jobs, + priorityGasPrice, }; } -const POOL_TYPES: PoolType[] = ["stableswap", "stableswapng", "fluiddext1", "fluiddexlite"]; - -function isPoolType(s: unknown): s is PoolType { - return typeof s === "string" && POOL_TYPES.includes(s as PoolType); -} - -/** Default values for Uniswap v4 hook parameters when not specified */ -const DEFAULT_HOOK_PARAMS = { - fee: 0, - tickSpacing: 60, - sqrtPriceX96: "79228162514264337593543950336", // 1:1 (2^96) -} as const; - -/** - * Get resolved hook parameters with defaults applied - */ -function getHookParams(config: StableSwapPoolConfig | FluidDexT1PoolConfig | FluidDexLitePoolConfig) { - return { - fee: config.fee ?? DEFAULT_HOOK_PARAMS.fee, - tickSpacing: config.tickSpacing ?? DEFAULT_HOOK_PARAMS.tickSpacing, - sqrtPriceX96: config.sqrtPriceX96 ?? DEFAULT_HOOK_PARAMS.sqrtPriceX96, - }; -} - -/** - * Build PoolKey records for registry. For stableswap/stableswapng: all token pairs. - * For fluiddext1/fluiddexlite: single pair. currency0 < currency1 for v4 ordering. - */ -function buildPoolKeys(poolConfig: PoolConfig, poolType: PoolType, hookAddress: string): PoolKeyRecord[] { - const params = getHookParams(poolConfig); - - const orderPair = (a: string, b: string): [string, string] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); - - switch (poolType) { - case "stableswap": - case "stableswapng": { - const config = poolConfig as StableSwapPoolConfig; - const tokens = config.tokens; - const keys: PoolKeyRecord[] = []; - for (let i = 0; i < tokens.length; i++) { - for (let j = i + 1; j < tokens.length; j++) { - const [c0, c1] = orderPair(tokens[i], tokens[j]); - keys.push({ - currency0: c0, - currency1: c1, - fee: params.fee, - tickSpacing: params.tickSpacing, - hooks: hookAddress, - }); - } - } - return keys; - } - case "fluiddext1": - case "fluiddexlite": { - const config = poolConfig as FluidDexT1PoolConfig | FluidDexLitePoolConfig; - const [c0, c1] = orderPair(config.currency0, config.currency1); - return [ - { - currency0: c0, - currency1: c1, - fee: params.fee, - tickSpacing: params.tickSpacing, - hooks: hookAddress, - }, - ]; - } - } -} - -/** - * Get external pool address/identifier for metadata - */ -function getExternalPool(poolConfig: PoolConfig, poolType: PoolType): string { - switch (poolType) { - case "stableswap": - case "stableswapng": - return (poolConfig as StableSwapPoolConfig).curvePool; - case "fluiddext1": - return (poolConfig as FluidDexT1PoolConfig).fluidPool; - case "fluiddexlite": - return (poolConfig as FluidDexLitePoolConfig).dexSalt; - } -} - -/** - * Append a deployed pool entry to the registry file for the given pool type. - * File is an array of PoolDeployedEntry. Creates file if it doesn't exist. - */ -function appendToRegistryFile(registryDir: string, poolType: PoolType, entry: PoolDeployedEntry): void { +function appendToRegistryFile(registryDir: string, poolType: string, entry: PoolDeployedEntry): void { const fileName = `deployed-${poolType}.json`; const filePath = join(registryDir, fileName); @@ -382,10 +214,14 @@ function appendToRegistryFile(registryDir: string, poolType: PoolType, entry: Po console.log(`Appended to registry: ${filePath}`); } -/** - * Load and parse JSON file for factory mode. - * Each config must have a "poolType" field; all configs must have the same poolType. - */ +function parseSqrtPriceX96(v: unknown): bigint | null { + if (v == null) return null; + if (typeof v === "bigint") return v; + if (typeof v === "string") return BigInt(v); + if (typeof v === "number") return BigInt(Math.floor(v)); + return null; +} + function loadJsonFile(filePath: string): PoolConfig[] { try { const content = readFileSync(filePath, "utf-8"); @@ -406,7 +242,7 @@ function loadJsonFile(filePath: string): PoolConfig[] { } } - const firstType = pools[0].poolType as PoolType; + const firstType = pools[0].poolType as string; for (let i = 1; i < pools.length; i++) { if (pools[i].poolType !== firstType) { throw new Error( @@ -415,7 +251,10 @@ function loadJsonFile(filePath: string): PoolConfig[] { } } - return pools; + return pools.map((p: Record) => ({ + ...p, + sqrtPriceX96: parseSqrtPriceX96(p.sqrtPriceX96), + })) as PoolConfig[]; } catch (error) { if (error instanceof Error) { console.error(`Error loading JSON file: ${error.message}`); @@ -426,198 +265,29 @@ function loadJsonFile(filePath: string): PoolConfig[] { } } -/** - * Read immutables from env vars for self-deploy mode. - * Uses VAR or VAR_ (e.g. POOL_MANAGER_1, DEX_LITE_ADDRESS_1). - */ -function getImmutablesFromEnv(chainId: number, poolType: PoolType): SelfDeployImmutables { - const immutables: SelfDeployImmutables = { - poolManager: mustEnvForChain("POOL_MANAGER", chainId), - }; - - if (poolType === "fluiddext1") { - immutables.fluidDexReservesResolver = mustEnvForChain("FLUID_DEX_RESOLVER", chainId); - immutables.fluidLiquidity = mustEnvForChain("FLUID_LIQUIDITY", chainId); - } else if (poolType === "fluiddexlite") { - const dexLite = getEnvForChain("FLUID_DEX_LITE", chainId) ?? getEnvForChain("DEX_LITE_ADDRESS", chainId); - const dexLiteResolver = - getEnvForChain("FLUID_DEX_LITE_RESOLVER", chainId) ?? getEnvForChain("DEX_LITE_RESOLVER_ADDRESS", chainId); - if (!dexLite) - throw new Error(`FLUID_DEX_LITE_${chainId} or DEX_LITE_ADDRESS_${chainId} required for fluiddexlite self-deploy`); - if (!dexLiteResolver) - throw new Error( - `FLUID_DEX_LITE_RESOLVER_${chainId} or DEX_LITE_RESOLVER_ADDRESS_${chainId} required for fluiddexlite self-deploy`, - ); - immutables.fluidDexLite = dexLite; - immutables.fluidDexLiteResolver = dexLiteResolver; - } - - return immutables; -} - -/** - * Convert SelfDeployImmutables to FactoryImmutables format - */ -function immutablesToFactoryFormat(immutables: SelfDeployImmutables): FactoryImmutables { - const result: FactoryImmutables = { - poolManager: immutables.poolManager, - }; - - if (immutables.fluidDexReservesResolver) { - result.fluidDexReservesResolver = immutables.fluidDexReservesResolver; - } - if (immutables.fluidLiquidity) { - result.fluidLiquidity = immutables.fluidLiquidity; - } - if (immutables.fluidDexLite) { - result.fluidDexLite = immutables.fluidDexLite; - } - if (immutables.fluidDexLiteResolver) { - result.fluidDexLiteResolver = immutables.fluidDexLiteResolver; - } - - return result; -} - -/** - * Read factory contract immutables - */ -async function readFactoryImmutables( - provider: ethers.Provider, - factoryAddress: string, - poolType: PoolType, -): Promise { - let abi: string[]; - - switch (poolType) { - case "stableswap": - abi = STABLE_SWAP_FACTORY_ABI; - break; - case "stableswapng": - abi = STABLE_SWAP_NG_FACTORY_ABI; - break; - case "fluiddext1": - abi = FLUID_DEX_T1_FACTORY_ABI; - break; - case "fluiddexlite": - abi = FLUID_DEX_LITE_FACTORY_ABI; - break; - } - - const factory = new ethers.Contract(factoryAddress, abi, provider); - const immutables: FactoryImmutables = { poolManager: "" }; - - // Read POOL_MANAGER (always present) - immutables.poolManager = await factory.POOL_MANAGER(); - - // Read additional immutables based on pool type - if (poolType === "fluiddext1") { - immutables.fluidDexReservesResolver = await factory.FLUID_DEX_RESOLVER(); - immutables.fluidLiquidity = await factory.FLUID_LIQUIDITY(); - } else if (poolType === "fluiddexlite") { - immutables.fluidDexLite = await factory.FLUID_DEX_LITE(); - immutables.fluidDexLiteResolver = await factory.FLUID_DEX_LITE_RESOLVER(); - } - - return immutables; -} - -/** - * Encode constructor arguments for salt mining - */ -function encodeConstructorArgs( - poolConfig: PoolConfig, - poolType: PoolType, - factoryImmutables: FactoryImmutables, -): string { - let encoded: string; - - switch (poolType) { - case "stableswap": - case "stableswapng": { - const config = poolConfig as StableSwapPoolConfig; - encoded = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "address"], - [factoryImmutables.poolManager, config.curvePool], - ); - break; - } - case "fluiddext1": { - const config = poolConfig as FluidDexT1PoolConfig; - encoded = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "address", "address", "address"], - [ - factoryImmutables.poolManager, - config.fluidPool, - factoryImmutables.fluidDexReservesResolver, - factoryImmutables.fluidLiquidity, - ], - ); - break; - } - case "fluiddexlite": { - const config = poolConfig as FluidDexLitePoolConfig; - encoded = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "address", "address", "bytes32"], - [ - factoryImmutables.poolManager, - factoryImmutables.fluidDexLite, - factoryImmutables.fluidDexLiteResolver, - config.dexSalt, - ], - ); - break; - } - } - - // Ensure 0x prefix - return encoded.startsWith("0x") ? encoded : `0x${encoded}`; -} - -/** - * Get protocol ID for pool type - */ -function getProtocolId(poolType: PoolType): number { - switch (poolType) { - case "stableswap": - return PROTOCOL_IDS.STABLESWAP; - case "stableswapng": - return PROTOCOL_IDS.STABLESWAPNG; - case "fluiddext1": - return PROTOCOL_IDS.FLUIDDEXT1; - case "fluiddexlite": - return PROTOCOL_IDS.FLUIDDEXLITE; - } -} - /** PoolManager Initialize event for verification */ const INITIALIZE_TOPIC = "0xdd466e674ea557f56295e2d0218a125ea4b4f0f6f3307b95f85e6110838d6438"; -/** - * When forge script fails, verify whether deployment actually succeeded (false positive). - * Checks: 1) Hook has code at address parsed from output; 2) Initialize events for that hook. - */ async function verifyDeploymentOnForgeFailure( provider: ethers.Provider, - poolManagerAddress: string, + poolManagerAddress: Address, poolConfig: PoolConfig, - poolType: PoolType, + poolType: string, forgeOutput: string, -): Promise<{ - hookAddress: string; - hookDeployed: boolean; - poolsInitialized: number; -} | null> { +): Promise<{ hookAddress: Address; hookDeployed: boolean; poolsInitialized: number } | null> { + const module = CREATION_MODULES[poolType]; + if (!module) return null; + const hookMatch = forgeOutput.match(/Hook Address:\s*(0x[a-fA-F0-9]{40})/); if (!hookMatch) return null; - const hookAddress = ethers.getAddress(hookMatch[1]); + const hookAddress = ethers.getAddress(hookMatch[1]) as Address; const code = await provider.getCode(hookAddress); const hookDeployed = !!code && code !== "0x" && code.length > 2; if (!hookDeployed) return { hookAddress, hookDeployed: false, poolsInitialized: 0 }; - const poolKeys = buildPoolKeys(poolConfig, poolType, hookAddress); + const poolKeys = module.buildPoolKeys(poolConfig, hookAddress); let poolsInitialized = 0; try { @@ -651,15 +321,12 @@ async function verifyDeploymentOnForgeFailure( } } } catch { - // Ignore log query errors + /* ignore */ } return { hookAddress, hookDeployed, poolsInitialized }; } -/** - * Run a single mine_hook.sh worker. Resolves with salt on success, rejects on failure. - */ function runMineHookWorker( scriptPath: string, args: string[], @@ -696,18 +363,10 @@ function runMineHookWorker( }); } -/** - * Mine salt using mine_hook.sh script - * @param constructorArgs - Hex-encoded constructor arguments - * @param protocolId - Protocol identifier - * @param deployerAddress - Optional deployer address (factory address or wallet address) - * @param verbose - When true, run forge verbosely (passed via FORGE_VERBOSE env) - * @param jobs - Number of parallel workers (default 1). Each searches a different random region. - */ async function mineSalt( constructorArgs: string, protocolId: number, - deployerAddress?: string, + deployerAddress?: Address, verbose = false, jobs = 1, ): Promise { @@ -716,21 +375,13 @@ async function mineSalt( console.log(`Mining salt for protocol ${protocolIdHex}...`); console.log(`Constructor args: ${constructorArgs.substring(0, 66)}...`); - if (deployerAddress) { - console.log(`Deployer address: ${deployerAddress}`); - } - if (jobs > 1) { - console.log(`Running ${jobs} parallel mining workers...`); - } + if (deployerAddress) console.log(`Deployer address: ${deployerAddress}`); + if (jobs > 1) console.log(`Running ${jobs} parallel mining workers...`); const baseArgs = [scriptPath, constructorArgs, protocolIdHex]; - if (deployerAddress) { - baseArgs.push("500", deployerAddress); - } - const execEnv = { - ...process.env, - ...(verbose && { FORGE_VERBOSE: "1" }), - }; + if (deployerAddress) baseArgs.push("500", deployerAddress); + + const execEnv = { ...process.env, ...(verbose && { FORGE_VERBOSE: "1" }) }; try { if (jobs === 1) { @@ -739,9 +390,8 @@ async function mineSalt( return result.salt; } - // Parallel: run N workers, first to find a valid salt wins const children: ReturnType[] = []; - const workerPromises: Promise<{ salt: string } | null>[] = []; + const workerPromises: Promise<{ salt: string } | { failed: true; output: string }>[] = []; for (let i = 0; i < jobs; i++) { const child = spawn("bash", baseArgs, { @@ -755,18 +405,21 @@ async function mineSalt( child.stdout!.on("data", (chunk) => { output += chunk.toString(); }); + child.stderr!.on("data", (chunk) => { + output += chunk.toString(); + }); - const promise = new Promise<{ salt: string } | null>((resolve) => { + const promise = new Promise<{ salt: string } | { failed: true; output: string }>((resolve) => { child.on("close", (code) => { if (code === 0) { const saltMatch = output.match(/Salt \(bytes32\):\s*(0x[a-fA-F0-9]{64})/); if (saltMatch) resolve({ salt: saltMatch[1] }); - else resolve(null); + else resolve({ failed: true, output }); } else { - resolve(null); + resolve({ failed: true, output }); } }); - child.on("error", () => resolve(null)); + child.on("error", (err) => resolve({ failed: true, output: err.message })); }); workerPromises.push(promise); } @@ -781,18 +434,21 @@ async function mineSalt( }); }; - // Race: first success wins; reject only when all fail const salt = await new Promise((resolve, reject) => { let failedCount = 0; + let lastFailedOutput = ""; workerPromises.forEach((p) => { p.then((result) => { - if (result) { + if ("salt" in result) { killAll(); resolve(result.salt); } else { failedCount++; + lastFailedOutput = result.output; if (failedCount === jobs) { - reject(new Error("All mining workers failed")); + const err = new Error("All mining workers failed") as Error & { stdout?: string }; + err.stdout = lastFailedOutput; + reject(err); } } }); @@ -806,84 +462,37 @@ async function mineSalt( if (error instanceof Error) { console.error(error.message); const execErr = error as { stdout?: string; stderr?: string }; - if (execErr.stdout) { - console.error("\n--- Forge/mine_hook stdout ---\n", execErr.stdout); - } - if (execErr.stderr) { - console.error("\n--- Forge/mine_hook stderr ---\n", execErr.stderr); - } + if (execErr.stdout) console.error("\n--- Forge/mine_hook output (from failed worker) ---\n", execErr.stdout); + if (execErr.stderr) console.error("\n--- Forge/mine_hook stderr ---\n", execErr.stderr); } throw error; } } -/** - * Self-deploy a hook using SelfCreateHook.s.sol forge script - */ function selfDeployPool( poolConfig: PoolConfig, - poolType: PoolType, - immutables: SelfDeployImmutables, + poolType: string, + immutables: FactoryImmutables, salt: string, rpcUrl: string, dryRun: boolean, verbose = false, -): string { - const protocolId = getProtocolId(poolType); + priorityGasPrice: string | null = null, +): Address | "deployed" { + const module = CREATION_MODULES[poolType]; + if (!module) throw new Error(`Unknown pool type: ${poolType}`); - // Build environment variables for the forge script - // Ensure PRIVATE_KEY has 0x prefix (vm.envUint requires it) const rawKey = (process.env.PRIVATE_KEY ?? "").trim(); const privateKey = rawKey.startsWith("0x") ? rawKey : `0x${rawKey}`; const envVars: Record = { - PROTOCOL_ID: protocolId.toString(), + PROTOCOL_ID: module.protocolId.toString(), SALT: salt, POOL_MANAGER: immutables.poolManager, PRIVATE_KEY: privateKey, + ...module.buildSelfDeployEnvVars(poolConfig, immutables), }; - // Add pool-specific environment variables - switch (poolType) { - case "stableswap": - case "stableswapng": { - const config = poolConfig as StableSwapPoolConfig; - const params = getHookParams(config); - envVars.CURVE_POOL = config.curvePool; - // Pass all tokens; forge script will init one Uniswap pool per pair - envVars.TOKENS = config.tokens.join(","); - envVars.FEE = params.fee.toString(); - envVars.TICK_SPACING = params.tickSpacing.toString(); - envVars.SQRT_PRICE_X96 = params.sqrtPriceX96; - break; - } - case "fluiddext1": { - const config = poolConfig as FluidDexT1PoolConfig; - const params = getHookParams(config); - envVars.FLUID_POOL = config.fluidPool; - envVars.FLUID_DEX_RESOLVER = immutables.fluidDexReservesResolver!; - envVars.FLUID_LIQUIDITY = immutables.fluidLiquidity!; - envVars.TOKENS = [config.currency0, config.currency1].join(","); - envVars.FEE = params.fee.toString(); - envVars.TICK_SPACING = params.tickSpacing.toString(); - envVars.SQRT_PRICE_X96 = params.sqrtPriceX96; - break; - } - case "fluiddexlite": { - const config = poolConfig as FluidDexLitePoolConfig; - const params = getHookParams(config); - envVars.FLUID_DEX_LITE = immutables.fluidDexLite!; - envVars.FLUID_DEX_LITE_RESOLVER = immutables.fluidDexLiteResolver!; - envVars.DEX_SALT = config.dexSalt; - envVars.TOKENS = [config.currency0, config.currency1].join(","); - envVars.FEE = params.fee.toString(); - envVars.TICK_SPACING = params.tickSpacing.toString(); - envVars.SQRT_PRICE_X96 = params.sqrtPriceX96; - break; - } - } - - // Build env string for command const envString = Object.entries(envVars) .map(([key, value]) => `${key}="${value}"`) .join(" "); @@ -895,26 +504,23 @@ function selfDeployPool( try { const broadcastFlag = dryRun ? "" : " --broadcast"; const verboseFlag = verbose ? " -vvvv" : ""; - const command = `${envString} forge script script/SelfCreateHook.s.sol:SelfCreateHookScript --via-ir --rpc-url "${rpcUrl}"${broadcastFlag}${verboseFlag}`; + const priorityFeeFlag = priorityGasPrice ? ` --priority-gas-price ${priorityGasPrice}` : ""; + const command = `${envString} forge script script/SelfCreateHook.s.sol:SelfCreateHookScript --rpc-url "${rpcUrl}"${broadcastFlag}${priorityFeeFlag}${verboseFlag}`; const output = execSync(command, { encoding: "utf-8", cwd: projectRoot, env: { ...process.env, ...envVars }, }); - if (verbose) { - console.log("\n--- Forge script output ---\n", output); - } + if (verbose) console.log("\n--- Forge script output ---\n", output); - // Parse hook address from output const hookMatch = output.match(/Hook Address:\s*(0x[a-fA-F0-9]{40})/); if (hookMatch) { - const hookAddress = hookMatch[1]; - console.log(`✓ Hook deployed at: ${hookAddress}`); - return hookAddress; + const addr = ethers.getAddress(hookMatch[1]) as Address; + console.log(`✓ Hook deployed at: ${addr}`); + return addr; } - // If we can't parse the address, just indicate success console.log(`✓ Self-deploy completed`); return "deployed"; } catch (error) { @@ -922,111 +528,39 @@ function selfDeployPool( if (error instanceof Error) { console.error(error.message); const execErr = error as { stdout?: string; stderr?: string }; - if (execErr.stdout) { - console.error("\n--- Forge stdout ---\n", execErr.stdout); - } - if (execErr.stderr) { - console.error("\n--- Forge stderr ---\n", execErr.stderr); - } + if (execErr.stdout) console.error("\n--- Forge stdout ---\n", execErr.stdout); + if (execErr.stderr) console.error("\n--- Forge stderr ---\n", execErr.stderr); } throw error; } } -/** - * Create pool via factory contract - */ async function createPool( signer: ethers.Signer, - factoryAddress: string, + factoryAddress: Address, poolConfig: PoolConfig, - poolType: PoolType, + poolType: string, salt: string, -): Promise<{ hookAddress: string; blockNumber: number; txHash: string }> { - let abi: string[]; - let args: any[]; - - switch (poolType) { - case "stableswap": { - abi = STABLE_SWAP_FACTORY_ABI; - const config = poolConfig as StableSwapPoolConfig; - const params = getHookParams(config); - // Currency is type alias for address, so pass addresses directly - args = [ - salt, - config.curvePool, - config.tokens, // Currency[] is address[] in ABI - params.fee, - params.tickSpacing, - BigInt(params.sqrtPriceX96), - ]; - break; - } - case "stableswapng": { - abi = STABLE_SWAP_NG_FACTORY_ABI; - const config = poolConfig as StableSwapPoolConfig; - const params = getHookParams(config); - // Currency is type alias for address, so pass addresses directly - args = [ - salt, - config.curvePool, - config.tokens, // Currency[] is address[] in ABI - params.fee, - params.tickSpacing, - BigInt(params.sqrtPriceX96), - ]; - break; - } - case "fluiddext1": { - abi = FLUID_DEX_T1_FACTORY_ABI; - const config = poolConfig as FluidDexT1PoolConfig; - const params = getHookParams(config); - // Currency is type alias for address, so pass addresses directly - args = [ - salt, - config.fluidPool, - config.currency0, // Currency is address in ABI - config.currency1, // Currency is address in ABI - params.fee, - params.tickSpacing, - BigInt(params.sqrtPriceX96), - ]; - break; - } - case "fluiddexlite": { - abi = FLUID_DEX_LITE_FACTORY_ABI; - const config = poolConfig as FluidDexLitePoolConfig; - const params = getHookParams(config); - // Currency is type alias for address, so pass addresses directly - args = [ - salt, - config.dexSalt, - config.currency0, // Currency is address in ABI - config.currency1, // Currency is address in ABI - params.fee, - params.tickSpacing, - BigInt(params.sqrtPriceX96), - ]; - break; - } - } +): Promise<{ hookAddress: Address | ""; blockNumber: number; txHash: string }> { + const module = CREATION_MODULES[poolType]; + if (!module) throw new Error(`Unknown pool type: ${poolType}`); - const factoryWithAbi = new ethers.Contract(factoryAddress, abi, signer); + const args = module.buildCreatePoolArgs(poolConfig, salt); + const factory = new ethers.Contract(factoryAddress, module.factoryAbi, signer); console.log(`Calling createPool on factory ${factoryAddress}...`); - console.log(`Args:`, args.map((a, i) => `${i}: ${a.toString().substring(0, 66)}...`).join(", ")); + console.log(`Args:`, args.map((a, i) => `${i}: ${String(a).substring(0, 66)}...`).join(", ")); try { - const tx = await factoryWithAbi.createPool(...args); + const tx = await factory.createPool(...args); console.log(`✓ Transaction sent: ${tx.hash}`); const receipt = await tx.wait(); - console.log(`✓ Transaction confirmed in block ${receipt.blockNumber}`); + console.log(`✓ Transaction confirmed in block ${receipt!.blockNumber}`); - // Extract hook address from events - const hookDeployedEvent = receipt.logs.find((log: any) => { + const hookDeployedEvent = receipt!.logs.find((log: ethers.Log) => { try { - const parsed = factoryWithAbi.interface.parseLog(log); + const parsed = factory.interface.parseLog({ topics: log.topics as string[], data: log.data }); return parsed?.name === "HookDeployed"; } catch { return false; @@ -1034,8 +568,11 @@ async function createPool( }); if (hookDeployedEvent) { - const parsed = factoryWithAbi.interface.parseLog(hookDeployedEvent); - const hookAddress = (parsed?.args.hook || parsed?.args[0]) as string; + const parsed = factory.interface.parseLog({ + topics: hookDeployedEvent.topics as string[], + data: hookDeployedEvent.data, + }); + const hookAddress = ethers.getAddress((parsed?.args.hook || parsed?.args[0]) as string) as Address; console.log(`✓ Hook deployed at: ${hookAddress}`); return { hookAddress, @@ -1053,21 +590,13 @@ async function createPool( console.error("Error creating pool:"); if (error instanceof Error) { console.error(error.message); - // Check for revert reason in error data - if ("data" in error && error.data) { - console.error("Error data:", error.data); - } - if ("reason" in error && error.reason) { - console.error("Revert reason:", error.reason); - } + if ("data" in error && error.data) console.error("Error data:", error.data); + if ("reason" in error && error.reason) console.error("Revert reason:", error.reason); } throw error; } } -/** - * Main execution function - */ async function main() { const { jsonFile, @@ -1080,33 +609,22 @@ async function main() { verbose, startAt, jobs, + priorityGasPrice, } = parseArgs(); console.log("=== Pool Creation Script ==="); console.log(`JSON File: ${jsonFile}`); console.log(`Mode: ${selfDeploy ? "Self-Deploy" : "Factory"}`); - if (factoryAddress) { - console.log(`Factory Address: ${factoryAddress}`); - } + if (factoryAddress) console.log(`Factory Address: ${factoryAddress}`); console.log(`RPC URL: ${rpcUrl}`); - if (registryDir) { - console.log(`Registry dir: ${registryDir}`); - } - if (dryRun) { - console.log("DRY RUN: forge scripts will simulate without broadcasting"); - } - if (verbose) { - console.log("VERBOSE: forge scripts will run with -vvvv"); - } - if (startAt > 1) { - console.log(`Starting at pool index: ${startAt} (skipping first ${startAt - 1} pool(s))`); - } - if (jobs > 1) { - console.log(`Salt mining: ${jobs} parallel workers`); - } + if (registryDir) console.log(`Registry dir: ${registryDir}`); + if (dryRun) console.log("DRY RUN: forge scripts will simulate without broadcasting"); + if (verbose) console.log("VERBOSE: forge scripts will run with -vvvv"); + if (startAt > 1) console.log(`Starting at pool index: ${startAt} (skipping first ${startAt - 1} pool(s))`); + if (jobs > 1) console.log(`Salt mining: ${jobs} parallel workers`); + if (priorityGasPrice) console.log(`Priority gas price: ${priorityGasPrice}`); console.log(""); - // Setup provider and signer const provider = new ethers.JsonRpcProvider(rpcUrl); const privateKey = process.env.PRIVATE_KEY!; const signer = new ethers.Wallet(privateKey, provider); @@ -1114,7 +632,6 @@ async function main() { console.log(""); if (selfDeploy) { - // Self-deploy mode: load configs (same format as factory), immutables from env const allPools = loadJsonFile(jsonFile); const pools = startAt > 1 ? allPools.slice(startAt - 1) : allPools; if (startAt > 1 && pools.length === 0) { @@ -1132,31 +649,33 @@ async function main() { const i = startAt - 1 + j; const poolConfig = pools[j]; const poolType = poolConfig.poolType; - const immutables = getImmutablesFromEnv(chainId, poolType); - const protocolId = getProtocolId(poolType); + const module = CREATION_MODULES[poolType]; + const immutables = module.getImmutablesFromEnv(chainId); + console.log(`\n--- Processing Pool ${i + 1}/${allPools.length} (${poolType}) ---`); try { - // Convert immutables to factory format for constructor encoding - const factoryImmutables = immutablesToFactoryFormat(immutables); - - // Encode constructor arguments - const constructorArgs = encodeConstructorArgs(poolConfig, poolType, factoryImmutables); - - // Mine salt using CREATE2 deployer (Foundry routes new X{salt} through 0x4e59...) - const salt = await mineSalt(constructorArgs, protocolId, CREATE2_DEPLOYER, verbose, jobs); - - // Self-deploy via forge script - const hookAddress = selfDeployPool(poolConfig, poolType, immutables, salt, rpcUrl, dryRun, verbose); + const constructorArgs = module.encodeConstructorArgs(poolConfig, immutables); + const salt = await mineSalt(constructorArgs, module.protocolId, CREATE2_DEPLOYER, verbose, jobs); + const hookAddress = selfDeployPool( + poolConfig, + poolType, + immutables, + salt, + rpcUrl, + dryRun, + verbose, + priorityGasPrice, + ); if (registryDir && hookAddress && hookAddress !== "deployed") { - const poolKeys = buildPoolKeys(poolConfig, poolType, hookAddress); + const poolKeys = module.buildPoolKeys(poolConfig, hookAddress); if (poolKeys.length > 0) { const blockNumber = Number(await provider.getBlockNumber()); appendToRegistryFile(registryDir, poolType, { poolKeys, metadata: { - externalPool: getExternalPool(poolConfig, poolType), + externalPool: module.getExternalPool(poolConfig), hookAddress, blockNumber, }, @@ -1169,12 +688,8 @@ async function main() { if (error instanceof Error) { console.error(error.message); const execErr = error as { stdout?: string; stderr?: string }; - if (execErr.stdout) { - console.error("\n--- Forge stdout ---\n", execErr.stdout); - } - if (execErr.stderr) { - console.error("\n--- Forge stderr ---\n", execErr.stderr); - } + if (execErr.stdout) console.error("\n--- Forge stdout ---\n", execErr.stdout); + if (execErr.stderr) console.error("\n--- Forge stderr ---\n", execErr.stderr); const forgeOutput = [execErr.stdout, execErr.stderr].filter(Boolean).join("\n"); if (forgeOutput) { const verification = await verifyDeploymentOnForgeFailure( @@ -1184,80 +699,63 @@ async function main() { poolType, forgeOutput, ); - if (verification) { - if (verification.hookDeployed) { - console.error(""); - console.error("Verification (possible false positive):"); - console.error(` Hook has code at ${verification.hookAddress} → deployment likely succeeded`); - if (verification.poolsInitialized > 0) { - console.error( - ` Found ${verification.poolsInitialized} Initialize event(s) for this hook → pool(s) initialized on-chain`, - ); - } - console.error(" Check block explorer to confirm."); + if (verification?.hookDeployed) { + console.error(""); + console.error("Verification (possible false positive):"); + console.error(` Hook has code at ${verification.hookAddress} → deployment likely succeeded`); + if (verification.poolsInitialized > 0) { + console.error( + ` Found ${verification.poolsInitialized} Initialize event(s) for this hook → pool(s) initialized on-chain`, + ); } + console.error(" Check block explorer to confirm."); } } } - // Continue with next pool continue; } } } else { - // Factory mode: read immutables from factory contract const allPools = loadJsonFile(jsonFile); const pools = startAt > 1 ? allPools.slice(startAt - 1) : allPools; if (startAt > 1 && pools.length === 0) { console.error(`Error: --start-at ${startAt} exceeds pool count (${allPools.length})`); process.exit(1); } - const poolType = pools[0].poolType; + const poolType = pools[0].poolType as string; + const module = CREATION_MODULES[poolType]; + console.log( `Loaded ${allPools.length} pool configuration(s) (poolType: ${poolType})${startAt > 1 ? `, processing from index ${startAt} (${pools.length} remaining)` : ""}`, ); console.log(""); - // Read factory immutables once console.log("Reading factory immutables..."); - const factoryImmutables = await readFactoryImmutables(provider, factoryAddress!, poolType); + const factoryImmutables = await module.readFactoryImmutables(provider, factoryAddress!); console.log(`POOL_MANAGER: ${factoryImmutables.poolManager}`); - if (factoryImmutables.fluidDexReservesResolver) { - console.log(`FLUID_DEX_RESOLVER: ${factoryImmutables.fluidDexReservesResolver}`); - } - if (factoryImmutables.fluidLiquidity) { - console.log(`FLUID_LIQUIDITY: ${factoryImmutables.fluidLiquidity}`); - } - if (factoryImmutables.fluidDexLite) { - console.log(`FLUID_DEX_LITE: ${factoryImmutables.fluidDexLite}`); - } - if (factoryImmutables.fluidDexLiteResolver) { - console.log(`FLUID_DEX_LITE_RESOLVER: ${factoryImmutables.fluidDexLiteResolver}`); + for (const [key, val] of Object.entries(factoryImmutables)) { + if (key !== "poolManager" && val) console.log(`${key}: ${val}`); } console.log(""); - const protocolId = getProtocolId(poolType); for (let j = 0; j < pools.length; j++) { const i = startAt - 1 + j; const poolConfig = pools[j]; + console.log(`\n--- Processing Pool ${i + 1}/${allPools.length} ---`); try { - // Encode constructor arguments - const constructorArgs = encodeConstructorArgs(poolConfig, poolType, factoryImmutables); - - // Mine salt using factory address as deployer - const salt = await mineSalt(constructorArgs, protocolId, factoryAddress!, false, jobs); - - // Create pool via factory + const constructorArgs = module.encodeConstructorArgs(poolConfig, factoryImmutables); + const salt = await mineSalt(constructorArgs, module.protocolId, factoryAddress!, false, jobs); const result = await createPool(signer, factoryAddress!, poolConfig, poolType, salt); - if (registryDir && result.hookAddress && result.hookAddress.length > 0) { - const poolKeys = buildPoolKeys(poolConfig, poolType, result.hookAddress); + if (registryDir && result.hookAddress) { + const poolKeys = module.buildPoolKeys(poolConfig, result.hookAddress); if (poolKeys.length > 0) { appendToRegistryFile(registryDir, poolType, { poolKeys, metadata: { - externalPool: getExternalPool(poolConfig, poolType), + externalPool: module.getExternalPool(poolConfig), hookAddress: result.hookAddress, txHash: result.txHash, blockNumber: result.blockNumber, @@ -1271,14 +769,9 @@ async function main() { if (error instanceof Error) { console.error(error.message); const execErr = error as { stdout?: string; stderr?: string }; - if (execErr.stdout) { - console.error("\n--- Forge stdout ---\n", execErr.stdout); - } - if (execErr.stderr) { - console.error("\n--- Forge stderr ---\n", execErr.stderr); - } + if (execErr.stdout) console.error("\n--- Forge stdout ---\n", execErr.stdout); + if (execErr.stderr) console.error("\n--- Forge stderr ---\n", execErr.stderr); } - // Continue with next pool continue; } } @@ -1287,7 +780,6 @@ async function main() { console.log("\n=== Done ==="); } -// Run main function main().catch((error) => { console.error("Fatal error:", error); process.exit(1); diff --git a/aggregator-hooks/tsconfig.json b/aggregator-hooks/tsconfig.json index c4a8f8ad..f994a21a 100644 --- a/aggregator-hooks/tsconfig.json +++ b/aggregator-hooks/tsconfig.json @@ -19,6 +19,6 @@ "declarationMap": true, "sourceMap": true }, - "include": ["src/**/*.ts", "historical/**/*.ts", "polling/**/*.ts"], + "include": ["src/**/*.ts", "creation-modules/*.ts", "historical/**/*.ts", "polling/**/*.ts"], "exclude": ["node_modules", "dist"] } diff --git a/mine_hook.sh b/mine_hook.sh index f0ad9bf8..02a4fa20 100644 --- a/mine_hook.sh +++ b/mine_hook.sh @@ -92,7 +92,7 @@ for ((i=0; i "$FORGE_OUTPUT" 2>&1 & + SALT_OFFSET=$OFFSET CONSTRUCTOR_ARGS=$CONSTRUCTOR_ARGS PROTOCOL_ID=$PROTOCOL_ID DEPLOYER=$DEPLOYER_ADDRESS forge script lib/v4-hooks-public/script/MineAggregatorHook.s.sol:MineAggregatorHookScript --gas-limit 30000000000 $VERBOSE_FLAG > "$FORGE_OUTPUT" 2>&1 & FORGE_PID=$! # Heartbeat: update same line every 15s so user knows it's still running From ad32e89e1af84aa1cfbf71cb688cd5c2432cb11d Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Thu, 26 Feb 2026 11:42:14 -0500 Subject: [PATCH 03/21] rm pk from cli --- aggregator-hooks/README.md | 4 ++++ aggregator-hooks/historical/FluidDexT1.ts | 2 +- aggregator-hooks/src/createPools.ts | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/aggregator-hooks/README.md b/aggregator-hooks/README.md index ada9e645..de24dbad 100644 --- a/aggregator-hooks/README.md +++ b/aggregator-hooks/README.md @@ -133,6 +133,10 @@ npx tsx src/createPools.ts detected/1/fluiddexlite-pools-curated.json 0xFactoryA | `RPC_URL` or `RPC_URL_` | RPC endpoint (use `RPC_URL_1` etc. when `--chain-id` is set) | | `PRIVATE_KEY` | Signing key for transactions (required even with `--dry-run`) | +### Security + +- `PRIVATE_KEY` is passed to forge via the process environment only (not the command line). + ### Dry run and verbose Use `--dry-run` to simulate without broadcasting: diff --git a/aggregator-hooks/historical/FluidDexT1.ts b/aggregator-hooks/historical/FluidDexT1.ts index 54a0d645..fe8166ef 100644 --- a/aggregator-hooks/historical/FluidDexT1.ts +++ b/aggregator-hooks/historical/FluidDexT1.ts @@ -103,7 +103,7 @@ function orderCurrencies(token0: Address, token1: Address): [Address, Address] { const mapped = [toUniswapV4Currency(token0), toUniswapV4Currency(token1)].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()), ); - return [mapped[0], mapped[1]]; + return [mapped[0]!, mapped[1]!]; } async function main() { diff --git a/aggregator-hooks/src/createPools.ts b/aggregator-hooks/src/createPools.ts index e6d48a82..5e819c1a 100644 --- a/aggregator-hooks/src/createPools.ts +++ b/aggregator-hooks/src/createPools.ts @@ -493,7 +493,8 @@ function selfDeployPool( ...module.buildSelfDeployEnvVars(poolConfig, immutables), }; - const envString = Object.entries(envVars) + const { PRIVATE_KEY: _pk, ...publicEnvVars } = envVars; + const envString = Object.entries(publicEnvVars) .map(([key, value]) => `${key}="${value}"`) .join(" "); From d9586067d04c428078e4b04c1a07ba170fc4bc74 Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Thu, 26 Feb 2026 11:43:54 -0500 Subject: [PATCH 04/21] precommit prettier --- aggregator-hooks/README.md | 89 +- aggregator-hooks/package-lock.json | 1416 +++++++++++++-------------- aggregator-hooks/package.json | 40 +- aggregator-hooks/src/createPools.ts | 16 +- aggregator-hooks/tsconfig.json | 42 +- script/SelfCreateHook.s.sol | 103 +- 6 files changed, 858 insertions(+), 848 deletions(-) diff --git a/aggregator-hooks/README.md b/aggregator-hooks/README.md index de24dbad..dd9f9c2d 100644 --- a/aggregator-hooks/README.md +++ b/aggregator-hooks/README.md @@ -10,16 +10,16 @@ Aggregator Hook factories should be deployed before running any of these scripts ### Historical discovery (one-time full scrape) -| Script | Description | -|--------|-------------| -| `historical/FluidDexLite.ts` | Scrape LogInitialize events from FluidDexLite | -| `historical/FluidDexT1.ts` | Scrape LogDexDeployed events from FluidDexFactory | +| Script | Description | +| ---------------------------- | ----------------------------------------------------------------- | +| `historical/FluidDexLite.ts` | Scrape LogInitialize events from FluidDexLite | +| `historical/FluidDexT1.ts` | Scrape LogDexDeployed events from FluidDexFactory | | `historical/StableSwapNG.ts` | Enumerate pool_count + pool_list from Curve StableSwap-NG factory | ### Pool creation -| Script | Description | -|--------|-------------| +| Script | Description | +| -------------------- | ---------------------------------------------------- | | `src/createPools.ts` | Create Uniswap v4 hooks from discovered pool configs | --- @@ -30,11 +30,11 @@ All discovery scripts use chain-ID-suffixed env vars. Use `VAR_` (e.g. ### By script -| Script | Required | Optional | -|--------|----------|----------| -| **fluiddexlite** | `RPC_URL` | `DEX_LITE_RESOLVER_ADDRESS` (default mainnet resolver) | -| **fluiddext1** | `RPC_URL`, `FLUID_DEX_RESOLVER` | `FLUID_DEX_FACTORY`, `FACTORY_ADDRESS`, `RPS`, `CONCURRENCY` | -| **stableswapng** | `RPC_URL` | `FACTORY_ADDRESS`, `RPS`, `CONCURRENCY` | +| Script | Required | Optional | +| ---------------- | ------------------------------- | ------------------------------------------------------------ | +| **fluiddexlite** | `RPC_URL` | `DEX_LITE_RESOLVER_ADDRESS` (default mainnet resolver) | +| **fluiddext1** | `RPC_URL`, `FLUID_DEX_RESOLVER` | `FLUID_DEX_FACTORY`, `FACTORY_ADDRESS`, `RPS`, `CONCURRENCY` | +| **stableswapng** | `RPC_URL` | `FACTORY_ADDRESS`, `RPS`, `CONCURRENCY` | --- @@ -44,26 +44,26 @@ All discovery scripts require `--chain-id `. ### Common args -| Arg | Default | Description | -|-----|---------|-------------| -| `--chain-id` | (required) | Chain ID; selects env vars | -| `--output-dir` | `detected` | Output base dir; files go to `output-dir/chain-id/.json` | -| `--chunk-blocks` | 100000 | Block chunk size for getLogs | -| `--start-block` | — | Start scan from this block | +| Arg | Default | Description | +| ---------------- | ---------- | ------------------------------------------------------------------ | +| `--chain-id` | (required) | Chain ID; selects env vars | +| `--output-dir` | `detected` | Output base dir; files go to `output-dir/chain-id/.json` | +| `--chunk-blocks` | 100000 | Block chunk size for getLogs | +| `--start-block` | — | Start scan from this block | ### Discovery args -| Arg | Default | Description | -|-----|---------|-------------| -| `--end-block` | latest | End block for event scan | +| Arg | Default | Description | +| ------------- | ------- | ------------------------ | +| `--end-block` | latest | End block for event scan | ### Script-specific args -| Script | Arg | Default | Description | -|--------|-----|---------|-------------| -| fluiddext1 | `--mode` | enumerate | `logs` \| `enumerate` \| `both` | -| stableswapng | `--chunk` | 500 | pool_list batch size | -| stableswapng | `--start-index` | 0 | Start pool_list index | +| Script | Arg | Default | Description | +| ------------ | --------------- | --------- | ------------------------------- | +| fluiddext1 | `--mode` | enumerate | `logs` \| `enumerate` \| `both` | +| stableswapng | `--chunk` | 500 | pool_list batch size | +| stableswapng | `--start-index` | 0 | Start pool_list index | --- @@ -71,10 +71,10 @@ All discovery scripts require `--chain-id `. - **Output**: `{OUTPUT_DIR}/{CHAIN_ID}/{OUTPUT_FILE}.json` -| Script | Output file | -|--------|-------------| +| Script | Output file | +| ------------ | ----------------------- | | fluiddexlite | fluiddexlite-pools.json | -| fluiddext1 | fluiddext1-pools.json | +| fluiddext1 | fluiddext1-pools.json | | stableswapng | stableswapng-pools.json | --- @@ -108,18 +108,18 @@ npx tsx src/createPools.ts detected/1/fluiddexlite-pools-curated.json 0xFactoryA ### Arguments -| Arg | Required | Default | Description | -|-----|----------|---------|-------------| -| `jsonFile` | yes | — | Path to JSON file with pool configs (each must have `poolType`) | -| `factoryAddress` | yes* | — | Factory contract address (*required when not using `--self-deploy`) | -| `--self-deploy` | no | — | Deploy hooks from wallet instead of via factory | -| `--chain-id ` | no | — | Chain ID; selects `RPC_URL_` from env | -| `--registry-dir ` | no | `created-pools` | Append deployed pools to `deployed-.json` in this dir | -| `--dry-run` | no | — | Simulate forge scripts without broadcasting | -| `--verbose`, `-v` | no | — | Run forge scripts with `-vvvv` | -| `--start-at ` | no | 1 | Start at 1-based pool index (skip earlier pools). Use to resume. | -| `--jobs `, `-j ` | no | 1 | Parallel salt mining workers (1–16). Speeds up mining. | -| `--priority-gas-price ` | no | RPC default | Max priority fee per gas for EIP1559 (e.g. `3gwei`). Speeds up tx inclusion. | +| Arg | Required | Default | Description | +| ------------------------------ | -------- | --------------- | ---------------------------------------------------------------------------- | +| `jsonFile` | yes | — | Path to JSON file with pool configs (each must have `poolType`) | +| `factoryAddress` | yes\* | — | Factory contract address (\*required when not using `--self-deploy`) | +| `--self-deploy` | no | — | Deploy hooks from wallet instead of via factory | +| `--chain-id ` | no | — | Chain ID; selects `RPC_URL_` from env | +| `--registry-dir ` | no | `created-pools` | Append deployed pools to `deployed-.json` in this dir | +| `--dry-run` | no | — | Simulate forge scripts without broadcasting | +| `--verbose`, `-v` | no | — | Run forge scripts with `-vvvv` | +| `--start-at ` | no | 1 | Start at 1-based pool index (skip earlier pools). Use to resume. | +| `--jobs `, `-j ` | no | 1 | Parallel salt mining workers (1–16). Speeds up mining. | +| `--priority-gas-price ` | no | RPC default | Max priority fee per gas for EIP1559 (e.g. `3gwei`). Speeds up tx inclusion. | **Modes:** @@ -128,10 +128,10 @@ npx tsx src/createPools.ts detected/1/fluiddexlite-pools-curated.json 0xFactoryA ### Environment variables -| Env | Description | -|-----|-------------| -| `RPC_URL` or `RPC_URL_` | RPC endpoint (use `RPC_URL_1` etc. when `--chain-id` is set) | -| `PRIVATE_KEY` | Signing key for transactions (required even with `--dry-run`) | +| Env | Description | +| -------------------------------- | ------------------------------------------------------------- | +| `RPC_URL` or `RPC_URL_` | RPC endpoint (use `RPC_URL_1` etc. when `--chain-id` is set) | +| `PRIVATE_KEY` | Signing key for transactions (required even with `--dry-run`) | ### Security @@ -169,6 +169,7 @@ The `createPools` script and `mine_hook.sh` run from the **contracts/** director > **Note:** When running `createPools` via `npx tsx src/createPools.ts`, run it from **aggregator-hooks/** so it loads `aggregator-hooks/.env`. The forge scripts invoked by createPools run from contracts/ but inherit env vars from the parent process. 1. **v4-hooks-public** (aggregator-hooks branch): Already added as submodule. Ensure it's on the `aggregator-hooks` branch: + ```bash cd lib/v4-hooks-public && git fetch origin aggregator-hooks && git checkout aggregator-hooks git submodule update --init --recursive diff --git a/aggregator-hooks/package-lock.json b/aggregator-hooks/package-lock.json index fe004297..15fb1861 100644 --- a/aggregator-hooks/package-lock.json +++ b/aggregator-hooks/package-lock.json @@ -1,712 +1,712 @@ { - "name": "agg-hook-scripts", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "agg-hook-scripts", - "version": "1.0.0", - "dependencies": { - "dotenv": "^17.3.1", - "ethers": "^6.13.0" - }, - "devDependencies": { - "@types/node": "^22.10.0", - "tsx": "^4.19.2", - "typescript": "^5.7.0" - } - }, - "node_modules/@adraffy/ens-normalize": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", - "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", - "license": "MIT" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.3.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@types/node": { - "version": "22.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", - "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/aes-js": { - "version": "4.0.0-beta.5", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", - "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", - "license": "MIT" - }, - "node_modules/dotenv": { - "version": "17.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", - "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "node_modules/ethers": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", - "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/ethers-io/" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "1.10.1", - "@noble/curves": "1.2.0", - "@noble/hashes": "1.3.2", - "@types/node": "22.7.5", - "aes-js": "4.0.0-beta.5", - "tslib": "2.7.0", - "ws": "8.17.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/ethers/node_modules/@types/node": { - "version": "22.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", - "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/ethers/node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "license": "MIT" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "license": "0BSD" - }, - "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true + "name": "agg-hook-scripts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agg-hook-scripts", + "version": "1.0.0", + "dependencies": { + "dotenv": "^17.3.1", + "ethers": "^6.13.0" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "tsx": "^4.19.2", + "typescript": "^5.7.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } } - } } - } } diff --git a/aggregator-hooks/package.json b/aggregator-hooks/package.json index ecd95f28..aab8acef 100644 --- a/aggregator-hooks/package.json +++ b/aggregator-hooks/package.json @@ -1,22 +1,22 @@ { - "name": "agg-hook-scripts", - "version": "1.0.0", - "description": "Aggregator hook pool discovery and creation scripts", - "type": "module", - "scripts": { - "build": "tsc", - "stableswapng": "tsx historical/StableSwapNG.ts", - "fluiddext1": "tsx historical/FluidDexT1.ts", - "fluiddexlite": "tsx historical/FluidDexLite.ts", - "create-pools": "tsx src/createPools.ts" - }, - "devDependencies": { - "@types/node": "^22.10.0", - "tsx": "^4.19.2", - "typescript": "^5.7.0" - }, - "dependencies": { - "dotenv": "^17.3.1", - "ethers": "^6.13.0" - } + "name": "agg-hook-scripts", + "version": "1.0.0", + "description": "Aggregator hook pool discovery and creation scripts", + "type": "module", + "scripts": { + "build": "tsc", + "stableswapng": "tsx historical/StableSwapNG.ts", + "fluiddext1": "tsx historical/FluidDexT1.ts", + "fluiddexlite": "tsx historical/FluidDexLite.ts", + "create-pools": "tsx src/createPools.ts" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "tsx": "^4.19.2", + "typescript": "^5.7.0" + }, + "dependencies": { + "dotenv": "^17.3.1", + "ethers": "^6.13.0" + } } diff --git a/aggregator-hooks/src/createPools.ts b/aggregator-hooks/src/createPools.ts index 5e819c1a..b532d39a 100644 --- a/aggregator-hooks/src/createPools.ts +++ b/aggregator-hooks/src/createPools.ts @@ -113,8 +113,8 @@ function parseArgs(): ParsedArgs { const factoryAddress: Address | null = selfDeploy ? null : positionalArgs[1] - ? (ethers.getAddress(positionalArgs[1]) as Address) - : null; + ? (ethers.getAddress(positionalArgs[1]) as Address) + : null; if (selfDeploy && positionalArgs.length >= 2 && positionalArgs[1].startsWith("0x")) { console.error("Error: --self-deploy and factoryAddress are mutually exclusive"); @@ -246,7 +246,9 @@ function loadJsonFile(filePath: string): PoolConfig[] { for (let i = 1; i < pools.length; i++) { if (pools[i].poolType !== firstType) { throw new Error( - `Pool ${i + 1} has poolType "${pools[i].poolType}" but all configs must have the same poolType (first is "${firstType}")`, + `Pool ${i + 1} has poolType "${ + pools[i].poolType + }" but all configs must have the same poolType (first is "${firstType}")`, ); } } @@ -640,7 +642,9 @@ async function main() { process.exit(1); } console.log( - `Loaded ${allPools.length} pool configuration(s)${startAt > 1 ? `, processing from index ${startAt} (${pools.length} remaining)` : ""}`, + `Loaded ${allPools.length} pool configuration(s)${ + startAt > 1 ? `, processing from index ${startAt} (${pools.length} remaining)` : "" + }`, ); console.log(""); @@ -727,7 +731,9 @@ async function main() { const module = CREATION_MODULES[poolType]; console.log( - `Loaded ${allPools.length} pool configuration(s) (poolType: ${poolType})${startAt > 1 ? `, processing from index ${startAt} (${pools.length} remaining)` : ""}`, + `Loaded ${allPools.length} pool configuration(s) (poolType: ${poolType})${ + startAt > 1 ? `, processing from index ${startAt} (${pools.length} remaining)` : "" + }`, ); console.log(""); diff --git a/aggregator-hooks/tsconfig.json b/aggregator-hooks/tsconfig.json index f994a21a..69e6f9f4 100644 --- a/aggregator-hooks/tsconfig.json +++ b/aggregator-hooks/tsconfig.json @@ -1,24 +1,24 @@ { - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "baseUrl": ".", - "paths": { - "@src/cli": ["src/cli.ts"] + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "baseUrl": ".", + "paths": { + "@src/cli": ["src/cli.ts"] + }, + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true }, - "lib": ["ES2022"], - "outDir": "dist", - "rootDir": ".", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true - }, - "include": ["src/**/*.ts", "creation-modules/*.ts", "historical/**/*.ts", "polling/**/*.ts"], - "exclude": ["node_modules", "dist"] + "include": ["src/**/*.ts", "creation-modules/*.ts", "historical/**/*.ts", "polling/**/*.ts"], + "exclude": ["node_modules", "dist"] } diff --git a/script/SelfCreateHook.s.sol b/script/SelfCreateHook.s.sol index 90698b5d..71814e40 100644 --- a/script/SelfCreateHook.s.sol +++ b/script/SelfCreateHook.s.sol @@ -1,23 +1,27 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import "forge-std/Script.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; - -import {StableSwapAggregator} from "@aggregator-hooks/implementations/StableSwap/StableSwapAggregator.sol"; -import {StableSwapNGAggregator} from "@aggregator-hooks/implementations/StableSwapNG/StableSwapNGAggregator.sol"; -import {FluidDexT1Aggregator} from "@aggregator-hooks/implementations/FluidDexT1/FluidDexT1Aggregator.sol"; -import {FluidDexLiteAggregator} from "@aggregator-hooks/implementations/FluidDexLite/FluidDexLiteAggregator.sol"; - -import {ICurveStableSwap} from "@aggregator-hooks/implementations/StableSwap/interfaces/IStableSwap.sol"; -import {ICurveStableSwapNG} from "@aggregator-hooks/implementations/StableSwapNG/interfaces/IStableSwapNG.sol"; -import {IFluidDexT1} from "@aggregator-hooks/implementations/FluidDexT1/interfaces/IFluidDexT1.sol"; -import {IFluidDexReservesResolver} from "@aggregator-hooks/implementations/FluidDexT1/interfaces/IFluidDexReservesResolver.sol"; -import {IFluidDexLite} from "@aggregator-hooks/implementations/FluidDexLite/interfaces/IFluidDexLite.sol"; -import {IFluidDexLiteResolver} from "@aggregator-hooks/implementations/FluidDexLite/interfaces/IFluidDexLiteResolver.sol"; +import {IHooks} from '@uniswap/v4-core/src/interfaces/IHooks.sol'; +import {IPoolManager} from '@uniswap/v4-core/src/interfaces/IPoolManager.sol'; +import {Currency} from '@uniswap/v4-core/src/types/Currency.sol'; +import {PoolKey} from '@uniswap/v4-core/src/types/PoolKey.sol'; +import 'forge-std/Script.sol'; + +import {FluidDexLiteAggregator} from '@aggregator-hooks/implementations/FluidDexLite/FluidDexLiteAggregator.sol'; +import {FluidDexT1Aggregator} from '@aggregator-hooks/implementations/FluidDexT1/FluidDexT1Aggregator.sol'; +import {StableSwapAggregator} from '@aggregator-hooks/implementations/StableSwap/StableSwapAggregator.sol'; +import {StableSwapNGAggregator} from '@aggregator-hooks/implementations/StableSwapNG/StableSwapNGAggregator.sol'; + +import {IFluidDexLite} from '@aggregator-hooks/implementations/FluidDexLite/interfaces/IFluidDexLite.sol'; +import { + IFluidDexLiteResolver +} from '@aggregator-hooks/implementations/FluidDexLite/interfaces/IFluidDexLiteResolver.sol'; +import { + IFluidDexReservesResolver +} from '@aggregator-hooks/implementations/FluidDexT1/interfaces/IFluidDexReservesResolver.sol'; +import {IFluidDexT1} from '@aggregator-hooks/implementations/FluidDexT1/interfaces/IFluidDexT1.sol'; +import {ICurveStableSwap} from '@aggregator-hooks/implementations/StableSwap/interfaces/IStableSwap.sol'; +import {ICurveStableSwapNG} from '@aggregator-hooks/implementations/StableSwapNG/interfaces/IStableSwapNG.sol'; /// @notice Self-deploys an aggregator hook and initializes the pool without using a factory /// @dev Broadcasts from PRIVATE_KEY and deploys using CREATE2 with the provided salt @@ -29,16 +33,16 @@ contract SelfCreateHookScript is Script { function run() public { // Load private key for broadcasting - uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + uint256 deployerPrivateKey = vm.envUint('PRIVATE_KEY'); // Common parameters - uint8 protocolId = uint8(vm.envUint("PROTOCOL_ID")); - bytes32 salt = vm.envBytes32("SALT"); - address poolManager = vm.envAddress("POOL_MANAGER"); + uint8 protocolId = uint8(vm.envUint('PROTOCOL_ID')); + bytes32 salt = vm.envBytes32('SALT'); + address poolManager = vm.envAddress('POOL_MANAGER'); - uint24 fee = uint24(vm.envUint("FEE")); - int24 tickSpacing = int24(int256(vm.envUint("TICK_SPACING"))); - uint160 sqrtPriceX96 = uint160(vm.envUint("SQRT_PRICE_X96")); + uint24 fee = uint24(vm.envUint('FEE')); + int24 tickSpacing = int24(int256(vm.envUint('TICK_SPACING'))); + uint160 sqrtPriceX96 = uint160(vm.envUint('SQRT_PRICE_X96')); address hookAddress; @@ -53,16 +57,15 @@ contract SelfCreateHookScript is Script { } else if (protocolId == ID_FLUIDDEXLITE) { hookAddress = _deployFluidDexLite(salt, poolManager); } else { - revert("Invalid protocol ID"); + revert('Invalid protocol ID'); } // Initialize one Uniswap pool per token pair. TOKENS is comma-separated (2+ for fluid, 2+ for stableswap). - address[] memory tokens = vm.envAddress("TOKENS", ","); - require(tokens.length >= 2, "TOKENS must have at least 2 addresses"); + address[] memory tokens = vm.envAddress('TOKENS', ','); + require(tokens.length >= 2, 'TOKENS must have at least 2 addresses'); for (uint256 i = 0; i < tokens.length; i++) { for (uint256 j = i + 1; j < tokens.length; j++) { - (address c0, address c1) = - tokens[i] < tokens[j] ? (tokens[i], tokens[j]) : (tokens[j], tokens[i]); + (address c0, address c1) = tokens[i] < tokens[j] ? (tokens[i], tokens[j]) : (tokens[j], tokens[i]); PoolKey memory poolKey = PoolKey({ currency0: Currency.wrap(c0), currency1: Currency.wrap(c1), @@ -71,31 +74,31 @@ contract SelfCreateHookScript is Script { hooks: IHooks(hookAddress) }); IPoolManager(poolManager).initialize(poolKey, sqrtPriceX96); - console.log("Initialized pool:", c0, c1); + console.log('Initialized pool:', c0, c1); } } vm.stopBroadcast(); // Output results - console.log("=== Self-Deploy Hook Results ==="); - console.log("Hook Address:", hookAddress); - console.log("Salt:", vm.toString(salt)); - console.log("Protocol ID:", protocolId); - console.log("Pool Manager:", poolManager); - console.log("Tokens:"); - console.log("Tokens length:", tokens.length); + console.log('=== Self-Deploy Hook Results ==='); + console.log('Hook Address:', hookAddress); + console.log('Salt:', vm.toString(salt)); + console.log('Protocol ID:', protocolId); + console.log('Pool Manager:', poolManager); + console.log('Tokens:'); + console.log('Tokens length:', tokens.length); for (uint256 i = 0; i < tokens.length; i++) { - console.log("Token:", tokens[i]); + console.log('Token:', tokens[i]); } - console.log("Fee:", fee); - console.log("Tick Spacing:", uint24(tickSpacing)); - console.log("Sqrt Price X96:", sqrtPriceX96); - console.log("================================"); + console.log('Fee:', fee); + console.log('Tick Spacing:', uint24(tickSpacing)); + console.log('Sqrt Price X96:', sqrtPriceX96); + console.log('================================'); } function _deployStableSwap(bytes32 salt, address poolManager) internal returns (address) { - address curvePool = vm.envAddress("CURVE_POOL"); + address curvePool = vm.envAddress('CURVE_POOL'); StableSwapAggregator hook = new StableSwapAggregator{salt: salt}(IPoolManager(poolManager), ICurveStableSwap(curvePool)); @@ -104,7 +107,7 @@ contract SelfCreateHookScript is Script { } function _deployStableSwapNG(bytes32 salt, address poolManager) internal returns (address) { - address curvePool = vm.envAddress("CURVE_POOL"); + address curvePool = vm.envAddress('CURVE_POOL'); StableSwapNGAggregator hook = new StableSwapNGAggregator{salt: salt}(IPoolManager(poolManager), ICurveStableSwapNG(curvePool)); @@ -113,9 +116,9 @@ contract SelfCreateHookScript is Script { } function _deployFluidDexT1(bytes32 salt, address poolManager) internal returns (address) { - address fluidPool = vm.envAddress("FLUID_POOL"); - address fluidDexReservesResolver = vm.envAddress("FLUID_DEX_RESOLVER"); - address fluidLiquidity = vm.envAddress("FLUID_LIQUIDITY"); + address fluidPool = vm.envAddress('FLUID_POOL'); + address fluidDexReservesResolver = vm.envAddress('FLUID_DEX_RESOLVER'); + address fluidLiquidity = vm.envAddress('FLUID_LIQUIDITY'); FluidDexT1Aggregator hook = new FluidDexT1Aggregator{salt: salt}( IPoolManager(poolManager), @@ -128,9 +131,9 @@ contract SelfCreateHookScript is Script { } function _deployFluidDexLite(bytes32 salt, address poolManager) internal returns (address) { - address fluidDexLite = vm.envAddress("FLUID_DEX_LITE"); - address fluidDexLiteResolver = vm.envAddress("FLUID_DEX_LITE_RESOLVER"); - bytes32 dexSalt = vm.envBytes32("DEX_SALT"); + address fluidDexLite = vm.envAddress('FLUID_DEX_LITE'); + address fluidDexLiteResolver = vm.envAddress('FLUID_DEX_LITE_RESOLVER'); + bytes32 dexSalt = vm.envBytes32('DEX_SALT'); FluidDexLiteAggregator hook = new FluidDexLiteAggregator{salt: salt}( IPoolManager(poolManager), IFluidDexLite(fluidDexLite), IFluidDexLiteResolver(fluidDexLiteResolver), dexSalt From 97a89acc63dc6b4901b6188f0fd705087eb2e691 Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Fri, 27 Feb 2026 13:08:27 -0500 Subject: [PATCH 05/21] address pr comments --- .../abis/FluidDexLiteFactory.json | 6 + .../abis/FluidDexLiteResolver.json | 4 + aggregator-hooks/abis/FluidDexT1Factory.json | 6 + .../abis/FluidDexT1HistoricalFactory.json | 6 + aggregator-hooks/abis/FluidDexT1Resolver.json | 1 + .../abis/StableSwapNGHistoricalFactory.json | 7 + aggregator-hooks/abis/StableswapFactory.json | 4 + aggregator-hooks/abis/index.ts | 14 + .../creation-modules/FluidDexLite.ts | 12 +- .../creation-modules/FluidDexT1.ts | 12 +- .../creation-modules/StableSwap.ts | 10 +- .../creation-modules/StableSwapNG.ts | 10 +- aggregator-hooks/historical/FluidDexLite.ts | 11 +- aggregator-hooks/historical/FluidDexT1.ts | 18 +- aggregator-hooks/historical/StableSwapNG.ts | 11 +- aggregator-hooks/package.json | 2 +- aggregator-hooks/src/createPools.ts | 295 +++++++++--------- aggregator-hooks/src/logger.ts | 77 +++++ aggregator-hooks/tsconfig.json | 2 +- mine_hook.sh | 7 + 20 files changed, 303 insertions(+), 212 deletions(-) create mode 100644 aggregator-hooks/abis/FluidDexLiteFactory.json create mode 100644 aggregator-hooks/abis/FluidDexLiteResolver.json create mode 100644 aggregator-hooks/abis/FluidDexT1Factory.json create mode 100644 aggregator-hooks/abis/FluidDexT1HistoricalFactory.json create mode 100644 aggregator-hooks/abis/FluidDexT1Resolver.json create mode 100644 aggregator-hooks/abis/StableSwapNGHistoricalFactory.json create mode 100644 aggregator-hooks/abis/StableswapFactory.json create mode 100644 aggregator-hooks/abis/index.ts create mode 100644 aggregator-hooks/src/logger.ts diff --git a/aggregator-hooks/abis/FluidDexLiteFactory.json b/aggregator-hooks/abis/FluidDexLiteFactory.json new file mode 100644 index 00000000..e45a0331 --- /dev/null +++ b/aggregator-hooks/abis/FluidDexLiteFactory.json @@ -0,0 +1,6 @@ +[ + "function POOL_MANAGER() external view returns (address)", + "function FLUID_DEX_LITE() external view returns (address)", + "function FLUID_DEX_LITE_RESOLVER() external view returns (address)", + "function createPool(bytes32 salt, bytes32 dexSalt, address currency0, address currency1, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)" +] diff --git a/aggregator-hooks/abis/FluidDexLiteResolver.json b/aggregator-hooks/abis/FluidDexLiteResolver.json new file mode 100644 index 00000000..7a5011b3 --- /dev/null +++ b/aggregator-hooks/abis/FluidDexLiteResolver.json @@ -0,0 +1,4 @@ +[ + "function getAllDexes() external view returns (tuple(address token0, address token1, bytes32 salt)[] memory)", + "function getDexState(tuple(address token0, address token1, bytes32 salt) dexKey) external view returns (tuple(tuple(uint256 fee, uint256 revenueCut, uint256 rebalancingStatus, bool isCenterPriceShiftActive, uint256 centerPrice, address centerPriceAddress, bool isRangePercentShiftActive, uint256 upperRangePercent, uint256 lowerRangePercent, bool isThresholdPercentShiftActive, uint256 upperShiftThresholdPercent, uint256 lowerShiftThresholdPercent, uint256 token0Decimals, uint256 token1Decimals, uint256 totalToken0AdjustedAmount, uint256 totalToken1AdjustedAmount) dexVariables, tuple(uint256 lastInteractionTimestamp, uint256 rebalancingShiftingTime, uint256 maxCenterPrice, uint256 minCenterPrice, uint256 shiftPercentage, uint256 centerPriceShiftingTime, uint256 startTimestamp) centerPriceShift, tuple(uint256 oldUpperRangePercent, uint256 oldLowerRangePercent, uint256 shiftingTime, uint256 startTimestamp) rangeShift, tuple(uint256 oldUpperThresholdPercent, uint256 oldLowerThresholdPercent, uint256 shiftingTime, uint256 startTimestamp) thresholdShift) dexState)" +] diff --git a/aggregator-hooks/abis/FluidDexT1Factory.json b/aggregator-hooks/abis/FluidDexT1Factory.json new file mode 100644 index 00000000..5638f52d --- /dev/null +++ b/aggregator-hooks/abis/FluidDexT1Factory.json @@ -0,0 +1,6 @@ +[ + "function POOL_MANAGER() external view returns (address)", + "function fluidDexReservesResolver() external view returns (address)", + "function FLUID_LIQUIDITY() external view returns (address)", + "function createPool(bytes32 salt, address fluidPool, address currency0, address currency1, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)" +] diff --git a/aggregator-hooks/abis/FluidDexT1HistoricalFactory.json b/aggregator-hooks/abis/FluidDexT1HistoricalFactory.json new file mode 100644 index 00000000..97842aaf --- /dev/null +++ b/aggregator-hooks/abis/FluidDexT1HistoricalFactory.json @@ -0,0 +1,6 @@ +[ + "event LogDexDeployed(address indexed dex, uint256 indexed dexId)", + "function totalDexes() external view returns (uint256)", + "function getDexAddress(uint256 dexId) public view returns (address)", + "function isDex(address dex) public view returns (bool)" +] diff --git a/aggregator-hooks/abis/FluidDexT1Resolver.json b/aggregator-hooks/abis/FluidDexT1Resolver.json new file mode 100644 index 00000000..627bd185 --- /dev/null +++ b/aggregator-hooks/abis/FluidDexT1Resolver.json @@ -0,0 +1 @@ +["function getPoolTokens(address pool) external view returns (address token0, address token1)"] diff --git a/aggregator-hooks/abis/StableSwapNGHistoricalFactory.json b/aggregator-hooks/abis/StableSwapNGHistoricalFactory.json new file mode 100644 index 00000000..9848faab --- /dev/null +++ b/aggregator-hooks/abis/StableSwapNGHistoricalFactory.json @@ -0,0 +1,7 @@ +[ + "function pool_count() view returns (uint256)", + "function pool_list(uint256) view returns (address)", + "function get_n_coins(address) view returns (uint256)", + "function get_coins(address) view returns (address[])", + "function get_base_pool(address) view returns (address)" +] diff --git a/aggregator-hooks/abis/StableswapFactory.json b/aggregator-hooks/abis/StableswapFactory.json new file mode 100644 index 00000000..4d2fd014 --- /dev/null +++ b/aggregator-hooks/abis/StableswapFactory.json @@ -0,0 +1,4 @@ +[ + "function POOL_MANAGER() external view returns (address)", + "function createPool(bytes32 salt, address curvePool, address[] calldata tokens, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)" +] diff --git a/aggregator-hooks/abis/index.ts b/aggregator-hooks/abis/index.ts new file mode 100644 index 00000000..2207459b --- /dev/null +++ b/aggregator-hooks/abis/index.ts @@ -0,0 +1,14 @@ +/** + * Load ABIs via createRequire (avoids import attributes for Prettier compatibility). + */ +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); + +export const STABLESWAP_FACTORY_ABI: string[] = [...require("./StableswapFactory.json")]; +export const FLUIDDEXT1_FACTORY_ABI: string[] = [...require("./FluidDexT1Factory.json")]; +export const FLUIDDEXLITE_FACTORY_ABI: string[] = [...require("./FluidDexLiteFactory.json")]; +export const FLUIDDEXT1_HISTORICAL_FACTORY_ABI = require("./FluidDexT1HistoricalFactory.json") as readonly string[]; +export const FLUIDDEXT1_RESOLVER_ABI = require("./FluidDexT1Resolver.json") as readonly string[]; +export const STABLESWAPNG_HISTORICAL_FACTORY_ABI = require("./StableSwapNGHistoricalFactory.json") as readonly string[]; +export const FLUIDDEXLITE_RESOLVER_ABI = require("./FluidDexLiteResolver.json") as readonly string[]; diff --git a/aggregator-hooks/creation-modules/FluidDexLite.ts b/aggregator-hooks/creation-modules/FluidDexLite.ts index 0e8e1978..f124e4c7 100644 --- a/aggregator-hooks/creation-modules/FluidDexLite.ts +++ b/aggregator-hooks/creation-modules/FluidDexLite.ts @@ -3,6 +3,7 @@ */ import { ethers } from "ethers"; import { getEnvForChain, mustEnvForChain } from "../src/cli.js"; +import { FLUIDDEXLITE_FACTORY_ABI } from "../abis/index.js"; import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables } from "./types.js"; export interface FluidDexLitePoolConfig { @@ -15,13 +16,6 @@ export interface FluidDexLitePoolConfig { sqrtPriceX96: bigint | null; } -const FACTORY_ABI = [ - "function POOL_MANAGER() external view returns (address)", - "function FLUID_DEX_LITE() external view returns (address)", - "function FLUID_DEX_LITE_RESOLVER() external view returns (address)", - "function createPool(bytes32 salt, bytes32 dexSalt, address currency0, address currency1, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)", -]; - const PROTOCOL_ID = 0xf3; const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); @@ -29,7 +23,7 @@ const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase export const fluiddexliteModule: CreationModule = { poolType: "fluiddexlite", protocolId: PROTOCOL_ID, - factoryAbi: FACTORY_ABI, + factoryAbi: FLUIDDEXLITE_FACTORY_ABI, getHookParams(config) { return { @@ -75,7 +69,7 @@ export const fluiddexliteModule: CreationModule = { }, async readFactoryImmutables(provider, factoryAddress) { - const factory = new ethers.Contract(factoryAddress, FACTORY_ABI, provider); + const factory = new ethers.Contract(factoryAddress, FLUIDDEXLITE_FACTORY_ABI, provider); const [poolManager, fluidDexLite, fluidDexLiteResolver] = await Promise.all([ factory.POOL_MANAGER(), factory.FLUID_DEX_LITE(), diff --git a/aggregator-hooks/creation-modules/FluidDexT1.ts b/aggregator-hooks/creation-modules/FluidDexT1.ts index ff3fe0d7..6f70343e 100644 --- a/aggregator-hooks/creation-modules/FluidDexT1.ts +++ b/aggregator-hooks/creation-modules/FluidDexT1.ts @@ -3,6 +3,7 @@ */ import { ethers } from "ethers"; import { mustEnvForChain } from "../src/cli.js"; +import { FLUIDDEXT1_FACTORY_ABI } from "../abis/index.js"; import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables } from "./types.js"; export interface FluidDexT1PoolConfig { @@ -15,13 +16,6 @@ export interface FluidDexT1PoolConfig { sqrtPriceX96: bigint | null; } -const FACTORY_ABI = [ - "function POOL_MANAGER() external view returns (address)", - "function fluidDexReservesResolver() external view returns (address)", - "function FLUID_LIQUIDITY() external view returns (address)", - "function createPool(bytes32 salt, address fluidPool, address currency0, address currency1, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)", -]; - const PROTOCOL_ID = 0xf1; const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); @@ -29,7 +23,7 @@ const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase export const fluiddext1Module: CreationModule = { poolType: "fluiddext1", protocolId: PROTOCOL_ID, - factoryAbi: FACTORY_ABI, + factoryAbi: FLUIDDEXT1_FACTORY_ABI, getHookParams(config) { return { @@ -66,7 +60,7 @@ export const fluiddext1Module: CreationModule = { }, async readFactoryImmutables(provider, factoryAddress) { - const factory = new ethers.Contract(factoryAddress, FACTORY_ABI, provider); + const factory = new ethers.Contract(factoryAddress, FLUIDDEXT1_FACTORY_ABI, provider); const [poolManager, fluidDexReservesResolver, fluidLiquidity] = await Promise.all([ factory.POOL_MANAGER(), factory.fluidDexReservesResolver(), diff --git a/aggregator-hooks/creation-modules/StableSwap.ts b/aggregator-hooks/creation-modules/StableSwap.ts index f0205df2..a9f14072 100644 --- a/aggregator-hooks/creation-modules/StableSwap.ts +++ b/aggregator-hooks/creation-modules/StableSwap.ts @@ -3,6 +3,7 @@ */ import { ethers } from "ethers"; import { mustEnvForChain } from "../src/cli.js"; +import { STABLESWAP_FACTORY_ABI } from "../abis/index.js"; import { DEFAULT_SQRT_PRICE_X96, type Address, @@ -20,11 +21,6 @@ export interface StableSwapPoolConfig { sqrtPriceX96: bigint | null; } -const FACTORY_ABI = [ - "function POOL_MANAGER() external view returns (address)", - "function createPool(bytes32 salt, address curvePool, address[] calldata tokens, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)", -]; - const PROTOCOL_ID = 0xc1; const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); @@ -32,7 +28,7 @@ const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase export const stableswapModule: CreationModule = { poolType: "stableswap", protocolId: PROTOCOL_ID, - factoryAbi: FACTORY_ABI, + factoryAbi: STABLESWAP_FACTORY_ABI, getHookParams(config) { return { @@ -72,7 +68,7 @@ export const stableswapModule: CreationModule = { }, async readFactoryImmutables(provider, factoryAddress) { - const factory = new ethers.Contract(factoryAddress, FACTORY_ABI, provider); + const factory = new ethers.Contract(factoryAddress, STABLESWAP_FACTORY_ABI, provider); const poolManager = await factory.POOL_MANAGER(); return { poolManager: poolManager as Address }; }, diff --git a/aggregator-hooks/creation-modules/StableSwapNG.ts b/aggregator-hooks/creation-modules/StableSwapNG.ts index 4451d7cd..c924ce9b 100644 --- a/aggregator-hooks/creation-modules/StableSwapNG.ts +++ b/aggregator-hooks/creation-modules/StableSwapNG.ts @@ -3,6 +3,7 @@ */ import { ethers } from "ethers"; import { mustEnvForChain } from "../src/cli.js"; +import { STABLESWAP_FACTORY_ABI } from "../abis/index.js"; import { DEFAULT_SQRT_PRICE_X96, type Address, @@ -20,11 +21,6 @@ export interface StableSwapNGPoolConfig { sqrtPriceX96: bigint | null; } -const FACTORY_ABI = [ - "function POOL_MANAGER() external view returns (address)", - "function createPool(bytes32 salt, address curvePool, address[] calldata tokens, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)", -]; - const PROTOCOL_ID = 0xc2; const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); @@ -32,7 +28,7 @@ const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase export const stableswapngModule: CreationModule = { poolType: "stableswapng", protocolId: PROTOCOL_ID, - factoryAbi: FACTORY_ABI, + factoryAbi: STABLESWAP_FACTORY_ABI, getHookParams(config) { return { @@ -72,7 +68,7 @@ export const stableswapngModule: CreationModule = { }, async readFactoryImmutables(provider, factoryAddress) { - const factory = new ethers.Contract(factoryAddress, FACTORY_ABI, provider); + const factory = new ethers.Contract(factoryAddress, STABLESWAP_FACTORY_ABI, provider); const poolManager = await factory.POOL_MANAGER(); return { poolManager: poolManager as Address }; }, diff --git a/aggregator-hooks/historical/FluidDexLite.ts b/aggregator-hooks/historical/FluidDexLite.ts index 1b0247b6..e2900607 100644 --- a/aggregator-hooks/historical/FluidDexLite.ts +++ b/aggregator-hooks/historical/FluidDexLite.ts @@ -26,6 +26,7 @@ import fs from "node:fs"; import path from "node:path"; import { ethers } from "ethers"; import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; +import { FLUIDDEXLITE_RESOLVER_ABI } from "../abis/index.js"; import type { Address } from "../creation-modules/types.js"; const OUTPUT_FILE = "fluiddexlite-pools.json"; @@ -50,11 +51,6 @@ type CreatePoolsFluidLiteConfig = { sqrtPriceX96: bigint | null; }; -const RESOLVER_ABI = [ - "function getAllDexes() external view returns (tuple(address token0, address token1, bytes32 salt)[] memory)", - "function getDexState(tuple(address token0, address token1, bytes32 salt) dexKey) external view returns (tuple(tuple(uint256 fee, uint256 revenueCut, uint256 rebalancingStatus, bool isCenterPriceShiftActive, uint256 centerPrice, address centerPriceAddress, bool isRangePercentShiftActive, uint256 upperRangePercent, uint256 lowerRangePercent, bool isThresholdPercentShiftActive, uint256 upperShiftThresholdPercent, uint256 lowerShiftThresholdPercent, uint256 token0Decimals, uint256 token1Decimals, uint256 totalToken0AdjustedAmount, uint256 totalToken1AdjustedAmount) dexVariables, tuple(uint256 lastInteractionTimestamp, uint256 rebalancingShiftingTime, uint256 maxCenterPrice, uint256 minCenterPrice, uint256 shiftPercentage, uint256 centerPriceShiftingTime, uint256 startTimestamp) centerPriceShift, tuple(uint256 oldUpperRangePercent, uint256 oldLowerRangePercent, uint256 shiftingTime, uint256 startTimestamp) rangeShift, tuple(uint256 oldUpperThresholdPercent, uint256 oldLowerThresholdPercent, uint256 shiftingTime, uint256 startTimestamp) thresholdShift) dexState)", -] as const; - /** * Converts Fluid fee (1e4 basis) to Uniswap v4 fee (1e6 basis). * Fluid: 10000 = 100%. Uniswap v4: 10000 = 1%. @@ -98,7 +94,7 @@ async function main() { const outputDir = (args["output-dir"] as string) ?? "detected"; const provider = new ethers.JsonRpcProvider(rpcUrl); - const resolverContract = new ethers.Contract(resolver, RESOLVER_ABI as unknown as string[], provider); + const resolverContract = new ethers.Contract(resolver, FLUIDDEXLITE_RESOLVER_ABI as unknown as string[], provider); console.error(`[enum] FluidDexLiteResolver at ${resolver}`); const dexKeys = (await resolverContract.getAllDexes()) as Array<{ @@ -115,8 +111,8 @@ async function main() { const [currency0, currency1] = [mapped[0], mapped[1]]; let fee: number | null = null; + const dexKey = { token0: dk.token0, token1: dk.token1, salt: dk.salt }; try { - const dexKey = { token0: dk.token0, token1: dk.token1, salt: dk.salt }; const dexState = (await resolverContract.getDexState(dexKey)) as { dexVariables: { fee: bigint | number }; }; @@ -126,6 +122,7 @@ async function main() { } } catch { // getDexState reverted; keep fee null + console.error(`getDexState reverted for dexKey: ${JSON.stringify(dexKey)}`); } configs.push({ diff --git a/aggregator-hooks/historical/FluidDexT1.ts b/aggregator-hooks/historical/FluidDexT1.ts index fe8166ef..be5c88d5 100644 --- a/aggregator-hooks/historical/FluidDexT1.ts +++ b/aggregator-hooks/historical/FluidDexT1.ts @@ -28,6 +28,7 @@ import fs from "node:fs"; import path from "node:path"; import { JsonRpcProvider, Contract, Interface, getAddress } from "ethers"; import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; +import { FLUIDDEXT1_HISTORICAL_FACTORY_ABI, FLUIDDEXT1_RESOLVER_ABI } from "../abis/index.js"; import type { Address } from "../creation-modules/types.js"; const OUTPUT_FILE = "fluiddext1-pools.json"; @@ -44,17 +45,6 @@ type CreatePoolsFluidDexT1Config = { sqrtPriceX96: bigint | null; }; -const FACTORY_ABI = [ - "event LogDexDeployed(address indexed dex, uint256 indexed dexId)", - "function totalDexes() external view returns (uint256)", - "function getDexAddress(uint256 dexId) public view returns (address)", - "function isDex(address dex) public view returns (bool)", -] as const; - -const RESOLVER_ABI = [ - "function getPoolTokens(address pool) external view returns (address token0, address token1)", -] as const; - /** Fluid native token; map to address(0) for Uniswap v4 pool init */ const FLUID_NATIVE: Address = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; const ZERO_ADDRESS: Address = "0x0000000000000000000000000000000000000000"; @@ -144,8 +134,8 @@ async function main() { const limit = pLimit(concurrency); const provider = new JsonRpcProvider(rpcUrl); - const factory = new Contract(factoryAddr, FACTORY_ABI, provider); - const resolver = new Contract(getAddress(reservesResolverAddr.toLowerCase()), RESOLVER_ABI, provider); + const factory = new Contract(factoryAddr, FLUIDDEXT1_HISTORICAL_FACTORY_ABI, provider); + const resolver = new Contract(getAddress(reservesResolverAddr.toLowerCase()), FLUIDDEXT1_RESOLVER_ABI, provider); const latest = BigInt(await provider.getBlockNumber()); const endBlock = endBlockRaw ? BigInt(String(endBlockRaw)) : latest; @@ -156,7 +146,7 @@ async function main() { const byDexAddr = new Map(); if (mode === "logs" || mode === "both") { - const iface = new Interface(FACTORY_ABI as unknown as string[]); + const iface = new Interface(FLUIDDEXT1_HISTORICAL_FACTORY_ABI as unknown as string[]); const topic0 = iface.getEvent("LogDexDeployed")!.topicHash; for (let from = startBlock; from <= endBlock; from += chunkSize) { diff --git a/aggregator-hooks/historical/StableSwapNG.ts b/aggregator-hooks/historical/StableSwapNG.ts index 35cabcca..d5181f9e 100644 --- a/aggregator-hooks/historical/StableSwapNG.ts +++ b/aggregator-hooks/historical/StableSwapNG.ts @@ -25,6 +25,7 @@ import fs from "node:fs"; import path from "node:path"; import { JsonRpcProvider, Contract, getAddress, ZeroAddress } from "ethers"; import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; +import { STABLESWAPNG_HISTORICAL_FACTORY_ABI } from "../abis/index.js"; import type { Address } from "../creation-modules/types.js"; const OUTPUT_FILE = "stableswapng-pools.json"; @@ -54,14 +55,6 @@ const CREATE_POOLS_DEFAULTS = { sqrtPriceX96: null as bigint | null, } as const; -const FACTORY_ABI = [ - "function pool_count() view returns (uint256)", - "function pool_list(uint256) view returns (address)", - "function get_n_coins(address) view returns (uint256)", - "function get_coins(address) view returns (address[])", - "function get_base_pool(address) view returns (address)", -]; - function saveJson(filePath: string, data: unknown): void { fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); } @@ -144,7 +137,7 @@ async function main() { const startIndex = toInt(args["start-index"], 0); const provider = new JsonRpcProvider(rpcUrl); - const factory = new Contract(factoryAddress, FACTORY_ABI, provider); + const factory = new Contract(factoryAddress, STABLESWAPNG_HISTORICAL_FACTORY_ABI, provider); const limit = pLimit(concurrency); const poolCountBn: bigint = await factory.pool_count(); diff --git a/aggregator-hooks/package.json b/aggregator-hooks/package.json index aab8acef..530065d1 100644 --- a/aggregator-hooks/package.json +++ b/aggregator-hooks/package.json @@ -4,7 +4,7 @@ "description": "Aggregator hook pool discovery and creation scripts", "type": "module", "scripts": { - "build": "tsc", + "build": "tsc && cp abis/*.json dist/abis/", "stableswapng": "tsx historical/StableSwapNG.ts", "fluiddext1": "tsx historical/FluidDexT1.ts", "fluiddexlite": "tsx historical/FluidDexLite.ts", diff --git a/aggregator-hooks/src/createPools.ts b/aggregator-hooks/src/createPools.ts index b532d39a..60910299 100644 --- a/aggregator-hooks/src/createPools.ts +++ b/aggregator-hooks/src/createPools.ts @@ -3,11 +3,12 @@ import "dotenv/config"; import { ethers } from "ethers"; import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"; -import { execSync, spawn } from "child_process"; +import { execFileSync, spawn } from "child_process"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; -import { getEnvForChain, mustEnvForChain, toInt } from "./cli.js"; +import { getEnvForChain, toInt } from "./cli.js"; +import { createLogger, type Logger } from "./logger.js"; import { CREATION_MODULES, POOL_TYPES, @@ -193,7 +194,7 @@ function parseArgs(): ParsedArgs { }; } -function appendToRegistryFile(registryDir: string, poolType: string, entry: PoolDeployedEntry): void { +function appendToRegistryFile(registryDir: string, poolType: string, entry: PoolDeployedEntry, log: Logger): void { const fileName = `deployed-${poolType}.json`; const filePath = join(registryDir, fileName); @@ -211,7 +212,7 @@ function appendToRegistryFile(registryDir: string, poolType: string, entry: Pool mkdirSync(registryDir, { recursive: true }); } writeFileSync(filePath, JSON.stringify(poolsDeployed, null, 2)); - console.log(`Appended to registry: ${filePath}`); + log.info(`Appended to registry: ${filePath}`); } function parseSqrtPriceX96(v: unknown): bigint | null { @@ -222,7 +223,7 @@ function parseSqrtPriceX96(v: unknown): bigint | null { return null; } -function loadJsonFile(filePath: string): PoolConfig[] { +function loadJsonFile(filePath: string, log: Logger): PoolConfig[] { try { const content = readFileSync(filePath, "utf-8"); const pools = JSON.parse(content); @@ -258,11 +259,10 @@ function loadJsonFile(filePath: string): PoolConfig[] { sqrtPriceX96: parseSqrtPriceX96(p.sqrtPriceX96), })) as PoolConfig[]; } catch (error) { - if (error instanceof Error) { - console.error(`Error loading JSON file: ${error.message}`); - } else { - console.error("Unknown error loading JSON file"); - } + log.error( + "Error loading JSON file:", + error instanceof Error ? error : new Error("Unknown error loading JSON file"), + ); process.exit(1); } } @@ -368,27 +368,27 @@ function runMineHookWorker( async function mineSalt( constructorArgs: string, protocolId: number, + log: Logger, deployerAddress?: Address, - verbose = false, jobs = 1, ): Promise { const scriptPath = join(projectRoot, "mine_hook.sh"); const protocolIdHex = `0x${protocolId.toString(16).toUpperCase()}`; - console.log(`Mining salt for protocol ${protocolIdHex}...`); - console.log(`Constructor args: ${constructorArgs.substring(0, 66)}...`); - if (deployerAddress) console.log(`Deployer address: ${deployerAddress}`); - if (jobs > 1) console.log(`Running ${jobs} parallel mining workers...`); + log.info(`Mining salt for protocol ${protocolIdHex}...`); + log.info(`Constructor args: ${constructorArgs.substring(0, 66)}...`); + if (deployerAddress) log.info(`Deployer address: ${deployerAddress}`); + if (jobs > 1) log.info(`Running ${jobs} parallel mining workers...`); const baseArgs = [scriptPath, constructorArgs, protocolIdHex]; if (deployerAddress) baseArgs.push("500", deployerAddress); - const execEnv = { ...process.env, ...(verbose && { FORGE_VERBOSE: "1" }) }; + const execEnv = { ...process.env, ...(log.verboseEnabled && { FORGE_VERBOSE: "1" }) }; try { if (jobs === 1) { const result = await runMineHookWorker(scriptPath, baseArgs, execEnv, projectRoot, true); - console.log(`✓ Found salt: ${result.salt}`); + log.success(`Found salt: ${result.salt}`); return result.salt; } @@ -457,16 +457,16 @@ async function mineSalt( }); }); - console.log(`✓ Found salt: ${salt}`); + log.success(`Found salt: ${salt}`); return salt; } catch (error) { - console.error("Error mining salt:"); - if (error instanceof Error) { - console.error(error.message); - const execErr = error as { stdout?: string; stderr?: string }; - if (execErr.stdout) console.error("\n--- Forge/mine_hook output (from failed worker) ---\n", execErr.stdout); - if (execErr.stderr) console.error("\n--- Forge/mine_hook stderr ---\n", execErr.stderr); - } + log.error("Error mining salt:", error); + const execErr = error as { stdout?: string; stderr?: string }; + log.dumpForgeOutput({ + stdout: execErr.stdout, + stderr: execErr.stderr, + label: "Forge/mine_hook (from failed worker)", + }); throw error; } } @@ -478,7 +478,7 @@ function selfDeployPool( salt: string, rpcUrl: string, dryRun: boolean, - verbose = false, + log: Logger, priorityGasPrice: string | null = null, ): Address | "deployed" { const module = CREATION_MODULES[poolType]; @@ -495,45 +495,44 @@ function selfDeployPool( ...module.buildSelfDeployEnvVars(poolConfig, immutables), }; - const { PRIVATE_KEY: _pk, ...publicEnvVars } = envVars; - const envString = Object.entries(publicEnvVars) - .map(([key, value]) => `${key}="${value}"`) - .join(" "); - - console.log(`Self-deploying hook via SelfCreateHook.s.sol...`); - if (dryRun) console.log("(dry run - no broadcast)"); - console.log(`Protocol: ${poolType}, Salt: ${salt.substring(0, 18)}...`); + log.info(`Self-deploying hook via SelfCreateHook.s.sol...`); + if (dryRun) log.info("(dry run - no broadcast)"); + log.info(`Protocol: ${poolType}, Salt: ${salt.substring(0, 18)}...`); try { - const broadcastFlag = dryRun ? "" : " --broadcast"; - const verboseFlag = verbose ? " -vvvv" : ""; - const priorityFeeFlag = priorityGasPrice ? ` --priority-gas-price ${priorityGasPrice}` : ""; - const command = `${envString} forge script script/SelfCreateHook.s.sol:SelfCreateHookScript --rpc-url "${rpcUrl}"${broadcastFlag}${priorityFeeFlag}${verboseFlag}`; - const output = execSync(command, { - encoding: "utf-8", - cwd: projectRoot, - env: { ...process.env, ...envVars }, - }); + const output = execFileSync( + "forge", + [ + "script", + "script/SelfCreateHook.s.sol:SelfCreateHookScript", + "--rpc-url", + rpcUrl, + ...(dryRun ? [] : ["--broadcast"]), + ...(priorityGasPrice ? ["--priority-gas-price", priorityGasPrice] : []), + ...(log.verboseEnabled ? ["-vvvv"] : []), + ], + { + encoding: "utf-8", + cwd: projectRoot, + env: { ...process.env, ...envVars }, + }, + ); - if (verbose) console.log("\n--- Forge script output ---\n", output); + if (log.verboseEnabled) log.verbose(`\n--- Forge script output ---\n${output}`); const hookMatch = output.match(/Hook Address:\s*(0x[a-fA-F0-9]{40})/); if (hookMatch) { const addr = ethers.getAddress(hookMatch[1]) as Address; - console.log(`✓ Hook deployed at: ${addr}`); + log.success(`Hook deployed at: ${addr}`); return addr; } - console.log(`✓ Self-deploy completed`); + log.success(`Self-deploy completed`); return "deployed"; } catch (error) { - console.error("Error in self-deploy:"); - if (error instanceof Error) { - console.error(error.message); - const execErr = error as { stdout?: string; stderr?: string }; - if (execErr.stdout) console.error("\n--- Forge stdout ---\n", execErr.stdout); - if (execErr.stderr) console.error("\n--- Forge stderr ---\n", execErr.stderr); - } + log.error("Error in self-deploy:", error); + const execErr = error as { stdout?: string; stderr?: string }; + log.dumpForgeOutput({ stdout: execErr.stdout, stderr: execErr.stderr, label: "Forge" }); throw error; } } @@ -544,6 +543,7 @@ async function createPool( poolConfig: PoolConfig, poolType: string, salt: string, + log: Logger, ): Promise<{ hookAddress: Address | ""; blockNumber: number; txHash: string }> { const module = CREATION_MODULES[poolType]; if (!module) throw new Error(`Unknown pool type: ${poolType}`); @@ -551,19 +551,19 @@ async function createPool( const args = module.buildCreatePoolArgs(poolConfig, salt); const factory = new ethers.Contract(factoryAddress, module.factoryAbi, signer); - console.log(`Calling createPool on factory ${factoryAddress}...`); - console.log(`Args:`, args.map((a, i) => `${i}: ${String(a).substring(0, 66)}...`).join(", ")); + log.info(`Calling createPool on factory ${factoryAddress}...`); + log.info(`Args: ${args.map((a, i) => `${i}: ${String(a).substring(0, 66)}...`).join(", ")}`); try { const tx = await factory.createPool(...args); - console.log(`✓ Transaction sent: ${tx.hash}`); + log.success(`Transaction sent: ${tx.hash}`); const receipt = await tx.wait(); - console.log(`✓ Transaction confirmed in block ${receipt!.blockNumber}`); + log.success(`Transaction confirmed in block ${receipt!.blockNumber}`); - const hookDeployedEvent = receipt!.logs.find((log: ethers.Log) => { + const hookDeployedEvent = receipt!.logs.find((l: ethers.Log) => { try { - const parsed = factory.interface.parseLog({ topics: log.topics as string[], data: log.data }); + const parsed = factory.interface.parseLog({ topics: l.topics as string[], data: l.data }); return parsed?.name === "HookDeployed"; } catch { return false; @@ -576,7 +576,7 @@ async function createPool( data: hookDeployedEvent.data, }); const hookAddress = ethers.getAddress((parsed?.args.hook || parsed?.args[0]) as string) as Address; - console.log(`✓ Hook deployed at: ${hookAddress}`); + log.success(`Hook deployed at: ${hookAddress}`); return { hookAddress, blockNumber: Number(receipt!.blockNumber), @@ -590,12 +590,7 @@ async function createPool( txHash: tx.hash, }; } catch (error) { - console.error("Error creating pool:"); - if (error instanceof Error) { - console.error(error.message); - if ("data" in error && error.data) console.error("Error data:", error.data); - if ("reason" in error && error.reason) console.error("Revert reason:", error.reason); - } + log.error("Error creating pool:", error); throw error; } } @@ -615,38 +610,40 @@ async function main() { priorityGasPrice, } = parseArgs(); - console.log("=== Pool Creation Script ==="); - console.log(`JSON File: ${jsonFile}`); - console.log(`Mode: ${selfDeploy ? "Self-Deploy" : "Factory"}`); - if (factoryAddress) console.log(`Factory Address: ${factoryAddress}`); - console.log(`RPC URL: ${rpcUrl}`); - if (registryDir) console.log(`Registry dir: ${registryDir}`); - if (dryRun) console.log("DRY RUN: forge scripts will simulate without broadcasting"); - if (verbose) console.log("VERBOSE: forge scripts will run with -vvvv"); - if (startAt > 1) console.log(`Starting at pool index: ${startAt} (skipping first ${startAt - 1} pool(s))`); - if (jobs > 1) console.log(`Salt mining: ${jobs} parallel workers`); - if (priorityGasPrice) console.log(`Priority gas price: ${priorityGasPrice}`); - console.log(""); + const log = createLogger({ verbose }); const provider = new ethers.JsonRpcProvider(rpcUrl); const privateKey = process.env.PRIVATE_KEY!; const signer = new ethers.Wallet(privateKey, provider); - console.log(`Using signer: ${signer.address}`); - console.log(""); + + log.banner({ + title: "Pool Creation Script", + jsonFile, + mode: selfDeploy ? "Self-Deploy" : "Factory", + factoryAddress: factoryAddress ?? undefined, + rpcUrl, + registryDir, + dryRun, + verbose, + startAt, + jobs, + priorityGasPrice, + signerAddress: signer.address, + }); if (selfDeploy) { - const allPools = loadJsonFile(jsonFile); + const allPools = loadJsonFile(jsonFile, log); const pools = startAt > 1 ? allPools.slice(startAt - 1) : allPools; if (startAt > 1 && pools.length === 0) { - console.error(`Error: --start-at ${startAt} exceeds pool count (${allPools.length})`); + log.error(`Error: --start-at ${startAt} exceeds pool count (${allPools.length})`); process.exit(1); } - console.log( + log.info( `Loaded ${allPools.length} pool configuration(s)${ startAt > 1 ? `, processing from index ${startAt} (${pools.length} remaining)` : "" }`, ); - console.log(""); + log.info(""); const chainId = parsedChainId ?? Number((await provider.getNetwork()).chainId); @@ -657,11 +654,11 @@ async function main() { const module = CREATION_MODULES[poolType]; const immutables = module.getImmutablesFromEnv(chainId); - console.log(`\n--- Processing Pool ${i + 1}/${allPools.length} (${poolType}) ---`); + log.section(`Processing Pool ${i + 1}/${allPools.length} (${poolType})`); try { const constructorArgs = module.encodeConstructorArgs(poolConfig, immutables); - const salt = await mineSalt(constructorArgs, module.protocolId, CREATE2_DEPLOYER, verbose, jobs); + const salt = await mineSalt(constructorArgs, module.protocolId, log, CREATE2_DEPLOYER, jobs); const hookAddress = selfDeployPool( poolConfig, poolType, @@ -669,7 +666,7 @@ async function main() { salt, rpcUrl, dryRun, - verbose, + log, priorityGasPrice, ); @@ -677,114 +674,116 @@ async function main() { const poolKeys = module.buildPoolKeys(poolConfig, hookAddress); if (poolKeys.length > 0) { const blockNumber = Number(await provider.getBlockNumber()); - appendToRegistryFile(registryDir, poolType, { - poolKeys, - metadata: { - externalPool: module.getExternalPool(poolConfig), - hookAddress, - blockNumber, + appendToRegistryFile( + registryDir, + poolType, + { + poolKeys, + metadata: { + externalPool: module.getExternalPool(poolConfig), + hookAddress, + blockNumber, + }, }, - }); + log, + ); } } - console.log(`✓ Successfully created pool ${i + 1}`); + log.success(`Successfully created pool ${i + 1}`); } catch (error) { - console.error(`✗ Failed to create pool ${i + 1}:`); - if (error instanceof Error) { - console.error(error.message); - const execErr = error as { stdout?: string; stderr?: string }; - if (execErr.stdout) console.error("\n--- Forge stdout ---\n", execErr.stdout); - if (execErr.stderr) console.error("\n--- Forge stderr ---\n", execErr.stderr); - const forgeOutput = [execErr.stdout, execErr.stderr].filter(Boolean).join("\n"); - if (forgeOutput) { - const verification = await verifyDeploymentOnForgeFailure( - provider, - immutables.poolManager, - poolConfig, - poolType, - forgeOutput, - ); - if (verification?.hookDeployed) { - console.error(""); - console.error("Verification (possible false positive):"); - console.error(` Hook has code at ${verification.hookAddress} → deployment likely succeeded`); - if (verification.poolsInitialized > 0) { - console.error( - ` Found ${verification.poolsInitialized} Initialize event(s) for this hook → pool(s) initialized on-chain`, - ); - } - console.error(" Check block explorer to confirm."); + log.error(`Failed to create pool ${i + 1}:`, error); + const execErr = error as { stdout?: string; stderr?: string }; + log.dumpForgeOutput({ stdout: execErr.stdout, stderr: execErr.stderr, label: "Forge" }); + const forgeOutput = [execErr.stdout, execErr.stderr].filter(Boolean).join("\n"); + if (forgeOutput) { + const verification = await verifyDeploymentOnForgeFailure( + provider, + immutables.poolManager, + poolConfig, + poolType, + forgeOutput, + ); + if (verification?.hookDeployed) { + log.error(""); + log.error("Verification (possible false positive):"); + log.error(` Hook has code at ${verification.hookAddress} → deployment likely succeeded`); + if (verification.poolsInitialized > 0) { + log.error( + ` Found ${verification.poolsInitialized} Initialize event(s) for this hook → pool(s) initialized on-chain`, + ); } + log.error(" Check block explorer to confirm."); } } continue; } } } else { - const allPools = loadJsonFile(jsonFile); + const allPools = loadJsonFile(jsonFile, log); const pools = startAt > 1 ? allPools.slice(startAt - 1) : allPools; if (startAt > 1 && pools.length === 0) { - console.error(`Error: --start-at ${startAt} exceeds pool count (${allPools.length})`); + log.error(`Error: --start-at ${startAt} exceeds pool count (${allPools.length})`); process.exit(1); } const poolType = pools[0].poolType as string; const module = CREATION_MODULES[poolType]; - console.log( + log.info( `Loaded ${allPools.length} pool configuration(s) (poolType: ${poolType})${ startAt > 1 ? `, processing from index ${startAt} (${pools.length} remaining)` : "" }`, ); - console.log(""); + log.info(""); - console.log("Reading factory immutables..."); + log.info("Reading factory immutables..."); const factoryImmutables = await module.readFactoryImmutables(provider, factoryAddress!); - console.log(`POOL_MANAGER: ${factoryImmutables.poolManager}`); + log.info(`POOL_MANAGER: ${factoryImmutables.poolManager}`); for (const [key, val] of Object.entries(factoryImmutables)) { - if (key !== "poolManager" && val) console.log(`${key}: ${val}`); + if (key !== "poolManager" && val) log.info(`${key}: ${val}`); } - console.log(""); + log.info(""); for (let j = 0; j < pools.length; j++) { const i = startAt - 1 + j; const poolConfig = pools[j]; - console.log(`\n--- Processing Pool ${i + 1}/${allPools.length} ---`); + log.section(`Processing Pool ${i + 1}/${allPools.length}`); try { const constructorArgs = module.encodeConstructorArgs(poolConfig, factoryImmutables); - const salt = await mineSalt(constructorArgs, module.protocolId, factoryAddress!, false, jobs); - const result = await createPool(signer, factoryAddress!, poolConfig, poolType, salt); + const salt = await mineSalt(constructorArgs, module.protocolId, log, factoryAddress!, jobs); + const result = await createPool(signer, factoryAddress!, poolConfig, poolType, salt, log); if (registryDir && result.hookAddress) { const poolKeys = module.buildPoolKeys(poolConfig, result.hookAddress); if (poolKeys.length > 0) { - appendToRegistryFile(registryDir, poolType, { - poolKeys, - metadata: { - externalPool: module.getExternalPool(poolConfig), - hookAddress: result.hookAddress, - txHash: result.txHash, - blockNumber: result.blockNumber, + appendToRegistryFile( + registryDir, + poolType, + { + poolKeys, + metadata: { + externalPool: module.getExternalPool(poolConfig), + hookAddress: result.hookAddress, + txHash: result.txHash, + blockNumber: result.blockNumber, + }, }, - }); + log, + ); } } - console.log(`✓ Successfully created pool ${i + 1}`); + log.success(`Successfully created pool ${i + 1}`); } catch (error) { - console.error(`✗ Failed to create pool ${i + 1}:`); - if (error instanceof Error) { - console.error(error.message); - const execErr = error as { stdout?: string; stderr?: string }; - if (execErr.stdout) console.error("\n--- Forge stdout ---\n", execErr.stdout); - if (execErr.stderr) console.error("\n--- Forge stderr ---\n", execErr.stderr); - } + log.error(`Failed to create pool ${i + 1}:`, error); + const execErr = error as { stdout?: string; stderr?: string }; + log.dumpForgeOutput({ stdout: execErr.stdout, stderr: execErr.stderr, label: "Forge" }); continue; } } } - console.log("\n=== Done ==="); + log.info("\n=== Done ==="); } main().catch((error) => { diff --git a/aggregator-hooks/src/logger.ts b/aggregator-hooks/src/logger.ts new file mode 100644 index 00000000..17d17061 --- /dev/null +++ b/aggregator-hooks/src/logger.ts @@ -0,0 +1,77 @@ +/** + * Config passed to banner() for startup output. + */ +export interface BannerConfig { + title: string; + jsonFile: string; + mode: string; + factoryAddress?: string | null; + rpcUrl: string; + registryDir?: string; + dryRun?: boolean; + verbose?: boolean; + startAt?: number; + jobs?: number; + priorityGasPrice?: string | null; + signerAddress?: string; +} + +/** + * Lightweight logger for createPools CLI. + * Centralizes output formatting and verbose-mode handling. + */ +export interface Logger { + verboseEnabled: boolean; + info: (msg: string) => void; + success: (msg: string) => void; + error: (msg: string, err?: unknown) => void; + verbose: (msg: string) => void; + section: (title: string) => void; + banner: (config: BannerConfig) => void; + dumpForgeOutput: (opts: { stdout?: string; stderr?: string; label?: string }) => void; +} + +export function createLogger(opts: { verbose: boolean }): Logger { + const { verbose } = opts; + + return { + verboseEnabled: verbose, + info: (msg: string) => console.log(msg), + success: (msg: string) => console.log(`✓ ${msg}`), + error: (msg: string, err?: unknown) => { + console.error(msg); + if (err instanceof Error) console.error(err.message); + const e = err as { data?: unknown; reason?: string }; + if (e?.data) console.error("Error data:", e.data); + if (e?.reason) console.error("Revert reason:", e.reason); + }, + verbose: (msg: string) => { + if (verbose) console.log(msg); + }, + section: (title: string) => console.log(`\n--- ${title} ---`), + banner: (config: BannerConfig) => { + const lines: string[] = [ + `=== ${config.title} ===`, + `JSON File: ${config.jsonFile}`, + `Mode: ${config.mode}`, + ...(config.factoryAddress ? [`Factory Address: ${config.factoryAddress}`] : []), + `RPC URL: ${config.rpcUrl}`, + ...(config.registryDir ? [`Registry dir: ${config.registryDir}`] : []), + ...(config.dryRun ? ["DRY RUN: forge scripts will simulate without broadcasting"] : []), + ...(config.verbose ? ["VERBOSE: forge scripts will run with -vvvv"] : []), + ...(config.startAt && config.startAt > 1 + ? [`Starting at pool index: ${config.startAt} (skipping first ${config.startAt - 1} pool(s))`] + : []), + ...(config.jobs && config.jobs > 1 ? [`Salt mining: ${config.jobs} parallel workers`] : []), + ...(config.priorityGasPrice ? [`Priority gas price: ${config.priorityGasPrice}`] : []), + ...(config.signerAddress ? [`Using signer: ${config.signerAddress}`] : []), + ]; + lines.forEach((line) => console.log(line)); + if (lines.length > 0) console.log(""); + }, + dumpForgeOutput: ({ stdout, stderr, label = "Forge" }) => { + if (stdout != null && stdout !== "") console.error(`\n--- ${label} stdout ---\n`, stdout); + if (stderr != null && stderr !== "") console.error(`\n--- ${label} stderr ---\n`, stderr); + }, + }; +} diff --git a/aggregator-hooks/tsconfig.json b/aggregator-hooks/tsconfig.json index 69e6f9f4..04f76275 100644 --- a/aggregator-hooks/tsconfig.json +++ b/aggregator-hooks/tsconfig.json @@ -19,6 +19,6 @@ "declarationMap": true, "sourceMap": true }, - "include": ["src/**/*.ts", "creation-modules/*.ts", "historical/**/*.ts", "polling/**/*.ts"], + "include": ["src/**/*.ts", "creation-modules/*.ts", "historical/**/*.ts", "polling/**/*.ts", "abis/**/*.ts"], "exclude": ["node_modules", "dist"] } diff --git a/mine_hook.sh b/mine_hook.sh index 02a4fa20..7c566c05 100644 --- a/mine_hook.sh +++ b/mine_hook.sh @@ -53,6 +53,13 @@ MAX_ATTEMPTS=${3:-500} # Default to 500 attempts DEPLOYER_ADDRESS=${4:-0x4e59b44847b379578588920cA78FbF26c0B4956C} # Default to CREATE2_DEPLOYER SALT_INCREMENT=160444 # Must match MAX_LOOP in AggregatorHookMiner.sol +# Validate DEPLOYER_ADDRESS format (0x + 40 hex chars) +if ! [[ "$DEPLOYER_ADDRESS" =~ ^0x[0-9a-fA-F]{40}$ ]]; then + echo "Error: Invalid DEPLOYER_ADDRESS '$DEPLOYER_ADDRESS'" + echo "Expected format: 0x followed by 40 hex characters (e.g., 0x4e59b44847b379578588920cA78FbF26c0B4956C)" + exit 1 +fi + # Random base offset (0 to 1 quadrillion) so each run searches a different salt region. # Avoids createCollision when redeploying pools with the same constructor args. # Use 4 bytes (not 8): bash uses signed 64-bit; od -tu8 can overflow to negative, breaking Solidity's uint256 env parsing and causing the same salts to be searched repeatedly. From 0c14fc2a7d9276180d58449400b5f67527a57a0e Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Fri, 27 Feb 2026 17:17:33 -0500 Subject: [PATCH 06/21] add polling to contracts --- aggregator-hooks/README.md | 46 ++- aggregator-hooks/polling/FluidDexLite.ts | 310 ++++++++++++++++++++ aggregator-hooks/polling/FluidDexT1.ts | 309 ++++++++++++++++++++ aggregator-hooks/polling/StableSwapNG.ts | 349 +++++++++++++++++++++++ 4 files changed, 1009 insertions(+), 5 deletions(-) create mode 100644 aggregator-hooks/polling/FluidDexLite.ts create mode 100644 aggregator-hooks/polling/FluidDexT1.ts create mode 100644 aggregator-hooks/polling/StableSwapNG.ts diff --git a/aggregator-hooks/README.md b/aggregator-hooks/README.md index dd9f9c2d..d5191562 100644 --- a/aggregator-hooks/README.md +++ b/aggregator-hooks/README.md @@ -16,6 +16,32 @@ Aggregator Hook factories should be deployed before running any of these scripts | `historical/FluidDexT1.ts` | Scrape LogDexDeployed events from FluidDexFactory | | `historical/StableSwapNG.ts` | Enumerate pool_count + pool_list from Curve StableSwap-NG factory | +### Polling discovery (incremental, checkpoint-based) + +Polling scripts scan only new blocks since the last run. They load and update a checkpoint file to avoid re-processing. Use these for ongoing discovery (e.g. cron) instead of historical one-time scrapes. + +| Script | Description | +| ------------------------- | ---------------------------------------------------------------------------- | +| `polling/FluidDexLite.ts` | Scan LogInitialize events from FluidDexLite since checkpoint | +| `polling/FluidDexT1.ts` | Scan LogDexDeployed events from FluidDexFactory; fetch tokens via resolver | +| `polling/StableSwapNG.ts` | Scan PlainPoolDeployed/MetaPoolDeployed events; fetch new pools from factory | + +All polling scripts append new pools to the same output files used by historical scripts, in createPools format. + +**Polling options:** + +| Arg | Default | Description | +| ------------------ | ------------- | --------------------------------------------------------------- | +| `--checkpoint-dir` | `checkpoints` | Where to store/load checkpoint files (`{dir}/{chainId}/*.json`) | +| `--start-block` | — | Override checkpoint; start scan from this block | + +**Polling env vars:** + +| Env | Default | Description | +| ----------------- | ------- | --------------------------------------------------------------------------- | +| `FINALITY_BLOCKS` | 10 | Blocks to subtract from latest; checkpoint stops before finality for safety | +| `LOOKBACK_BLOCKS` | 200000 | Blocks to scan when checkpoint is missing and no `--start-block` given | + ### Pool creation | Script | Description | @@ -36,6 +62,8 @@ All discovery scripts use chain-ID-suffixed env vars. Use `VAR_` (e.g. | **fluiddext1** | `RPC_URL`, `FLUID_DEX_RESOLVER` | `FLUID_DEX_FACTORY`, `FACTORY_ADDRESS`, `RPS`, `CONCURRENCY` | | **stableswapng** | `RPC_URL` | `FACTORY_ADDRESS`, `RPS`, `CONCURRENCY` | +**Polling scripts** use the same env vars as their historical counterparts (fluiddexlite, fluiddext1, stableswapng), plus `FINALITY_BLOCKS` and `LOOKBACK_BLOCKS` (see Polling env vars above). FluidDexLite polling also requires `DEX_LITE_ADDRESS`. + --- ## CLI arguments @@ -59,11 +87,12 @@ All discovery scripts require `--chain-id `. ### Script-specific args -| Script | Arg | Default | Description | -| ------------ | --------------- | --------- | ------------------------------- | -| fluiddext1 | `--mode` | enumerate | `logs` \| `enumerate` \| `both` | -| stableswapng | `--chunk` | 500 | pool_list batch size | -| stableswapng | `--start-index` | 0 | Start pool_list index | +| Script | Arg | Default | Description | +| ------------ | ------------------ | ------------- | ------------------------------- | +| fluiddext1 | `--mode` | enumerate | `logs` \| `enumerate` \| `both` | +| stableswapng | `--chunk` | 500 | pool_list batch size | +| stableswapng | `--start-index` | 0 | Start pool_list index | +| **polling** | `--checkpoint-dir` | `checkpoints` | Checkpoint storage directory | --- @@ -77,6 +106,8 @@ All discovery scripts require `--chain-id `. | fluiddext1 | fluiddext1-pools.json | | stableswapng | stableswapng-pools.json | +Polling scripts write to the same output paths and append new pools. Checkpoints go to `{checkpoint-dir}/{chainId}/{protocol}_checkpoint.json`. + --- ## Example invocations @@ -98,6 +129,11 @@ npx tsx historical/FluidDexT1.ts --chain-id 8453 --output-dir output # Historical stableswapng with custom chunk npx tsx historical/StableSwapNG.ts --chain-id 1 --chunk 200 +# Polling (incremental; use cron or run periodically) +npx tsx polling/FluidDexLite.ts --chain-id 1 +npx tsx polling/FluidDexT1.ts --chain-id 8453 --output-dir output +npx tsx polling/StableSwapNG.ts --chain-id 1 --checkpoint-dir checkpoints + # createPools with chain-id (uses RPC_URL_1 from env) npx tsx src/createPools.ts detected/1/fluiddexlite-pools-curated.json 0xFactoryAddr --chain-id 1 ``` diff --git a/aggregator-hooks/polling/FluidDexLite.ts b/aggregator-hooks/polling/FluidDexLite.ts new file mode 100644 index 00000000..6ade57d0 --- /dev/null +++ b/aggregator-hooks/polling/FluidDexLite.ts @@ -0,0 +1,310 @@ +/** + * Fluid Dex Lite pool discovery (checkpoint-based polling): + * - loads checkpoint which stores lastProcessedBlock + * - scans blocks since checkpoint for LogInitialize events from FluidDexLite + * - decodes events and appends new pool records to output + * - updates checkpoint to the latest safely processed block + * + * Usage: + * npx tsx polling/fluiddexlite.ts --chain-id 1 + * + * Options: + * --chain-id (required) Chain ID; loads RPC_URL_, DEX_LITE_ADDRESS_ + * --output-dir output directory (default: detected); writes to output-dir/chain-id/fluiddexlite-pools.json + * --checkpoint-dir checkpoint directory (default: checkpoints); writes to checkpoint-dir/chain-id/dexlite_checkpoint.json + * --chunk-blocks block chunk size for getLogs (default: 10000) + * --start-block (optional) override checkpoint; start scan from this block + * + * Env vars (use VAR_ or VAR for single chain): + * RPC_URL (required) + * DEX_LITE_ADDRESS (required) FluidDexLite singleton + * FINALITY_BLOCKS (optional, default 10) subtract from latest; checkpoint = last scanned block + * LOOKBACK_BLOCKS (optional, default 200000) used when checkpoint missing and no --start-block + */ + +import "dotenv/config"; +import fs from "node:fs"; +import path from "node:path"; +import { ethers } from "ethers"; +import { + parseArgs, + getEnvForChain, + toInt, + resolveOutputPath, + resolveCheckpointPath, +} from "@src/cli"; + +const OUTPUT_FILE = "fluiddexlite-pools.json"; +const CHECKPOINT_FILE = "dexlite_checkpoint.json"; + +/** Fluid native token; map to address(0) for Uniswap v4 pool init */ +const FLUID_NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + +function toUniswapV4Currency(addr: string): string { + return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() + ? ZERO_ADDRESS + : ethers.getAddress(addr); +} + +type Checkpoint = { + chainId: number; + dexLite: string; + lastProcessedBlock: number; + updatedAt: string; +}; + +/** Same shape as createPools.ts FluidDexLitePoolConfig */ +type CreatePoolsFluidLiteConfig = { + poolType: "fluiddexlite"; + dexSalt: string; + currency0: string; + currency1: string; + fee: number | null; + tickSpacing: number | null; + sqrtPriceX96: string | null; +}; + +const LOG_INITIALIZE_ABI = [ + "event LogInitialize(uint256 dexId, tuple(address token0,address token1,bytes32 salt) dexKey, tuple(tuple(address token0,address token1,bytes32 salt) dexKey,uint256 fee,uint256 revenueCut,bool rebalancingStatus,uint256 centerPrice,uint256 centerPriceContract,uint256 upperPercent,uint256 lowerPercent,uint256 upperShiftThreshold,uint256 lowerShiftThreshold,uint256 shiftTime,uint256 minCenterPrice,uint256 maxCenterPrice,uint256 token0Amount,uint256 token1Amount) params, uint256 time)", +] as const; + +function ensureDirForFile(filePath: string) { + const dir = path.dirname(path.resolve(filePath)); + fs.mkdirSync(dir, { recursive: true }); +} + +function safeReadJson(filePath: string): T | null { + try { + const raw = fs.readFileSync(filePath, "utf8"); + return JSON.parse(raw) as T; + } catch { + return null; + } +} + +function atomicWriteFile(filePath: string, contents: string) { + ensureDirForFile(filePath); + const abs = path.resolve(filePath); + const tmp = abs + ".tmp"; + fs.writeFileSync(tmp, contents); + fs.renameSync(tmp, abs); +} + +function loadExistingKeys(outFile: string): Set { + const keys = new Set(); + if (!fs.existsSync(outFile)) return keys; + const raw = fs.readFileSync(outFile, "utf8").trim(); + if (!raw) return keys; + try { + const arr = JSON.parse(raw) as CreatePoolsFluidLiteConfig[]; + if (!Array.isArray(arr)) return keys; + for (const x of arr) { + keys.add(`${x.dexSalt}:${x.currency0}:${x.currency1}`); + } + } catch { + // ignore + } + return keys; +} + +/** Return [currency0, currency1] with currency0 < currency1 (lexicographic on mapped addrs) */ +function orderCurrencies(a: string, b: string): [string, string] { + const mapped = [toUniswapV4Currency(a), toUniswapV4Currency(b)].sort((x, y) => + x.toLowerCase().localeCompare(y.toLowerCase()), + ); + return [mapped[0], mapped[1]]; +} + +function getEnvInt(name: string, chainId: number, def: number): number { + const v = getEnvForChain(name, chainId); + if (!v) return def; + const n = Number(v); + return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : def; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + const chainIdRaw = args["chain-id"]; + if (chainIdRaw == null) { + console.error("Missing required --chain-id "); + process.exit(1); + } + const chainId = toInt(chainIdRaw, 0); + if (chainId <= 0) { + console.error("--chain-id must be a positive integer"); + process.exit(1); + } + + const rpcUrl = getEnvForChain("RPC_URL", chainId); + const dexLiteRaw = getEnvForChain("DEX_LITE_ADDRESS", chainId); + if (!rpcUrl || !dexLiteRaw) { + throw new Error( + "Missing required env: RPC_URL and DEX_LITE_ADDRESS (or RPC_URL_, DEX_LITE_ADDRESS_)", + ); + } + const dexLite = ethers.getAddress(dexLiteRaw); + + const outputDir = (args["output-dir"] as string) ?? "detected"; + const checkpointDir = (args["checkpoint-dir"] as string) ?? "checkpoints"; + const chunkBlocks = Math.max(1, toInt(args["chunk-blocks"], 10000)); + + const finality = getEnvInt("FINALITY_BLOCKS", chainId, 10); + const lookbackBlocks = getEnvInt("LOOKBACK_BLOCKS", chainId, 200000); + const startBlockArg = args["start-block"]; + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const iface = new ethers.Interface(LOG_INITIALIZE_ABI as unknown as string[]); + const topic0 = iface.getEvent("LogInitialize")!.topicHash; + + const latestBlock = await provider.getBlockNumber(); + const toBlock = Math.max(0, latestBlock - finality); + + const cpPath = resolveCheckpointPath(checkpointDir, chainId, CHECKPOINT_FILE); + const cp = safeReadJson(cpPath); + + let fromBlock: number; + if (startBlockArg != null) { + fromBlock = Math.max(0, toInt(startBlockArg, 0)); + } else if ( + cp && + cp.chainId === chainId && + cp.dexLite.toLowerCase() === dexLite.toLowerCase() + ) { + fromBlock = cp.lastProcessedBlock + 1; + } else { + fromBlock = Math.max(0, toBlock - lookbackBlocks); + } + + if (fromBlock > toBlock) { + const newCp: Checkpoint = { + chainId, + dexLite, + lastProcessedBlock: toBlock, + updatedAt: new Date().toISOString(), + }; + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); + console.log( + JSON.stringify( + { + ok: true, + note: "No new blocks to scan", + chainId, + dexLite, + fromBlock, + toBlock, + latestBlock, + finality, + checkpointFile: cpPath, + }, + null, + 2, + ), + ); + return; + } + + const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); + const seenKeys = loadExistingKeys(outPath); + ensureDirForFile(outPath); + + let totalLogs = 0; + let newPools = 0; + + for (let start = fromBlock; start <= toBlock; start += chunkBlocks) { + const end = Math.min(toBlock, start + chunkBlocks - 1); + + const logs = await provider.getLogs({ + address: dexLite, + topics: [topic0], + fromBlock: start, + toBlock: end, + }); + + totalLogs += logs.length; + const newRecords: CreatePoolsFluidLiteConfig[] = []; + + for (const log of logs) { + let parsed: ethers.LogDescription | null; + try { + parsed = iface.parseLog(log); + } catch { + continue; + } + + const dexKey = parsed?.args[1] as { + token0: string; + token1: string; + salt: string; + }; + + const token0 = ethers.getAddress(dexKey.token0); + const token1 = ethers.getAddress(dexKey.token1); + const salt = dexKey.salt as string; + + const [currency0, currency1] = orderCurrencies(token0, token1); + const dedupeKey = `${salt}:${currency0}:${currency1}`; + if (seenKeys.has(dedupeKey)) continue; + seenKeys.add(dedupeKey); + + newPools++; + newRecords.push({ + poolType: "fluiddexlite", + dexSalt: salt, + currency0, + currency1, + fee: null, + tickSpacing: null, + sqrtPriceX96: null, + }); + } + + if (newRecords.length > 0) { + const existing = + safeReadJson(outPath) ?? []; + const merged = existing.concat(newRecords); + atomicWriteFile(outPath, JSON.stringify(merged, null, 2) + "\n"); + } + + const interimCp: Checkpoint = { + chainId, + dexLite, + lastProcessedBlock: end, + updatedAt: new Date().toISOString(), + }; + atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + "\n"); + + console.error( + `[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`, + ); + } + + console.log( + JSON.stringify( + { + ok: true, + chainId, + dexLite, + scanned: { + fromBlock, + toBlock, + latestBlock, + finality, + chunkBlocks, + }, + logsFound: totalLogs, + newPools, + outFile: outPath, + checkpointFile: cpPath, + }, + null, + 2, + ), + ); +} + +main().catch((err) => { + console.error(err); + process.exitCode = 1; +}); diff --git a/aggregator-hooks/polling/FluidDexT1.ts b/aggregator-hooks/polling/FluidDexT1.ts new file mode 100644 index 00000000..0e2bd54c --- /dev/null +++ b/aggregator-hooks/polling/FluidDexT1.ts @@ -0,0 +1,309 @@ +/** + * Fluid Dex T1 pool discovery (checkpoint-based polling): + * - loads checkpoint which stores lastProcessedBlock + * - scans blocks since checkpoint for LogDexDeployed events from FluidDexFactory + * - for each new dex, fetches currency0/currency1 via resolver.getDexTokens + * - appends to output in createPools format + * + * Usage: + * npx tsx polling/fluiddext1.ts --chain-id 1 + * + * Options: + * --chain-id (required) Chain ID; loads RPC_URL_, FLUID_DEX_RESOLVER_, etc. + * --output-dir output directory (default: detected); writes to output-dir/chain-id/fluiddext1-pools.json + * --checkpoint-dir checkpoint directory (default: checkpoints) + * --chunk-blocks block chunk size for getLogs (default: 10000) + * --start-block (optional) override checkpoint; start scan from this block + * + * Env vars (use VAR_ or VAR for single chain): + * RPC_URL (required) + * FLUID_DEX_RESOLVER (required) + * FLUID_DEX_FACTORY (optional, default mainnet) + * FINALITY_BLOCKS (optional, default 10) subtract from latest; checkpoint = last scanned block + * LOOKBACK_BLOCKS (optional, default 200000) used when checkpoint missing and no --start-block + */ + +import "dotenv/config"; +import fs from "node:fs"; +import path from "node:path"; +import { ethers } from "ethers"; +import { + parseArgs, + getEnvForChain, + toInt, + resolveOutputPath, + resolveCheckpointPath, +} from "@src/cli"; + +const OUTPUT_FILE = "fluiddext1-pools.json"; +const CHECKPOINT_FILE = "fluiddext1_checkpoint.json"; +const DEFAULT_FACTORY = "0x91716c4eDA1fB55e84Bf8b4c7085f84285c19085"; + +/** Fluid native token; map to address(0) for Uniswap v4 pool init */ +const FLUID_NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + +function toUniswapV4Currency(addr: string): string { + return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() + ? ZERO_ADDRESS + : ethers.getAddress(addr); +} + +type Checkpoint = { + chainId: number; + factory: string; + lastProcessedBlock: number; + updatedAt: string; +}; + +/** Same shape as createPools.ts FluidDexT1PoolConfig */ +type CreatePoolsFluidDexT1Config = { + poolType: "fluiddext1"; + fluidPool: string; + currency0: string; + currency1: string; + fee: number | null; + tickSpacing: number | null; + sqrtPriceX96: string | null; +}; + +const FACTORY_ABI = [ + "event LogDexDeployed(address indexed dex, uint256 indexed dexId)", +]; + +const RESOLVER_ABI = [ + "function getPoolTokens(address pool) external view returns (address token0, address token1)", +]; + +function ensureDirForFile(filePath: string) { + fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true }); +} + +function safeReadJson(filePath: string): T | null { + try { + return JSON.parse(fs.readFileSync(filePath, "utf8")) as T; + } catch { + return null; + } +} + +function atomicWriteFile(filePath: string, contents: string) { + ensureDirForFile(filePath); + const abs = path.resolve(filePath); + fs.writeFileSync(abs + ".tmp", contents); + fs.renameSync(abs + ".tmp", abs); +} + +function loadExistingKeys(outFile: string): Set { + const keys = new Set(); + if (!fs.existsSync(outFile)) return keys; + try { + const arr = safeReadJson(outFile); + if (!Array.isArray(arr)) return keys; + for (const x of arr) + keys.add(`${x.fluidPool}:${x.currency0}:${x.currency1}`); + } catch { + // ignore + } + return keys; +} + +/** Return [currency0, currency1] with currency0 < currency1 (lexicographic on mapped addrs) */ +function orderCurrencies(a: string, b: string): [string, string] { + const mapped = [toUniswapV4Currency(a), toUniswapV4Currency(b)].sort((x, y) => + x.toLowerCase().localeCompare(y.toLowerCase()), + ); + return [mapped[0], mapped[1]]; +} + +function getEnvInt(name: string, chainId: number, def: number): number { + const v = getEnvForChain(name, chainId); + if (!v) return def; + const n = Number(v); + return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : def; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + const chainIdRaw = args["chain-id"]; + if (chainIdRaw == null) { + console.error("Missing required --chain-id "); + process.exit(1); + } + const chainId = toInt(chainIdRaw, 0); + if (chainId <= 0) { + console.error("--chain-id must be a positive integer"); + process.exit(1); + } + + const rpcUrl = getEnvForChain("RPC_URL", chainId); + const resolverAddr = getEnvForChain("FLUID_DEX_RESOLVER", chainId); + const factoryRaw = + getEnvForChain("FLUID_DEX_FACTORY", chainId) ?? + getEnvForChain("FACTORY_ADDRESS", chainId) ?? + DEFAULT_FACTORY; + + if (!rpcUrl || !resolverAddr) { + throw new Error("Missing required env: RPC_URL and FLUID_DEX_RESOLVER"); + } + + const factory = ethers.getAddress(factoryRaw); + + const outputDir = (args["output-dir"] as string) ?? "detected"; + const checkpointDir = (args["checkpoint-dir"] as string) ?? "checkpoints"; + const chunkBlocks = Math.max(1, toInt(args["chunk-blocks"], 10000)); + + const finality = getEnvInt("FINALITY_BLOCKS", chainId, 10); + const lookbackBlocks = getEnvInt("LOOKBACK_BLOCKS", chainId, 200000); + const startBlockArg = args["start-block"]; + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const resolver = new ethers.Contract( + ethers.getAddress(resolverAddr), + RESOLVER_ABI, + provider, + ); + + const iface = new ethers.Interface(FACTORY_ABI as unknown as string[]); + const topic0 = iface.getEvent("LogDexDeployed")!.topicHash; + + const latestBlock = await provider.getBlockNumber(); + const toBlock = Math.max(0, latestBlock - finality); + + const cpPath = resolveCheckpointPath(checkpointDir, chainId, CHECKPOINT_FILE); + const cp = safeReadJson(cpPath); + + let fromBlock: number; + if (startBlockArg != null) { + fromBlock = Math.max(0, toInt(startBlockArg, 0)); + } else if ( + cp?.chainId === chainId && + cp?.factory?.toLowerCase() === factory.toLowerCase() + ) { + fromBlock = cp.lastProcessedBlock + 1; + } else { + fromBlock = Math.max(0, toBlock - lookbackBlocks); + } + + if (fromBlock > toBlock) { + const newCp: Checkpoint = { + chainId, + factory, + lastProcessedBlock: toBlock, + updatedAt: new Date().toISOString(), + }; + ensureDirForFile(cpPath); + fs.writeFileSync(cpPath, JSON.stringify(newCp, null, 2) + "\n"); + console.log( + JSON.stringify( + { ok: true, note: "No new blocks to scan", fromBlock, toBlock }, + null, + 2, + ), + ); + return; + } + + const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); + const seenKeys = loadExistingKeys(outPath); + let allRecords = safeReadJson(outPath) ?? []; + let totalLogs = 0; + let newPools = 0; + + for (let start = fromBlock; start <= toBlock; start += chunkBlocks) { + const end = Math.min(toBlock, start + chunkBlocks - 1); + + const logs = await provider.getLogs({ + address: factory, + topics: [topic0], + fromBlock: start, + toBlock: end, + }); + + totalLogs += logs.length; + const newRecords: CreatePoolsFluidDexT1Config[] = []; + + for (const log of logs) { + let parsed: ethers.LogDescription | null; + try { + parsed = iface.parseLog(log); + } catch { + continue; + } + if (!parsed) continue; + + const dex = ethers.getAddress(parsed.args.dex as string); + + let token0: string; + let token1: string; + try { + [token0, token1] = await resolver.getPoolTokens(dex); + } catch { + console.error(`Failed getPoolTokens for ${dex}`); + continue; + } + + const [currency0, currency1] = orderCurrencies(token0, token1); + const key = `${dex}:${currency0}:${currency1}`; + if (seenKeys.has(key)) continue; + seenKeys.add(key); + + newPools++; + newRecords.push({ + poolType: "fluiddext1", + fluidPool: dex, + currency0, + currency1, + fee: null, + tickSpacing: null, + sqrtPriceX96: null, + }); + } + + if (newRecords.length > 0) { + allRecords = allRecords.concat(newRecords); + atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + "\n"); + } + + const interimCp: Checkpoint = { + chainId, + factory, + lastProcessedBlock: end, + updatedAt: new Date().toISOString(), + }; + ensureDirForFile(cpPath); + fs.writeFileSync(cpPath, JSON.stringify(interimCp, null, 2) + "\n"); + console.error( + `[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`, + ); + } + + console.log( + JSON.stringify( + { + ok: true, + chainId, + factory, + scanned: { + fromBlock, + toBlock, + latestBlock, + finality, + chunkBlocks, + }, + logsFound: totalLogs, + newPools, + outFile: outPath, + checkpointFile: cpPath, + }, + null, + 2, + ), + ); +} + +main().catch((err) => { + console.error(err); + process.exitCode = 1; +}); diff --git a/aggregator-hooks/polling/StableSwapNG.ts b/aggregator-hooks/polling/StableSwapNG.ts new file mode 100644 index 00000000..f095db9b --- /dev/null +++ b/aggregator-hooks/polling/StableSwapNG.ts @@ -0,0 +1,349 @@ +/** + * StableSwap-NG pool discovery (block-based polling): + * - loads checkpoint which stores lastProcessedBlock + * - scans blocks since checkpoint for PlainPoolDeployed/MetaPoolDeployed events + * - event count = number of new pools; fetches pool_list(n-i+1)..pool_list(n) where n=poolCount-1, i=eventCount + * - appends new pool records to output in createPools format + * - updates checkpoint to last scanned block + * + * Usage: + * npx tsx polling/stableswapng.ts --chain-id 1 + * + * Options: + * --chain-id (required) Chain ID; loads RPC_URL_, FACTORY_ADDRESS_ + * --output-dir output directory (default: detected) + * --checkpoint-dir checkpoint directory (default: checkpoints) + * --chunk-blocks block chunk size for getLogs (default: 10000) + * --start-block (optional) override checkpoint; start scan from this block + * + * Env vars (use VAR_ or VAR for single chain): + * RPC_URL (required) + * FACTORY_ADDRESS (optional, default mainnet Curve StableSwap-NG factory) + * FINALITY_BLOCKS (optional, default 10) subtract from latest; checkpoint = last scanned block + * LOOKBACK_BLOCKS (optional, default 200000) used when checkpoint missing and no --start-block + * RPS (optional, default 80) max RPC requests per second + * CONCURRENCY (optional, default 8) max concurrent RPC calls + */ + +import "dotenv/config"; +import fs from "node:fs"; +import path from "node:path"; +import { ethers } from "ethers"; +import { + parseArgs, + getEnvForChain, + toInt, + resolveOutputPath, + resolveCheckpointPath, +} from "@src/cli"; + +const OUTPUT_FILE = "stableswapng-pools.json"; +const CHECKPOINT_FILE = "stableswapng_checkpoint.json"; +const DEFAULT_FACTORY = "0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf"; + +type Checkpoint = { + chainId: number; + factory: string; + lastProcessedBlock: number; + updatedAt: string; +}; + +/** Same shape as createPools.ts StableSwapPoolConfig for stableswapng */ +type CreatePoolsStableSwapConfig = { + poolType: "stableswapng"; + curvePool: string; + tokens: string[]; + fee: number | null; + tickSpacing: number | null; + sqrtPriceX96: string | null; +}; + +const FACTORY_ABI = [ + "function pool_count() view returns (uint256)", + "function pool_list(uint256) view returns (address)", + "function get_n_coins(address) view returns (uint256)", + "function get_coins(address) view returns (address[])", + "function get_base_pool(address) view returns (address)", + "event PlainPoolDeployed(address[] coins, uint256 A, uint256 fee, address deployer)", + "event MetaPoolDeployed(address coin, address base_pool, uint256 A, uint256 fee, address deployer)", +]; + +function ensureDirForFile(filePath: string) { + fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true }); +} + +function safeReadJson(filePath: string): T | null { + try { + return JSON.parse(fs.readFileSync(filePath, "utf8")) as T; + } catch { + return null; + } +} + +function atomicWriteFile(filePath: string, contents: string) { + ensureDirForFile(filePath); + const abs = path.resolve(filePath); + fs.writeFileSync(abs + ".tmp", contents); + fs.renameSync(abs + ".tmp", abs); +} + +function loadExistingPoolAddrs(outFile: string): Set { + const keys = new Set(); + if (!fs.existsSync(outFile)) return keys; + try { + const arr = safeReadJson(outFile); + if (!Array.isArray(arr)) return keys; + for (const x of arr) keys.add(x.curvePool.toLowerCase()); + } catch { + // ignore + } + return keys; +} + +function pRateLimit(rps: number): () => Promise { + if (rps <= 0) return async () => {}; + const minGapMs = 1000 / rps; + let nextAllowed = 0; + return async function acquire() { + const now = Date.now(); + if (now < nextAllowed) + await new Promise((r) => setTimeout(r, nextAllowed - now)); + nextAllowed = Math.max(now, nextAllowed) + minGapMs; + }; +} + +function pLimit(concurrency: number) { + let active = 0; + const queue: (() => void)[] = []; + return async function limit(fn: () => Promise): Promise { + if (active >= concurrency) await new Promise((r) => queue.push(r)); + active++; + try { + return await fn(); + } finally { + active--; + queue.shift()?.(); + } + }; +} + +function uniqAddresses(addrs: string[]): string[] { + const s = new Set(); + for (const a of addrs) { + if (!a) continue; + const norm = a.toLowerCase(); + if (norm === ethers.ZeroAddress.toLowerCase()) continue; + s.add(ethers.getAddress(a)); + } + return [...s]; +} + +function getEnvInt(name: string, chainId: number, def: number): number { + const v = getEnvForChain(name, chainId); + if (!v) return def; + const n = Number(v); + return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : def; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + const chainIdRaw = args["chain-id"]; + if (chainIdRaw == null) { + console.error("Missing required --chain-id "); + process.exit(1); + } + const chainId = toInt(chainIdRaw, 0); + if (chainId <= 0) { + console.error("--chain-id must be a positive integer"); + process.exit(1); + } + + const rpcUrl = getEnvForChain("RPC_URL", chainId); + const factoryRaw = + getEnvForChain("FACTORY_ADDRESS", chainId) ?? DEFAULT_FACTORY; + + if (!rpcUrl) { + throw new Error("Missing required env: RPC_URL (or RPC_URL_)"); + } + + const factory = ethers.getAddress(factoryRaw); + + const outputDir = (args["output-dir"] as string) ?? "detected"; + const checkpointDir = (args["checkpoint-dir"] as string) ?? "checkpoints"; + const chunkBlocks = Math.max(1, toInt(args["chunk-blocks"], 10000)); + + const finality = getEnvInt("FINALITY_BLOCKS", chainId, 10); + const lookbackBlocks = getEnvInt("LOOKBACK_BLOCKS", chainId, 200000); + const startBlockArg = args["start-block"]; + + const concurrency = Math.max( + 1, + toInt(getEnvForChain("CONCURRENCY", chainId), 8), + ); + const rps = toInt(getEnvForChain("RPS", chainId), 80); + const rateLimit = pRateLimit(rps); + const limit = pLimit(concurrency); + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const contract = new ethers.Contract(factory, FACTORY_ABI, provider); + + const iface = new ethers.Interface(FACTORY_ABI as unknown as string[]); + const plainTopic = iface.getEvent("PlainPoolDeployed")!.topicHash; + const metaTopic = iface.getEvent("MetaPoolDeployed")!.topicHash; + + const latestBlock = await provider.getBlockNumber(); + const toBlock = Math.max(0, latestBlock - finality); + + const cpPath = resolveCheckpointPath(checkpointDir, chainId, CHECKPOINT_FILE); + const cp = safeReadJson(cpPath); + + let fromBlock: number; + if (startBlockArg != null) { + fromBlock = Math.max(0, toInt(startBlockArg, 0)); + } else if ( + cp?.chainId === chainId && + cp?.factory?.toLowerCase() === factory.toLowerCase() + ) { + fromBlock = cp.lastProcessedBlock + 1; + } else { + fromBlock = Math.max(0, toBlock - lookbackBlocks); + } + + if (fromBlock > toBlock) { + const newCp: Checkpoint = { + chainId, + factory, + lastProcessedBlock: toBlock, + updatedAt: new Date().toISOString(), + }; + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); + console.log( + JSON.stringify( + { ok: true, note: "No new blocks to scan", fromBlock, toBlock }, + null, + 2, + ), + ); + return; + } + + // Scan for PlainPoolDeployed and MetaPoolDeployed events + let eventCount = 0; + for (let start = fromBlock; start <= toBlock; start += chunkBlocks) { + const end = Math.min(toBlock, start + chunkBlocks - 1); + + const logs = await provider.getLogs({ + address: factory, + fromBlock: start, + toBlock: end, + topics: [[plainTopic, metaTopic]], + }); + + eventCount += logs.length; + console.error( + `[scan] ${start}..${end} events=${logs.length} total=${eventCount}`, + ); + } + + // Update checkpoint + const newCp: Checkpoint = { + chainId, + factory, + lastProcessedBlock: toBlock, + updatedAt: new Date().toISOString(), + }; + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); + + if (eventCount === 0) { + console.log( + JSON.stringify( + { + ok: true, + chainId, + factory, + eventCount: 0, + outFile: resolveOutputPath(outputDir, chainId, OUTPUT_FILE), + checkpointFile: cpPath, + }, + null, + 2, + ), + ); + return; + } + + // Fetch the last eventCount pools: indices (poolCount - eventCount) .. (poolCount - 1) + const poolCountBn = await contract.pool_count(); + const poolCount = Number(poolCountBn); + if (!Number.isFinite(poolCount) || poolCount < eventCount) { + console.error( + `pool_count=${poolCount} < eventCount=${eventCount}; capping to poolCount`, + ); + } + const fetchCount = Math.min(eventCount, poolCount); + const startIndex = Math.max(0, poolCount - fetchCount); + + const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); + const seenAddrs = loadExistingPoolAddrs(outPath); + let allRecords = safeReadJson(outPath) ?? []; + let newCount = 0; + + for (let i = startIndex; i < poolCount; i++) { + const curvePool = await limit(async () => { + await rateLimit(); + const addr = await contract.pool_list(i); + return ethers.getAddress(addr); + }); + + if (seenAddrs.has(curvePool.toLowerCase())) continue; + seenAddrs.add(curvePool.toLowerCase()); + + const meta = await limit(async () => { + await rateLimit(); + const [nCoinsBn, coinsRaw] = await Promise.all([ + contract.get_n_coins(curvePool) as Promise, + contract.get_coins(curvePool) as Promise, + ]); + const coins = uniqAddresses(coinsRaw as string[]); + return { nCoins: Number(nCoinsBn), coins }; + }); + + allRecords.push({ + poolType: "stableswapng", + curvePool, + tokens: meta.coins, + fee: null, + tickSpacing: null, + sqrtPriceX96: null, + }); + newCount++; + } + + if (newCount > 0) { + atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + "\n"); + } + + console.log( + JSON.stringify( + { + ok: true, + chainId, + factory, + scanned: { fromBlock, toBlock, latestBlock, finality, chunkBlocks }, + eventCount, + poolsFetched: fetchCount, + newPools: newCount, + outFile: outPath, + checkpointFile: cpPath, + }, + null, + 2, + ), + ); +} + +main().catch((err) => { + console.error(err); + process.exitCode = 1; +}); From 2640f993f3fa291a0d35315f5166771d055929ee Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Tue, 3 Mar 2026 10:41:19 -0500 Subject: [PATCH 07/21] minor fixes on polling --- aggregator-hooks/polling/FluidDexLite.ts | 25 +---- aggregator-hooks/polling/FluidDexT1.ts | 56 +++-------- aggregator-hooks/polling/StableSwapNG.ts | 115 +++++++++++------------ 3 files changed, 71 insertions(+), 125 deletions(-) diff --git a/aggregator-hooks/polling/FluidDexLite.ts b/aggregator-hooks/polling/FluidDexLite.ts index 6ade57d0..1d63c416 100644 --- a/aggregator-hooks/polling/FluidDexLite.ts +++ b/aggregator-hooks/polling/FluidDexLite.ts @@ -26,13 +26,7 @@ import "dotenv/config"; import fs from "node:fs"; import path from "node:path"; import { ethers } from "ethers"; -import { - parseArgs, - getEnvForChain, - toInt, - resolveOutputPath, - resolveCheckpointPath, -} from "@src/cli"; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath, resolveCheckpointPath } from "@src/cli"; const OUTPUT_FILE = "fluiddexlite-pools.json"; const CHECKPOINT_FILE = "dexlite_checkpoint.json"; @@ -42,9 +36,7 @@ const FLUID_NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; function toUniswapV4Currency(addr: string): string { - return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() - ? ZERO_ADDRESS - : ethers.getAddress(addr); + return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() ? ZERO_ADDRESS : ethers.getAddress(addr); } type Checkpoint = { @@ -167,11 +159,7 @@ async function main() { let fromBlock: number; if (startBlockArg != null) { fromBlock = Math.max(0, toInt(startBlockArg, 0)); - } else if ( - cp && - cp.chainId === chainId && - cp.dexLite.toLowerCase() === dexLite.toLowerCase() - ) { + } else if (cp && cp.chainId === chainId && cp.dexLite.toLowerCase() === dexLite.toLowerCase()) { fromBlock = cp.lastProcessedBlock + 1; } else { fromBlock = Math.max(0, toBlock - lookbackBlocks); @@ -261,8 +249,7 @@ async function main() { } if (newRecords.length > 0) { - const existing = - safeReadJson(outPath) ?? []; + const existing = safeReadJson(outPath) ?? []; const merged = existing.concat(newRecords); atomicWriteFile(outPath, JSON.stringify(merged, null, 2) + "\n"); } @@ -275,9 +262,7 @@ async function main() { }; atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + "\n"); - console.error( - `[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`, - ); + console.error(`[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`); } console.log( diff --git a/aggregator-hooks/polling/FluidDexT1.ts b/aggregator-hooks/polling/FluidDexT1.ts index 0e2bd54c..bed57928 100644 --- a/aggregator-hooks/polling/FluidDexT1.ts +++ b/aggregator-hooks/polling/FluidDexT1.ts @@ -27,13 +27,7 @@ import "dotenv/config"; import fs from "node:fs"; import path from "node:path"; import { ethers } from "ethers"; -import { - parseArgs, - getEnvForChain, - toInt, - resolveOutputPath, - resolveCheckpointPath, -} from "@src/cli"; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath, resolveCheckpointPath } from "@src/cli"; const OUTPUT_FILE = "fluiddext1-pools.json"; const CHECKPOINT_FILE = "fluiddext1_checkpoint.json"; @@ -44,9 +38,7 @@ const FLUID_NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; function toUniswapV4Currency(addr: string): string { - return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() - ? ZERO_ADDRESS - : ethers.getAddress(addr); + return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() ? ZERO_ADDRESS : ethers.getAddress(addr); } type Checkpoint = { @@ -67,13 +59,9 @@ type CreatePoolsFluidDexT1Config = { sqrtPriceX96: string | null; }; -const FACTORY_ABI = [ - "event LogDexDeployed(address indexed dex, uint256 indexed dexId)", -]; +const FACTORY_ABI = ["event LogDexDeployed(address indexed dex, uint256 indexed dexId)"]; -const RESOLVER_ABI = [ - "function getPoolTokens(address pool) external view returns (address token0, address token1)", -]; +const RESOLVER_ABI = ["function getPoolTokens(address pool) external view returns (address token0, address token1)"]; function ensureDirForFile(filePath: string) { fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true }); @@ -100,8 +88,7 @@ function loadExistingKeys(outFile: string): Set { try { const arr = safeReadJson(outFile); if (!Array.isArray(arr)) return keys; - for (const x of arr) - keys.add(`${x.fluidPool}:${x.currency0}:${x.currency1}`); + for (const x of arr) keys.add(`${x.fluidPool}:${x.currency0}:${x.currency1}`); } catch { // ignore } @@ -140,9 +127,7 @@ async function main() { const rpcUrl = getEnvForChain("RPC_URL", chainId); const resolverAddr = getEnvForChain("FLUID_DEX_RESOLVER", chainId); const factoryRaw = - getEnvForChain("FLUID_DEX_FACTORY", chainId) ?? - getEnvForChain("FACTORY_ADDRESS", chainId) ?? - DEFAULT_FACTORY; + getEnvForChain("FLUID_DEX_FACTORY", chainId) ?? getEnvForChain("FACTORY_ADDRESS", chainId) ?? DEFAULT_FACTORY; if (!rpcUrl || !resolverAddr) { throw new Error("Missing required env: RPC_URL and FLUID_DEX_RESOLVER"); @@ -159,11 +144,7 @@ async function main() { const startBlockArg = args["start-block"]; const provider = new ethers.JsonRpcProvider(rpcUrl); - const resolver = new ethers.Contract( - ethers.getAddress(resolverAddr), - RESOLVER_ABI, - provider, - ); + const resolver = new ethers.Contract(ethers.getAddress(resolverAddr), RESOLVER_ABI, provider); const iface = new ethers.Interface(FACTORY_ABI as unknown as string[]); const topic0 = iface.getEvent("LogDexDeployed")!.topicHash; @@ -177,10 +158,7 @@ async function main() { let fromBlock: number; if (startBlockArg != null) { fromBlock = Math.max(0, toInt(startBlockArg, 0)); - } else if ( - cp?.chainId === chainId && - cp?.factory?.toLowerCase() === factory.toLowerCase() - ) { + } else if (cp?.chainId === chainId && cp?.factory?.toLowerCase() === factory.toLowerCase()) { fromBlock = cp.lastProcessedBlock + 1; } else { fromBlock = Math.max(0, toBlock - lookbackBlocks); @@ -193,15 +171,8 @@ async function main() { lastProcessedBlock: toBlock, updatedAt: new Date().toISOString(), }; - ensureDirForFile(cpPath); - fs.writeFileSync(cpPath, JSON.stringify(newCp, null, 2) + "\n"); - console.log( - JSON.stringify( - { ok: true, note: "No new blocks to scan", fromBlock, toBlock }, - null, - 2, - ), - ); + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); + console.log(JSON.stringify({ ok: true, note: "No new blocks to scan", fromBlock, toBlock }, null, 2)); return; } @@ -272,11 +243,8 @@ async function main() { lastProcessedBlock: end, updatedAt: new Date().toISOString(), }; - ensureDirForFile(cpPath); - fs.writeFileSync(cpPath, JSON.stringify(interimCp, null, 2) + "\n"); - console.error( - `[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`, - ); + atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + "\n"); + console.error(`[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`); } console.log( diff --git a/aggregator-hooks/polling/StableSwapNG.ts b/aggregator-hooks/polling/StableSwapNG.ts index f095db9b..8c371446 100644 --- a/aggregator-hooks/polling/StableSwapNG.ts +++ b/aggregator-hooks/polling/StableSwapNG.ts @@ -29,13 +29,7 @@ import "dotenv/config"; import fs from "node:fs"; import path from "node:path"; import { ethers } from "ethers"; -import { - parseArgs, - getEnvForChain, - toInt, - resolveOutputPath, - resolveCheckpointPath, -} from "@src/cli"; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath, resolveCheckpointPath } from "@src/cli"; const OUTPUT_FILE = "stableswapng-pools.json"; const CHECKPOINT_FILE = "stableswapng_checkpoint.json"; @@ -45,6 +39,7 @@ type Checkpoint = { chainId: number; factory: string; lastProcessedBlock: number; + lastKnownPoolCount: number; updatedAt: string; }; @@ -106,8 +101,7 @@ function pRateLimit(rps: number): () => Promise { let nextAllowed = 0; return async function acquire() { const now = Date.now(); - if (now < nextAllowed) - await new Promise((r) => setTimeout(r, nextAllowed - now)); + if (now < nextAllowed) await new Promise((r) => setTimeout(r, nextAllowed - now)); nextAllowed = Math.max(now, nextAllowed) + minGapMs; }; } @@ -160,8 +154,7 @@ async function main() { } const rpcUrl = getEnvForChain("RPC_URL", chainId); - const factoryRaw = - getEnvForChain("FACTORY_ADDRESS", chainId) ?? DEFAULT_FACTORY; + const factoryRaw = getEnvForChain("FACTORY_ADDRESS", chainId) ?? DEFAULT_FACTORY; if (!rpcUrl) { throw new Error("Missing required env: RPC_URL (or RPC_URL_)"); @@ -177,10 +170,7 @@ async function main() { const lookbackBlocks = getEnvInt("LOOKBACK_BLOCKS", chainId, 200000); const startBlockArg = args["start-block"]; - const concurrency = Math.max( - 1, - toInt(getEnvForChain("CONCURRENCY", chainId), 8), - ); + const concurrency = Math.max(1, toInt(getEnvForChain("CONCURRENCY", chainId), 8)); const rps = toInt(getEnvForChain("RPS", chainId), 80); const rateLimit = pRateLimit(rps); const limit = pLimit(concurrency); @@ -201,68 +191,73 @@ async function main() { let fromBlock: number; if (startBlockArg != null) { fromBlock = Math.max(0, toInt(startBlockArg, 0)); - } else if ( - cp?.chainId === chainId && - cp?.factory?.toLowerCase() === factory.toLowerCase() - ) { + } else if (cp?.chainId === chainId && cp?.factory?.toLowerCase() === factory.toLowerCase()) { fromBlock = cp.lastProcessedBlock + 1; } else { fromBlock = Math.max(0, toBlock - lookbackBlocks); } - if (fromBlock > toBlock) { + const poolCountBn = await contract.pool_count(); + const poolCount = Number(poolCountBn); + if (!Number.isFinite(poolCount) || poolCount < 0) { + throw new Error(`Unexpected pool_count: ${poolCountBn}`); + } + + const lastKnownPoolCount = + cp?.chainId === chainId && cp?.factory?.toLowerCase() === factory.toLowerCase() ? cp.lastKnownPoolCount ?? 0 : 0; + + if (fromBlock > toBlock && poolCount <= lastKnownPoolCount) { const newCp: Checkpoint = { chainId, factory, lastProcessedBlock: toBlock, + lastKnownPoolCount: poolCount, updatedAt: new Date().toISOString(), }; atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); console.log( - JSON.stringify( - { ok: true, note: "No new blocks to scan", fromBlock, toBlock }, - null, - 2, - ), + JSON.stringify({ ok: true, note: "No new blocks or pools to process", fromBlock, toBlock, poolCount }, null, 2), ); return; } - // Scan for PlainPoolDeployed and MetaPoolDeployed events let eventCount = 0; - for (let start = fromBlock; start <= toBlock; start += chunkBlocks) { - const end = Math.min(toBlock, start + chunkBlocks - 1); - - const logs = await provider.getLogs({ - address: factory, - fromBlock: start, - toBlock: end, - topics: [[plainTopic, metaTopic]], - }); - - eventCount += logs.length; - console.error( - `[scan] ${start}..${end} events=${logs.length} total=${eventCount}`, - ); + if (fromBlock <= toBlock) { + for (let start = fromBlock; start <= toBlock; start += chunkBlocks) { + const end = Math.min(toBlock, start + chunkBlocks - 1); + + const logs = await provider.getLogs({ + address: factory, + fromBlock: start, + toBlock: end, + topics: [[plainTopic, metaTopic]], + }); + + eventCount += logs.length; + console.error(`[scan] ${start}..${end} events=${logs.length} total=${eventCount}`); + } } - // Update checkpoint - const newCp: Checkpoint = { - chainId, - factory, - lastProcessedBlock: toBlock, - updatedAt: new Date().toISOString(), - }; - atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); + const startIndex = Math.max(0, lastKnownPoolCount); + const fetchCount = poolCount - startIndex; - if (eventCount === 0) { + if (fetchCount <= 0) { + const newCp: Checkpoint = { + chainId, + factory, + lastProcessedBlock: Math.max(toBlock, cp?.lastProcessedBlock ?? 0), + lastKnownPoolCount: poolCount, + updatedAt: new Date().toISOString(), + }; + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); console.log( JSON.stringify( { ok: true, chainId, factory, - eventCount: 0, + eventCount, + newPools: 0, outFile: resolveOutputPath(outputDir, chainId, OUTPUT_FILE), checkpointFile: cpPath, }, @@ -273,17 +268,6 @@ async function main() { return; } - // Fetch the last eventCount pools: indices (poolCount - eventCount) .. (poolCount - 1) - const poolCountBn = await contract.pool_count(); - const poolCount = Number(poolCountBn); - if (!Number.isFinite(poolCount) || poolCount < eventCount) { - console.error( - `pool_count=${poolCount} < eventCount=${eventCount}; capping to poolCount`, - ); - } - const fetchCount = Math.min(eventCount, poolCount); - const startIndex = Math.max(0, poolCount - fetchCount); - const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); const seenAddrs = loadExistingPoolAddrs(outPath); let allRecords = safeReadJson(outPath) ?? []; @@ -324,6 +308,15 @@ async function main() { atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + "\n"); } + const newCp: Checkpoint = { + chainId, + factory, + lastProcessedBlock: Math.max(toBlock, cp?.lastProcessedBlock ?? 0), + lastKnownPoolCount: poolCount, + updatedAt: new Date().toISOString(), + }; + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); + console.log( JSON.stringify( { From 04081438a24d7ae2b70678a3eebcbaa723e54f0c Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Tue, 3 Mar 2026 18:16:12 -0500 Subject: [PATCH 08/21] add poolId to deployed pool json --- aggregator-hooks/src/createPools.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/aggregator-hooks/src/createPools.ts b/aggregator-hooks/src/createPools.ts index 60910299..5440b2ca 100644 --- a/aggregator-hooks/src/createPools.ts +++ b/aggregator-hooks/src/createPools.ts @@ -15,12 +15,22 @@ import { type Address, type PoolConfig, type PoolDeployedEntry, + type PoolKeyRecord, type FactoryImmutables, } from "../creation-modules/index.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const projectRoot = join(__dirname, "..", ".."); +/** Compute PoolId = keccak256(abi.encode(poolKey)) matching Uniswap v4 PoolIdLibrary.toId() */ +function computePoolId(poolKey: PoolKeyRecord): string { + const encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "uint24", "int24", "address"], + [poolKey.currency0, poolKey.currency1, poolKey.fee, poolKey.tickSpacing, poolKey.hooks], + ); + return ethers.keccak256(encoded); +} + // Foundry's default CREATE2 deployer const CREATE2_DEPLOYER = "0x4e59b44847b379578588920cA78FbF26c0B4956C"; @@ -682,6 +692,7 @@ async function main() { metadata: { externalPool: module.getExternalPool(poolConfig), hookAddress, + poolId: computePoolId(poolKeys[0]), blockNumber, }, }, @@ -765,6 +776,7 @@ async function main() { metadata: { externalPool: module.getExternalPool(poolConfig), hookAddress: result.hookAddress, + poolId: computePoolId(poolKeys[0]), txHash: result.txHash, blockNumber: result.blockNumber, }, From ea35a11100d82adccb0a742fa3d9cd9f380a51d5 Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Mon, 9 Mar 2026 21:56:59 -0400 Subject: [PATCH 09/21] add verification to agg hook deployments --- aggregator-hooks/README.md | 101 ++++++-- .../creation-modules/FluidDexLite.ts | 2 + .../creation-modules/FluidDexT1.ts | 2 + .../creation-modules/StableSwap.ts | 2 + .../creation-modules/StableSwapNG.ts | 2 + aggregator-hooks/creation-modules/types.ts | 3 + aggregator-hooks/src/createPools.ts | 219 +++++++++++++++--- aggregator-hooks/src/logger.ts | 2 + 8 files changed, 280 insertions(+), 53 deletions(-) diff --git a/aggregator-hooks/README.md b/aggregator-hooks/README.md index d5191562..626393cb 100644 --- a/aggregator-hooks/README.md +++ b/aggregator-hooks/README.md @@ -136,6 +136,12 @@ npx tsx polling/StableSwapNG.ts --chain-id 1 --checkpoint-dir checkpoints # createPools with chain-id (uses RPC_URL_1 from env) npx tsx src/createPools.ts detected/1/fluiddexlite-pools-curated.json 0xFactoryAddr --chain-id 1 + +# createPools with Etherscan verification (ETHERSCAN_API_KEY must be set) +npx tsx src/createPools.ts detected/1/fluiddexlite-pools-curated.json 0xFactoryAddr --chain-id 1 --verify + +# createPools self-deploy with Blockscout verification (BLOCKSCOUT_API_URL_1 must be set in .env) +npx tsx src/createPools.ts detected/1/fluiddext1-pools-curated.json --self-deploy --chain-id 1 --verify ``` --- @@ -144,18 +150,22 @@ npx tsx src/createPools.ts detected/1/fluiddexlite-pools-curated.json 0xFactoryA ### Arguments -| Arg | Required | Default | Description | -| ------------------------------ | -------- | --------------- | ---------------------------------------------------------------------------- | -| `jsonFile` | yes | — | Path to JSON file with pool configs (each must have `poolType`) | -| `factoryAddress` | yes\* | — | Factory contract address (\*required when not using `--self-deploy`) | -| `--self-deploy` | no | — | Deploy hooks from wallet instead of via factory | -| `--chain-id ` | no | — | Chain ID; selects `RPC_URL_` from env | -| `--registry-dir ` | no | `created-pools` | Append deployed pools to `deployed-.json` in this dir | -| `--dry-run` | no | — | Simulate forge scripts without broadcasting | -| `--verbose`, `-v` | no | — | Run forge scripts with `-vvvv` | -| `--start-at ` | no | 1 | Start at 1-based pool index (skip earlier pools). Use to resume. | -| `--jobs `, `-j ` | no | 1 | Parallel salt mining workers (1–16). Speeds up mining. | -| `--priority-gas-price ` | no | RPC default | Max priority fee per gas for EIP1559 (e.g. `3gwei`). Speeds up tx inclusion. | +| Arg | Required | Default | Description | +| ------------------------------ | -------- | --------------- | ----------------------------------------------------------------------------- | +| `jsonFile` | yes | — | Path to JSON file with pool configs (each must have `poolType`) | +| `factoryAddress` | yes\* | — | Factory contract address (\*required when not using `--self-deploy`) | +| `--self-deploy` | no | — | Deploy hooks from wallet instead of via factory | +| `--chain-id ` | no | — | Chain ID; selects `RPC_URL_` from env | +| `--registry-dir ` | no | `created-pools` | Append deployed pools to `deployed-.json` in this dir | +| `--dry-run` | no | — | Simulate forge scripts without broadcasting | +| `--verbose`, `-v` | no | — | Run forge scripts with `-vvvv` | +| `--start-at ` | no | 1 | Start at 1-based pool index (skip earlier pools). Use to resume. | +| `--jobs `, `-j ` | no | 1 | Parallel salt mining workers (1–16). Speeds up mining. | +| `--priority-gas-price ` | no | RPC default | Max priority fee per gas for EIP1559 (e.g. `3gwei`). Speeds up tx inclusion. | +| `--verify` | no | — | Submit hook contract for block explorer verification after deployment | +| `--verifier ` | no | `etherscan` | Verifier backend: `etherscan`, `blockscout`, or `sourcify` | +| `--verifier-url ` | no | — | Custom verifier API URL. Auto-selected if `BLOCKSCOUT_API_URL` env is set. | +| `--compiler-version ` | no | from artifact | Solc version to pass to the verifier (e.g. `0.8.24`). See note below. | **Modes:** @@ -164,10 +174,12 @@ npx tsx src/createPools.ts detected/1/fluiddexlite-pools-curated.json 0xFactoryA ### Environment variables -| Env | Description | -| -------------------------------- | ------------------------------------------------------------- | -| `RPC_URL` or `RPC_URL_` | RPC endpoint (use `RPC_URL_1` etc. when `--chain-id` is set) | -| `PRIVATE_KEY` | Signing key for transactions (required even with `--dry-run`) | +| Env | Description | +| -------------------------------------------- | -------------------------------------------------------------------------------------- | +| `RPC_URL` or `RPC_URL_` | RPC endpoint (use `RPC_URL_1` etc. when `--chain-id` is set) | +| `PRIVATE_KEY` | Signing key for transactions (required even with `--dry-run`) | +| `ETHERSCAN_API_KEY` or `ETHERSCAN_API_KEY_` | API key for Etherscan verification (required when using `--verify` with Etherscan) | +| `BLOCKSCOUT_API_URL` or `BLOCKSCOUT_API_URL_` | Blockscout API URL. If set, `--verify` automatically uses Blockscout. | ### Security @@ -183,6 +195,63 @@ npx tsx src/createPools.ts your-pools.json --self-deploy --chain-id 1 --dry-run Use `--verbose` or `-v` to run forge scripts with maximum verbosity (`-vvvv`) and log full forge output on errors. +### Contract verification + +Pass `--verify` to automatically submit each deployed hook for block explorer verification after deployment. Verification failures are non-fatal — the script logs a warning and continues. + +**Etherscan:** + +```bash +# Requires ETHERSCAN_API_KEY or ETHERSCAN_API_KEY_1 in env +npx tsx src/createPools.ts pools.json 0xFactory --chain-id 1 --verify +npx tsx src/createPools.ts pools.json --self-deploy --chain-id 1 --verify +``` + +**Blockscout (via `--verifier-url`):** + +```bash +npx tsx src/createPools.ts pools.json 0xFactory --chain-id 1 --verify \ + --verifier blockscout \ + --verifier-url https://eth.blockscout.com/api +``` + +**Blockscout (via env var, no extra flags needed):** + +Set `BLOCKSCOUT_API_URL` (or `BLOCKSCOUT_API_URL_`) in `.env`. When this is set, `--verify` automatically uses Blockscout without needing `--verifier` or `--verifier-url` on the command line: + +```bash +# .env +BLOCKSCOUT_API_URL_1=https://eth.blockscout.com/api +BLOCKSCOUT_API_URL_8453=https://base.blockscout.com/api + +# Then just: +npx tsx src/createPools.ts pools.json 0xFactory --chain-id 1 --verify +``` + +**Well-known Blockscout API URLs:** + +| Chain | Chain ID | URL | +| ------- | -------- | --------------------------------------- | +| Mainnet | 1 | `https://eth.blockscout.com/api` | +| Base | 8453 | `https://base.blockscout.com/api` | +| Arbitrum| 42161 | `https://arbitrum.blockscout.com/api` | +| Optimism| 10 | `https://optimism.blockscout.com/api` | + +> **Note:** Blockscout's public instances do not require an API key. `ETHERSCAN_API_KEY` can be omitted when using Blockscout. + +#### Compiler version and factory mode + +In factory mode, the factory embeds the hook's bytecode at *its own* compile time. Verification must use the same solc version the factory was compiled with — not necessarily the version currently installed locally. + +The script auto-detects the compiler version from the build artifact in `out/` (populated by `forge build`). This is reliable **as long as** the installed forge/solc version matches what built the factory. If it doesn't, pass `--compiler-version` explicitly: + +```bash +# Factory was compiled with 0.8.24; local solc is different +npx tsx src/createPools.ts pools.json 0xFactory --chain-id 1 --verify --compiler-version 0.8.24 +``` + +To check what version the factory was originally built with, look at the git history or the `out/` artifact at the time of factory deployment. In self-deploy mode this is not an issue — forge compiles and verifies with the same local version automatically. + --- ## Setup diff --git a/aggregator-hooks/creation-modules/FluidDexLite.ts b/aggregator-hooks/creation-modules/FluidDexLite.ts index f124e4c7..d70a7cc7 100644 --- a/aggregator-hooks/creation-modules/FluidDexLite.ts +++ b/aggregator-hooks/creation-modules/FluidDexLite.ts @@ -24,6 +24,8 @@ export const fluiddexliteModule: CreationModule = { poolType: "fluiddexlite", protocolId: PROTOCOL_ID, factoryAbi: FLUIDDEXLITE_FACTORY_ABI, + contractIdentifier: + "lib/v4-hooks-public/src/aggregator-hooks/implementations/FluidDexLite/FluidDexLiteAggregator.sol:FluidDexLiteAggregator", getHookParams(config) { return { diff --git a/aggregator-hooks/creation-modules/FluidDexT1.ts b/aggregator-hooks/creation-modules/FluidDexT1.ts index 6f70343e..6b347021 100644 --- a/aggregator-hooks/creation-modules/FluidDexT1.ts +++ b/aggregator-hooks/creation-modules/FluidDexT1.ts @@ -24,6 +24,8 @@ export const fluiddext1Module: CreationModule = { poolType: "fluiddext1", protocolId: PROTOCOL_ID, factoryAbi: FLUIDDEXT1_FACTORY_ABI, + contractIdentifier: + "lib/v4-hooks-public/src/aggregator-hooks/implementations/FluidDexT1/FluidDexT1Aggregator.sol:FluidDexT1Aggregator", getHookParams(config) { return { diff --git a/aggregator-hooks/creation-modules/StableSwap.ts b/aggregator-hooks/creation-modules/StableSwap.ts index a9f14072..322ff2d6 100644 --- a/aggregator-hooks/creation-modules/StableSwap.ts +++ b/aggregator-hooks/creation-modules/StableSwap.ts @@ -29,6 +29,8 @@ export const stableswapModule: CreationModule = { poolType: "stableswap", protocolId: PROTOCOL_ID, factoryAbi: STABLESWAP_FACTORY_ABI, + contractIdentifier: + "lib/v4-hooks-public/src/aggregator-hooks/implementations/StableSwap/StableSwapAggregator.sol:StableSwapAggregator", getHookParams(config) { return { diff --git a/aggregator-hooks/creation-modules/StableSwapNG.ts b/aggregator-hooks/creation-modules/StableSwapNG.ts index c924ce9b..401efda2 100644 --- a/aggregator-hooks/creation-modules/StableSwapNG.ts +++ b/aggregator-hooks/creation-modules/StableSwapNG.ts @@ -29,6 +29,8 @@ export const stableswapngModule: CreationModule = { poolType: "stableswapng", protocolId: PROTOCOL_ID, factoryAbi: STABLESWAP_FACTORY_ABI, + contractIdentifier: + "lib/v4-hooks-public/src/aggregator-hooks/implementations/StableSwapNG/StableSwapNGAggregator.sol:StableSwapNGAggregator", getHookParams(config) { return { diff --git a/aggregator-hooks/creation-modules/types.ts b/aggregator-hooks/creation-modules/types.ts index 5d57de0e..7a87e6aa 100644 --- a/aggregator-hooks/creation-modules/types.ts +++ b/aggregator-hooks/creation-modules/types.ts @@ -64,6 +64,9 @@ export interface CreationModule { /** Factory contract ABI for createPool and reading immutables */ factoryAbi: string[]; + /** Solidity contract identifier for forge verify-contract (path:ContractName) */ + contractIdentifier: string; + /** Resolve hook params with defaults */ getHookParams(config: TConfig): HookParams; diff --git a/aggregator-hooks/src/createPools.ts b/aggregator-hooks/src/createPools.ts index 5440b2ca..d6779334 100644 --- a/aggregator-hooks/src/createPools.ts +++ b/aggregator-hooks/src/createPools.ts @@ -34,6 +34,14 @@ function computePoolId(poolKey: PoolKeyRecord): string { // Foundry's default CREATE2 deployer const CREATE2_DEPLOYER = "0x4e59b44847b379578588920cA78FbF26c0B4956C"; +interface VerifyOptions { + enabled: boolean; + etherscanApiKey: string | null; + verifierUrl: string | null; + verifier: string | null; + compilerVersion: string | null; +} + interface ParsedArgs { jsonFile: string; factoryAddress: Address | null; @@ -46,6 +54,7 @@ interface ParsedArgs { startAt: number; jobs: number; priorityGasPrice: string | null; + verify: VerifyOptions; } function isPoolType(s: unknown): s is string { @@ -68,6 +77,10 @@ function parseArgs(): ParsedArgs { "--jobs", "-j", "--priority-gas-price", + "--verify", + "--verifier-url", + "--verifier", + "--compiler-version", ]; const positionalArgs: string[] = []; for (let i = 0; i < args.length; i++) { @@ -79,7 +92,10 @@ function parseArgs(): ParsedArgs { a === "--start-at" || a === "--jobs" || a === "-j" || - a === "--priority-gas-price" + a === "--priority-gas-price" || + a === "--verifier-url" || + a === "--verifier" || + a === "--compiler-version" ) i++; continue; @@ -113,10 +129,18 @@ function parseArgs(): ParsedArgs { console.error( " --priority-gas-price : Max priority fee per gas for EIP1559 (e.g. 3gwei). Speeds up tx inclusion.", ); + console.error(" --verify: Submit hook contract for block explorer verification after deployment"); + console.error(" --verifier : Verifier backend (etherscan|blockscout|sourcify). Default: etherscan"); + console.error(" --verifier-url : Custom verifier API URL (e.g. for blockscout)"); + console.error( + " --compiler-version : Solc version used to compile the hook (e.g. 0.8.24). Required when the factory", + ); + console.error(" was deployed with a different solc than the current local environment."); console.error(""); console.error("Environment variables:"); console.error(" RPC_URL_: RPC endpoint (required when --chain-id set)"); console.error(" PRIVATE_KEY: Private key for signing transactions (required)"); + console.error(" ETHERSCAN_API_KEY or ETHERSCAN_API_KEY_: API key for block explorer verification"); process.exit(1); } @@ -124,8 +148,8 @@ function parseArgs(): ParsedArgs { const factoryAddress: Address | null = selfDeploy ? null : positionalArgs[1] - ? (ethers.getAddress(positionalArgs[1]) as Address) - : null; + ? (ethers.getAddress(positionalArgs[1]) as Address) + : null; if (selfDeploy && positionalArgs.length >= 2 && positionalArgs[1].startsWith("0x")) { console.error("Error: --self-deploy and factoryAddress are mutually exclusive"); @@ -189,6 +213,15 @@ function parseArgs(): ParsedArgs { priorityGasPriceIndex !== -1 && args[priorityGasPriceIndex + 1] ? args[priorityGasPriceIndex + 1] : null; const priorityGasPrice = priorityGasPriceRaw?.trim() || null; + const verifyEnabled = args.includes("--verify"); + const verifierIndex = args.indexOf("--verifier"); + const verifier = verifierIndex !== -1 && args[verifierIndex + 1] ? args[verifierIndex + 1] : null; + const verifierUrlIndex = args.indexOf("--verifier-url"); + const verifierUrl = verifierUrlIndex !== -1 && args[verifierUrlIndex + 1] ? args[verifierUrlIndex + 1] : null; + const compilerVersionIndex = args.indexOf("--compiler-version"); + const compilerVersion = + compilerVersionIndex !== -1 && args[compilerVersionIndex + 1] ? args[compilerVersionIndex + 1] : null; + return { jsonFile, factoryAddress, @@ -201,6 +234,13 @@ function parseArgs(): ParsedArgs { startAt, jobs, priorityGasPrice, + verify: { + enabled: verifyEnabled, + etherscanApiKey: null, // resolved later from env once chainId is known + verifierUrl, + verifier, + compilerVersion, + }, }; } @@ -225,6 +265,98 @@ function appendToRegistryFile(registryDir: string, poolType: string, entry: Pool log.info(`Appended to registry: ${filePath}`); } +/** + * Read the solc version from a forge build artifact. + * The artifact lives at out/.sol/.json and + * contains a "metadata" object with compiler.version (e.g. "0.8.24+commit.e11b9ed9"). + * Returns null if the artifact is missing or unparseable. + */ +function readCompilerVersionFromArtifact(contractIdentifier: string): string | null { + try { + // contractIdentifier: "path/to/Foo.sol:Foo" + const contractName = contractIdentifier.split(":")[1]; + const solFile = contractIdentifier.split(":")[0].split("/").pop()!; // "Foo.sol" + const artifactPath = join(projectRoot, "out", solFile, `${contractName}.json`); + if (!existsSync(artifactPath)) return null; + const artifact = JSON.parse(readFileSync(artifactPath, "utf-8")) as { + metadata?: { compiler?: { version?: string } }; + }; + return artifact?.metadata?.compiler?.version ?? null; + } catch { + return null; + } +} + +function verifyContract( + hookAddress: Address, + contractIdentifier: string, + constructorArgs: string, + chainId: number, + verifyOptions: VerifyOptions, + log: Logger, +): void { + // Resolve verifier URL: explicit flag > BLOCKSCOUT_API_URL env > none + const resolvedVerifierUrl = verifyOptions.verifierUrl ?? getEnvForChain("BLOCKSCOUT_API_URL", chainId) ?? null; + + // If a Blockscout URL is in play but no verifier was explicitly named, use blockscout + const isBlockscout = + verifyOptions.verifier === "blockscout" || (!verifyOptions.verifier && resolvedVerifierUrl != null); + const verifier = verifyOptions.verifier ?? (isBlockscout ? "blockscout" : "etherscan"); + + // API key: explicit flag > ETHERSCAN_API_KEY env (blockscout public instances don't require one) + const apiKey = verifyOptions.etherscanApiKey ?? getEnvForChain("ETHERSCAN_API_KEY", chainId) ?? null; + + // Compiler version: explicit flag > build artifact > let forge auto-detect + // In factory mode this matters: the factory embeds the hook bytecode at its own compile time, + // so the solc version used then must match what forge verify-contract uses now. + const compilerVersion = verifyOptions.compilerVersion ?? readCompilerVersionFromArtifact(contractIdentifier); + if (!compilerVersion) { + log.info( + ` Warning: compiler version not found in build artifacts. If the factory was compiled with a different` + + ` solc than is installed locally, verification may fail. Pass --compiler-version to fix this.`, + ); + } else { + log.info(` Compiler version: ${compilerVersion}`); + } + + log.info(`Submitting ${contractIdentifier} at ${hookAddress} for verification (verifier: ${verifier})...`); + + const forgeArgs = [ + "verify-contract", + hookAddress, + contractIdentifier, + "--constructor-args", + constructorArgs, + "--chain-id", + chainId.toString(), + "--verifier", + verifier, + "--watch", + ]; + + if (apiKey) forgeArgs.push("--etherscan-api-key", apiKey); + if (resolvedVerifierUrl) forgeArgs.push("--verifier-url", resolvedVerifierUrl); + if (compilerVersion) forgeArgs.push("--compiler-version", compilerVersion); + + try { + const output = execFileSync("forge", forgeArgs, { + encoding: "utf-8", + cwd: projectRoot, + }); + log.success(`Verification submitted successfully for ${hookAddress}`); + if (output.trim()) log.verbose(`\n--- forge verify-contract output ---\n${output}`); + } catch (error) { + const execErr = error as { stdout?: string; stderr?: string; message?: string }; + log.error( + `Verification failed for ${hookAddress} (non-fatal): ${execErr.message ?? "unknown error"}`, + error instanceof Error ? error : new Error(String(error)), + ); + if (execErr.stdout || execErr.stderr) { + log.dumpForgeOutput({ stdout: execErr.stdout, stderr: execErr.stderr, label: "forge verify-contract" }); + } + } +} + function parseSqrtPriceX96(v: unknown): bigint | null { if (v == null) return null; if (typeof v === "bigint") return v; @@ -618,6 +750,7 @@ async function main() { startAt, jobs, priorityGasPrice, + verify, } = parseArgs(); const log = createLogger({ verbose }); @@ -638,6 +771,7 @@ async function main() { startAt, jobs, priorityGasPrice, + verify: verify.enabled, signerAddress: signer.address, }); @@ -680,24 +814,29 @@ async function main() { priorityGasPrice, ); - if (registryDir && hookAddress && hookAddress !== "deployed") { - const poolKeys = module.buildPoolKeys(poolConfig, hookAddress); - if (poolKeys.length > 0) { - const blockNumber = Number(await provider.getBlockNumber()); - appendToRegistryFile( - registryDir, - poolType, - { - poolKeys, - metadata: { - externalPool: module.getExternalPool(poolConfig), - hookAddress, - poolId: computePoolId(poolKeys[0]), - blockNumber, + if (hookAddress && hookAddress !== "deployed") { + if (registryDir) { + const poolKeys = module.buildPoolKeys(poolConfig, hookAddress); + if (poolKeys.length > 0) { + const blockNumber = Number(await provider.getBlockNumber()); + appendToRegistryFile( + registryDir, + poolType, + { + poolKeys, + metadata: { + externalPool: module.getExternalPool(poolConfig), + hookAddress, + poolId: computePoolId(poolKeys[0]), + blockNumber, + }, }, - }, - log, - ); + log, + ); + } + } + if (verify.enabled && !dryRun) { + verifyContract(hookAddress, module.contractIdentifier, constructorArgs, chainId, verify, log); } } log.success(`Successfully created pool ${i + 1}`); @@ -748,6 +887,7 @@ async function main() { log.info("Reading factory immutables..."); const factoryImmutables = await module.readFactoryImmutables(provider, factoryAddress!); + const chainId = parsedChainId ?? Number((await provider.getNetwork()).chainId); log.info(`POOL_MANAGER: ${factoryImmutables.poolManager}`); for (const [key, val] of Object.entries(factoryImmutables)) { if (key !== "poolManager" && val) log.info(`${key}: ${val}`); @@ -765,24 +905,29 @@ async function main() { const salt = await mineSalt(constructorArgs, module.protocolId, log, factoryAddress!, jobs); const result = await createPool(signer, factoryAddress!, poolConfig, poolType, salt, log); - if (registryDir && result.hookAddress) { - const poolKeys = module.buildPoolKeys(poolConfig, result.hookAddress); - if (poolKeys.length > 0) { - appendToRegistryFile( - registryDir, - poolType, - { - poolKeys, - metadata: { - externalPool: module.getExternalPool(poolConfig), - hookAddress: result.hookAddress, - poolId: computePoolId(poolKeys[0]), - txHash: result.txHash, - blockNumber: result.blockNumber, + if (result.hookAddress) { + if (registryDir) { + const poolKeys = module.buildPoolKeys(poolConfig, result.hookAddress); + if (poolKeys.length > 0) { + appendToRegistryFile( + registryDir, + poolType, + { + poolKeys, + metadata: { + externalPool: module.getExternalPool(poolConfig), + hookAddress: result.hookAddress, + poolId: computePoolId(poolKeys[0]), + txHash: result.txHash, + blockNumber: result.blockNumber, + }, }, - }, - log, - ); + log, + ); + } + } + if (verify.enabled) { + verifyContract(result.hookAddress, module.contractIdentifier, constructorArgs, chainId, verify, log); } } log.success(`Successfully created pool ${i + 1}`); diff --git a/aggregator-hooks/src/logger.ts b/aggregator-hooks/src/logger.ts index 17d17061..8fda0cbd 100644 --- a/aggregator-hooks/src/logger.ts +++ b/aggregator-hooks/src/logger.ts @@ -13,6 +13,7 @@ export interface BannerConfig { startAt?: number; jobs?: number; priorityGasPrice?: string | null; + verify?: boolean; signerAddress?: string; } @@ -64,6 +65,7 @@ export function createLogger(opts: { verbose: boolean }): Logger { : []), ...(config.jobs && config.jobs > 1 ? [`Salt mining: ${config.jobs} parallel workers`] : []), ...(config.priorityGasPrice ? [`Priority gas price: ${config.priorityGasPrice}`] : []), + ...(config.verify ? ["Contract verification: enabled (--verify)"] : []), ...(config.signerAddress ? [`Using signer: ${config.signerAddress}`] : []), ]; lines.forEach((line) => console.log(line)); From 954fc0072a72b87d511a82530917d136c3b91d3d Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Wed, 11 Mar 2026 12:23:11 -0400 Subject: [PATCH 10/21] use self create hook from lib --- aggregator-hooks/.env.example | 7 + aggregator-hooks/README.md | 61 ++++---- aggregator-hooks/abis/StableswapFactory.json | 2 + aggregator-hooks/abis/index.ts | 1 + .../creation-modules/StableSwap.ts | 29 +++- .../creation-modules/StableSwapNG.ts | 35 ++++- aggregator-hooks/historical/StableSwapNG.ts | 26 ++-- aggregator-hooks/polling/StableSwapNG.ts | 9 +- foundry.lock | 2 +- lib/v4-hooks-public | 2 +- script/SelfCreateHook.s.sol | 144 ------------------ 11 files changed, 117 insertions(+), 201 deletions(-) delete mode 100644 script/SelfCreateHook.s.sol diff --git a/aggregator-hooks/.env.example b/aggregator-hooks/.env.example index 87d3d8eb..466ebff3 100644 --- a/aggregator-hooks/.env.example +++ b/aggregator-hooks/.env.example @@ -19,5 +19,12 @@ #FLUID_DEX_RESOLVER_= #FACTORY_ADDRESS_= +# StableSwap (legacy Curve): MetaRegistry for meta pool rejection +#STABLESWAP_METAREGISTRY_ADDRESS_= + +# StableSwap-NG: Curve factory address (for meta pool rejection). Defaults to mainnet if not set. +#STABLESWAPNG_FACTORY_ADDRESS_= +#FACTORY_ADDRESS_= # fallback for discovery scripts + diff --git a/aggregator-hooks/README.md b/aggregator-hooks/README.md index 626393cb..482b24c8 100644 --- a/aggregator-hooks/README.md +++ b/aggregator-hooks/README.md @@ -150,21 +150,21 @@ npx tsx src/createPools.ts detected/1/fluiddext1-pools-curated.json --self-deplo ### Arguments -| Arg | Required | Default | Description | -| ------------------------------ | -------- | --------------- | ----------------------------------------------------------------------------- | -| `jsonFile` | yes | — | Path to JSON file with pool configs (each must have `poolType`) | -| `factoryAddress` | yes\* | — | Factory contract address (\*required when not using `--self-deploy`) | -| `--self-deploy` | no | — | Deploy hooks from wallet instead of via factory | -| `--chain-id ` | no | — | Chain ID; selects `RPC_URL_` from env | -| `--registry-dir ` | no | `created-pools` | Append deployed pools to `deployed-.json` in this dir | -| `--dry-run` | no | — | Simulate forge scripts without broadcasting | -| `--verbose`, `-v` | no | — | Run forge scripts with `-vvvv` | -| `--start-at ` | no | 1 | Start at 1-based pool index (skip earlier pools). Use to resume. | -| `--jobs `, `-j ` | no | 1 | Parallel salt mining workers (1–16). Speeds up mining. | -| `--priority-gas-price ` | no | RPC default | Max priority fee per gas for EIP1559 (e.g. `3gwei`). Speeds up tx inclusion. | -| `--verify` | no | — | Submit hook contract for block explorer verification after deployment | -| `--verifier ` | no | `etherscan` | Verifier backend: `etherscan`, `blockscout`, or `sourcify` | -| `--verifier-url ` | no | — | Custom verifier API URL. Auto-selected if `BLOCKSCOUT_API_URL` env is set. | +| Arg | Required | Default | Description | +| ------------------------------ | -------- | --------------- | ---------------------------------------------------------------------------- | +| `jsonFile` | yes | — | Path to JSON file with pool configs (each must have `poolType`) | +| `factoryAddress` | yes\* | — | Factory contract address (\*required when not using `--self-deploy`) | +| `--self-deploy` | no | — | Deploy hooks from wallet instead of via factory | +| `--chain-id ` | no | — | Chain ID; selects `RPC_URL_` from env | +| `--registry-dir ` | no | `created-pools` | Append deployed pools to `deployed-.json` in this dir | +| `--dry-run` | no | — | Simulate forge scripts without broadcasting | +| `--verbose`, `-v` | no | — | Run forge scripts with `-vvvv` | +| `--start-at ` | no | 1 | Start at 1-based pool index (skip earlier pools). Use to resume. | +| `--jobs `, `-j ` | no | 1 | Parallel salt mining workers (1–16). Speeds up mining. | +| `--priority-gas-price ` | no | RPC default | Max priority fee per gas for EIP1559 (e.g. `3gwei`). Speeds up tx inclusion. | +| `--verify` | no | — | Submit hook contract for block explorer verification after deployment | +| `--verifier ` | no | `etherscan` | Verifier backend: `etherscan`, `blockscout`, or `sourcify` | +| `--verifier-url ` | no | — | Custom verifier API URL. Auto-selected if `BLOCKSCOUT_API_URL` env is set. | | `--compiler-version ` | no | from artifact | Solc version to pass to the verifier (e.g. `0.8.24`). See note below. | **Modes:** @@ -174,12 +174,17 @@ npx tsx src/createPools.ts detected/1/fluiddext1-pools-curated.json --self-deplo ### Environment variables -| Env | Description | -| -------------------------------------------- | -------------------------------------------------------------------------------------- | -| `RPC_URL` or `RPC_URL_` | RPC endpoint (use `RPC_URL_1` etc. when `--chain-id` is set) | -| `PRIVATE_KEY` | Signing key for transactions (required even with `--dry-run`) | -| `ETHERSCAN_API_KEY` or `ETHERSCAN_API_KEY_` | API key for Etherscan verification (required when using `--verify` with Etherscan) | -| `BLOCKSCOUT_API_URL` or `BLOCKSCOUT_API_URL_` | Blockscout API URL. If set, `--verify` automatically uses Blockscout. | +| Env | Description | +| ------------------------------------------------------ | ---------------------------------------------------------------------------------- | +| `RPC_URL` or `RPC_URL_` | RPC endpoint (use `RPC_URL_1` etc. when `--chain-id` is set) | +| `PRIVATE_KEY` | Signing key for transactions (required even with `--dry-run`) | +| `POOL_MANAGER` or `POOL_MANAGER_` | Uniswap v4 PoolManager address (required for self-deploy) | +| `ETHERSCAN_API_KEY` or `ETHERSCAN_API_KEY_` | API key for Etherscan verification (required when using `--verify` with Etherscan) | +| `BLOCKSCOUT_API_URL` or `BLOCKSCOUT_API_URL_` | Blockscout API URL. If set, `--verify` automatically uses Blockscout. | + +**StableSwap (stableswap):** `STABLESWAP_METAREGISTRY_ADDRESS_` — Curve MetaRegistry for meta pool rejection (required for self-deploy). + +**StableSwap-NG (stableswapng):** `STABLESWAPNG_FACTORY_ADDRESS_` or `FACTORY_ADDRESS_` — Curve StableSwap NG factory for meta pool rejection. Defaults to mainnet `0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf` if unset. ### Security @@ -230,18 +235,18 @@ npx tsx src/createPools.ts pools.json 0xFactory --chain-id 1 --verify **Well-known Blockscout API URLs:** -| Chain | Chain ID | URL | -| ------- | -------- | --------------------------------------- | -| Mainnet | 1 | `https://eth.blockscout.com/api` | -| Base | 8453 | `https://base.blockscout.com/api` | -| Arbitrum| 42161 | `https://arbitrum.blockscout.com/api` | -| Optimism| 10 | `https://optimism.blockscout.com/api` | +| Chain | Chain ID | URL | +| -------- | -------- | ------------------------------------- | +| Mainnet | 1 | `https://eth.blockscout.com/api` | +| Base | 8453 | `https://base.blockscout.com/api` | +| Arbitrum | 42161 | `https://arbitrum.blockscout.com/api` | +| Optimism | 10 | `https://optimism.blockscout.com/api` | > **Note:** Blockscout's public instances do not require an API key. `ETHERSCAN_API_KEY` can be omitted when using Blockscout. #### Compiler version and factory mode -In factory mode, the factory embeds the hook's bytecode at *its own* compile time. Verification must use the same solc version the factory was compiled with — not necessarily the version currently installed locally. +In factory mode, the factory embeds the hook's bytecode at _its own_ compile time. Verification must use the same solc version the factory was compiled with — not necessarily the version currently installed locally. The script auto-detects the compiler version from the build artifact in `out/` (populated by `forge build`). This is reliable **as long as** the installed forge/solc version matches what built the factory. If it doesn't, pass `--compiler-version` explicitly: diff --git a/aggregator-hooks/abis/StableswapFactory.json b/aggregator-hooks/abis/StableswapFactory.json index 4d2fd014..6e88dafb 100644 --- a/aggregator-hooks/abis/StableswapFactory.json +++ b/aggregator-hooks/abis/StableswapFactory.json @@ -1,4 +1,6 @@ [ "function POOL_MANAGER() external view returns (address)", + "function poolManager() external view returns (address)", + "function metaRegistry() external view returns (address)", "function createPool(bytes32 salt, address curvePool, address[] calldata tokens, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)" ] diff --git a/aggregator-hooks/abis/index.ts b/aggregator-hooks/abis/index.ts index 2207459b..1c38be67 100644 --- a/aggregator-hooks/abis/index.ts +++ b/aggregator-hooks/abis/index.ts @@ -6,6 +6,7 @@ import { createRequire } from "node:module"; const require = createRequire(import.meta.url); export const STABLESWAP_FACTORY_ABI: string[] = [...require("./StableswapFactory.json")]; +export const STABLESWAPNG_FACTORY_ABI: string[] = [...require("./StableSwapNGFactory.json")]; export const FLUIDDEXT1_FACTORY_ABI: string[] = [...require("./FluidDexT1Factory.json")]; export const FLUIDDEXLITE_FACTORY_ABI: string[] = [...require("./FluidDexLiteFactory.json")]; export const FLUIDDEXT1_HISTORICAL_FACTORY_ABI = require("./FluidDexT1HistoricalFactory.json") as readonly string[]; diff --git a/aggregator-hooks/creation-modules/StableSwap.ts b/aggregator-hooks/creation-modules/StableSwap.ts index 322ff2d6..d8e21a9a 100644 --- a/aggregator-hooks/creation-modules/StableSwap.ts +++ b/aggregator-hooks/creation-modules/StableSwap.ts @@ -1,8 +1,9 @@ /** * StableSwap (Curve) aggregator hook deployment module. + * Requires Curve MetaRegistry address for meta pool rejection. */ import { ethers } from "ethers"; -import { mustEnvForChain } from "../src/cli.js"; +import { getEnvForChain, mustEnvForChain } from "../src/cli.js"; import { STABLESWAP_FACTORY_ABI } from "../abis/index.js"; import { DEFAULT_SQRT_PRICE_X96, @@ -64,29 +65,47 @@ export const stableswapModule: CreationModule = { }, getImmutablesFromEnv(chainId: number): FactoryImmutables { + const metaRegistry = mustEnvForChain("STABLESWAP_METAREGISTRY_ADDRESS", chainId) as Address; return { poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, + metaRegistry, }; }, async readFactoryImmutables(provider, factoryAddress) { const factory = new ethers.Contract(factoryAddress, STABLESWAP_FACTORY_ABI, provider); - const poolManager = await factory.POOL_MANAGER(); - return { poolManager: poolManager as Address }; + let poolManagerRaw: string; + try { + poolManagerRaw = await factory.poolManager(); + } catch { + poolManagerRaw = await factory.POOL_MANAGER(); + } + const metaRegistryRaw = await factory.metaRegistry(); + const poolManager = ethers.getAddress(poolManagerRaw); + const metaRegistry = ethers.getAddress(metaRegistryRaw); + return { poolManager: poolManager as Address, metaRegistry: metaRegistry as Address }; }, encodeConstructorArgs(config, immutables) { + const metaRegistry = immutables.metaRegistry ?? (immutables as { metaRegistry?: Address }).metaRegistry; + if (!metaRegistry) { + throw new Error( + "StableSwap requires metaRegistry. Set STABLESWAP_METAREGISTRY_ADDRESS or use StableSwapAggregatorFactory.", + ); + } const encoded = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "address"], - [immutables.poolManager, config.curvePool], + ["address", "address", "address"], + [immutables.poolManager, config.curvePool, metaRegistry], ); return encoded.startsWith("0x") ? encoded : `0x${encoded}`; }, buildSelfDeployEnvVars(config, immutables) { const params = this.getHookParams(config); + const metaRegistry = immutables.metaRegistry ?? (immutables as { metaRegistry?: Address }).metaRegistry; return { CURVE_POOL: config.curvePool, + METAREGISTRY: metaRegistry ?? "", TOKENS: config.tokens.join(","), FEE: params.fee.toString(), TICK_SPACING: params.tickSpacing.toString(), diff --git a/aggregator-hooks/creation-modules/StableSwapNG.ts b/aggregator-hooks/creation-modules/StableSwapNG.ts index 401efda2..8303c89f 100644 --- a/aggregator-hooks/creation-modules/StableSwapNG.ts +++ b/aggregator-hooks/creation-modules/StableSwapNG.ts @@ -1,9 +1,10 @@ /** * StableSwap-NG (Curve) aggregator hook deployment module. + * Requires Curve StableSwap NG factory address for meta pool rejection. */ import { ethers } from "ethers"; -import { mustEnvForChain } from "../src/cli.js"; -import { STABLESWAP_FACTORY_ABI } from "../abis/index.js"; +import { getEnvForChain, mustEnvForChain } from "../src/cli.js"; +import { STABLESWAPNG_FACTORY_ABI } from "../abis/index.js"; import { DEFAULT_SQRT_PRICE_X96, type Address, @@ -25,10 +26,12 @@ const PROTOCOL_ID = 0xc2; const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); +const DEFAULT_STABLESWAPNG_FACTORY = "0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf"; + export const stableswapngModule: CreationModule = { poolType: "stableswapng", protocolId: PROTOCOL_ID, - factoryAbi: STABLESWAP_FACTORY_ABI, + factoryAbi: STABLESWAPNG_FACTORY_ABI, contractIdentifier: "lib/v4-hooks-public/src/aggregator-hooks/implementations/StableSwapNG/StableSwapNGAggregator.sol:StableSwapNGAggregator", @@ -64,29 +67,45 @@ export const stableswapngModule: CreationModule = { }, getImmutablesFromEnv(chainId: number): FactoryImmutables { + const curveFactory = + getEnvForChain("STABLESWAPNG_FACTORY_ADDRESS", chainId) ?? + getEnvForChain("FACTORY_ADDRESS", chainId) ?? + DEFAULT_STABLESWAPNG_FACTORY; return { poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, + curveFactory: curveFactory as Address, }; }, async readFactoryImmutables(provider, factoryAddress) { - const factory = new ethers.Contract(factoryAddress, STABLESWAP_FACTORY_ABI, provider); - const poolManager = await factory.POOL_MANAGER(); - return { poolManager: poolManager as Address }; + const factory = new ethers.Contract(factoryAddress, STABLESWAPNG_FACTORY_ABI, provider); + const [poolManager, curveFactory] = await Promise.all([ + factory.poolManager().then((a: string) => ethers.getAddress(a)), + factory.curveFactory().then((a: string) => ethers.getAddress(a)), + ]); + return { poolManager: poolManager as Address, curveFactory: curveFactory as Address }; }, encodeConstructorArgs(config, immutables) { + const curveFactory = immutables.curveFactory ?? (immutables as { curveFactory?: Address }).curveFactory; + if (!curveFactory) { + throw new Error( + "StableSwapNG requires curveFactory. Set STABLESWAPNG_FACTORY_ADDRESS or use StableSwapNGAggregatorFactory.", + ); + } const encoded = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "address"], - [immutables.poolManager, config.curvePool], + ["address", "address", "address"], + [immutables.poolManager, config.curvePool, curveFactory], ); return encoded.startsWith("0x") ? encoded : `0x${encoded}`; }, buildSelfDeployEnvVars(config, immutables) { const params = this.getHookParams(config); + const curveFactory = immutables.curveFactory ?? (immutables as { curveFactory?: Address }).curveFactory; return { CURVE_POOL: config.curvePool, + CURVE_FACTORY: curveFactory ?? "", TOKENS: config.tokens.join(","), FEE: params.fee.toString(), TICK_SPACING: params.tickSpacing.toString(), diff --git a/aggregator-hooks/historical/StableSwapNG.ts b/aggregator-hooks/historical/StableSwapNG.ts index d5181f9e..9f78d720 100644 --- a/aggregator-hooks/historical/StableSwapNG.ts +++ b/aggregator-hooks/historical/StableSwapNG.ts @@ -201,18 +201,20 @@ async function main() { const uniquePools = uniqAddresses(pools); - const createPoolsConfigs: CreatePoolsStableSwapConfig[] = uniquePools.map((curvePool) => { - const meta = metaByPool.get(curvePool); - const tokens = meta?.coins ?? []; - return { - poolType: "stableswapng" as const, - curvePool, - tokens, - fee: CREATE_POOLS_DEFAULTS.fee, - tickSpacing: CREATE_POOLS_DEFAULTS.tickSpacing, - sqrtPriceX96: CREATE_POOLS_DEFAULTS.sqrtPriceX96, - }; - }); + const createPoolsConfigs: CreatePoolsStableSwapConfig[] = uniquePools + .filter((curvePool) => metaByPool.get(curvePool)?.kind === "plain") + .map((curvePool) => { + const meta = metaByPool.get(curvePool)!; + const tokens = meta?.coins ?? []; + return { + poolType: "stableswapng" as const, + curvePool, + tokens, + fee: CREATE_POOLS_DEFAULTS.fee, + tickSpacing: CREATE_POOLS_DEFAULTS.tickSpacing, + sqrtPriceX96: CREATE_POOLS_DEFAULTS.sqrtPriceX96, + }; + }); const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); fs.mkdirSync(path.dirname(outPath), { recursive: true }); diff --git a/aggregator-hooks/polling/StableSwapNG.ts b/aggregator-hooks/polling/StableSwapNG.ts index 8c371446..6f475d94 100644 --- a/aggregator-hooks/polling/StableSwapNG.ts +++ b/aggregator-hooks/polling/StableSwapNG.ts @@ -285,14 +285,19 @@ async function main() { const meta = await limit(async () => { await rateLimit(); - const [nCoinsBn, coinsRaw] = await Promise.all([ + const [nCoinsBn, coinsRaw, basePoolRaw] = await Promise.all([ contract.get_n_coins(curvePool) as Promise, contract.get_coins(curvePool) as Promise, + contract.get_base_pool(curvePool) as Promise, ]); + const basePool = ethers.getAddress(basePoolRaw); + const isPlain = basePool.toLowerCase() === ethers.ZeroAddress.toLowerCase(); const coins = uniqAddresses(coinsRaw as string[]); - return { nCoins: Number(nCoinsBn), coins }; + return { nCoins: Number(nCoinsBn), coins, isPlain }; }); + if (!meta.isPlain) continue; + allRecords.push({ poolType: "stableswapng", curvePool, diff --git a/foundry.lock b/foundry.lock index c3c6c931..4915ae00 100644 --- a/foundry.lock +++ b/foundry.lock @@ -26,7 +26,7 @@ "lib/v4-hooks-public": { "branch": { "name": "aggregator-hooks", - "rev": "bae291c271be69882e084baa5993a033cb09362b" + "rev": "f7b6196ad6677e296850ef06b183e2eed6bb833e" } }, "src/pkgs/calibur": { diff --git a/lib/v4-hooks-public b/lib/v4-hooks-public index 9a79f86b..f7b6196a 160000 --- a/lib/v4-hooks-public +++ b/lib/v4-hooks-public @@ -1 +1 @@ -Subproject commit 9a79f86b4ad0f2389e25125892a69400aaaa43ab +Subproject commit f7b6196ad6677e296850ef06b183e2eed6bb833e diff --git a/script/SelfCreateHook.s.sol b/script/SelfCreateHook.s.sol deleted file mode 100644 index 71814e40..00000000 --- a/script/SelfCreateHook.s.sol +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import {IHooks} from '@uniswap/v4-core/src/interfaces/IHooks.sol'; -import {IPoolManager} from '@uniswap/v4-core/src/interfaces/IPoolManager.sol'; -import {Currency} from '@uniswap/v4-core/src/types/Currency.sol'; -import {PoolKey} from '@uniswap/v4-core/src/types/PoolKey.sol'; -import 'forge-std/Script.sol'; - -import {FluidDexLiteAggregator} from '@aggregator-hooks/implementations/FluidDexLite/FluidDexLiteAggregator.sol'; -import {FluidDexT1Aggregator} from '@aggregator-hooks/implementations/FluidDexT1/FluidDexT1Aggregator.sol'; -import {StableSwapAggregator} from '@aggregator-hooks/implementations/StableSwap/StableSwapAggregator.sol'; -import {StableSwapNGAggregator} from '@aggregator-hooks/implementations/StableSwapNG/StableSwapNGAggregator.sol'; - -import {IFluidDexLite} from '@aggregator-hooks/implementations/FluidDexLite/interfaces/IFluidDexLite.sol'; -import { - IFluidDexLiteResolver -} from '@aggregator-hooks/implementations/FluidDexLite/interfaces/IFluidDexLiteResolver.sol'; -import { - IFluidDexReservesResolver -} from '@aggregator-hooks/implementations/FluidDexT1/interfaces/IFluidDexReservesResolver.sol'; -import {IFluidDexT1} from '@aggregator-hooks/implementations/FluidDexT1/interfaces/IFluidDexT1.sol'; -import {ICurveStableSwap} from '@aggregator-hooks/implementations/StableSwap/interfaces/IStableSwap.sol'; -import {ICurveStableSwapNG} from '@aggregator-hooks/implementations/StableSwapNG/interfaces/IStableSwapNG.sol'; - -/// @notice Self-deploys an aggregator hook and initializes the pool without using a factory -/// @dev Broadcasts from PRIVATE_KEY and deploys using CREATE2 with the provided salt -contract SelfCreateHookScript is Script { - uint8 constant ID_STABLESWAP = 0xC1; - uint8 constant ID_STABLESWAPNG = 0xC2; - uint8 constant ID_FLUIDDEXT1 = 0xF1; - uint8 constant ID_FLUIDDEXLITE = 0xF3; - - function run() public { - // Load private key for broadcasting - uint256 deployerPrivateKey = vm.envUint('PRIVATE_KEY'); - - // Common parameters - uint8 protocolId = uint8(vm.envUint('PROTOCOL_ID')); - bytes32 salt = vm.envBytes32('SALT'); - address poolManager = vm.envAddress('POOL_MANAGER'); - - uint24 fee = uint24(vm.envUint('FEE')); - int24 tickSpacing = int24(int256(vm.envUint('TICK_SPACING'))); - uint160 sqrtPriceX96 = uint160(vm.envUint('SQRT_PRICE_X96')); - - address hookAddress; - - vm.startBroadcast(deployerPrivateKey); - - if (protocolId == ID_STABLESWAP) { - hookAddress = _deployStableSwap(salt, poolManager); - } else if (protocolId == ID_STABLESWAPNG) { - hookAddress = _deployStableSwapNG(salt, poolManager); - } else if (protocolId == ID_FLUIDDEXT1) { - hookAddress = _deployFluidDexT1(salt, poolManager); - } else if (protocolId == ID_FLUIDDEXLITE) { - hookAddress = _deployFluidDexLite(salt, poolManager); - } else { - revert('Invalid protocol ID'); - } - - // Initialize one Uniswap pool per token pair. TOKENS is comma-separated (2+ for fluid, 2+ for stableswap). - address[] memory tokens = vm.envAddress('TOKENS', ','); - require(tokens.length >= 2, 'TOKENS must have at least 2 addresses'); - for (uint256 i = 0; i < tokens.length; i++) { - for (uint256 j = i + 1; j < tokens.length; j++) { - (address c0, address c1) = tokens[i] < tokens[j] ? (tokens[i], tokens[j]) : (tokens[j], tokens[i]); - PoolKey memory poolKey = PoolKey({ - currency0: Currency.wrap(c0), - currency1: Currency.wrap(c1), - fee: fee, - tickSpacing: tickSpacing, - hooks: IHooks(hookAddress) - }); - IPoolManager(poolManager).initialize(poolKey, sqrtPriceX96); - console.log('Initialized pool:', c0, c1); - } - } - - vm.stopBroadcast(); - - // Output results - console.log('=== Self-Deploy Hook Results ==='); - console.log('Hook Address:', hookAddress); - console.log('Salt:', vm.toString(salt)); - console.log('Protocol ID:', protocolId); - console.log('Pool Manager:', poolManager); - console.log('Tokens:'); - console.log('Tokens length:', tokens.length); - for (uint256 i = 0; i < tokens.length; i++) { - console.log('Token:', tokens[i]); - } - console.log('Fee:', fee); - console.log('Tick Spacing:', uint24(tickSpacing)); - console.log('Sqrt Price X96:', sqrtPriceX96); - console.log('================================'); - } - - function _deployStableSwap(bytes32 salt, address poolManager) internal returns (address) { - address curvePool = vm.envAddress('CURVE_POOL'); - - StableSwapAggregator hook = - new StableSwapAggregator{salt: salt}(IPoolManager(poolManager), ICurveStableSwap(curvePool)); - - return address(hook); - } - - function _deployStableSwapNG(bytes32 salt, address poolManager) internal returns (address) { - address curvePool = vm.envAddress('CURVE_POOL'); - - StableSwapNGAggregator hook = - new StableSwapNGAggregator{salt: salt}(IPoolManager(poolManager), ICurveStableSwapNG(curvePool)); - - return address(hook); - } - - function _deployFluidDexT1(bytes32 salt, address poolManager) internal returns (address) { - address fluidPool = vm.envAddress('FLUID_POOL'); - address fluidDexReservesResolver = vm.envAddress('FLUID_DEX_RESOLVER'); - address fluidLiquidity = vm.envAddress('FLUID_LIQUIDITY'); - - FluidDexT1Aggregator hook = new FluidDexT1Aggregator{salt: salt}( - IPoolManager(poolManager), - IFluidDexT1(fluidPool), - IFluidDexReservesResolver(fluidDexReservesResolver), - fluidLiquidity - ); - - return address(hook); - } - - function _deployFluidDexLite(bytes32 salt, address poolManager) internal returns (address) { - address fluidDexLite = vm.envAddress('FLUID_DEX_LITE'); - address fluidDexLiteResolver = vm.envAddress('FLUID_DEX_LITE_RESOLVER'); - bytes32 dexSalt = vm.envBytes32('DEX_SALT'); - - FluidDexLiteAggregator hook = new FluidDexLiteAggregator{salt: salt}( - IPoolManager(poolManager), IFluidDexLite(fluidDexLite), IFluidDexLiteResolver(fluidDexLiteResolver), dexSalt - ); - - return address(hook); - } -} From 116e738345edb697d949feed12ea9fedcd7dfb41 Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Wed, 11 Mar 2026 12:29:38 -0400 Subject: [PATCH 11/21] add factory abi --- aggregator-hooks/abis/StableSwapNGFactory.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 aggregator-hooks/abis/StableSwapNGFactory.json diff --git a/aggregator-hooks/abis/StableSwapNGFactory.json b/aggregator-hooks/abis/StableSwapNGFactory.json new file mode 100644 index 00000000..a63da0d8 --- /dev/null +++ b/aggregator-hooks/abis/StableSwapNGFactory.json @@ -0,0 +1,5 @@ +[ + "function poolManager() external view returns (address)", + "function curveFactory() external view returns (address)", + "function createPool(bytes32 salt, address curvePool, address[] calldata tokens, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)" +] From 7f97b415fa58959ad73b655a317aafc2cddcd02a Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Wed, 11 Mar 2026 12:33:37 -0400 Subject: [PATCH 12/21] run precommit --- aggregator-hooks/README.md | 2 +- aggregator-hooks/src/createPools.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aggregator-hooks/README.md b/aggregator-hooks/README.md index 482b24c8..cb204127 100644 --- a/aggregator-hooks/README.md +++ b/aggregator-hooks/README.md @@ -178,7 +178,7 @@ npx tsx src/createPools.ts detected/1/fluiddext1-pools-curated.json --self-deplo | ------------------------------------------------------ | ---------------------------------------------------------------------------------- | | `RPC_URL` or `RPC_URL_` | RPC endpoint (use `RPC_URL_1` etc. when `--chain-id` is set) | | `PRIVATE_KEY` | Signing key for transactions (required even with `--dry-run`) | -| `POOL_MANAGER` or `POOL_MANAGER_` | Uniswap v4 PoolManager address (required for self-deploy) | +| `POOL_MANAGER` or `POOL_MANAGER_` | Uniswap v4 PoolManager address (required for self-deploy) | | `ETHERSCAN_API_KEY` or `ETHERSCAN_API_KEY_` | API key for Etherscan verification (required when using `--verify` with Etherscan) | | `BLOCKSCOUT_API_URL` or `BLOCKSCOUT_API_URL_` | Blockscout API URL. If set, `--verify` automatically uses Blockscout. | diff --git a/aggregator-hooks/src/createPools.ts b/aggregator-hooks/src/createPools.ts index d6779334..d139678a 100644 --- a/aggregator-hooks/src/createPools.ts +++ b/aggregator-hooks/src/createPools.ts @@ -148,8 +148,8 @@ function parseArgs(): ParsedArgs { const factoryAddress: Address | null = selfDeploy ? null : positionalArgs[1] - ? (ethers.getAddress(positionalArgs[1]) as Address) - : null; + ? (ethers.getAddress(positionalArgs[1]) as Address) + : null; if (selfDeploy && positionalArgs.length >= 2 && positionalArgs[1].startsWith("0x")) { console.error("Error: --self-deploy and factoryAddress are mutually exclusive"); From 968cf80016e68d849ef27074c1f7a7fdb51cee93 Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Mon, 16 Mar 2026 12:42:48 -0400 Subject: [PATCH 13/21] update with newest branch --- .gitmodules | 2 +- aggregator-hooks/README.md | 4 ++-- aggregator-hooks/src/createPools.ts | 22 ++++++++++++++++------ lib/v4-hooks-public | 2 +- mine_hook.sh | 4 ++-- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/.gitmodules b/.gitmodules index 756c598d..8cff735f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -64,7 +64,7 @@ [submodule "lib/v4-hooks-public"] path = lib/v4-hooks-public url = https://github.com/Uniswap/v4-hooks-public - branch = aggregator-hooks + branch = aggregator-hooks-ported [submodule "lib/v4-core"] path = lib/v4-core url = https://github.com/Uniswap/v4-core \ No newline at end of file diff --git a/aggregator-hooks/README.md b/aggregator-hooks/README.md index cb204127..6e4bbd3e 100644 --- a/aggregator-hooks/README.md +++ b/aggregator-hooks/README.md @@ -278,10 +278,10 @@ The `createPools` script and `mine_hook.sh` run from the **contracts/** director > **Note:** When running `createPools` via `npx tsx src/createPools.ts`, run it from **aggregator-hooks/** so it loads `aggregator-hooks/.env`. The forge scripts invoked by createPools run from contracts/ but inherit env vars from the parent process. -1. **v4-hooks-public** (aggregator-hooks branch): Already added as submodule. Ensure it's on the `aggregator-hooks` branch: +1. **v4-hooks-public** (aggregator-hooks-ported branch): Already added as submodule. Ensure it's on the `aggregator-hooks-ported` branch: ```bash - cd lib/v4-hooks-public && git fetch origin aggregator-hooks && git checkout aggregator-hooks + cd lib/v4-hooks-public && git fetch origin aggregator-hooks-ported && git checkout aggregator-hooks-ported git submodule update --init --recursive ``` diff --git a/aggregator-hooks/src/createPools.ts b/aggregator-hooks/src/createPools.ts index d139678a..116b9d9d 100644 --- a/aggregator-hooks/src/createPools.ts +++ b/aggregator-hooks/src/createPools.ts @@ -120,7 +120,9 @@ function parseArgs(): ParsedArgs { console.error( " Self-deploy: ts-node createPools.ts pools.json --self-deploy [--chain-id 1] [--registry-dir ./deployed-pools]", ); - console.error(" --dry-run: Simulate without broadcasting (forge script runs but no txs sent)"); + console.error( + " --dry-run: Simulate without broadcasting (self-deploy: forge script without --broadcast; factory: staticCall only)", + ); console.error(" --verbose, -v: Run forge scripts verbosely and log full output on errors"); console.error( " --start-at : Start at 1-based pool index (skip earlier pools). e.g. --start-at 3 to resume from pool 3.", @@ -148,8 +150,8 @@ function parseArgs(): ParsedArgs { const factoryAddress: Address | null = selfDeploy ? null : positionalArgs[1] - ? (ethers.getAddress(positionalArgs[1]) as Address) - : null; + ? (ethers.getAddress(positionalArgs[1]) as Address) + : null; if (selfDeploy && positionalArgs.length >= 2 && positionalArgs[1].startsWith("0x")) { console.error("Error: --self-deploy and factoryAddress are mutually exclusive"); @@ -686,6 +688,7 @@ async function createPool( poolType: string, salt: string, log: Logger, + dryRun: boolean, ): Promise<{ hookAddress: Address | ""; blockNumber: number; txHash: string }> { const module = CREATION_MODULES[poolType]; if (!module) throw new Error(`Unknown pool type: ${poolType}`); @@ -694,9 +697,16 @@ async function createPool( const factory = new ethers.Contract(factoryAddress, module.factoryAbi, signer); log.info(`Calling createPool on factory ${factoryAddress}...`); + if (dryRun) log.info("(dry run - no broadcast)"); log.info(`Args: ${args.map((a, i) => `${i}: ${String(a).substring(0, 66)}...`).join(", ")}`); try { + if (dryRun) { + await factory.createPool.staticCall(...args); + log.success(`Dry run: createPool would succeed (simulation passed)`); + return { hookAddress: "", blockNumber: 0, txHash: "" }; + } + const tx = await factory.createPool(...args); log.success(`Transaction sent: ${tx.hash}`); @@ -903,9 +913,9 @@ async function main() { try { const constructorArgs = module.encodeConstructorArgs(poolConfig, factoryImmutables); const salt = await mineSalt(constructorArgs, module.protocolId, log, factoryAddress!, jobs); - const result = await createPool(signer, factoryAddress!, poolConfig, poolType, salt, log); + const result = await createPool(signer, factoryAddress!, poolConfig, poolType, salt, log, dryRun); - if (result.hookAddress) { + if (result.hookAddress && !dryRun) { if (registryDir) { const poolKeys = module.buildPoolKeys(poolConfig, result.hookAddress); if (poolKeys.length > 0) { @@ -930,7 +940,7 @@ async function main() { verifyContract(result.hookAddress, module.contractIdentifier, constructorArgs, chainId, verify, log); } } - log.success(`Successfully created pool ${i + 1}`); + log.success(`Successfully created pool ${i + 1}${dryRun ? " (dry run)" : ""}`); } catch (error) { log.error(`Failed to create pool ${i + 1}:`, error); const execErr = error as { stdout?: string; stderr?: string }; diff --git a/lib/v4-hooks-public b/lib/v4-hooks-public index f7b6196a..269a4bc7 160000 --- a/lib/v4-hooks-public +++ b/lib/v4-hooks-public @@ -1 +1 @@ -Subproject commit f7b6196ad6677e296850ef06b183e2eed6bb833e +Subproject commit 269a4bc7c24c99a9bf6408262df37a462d8a2b9c diff --git a/mine_hook.sh b/mine_hook.sh index 7c566c05..04169ba9 100644 --- a/mine_hook.sh +++ b/mine_hook.sh @@ -81,9 +81,9 @@ cd "$SCRIPT_DIR" if [ ! -f "lib/v4-hooks-public/script/MineAggregatorHook.s.sol" ]; then echo "Error: MineAggregatorHook.s.sol not found at lib/v4-hooks-public/script/MineAggregatorHook.s.sol" - echo "Add v4-hooks-public and checkout aggregator-hooks branch:" + echo "Add v4-hooks-public and checkout aggregator-hooks-ported branch:" echo " git submodule add https://github.com/Uniswap/v4-hooks-public lib/v4-hooks-public" - echo " cd lib/v4-hooks-public && git fetch origin aggregator-hooks && git checkout aggregator-hooks" + echo " cd lib/v4-hooks-public && git fetch origin aggregator-hooks-ported && git checkout aggregator-hooks-ported" echo " git submodule update --init --recursive" exit 1 fi From 1f1ce7d056c8cefd0841a4af5e250354ae3a1637 Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Wed, 18 Mar 2026 16:20:12 -0400 Subject: [PATCH 14/21] update createPools --- .gitmodules | 2 +- aggregator-hooks/.env.example | 4 +- aggregator-hooks/README.md | 2 +- aggregator-hooks/abis/FluidDexT1Factory.json | 1 + .../creation-modules/FluidDexT1.ts | 31 +++++++-- aggregator-hooks/historical/FluidDexT1.ts | 16 ++--- aggregator-hooks/polling/FluidDexT1.ts | 11 ++-- aggregator-hooks/src/createPools.ts | 65 +++++++++++++++++-- 8 files changed, 103 insertions(+), 29 deletions(-) diff --git a/.gitmodules b/.gitmodules index 8cff735f..b69de947 100644 --- a/.gitmodules +++ b/.gitmodules @@ -64,7 +64,7 @@ [submodule "lib/v4-hooks-public"] path = lib/v4-hooks-public url = https://github.com/Uniswap/v4-hooks-public - branch = aggregator-hooks-ported + branch = aggregator-hooks-mar-17 [submodule "lib/v4-core"] path = lib/v4-core url = https://github.com/Uniswap/v4-core \ No newline at end of file diff --git a/aggregator-hooks/.env.example b/aggregator-hooks/.env.example index 466ebff3..93f7c88d 100644 --- a/aggregator-hooks/.env.example +++ b/aggregator-hooks/.env.example @@ -16,7 +16,9 @@ #DEX_LITE_ADDRESS_= # discovery + createPools self-deploy (fluiddexlite) #DEX_LITE_RESOLVER_ADDRESS_= # discovery + createPools self-deploy (fluiddexlite) #FLUID_DEX_FACTORY_= -#FLUID_DEX_RESOLVER_= +# FluidDexT1: both resolvers required for pool creation; resolver only for discovery (historical/polling) +#FLUID_DEX_T1_RESOLVER_= # IFluidDexResolver (getPoolTokens, estimateSwap); FLUID_DEX_RESOLVER fallback +#FLUID_DEX_T1_RESERVES_RESOLVER_= # IFluidDexReservesResolver (getPoolReserves); FLUID_DEX_RESERVES_RESOLVER fallback #FACTORY_ADDRESS_= # StableSwap (legacy Curve): MetaRegistry for meta pool rejection diff --git a/aggregator-hooks/README.md b/aggregator-hooks/README.md index 6e4bbd3e..df1e6e26 100644 --- a/aggregator-hooks/README.md +++ b/aggregator-hooks/README.md @@ -59,7 +59,7 @@ All discovery scripts use chain-ID-suffixed env vars. Use `VAR_` (e.g. | Script | Required | Optional | | ---------------- | ------------------------------- | ------------------------------------------------------------ | | **fluiddexlite** | `RPC_URL` | `DEX_LITE_RESOLVER_ADDRESS` (default mainnet resolver) | -| **fluiddext1** | `RPC_URL`, `FLUID_DEX_RESOLVER` | `FLUID_DEX_FACTORY`, `FACTORY_ADDRESS`, `RPS`, `CONCURRENCY` | +| **fluiddext1** | `RPC_URL`, `FLUID_DEX_T1_RESOLVER` (or `FLUID_DEX_RESOLVER`) | `FLUID_DEX_FACTORY`, `FACTORY_ADDRESS`, `RPS`, `CONCURRENCY` | | **stableswapng** | `RPC_URL` | `FACTORY_ADDRESS`, `RPS`, `CONCURRENCY` | **Polling scripts** use the same env vars as their historical counterparts (fluiddexlite, fluiddext1, stableswapng), plus `FINALITY_BLOCKS` and `LOOKBACK_BLOCKS` (see Polling env vars above). FluidDexLite polling also requires `DEX_LITE_ADDRESS`. diff --git a/aggregator-hooks/abis/FluidDexT1Factory.json b/aggregator-hooks/abis/FluidDexT1Factory.json index 5638f52d..4237deb0 100644 --- a/aggregator-hooks/abis/FluidDexT1Factory.json +++ b/aggregator-hooks/abis/FluidDexT1Factory.json @@ -1,6 +1,7 @@ [ "function POOL_MANAGER() external view returns (address)", "function fluidDexReservesResolver() external view returns (address)", + "function fluidDexResolver() external view returns (address)", "function FLUID_LIQUIDITY() external view returns (address)", "function createPool(bytes32 salt, address fluidPool, address currency0, address currency1, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)" ] diff --git a/aggregator-hooks/creation-modules/FluidDexT1.ts b/aggregator-hooks/creation-modules/FluidDexT1.ts index 6b347021..86f21856 100644 --- a/aggregator-hooks/creation-modules/FluidDexT1.ts +++ b/aggregator-hooks/creation-modules/FluidDexT1.ts @@ -2,7 +2,7 @@ * FluidDex T1 aggregator hook deployment module. */ import { ethers } from "ethers"; -import { mustEnvForChain } from "../src/cli.js"; +import { getEnvForChain, mustEnvForChain } from "../src/cli.js"; import { FLUIDDEXT1_FACTORY_ABI } from "../abis/index.js"; import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables } from "./types.js"; @@ -54,31 +54,49 @@ export const fluiddext1Module: CreationModule = { }, getImmutablesFromEnv(chainId: number): FactoryImmutables { + const reservesResolver = + getEnvForChain("FLUID_DEX_T1_RESERVES_RESOLVER", chainId) ?? + getEnvForChain("FLUID_DEX_RESERVES_RESOLVER", chainId); + const resolver = getEnvForChain("FLUID_DEX_T1_RESOLVER", chainId) ?? getEnvForChain("FLUID_DEX_RESOLVER", chainId); + if (!reservesResolver) + throw new Error( + `Missing env: FLUID_DEX_T1_RESERVES_RESOLVER_${chainId} or FLUID_DEX_RESERVES_RESOLVER_${chainId}`, + ); + if (!resolver) throw new Error(`Missing env: FLUID_DEX_T1_RESOLVER_${chainId} or FLUID_DEX_RESOLVER_${chainId}`); return { poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, - fluidDexReservesResolver: mustEnvForChain("FLUID_DEX_RESOLVER", chainId) as Address, + fluidDexReservesResolver: reservesResolver as Address, + fluidDexResolver: resolver as Address, fluidLiquidity: mustEnvForChain("FLUID_LIQUIDITY", chainId) as Address, }; }, async readFactoryImmutables(provider, factoryAddress) { const factory = new ethers.Contract(factoryAddress, FLUIDDEXT1_FACTORY_ABI, provider); - const [poolManager, fluidDexReservesResolver, fluidLiquidity] = await Promise.all([ + const [poolManager, fluidDexReservesResolver, fluidDexResolver, fluidLiquidity] = await Promise.all([ factory.POOL_MANAGER(), factory.fluidDexReservesResolver(), + factory.fluidDexResolver(), factory.FLUID_LIQUIDITY(), ]); return { poolManager: poolManager as Address, fluidDexReservesResolver: fluidDexReservesResolver as Address, + fluidDexResolver: fluidDexResolver as Address, fluidLiquidity: fluidLiquidity as Address, }; }, encodeConstructorArgs(config, immutables) { const encoded = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "address", "address", "address"], - [immutables.poolManager, config.fluidPool, immutables.fluidDexReservesResolver, immutables.fluidLiquidity], + ["address", "address", "address", "address", "address"], + [ + immutables.poolManager, + config.fluidPool, + immutables.fluidDexReservesResolver, + immutables.fluidDexResolver, + immutables.fluidLiquidity, + ], ); return encoded.startsWith("0x") ? encoded : `0x${encoded}`; }, @@ -87,7 +105,8 @@ export const fluiddext1Module: CreationModule = { const params = this.getHookParams(config); return { FLUID_POOL: config.fluidPool, - FLUID_DEX_RESOLVER: immutables.fluidDexReservesResolver!, + FLUID_DEX_T1_RESERVES_RESOLVER: immutables.fluidDexReservesResolver!, + FLUID_DEX_T1_RESOLVER: immutables.fluidDexResolver!, FLUID_LIQUIDITY: immutables.fluidLiquidity!, TOKENS: [config.currency0, config.currency1].join(","), FEE: params.fee.toString(), diff --git a/aggregator-hooks/historical/FluidDexT1.ts b/aggregator-hooks/historical/FluidDexT1.ts index be5c88d5..af7dbb0b 100644 --- a/aggregator-hooks/historical/FluidDexT1.ts +++ b/aggregator-hooks/historical/FluidDexT1.ts @@ -14,9 +14,9 @@ * --mode logs|enumerate|both (default: logs) * * Env vars (use VAR_ or VAR for single chain): - * RPC_URL (required) - * FLUID_DEX_RESOLVER (required) - * FLUID_DEX_FACTORY (optional, default mainnet) + * RPC_URL (required) + * FLUID_DEX_T1_RESOLVER (required) IFluidDexResolver for getPoolTokens; FLUID_DEX_RESOLVER fallback + * FLUID_DEX_FACTORY (optional, default mainnet) * RPS (optional, default 80) max RPC requests per second * CONCURRENCY (optional, default 8) max concurrent RPC calls * @@ -111,13 +111,13 @@ async function main() { } const rpcUrl = getEnvForChain("RPC_URL", chainId); - const reservesResolverAddr = - getEnvForChain("FLUID_DEX_RESERVES_RESOLVER", chainId) ?? getEnvForChain("FLUID_DEX_RESOLVER", chainId); + const resolverAddr = + getEnvForChain("FLUID_DEX_T1_RESOLVER", chainId) ?? getEnvForChain("FLUID_DEX_RESOLVER", chainId); const factoryAddrRaw = getEnvForChain("FLUID_DEX_FACTORY", chainId) ?? getEnvForChain("FACTORY_ADDRESS", chainId) ?? DEFAULT_FACTORY; - if (!rpcUrl || !reservesResolverAddr) { - console.error("Missing env: RPC_URL and FLUID_DEX_RESOLVER"); + if (!rpcUrl || !resolverAddr) { + console.error("Missing env: RPC_URL and (FLUID_DEX_T1_RESOLVER or FLUID_DEX_RESOLVER)"); process.exit(1); } @@ -135,7 +135,7 @@ async function main() { const provider = new JsonRpcProvider(rpcUrl); const factory = new Contract(factoryAddr, FLUIDDEXT1_HISTORICAL_FACTORY_ABI, provider); - const resolver = new Contract(getAddress(reservesResolverAddr.toLowerCase()), FLUIDDEXT1_RESOLVER_ABI, provider); + const resolver = new Contract(getAddress(resolverAddr.toLowerCase()), FLUIDDEXT1_RESOLVER_ABI, provider); const latest = BigInt(await provider.getBlockNumber()); const endBlock = endBlockRaw ? BigInt(String(endBlockRaw)) : latest; diff --git a/aggregator-hooks/polling/FluidDexT1.ts b/aggregator-hooks/polling/FluidDexT1.ts index bed57928..ab381653 100644 --- a/aggregator-hooks/polling/FluidDexT1.ts +++ b/aggregator-hooks/polling/FluidDexT1.ts @@ -16,9 +16,9 @@ * --start-block (optional) override checkpoint; start scan from this block * * Env vars (use VAR_ or VAR for single chain): - * RPC_URL (required) - * FLUID_DEX_RESOLVER (required) - * FLUID_DEX_FACTORY (optional, default mainnet) + * RPC_URL (required) + * FLUID_DEX_T1_RESOLVER (required) IFluidDexResolver for getPoolTokens; FLUID_DEX_RESOLVER fallback + * FLUID_DEX_FACTORY (optional, default mainnet) * FINALITY_BLOCKS (optional, default 10) subtract from latest; checkpoint = last scanned block * LOOKBACK_BLOCKS (optional, default 200000) used when checkpoint missing and no --start-block */ @@ -125,12 +125,13 @@ async function main() { } const rpcUrl = getEnvForChain("RPC_URL", chainId); - const resolverAddr = getEnvForChain("FLUID_DEX_RESOLVER", chainId); + const resolverAddr = + getEnvForChain("FLUID_DEX_T1_RESOLVER", chainId) ?? getEnvForChain("FLUID_DEX_RESOLVER", chainId); const factoryRaw = getEnvForChain("FLUID_DEX_FACTORY", chainId) ?? getEnvForChain("FACTORY_ADDRESS", chainId) ?? DEFAULT_FACTORY; if (!rpcUrl || !resolverAddr) { - throw new Error("Missing required env: RPC_URL and FLUID_DEX_RESOLVER"); + throw new Error("Missing required env: RPC_URL and (FLUID_DEX_T1_RESOLVER or FLUID_DEX_RESOLVER)"); } const factory = ethers.getAddress(factoryRaw); diff --git a/aggregator-hooks/src/createPools.ts b/aggregator-hooks/src/createPools.ts index 116b9d9d..0899074b 100644 --- a/aggregator-hooks/src/createPools.ts +++ b/aggregator-hooks/src/createPools.ts @@ -420,7 +420,14 @@ async function verifyDeploymentOnForgeFailure( poolConfig: PoolConfig, poolType: string, forgeOutput: string, -): Promise<{ hookAddress: Address; hookDeployed: boolean; poolsInitialized: number } | null> { +): Promise<{ + hookAddress: Address; + hookDeployed: boolean; + poolsInitialized: number; + blockNumber?: number; + poolKeysFromEvent?: PoolKeyRecord[]; + poolIdFromEvent?: string; +} | null> { const module = CREATION_MODULES[poolType]; if (!module) return null; @@ -434,7 +441,9 @@ async function verifyDeploymentOnForgeFailure( if (!hookDeployed) return { hookAddress, hookDeployed: false, poolsInitialized: 0 }; const poolKeys = module.buildPoolKeys(poolConfig, hookAddress); - let poolsInitialized = 0; + const poolKeysFromEvent: PoolKeyRecord[] = []; + let initializeBlockNumber: number | undefined; + let poolIdFromEvent: string | undefined; try { const blockNumber = await provider.getBlockNumber(); @@ -448,12 +457,14 @@ async function verifyDeploymentOnForgeFailure( for (const log of logs) { if (log.topics.length < 4) continue; - const currency0 = "0x" + log.topics[2].slice(26); - const currency1 = "0x" + log.topics[3].slice(26); + const currency0 = ethers.getAddress("0x" + log.topics[2].slice(26)) as Address; + const currency1 = ethers.getAddress("0x" + log.topics[3].slice(26)) as Address; const data = ethers.AbiCoder.defaultAbiCoder().decode( ["uint24", "int24", "address", "uint160", "int24"], log.data, ); + const fee = Number(data[0]); + const tickSpacing = Number(data[1]); const hooks = data[2]; if (hooks?.toLowerCase() !== hookAddress.toLowerCase()) continue; if ( @@ -463,14 +474,29 @@ async function verifyDeploymentOnForgeFailure( k.currency1.toLowerCase() === currency1.toLowerCase(), ) ) { - poolsInitialized++; + if (initializeBlockNumber === undefined) initializeBlockNumber = Number(log.blockNumber); + if (poolIdFromEvent === undefined) poolIdFromEvent = log.topics[1]; + poolKeysFromEvent.push({ + currency0, + currency1, + fee, + tickSpacing, + hooks: hookAddress, + }); } } } catch { /* ignore */ } - return { hookAddress, hookDeployed, poolsInitialized }; + return { + hookAddress, + hookDeployed, + poolsInitialized: poolKeysFromEvent.length, + blockNumber: initializeBlockNumber, + poolKeysFromEvent: poolKeysFromEvent.length > 0 ? poolKeysFromEvent : undefined, + poolIdFromEvent, + }; } function runMineHookWorker( @@ -648,7 +674,7 @@ function selfDeployPool( "forge", [ "script", - "script/SelfCreateHook.s.sol:SelfCreateHookScript", + "lib/v4-hooks-public/script/SelfCreateHook.s.sol:SelfCreateHookScript", "--rpc-url", rpcUrl, ...(dryRun ? [] : ["--broadcast"]), @@ -873,6 +899,31 @@ async function main() { ); } log.error(" Check block explorer to confirm."); + + if (registryDir && !dryRun) { + const poolKeys = + verification.poolKeysFromEvent ?? module.buildPoolKeys(poolConfig, verification.hookAddress); + const poolId = + verification.poolIdFromEvent ?? (poolKeys.length > 0 ? computePoolId(poolKeys[0]) : undefined); + const blockNumber = verification.blockNumber ?? Number(await provider.getBlockNumber()); + if (poolKeys.length > 0 && poolId) { + appendToRegistryFile( + registryDir, + poolType, + { + poolKeys, + metadata: { + externalPool: module.getExternalPool(poolConfig), + hookAddress: verification.hookAddress, + poolId, + blockNumber, + }, + }, + log, + ); + log.info(" Appended to registry despite forge failure (deployment verified on-chain)."); + } + } } } continue; From 2ea3d3684f4752bfc3f359bfedaa5bbf209f171f Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Wed, 18 Mar 2026 16:33:42 -0400 Subject: [PATCH 15/21] update to certain branch --- lib/v4-hooks-public | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/v4-hooks-public b/lib/v4-hooks-public index 269a4bc7..a824e322 160000 --- a/lib/v4-hooks-public +++ b/lib/v4-hooks-public @@ -1 +1 @@ -Subproject commit 269a4bc7c24c99a9bf6408262df37a462d8a2b9c +Subproject commit a824e322384e296c6da392b4285452c81ca4ec64 From 8bf2946f64814a4424ed14e9254f4ecd1927e5d6 Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Fri, 27 Mar 2026 09:13:54 -0400 Subject: [PATCH 16/21] use latest v4-hooks-public main --- .gitmodules | 2 +- aggregator-hooks/.env.example | 20 +++++++++---------- aggregator-hooks/README.md | 19 +++++++++--------- .../creation-modules/FluidDexLite.ts | 15 +++----------- .../creation-modules/FluidDexT1.ts | 15 +++----------- .../creation-modules/StableSwap.ts | 4 ++-- .../creation-modules/StableSwapNG.ts | 7 ++----- aggregator-hooks/historical/FluidDexLite.ts | 7 +++---- aggregator-hooks/historical/FluidDexT1.ts | 13 ++++++------ aggregator-hooks/historical/StableSwapNG.ts | 6 +++--- aggregator-hooks/polling/FluidDexLite.ts | 8 ++++---- aggregator-hooks/polling/FluidDexT1.ts | 13 ++++++------ aggregator-hooks/polling/StableSwapNG.ts | 6 +++--- foundry.lock | 5 +---- lib/forge-chronicles | 2 +- lib/v4-hooks-public | 2 +- mine_hook.sh | 6 +++--- 17 files changed, 61 insertions(+), 89 deletions(-) diff --git a/.gitmodules b/.gitmodules index b69de947..90289983 100644 --- a/.gitmodules +++ b/.gitmodules @@ -64,7 +64,7 @@ [submodule "lib/v4-hooks-public"] path = lib/v4-hooks-public url = https://github.com/Uniswap/v4-hooks-public - branch = aggregator-hooks-mar-17 + branch = main [submodule "lib/v4-core"] path = lib/v4-core url = https://github.com/Uniswap/v4-core \ No newline at end of file diff --git a/aggregator-hooks/.env.example b/aggregator-hooks/.env.example index 93f7c88d..6fd2b366 100644 --- a/aggregator-hooks/.env.example +++ b/aggregator-hooks/.env.example @@ -13,20 +13,18 @@ #RPC_URL_= #POOL_MANAGER_= # createPools self-deploy #FLUID_LIQUIDITY_= # createPools self-deploy (fluiddext1 only) -#DEX_LITE_ADDRESS_= # discovery + createPools self-deploy (fluiddexlite) -#DEX_LITE_RESOLVER_ADDRESS_= # discovery + createPools self-deploy (fluiddexlite) -#FLUID_DEX_FACTORY_= +#FLUID_DEX_LITE_= # discovery + createPools self-deploy (fluiddexlite) +#FLUID_DEX_LITE_RESOLVER_= # discovery + createPools self-deploy (fluiddexlite) +#FLUID_DEX_T1_FACTORY_= # FluidDexT1: both resolvers required for pool creation; resolver only for discovery (historical/polling) -#FLUID_DEX_T1_RESOLVER_= # IFluidDexResolver (getPoolTokens, estimateSwap); FLUID_DEX_RESOLVER fallback -#FLUID_DEX_T1_RESERVES_RESOLVER_= # IFluidDexReservesResolver (getPoolReserves); FLUID_DEX_RESERVES_RESOLVER fallback -#FACTORY_ADDRESS_= +#FLUID_DEX_T1_RESOLVER_= # IFluidDexResolver (getPoolTokens, estimateSwap) +#FLUID_DEX_T1_RESERVES_RESOLVER_= # IFluidDexReservesResolver (getPoolReserves) -# StableSwap (legacy Curve): MetaRegistry for meta pool rejection -#STABLESWAP_METAREGISTRY_ADDRESS_= +# StableSwap (legacy Curve): MetaRegistry for meta pool rejection (createPools self-deploy) +#STABLESWAP_METAREGISTRY_= -# StableSwap-NG: Curve factory address (for meta pool rejection). Defaults to mainnet if not set. -#STABLESWAPNG_FACTORY_ADDRESS_= -#FACTORY_ADDRESS_= # fallback for discovery scripts +# StableSwap-NG: Curve factory (meta pool rejection). Defaults to mainnet if unset. +#STABLESWAPNG_FACTORY_= diff --git a/aggregator-hooks/README.md b/aggregator-hooks/README.md index df1e6e26..4d6fd041 100644 --- a/aggregator-hooks/README.md +++ b/aggregator-hooks/README.md @@ -58,11 +58,11 @@ All discovery scripts use chain-ID-suffixed env vars. Use `VAR_` (e.g. | Script | Required | Optional | | ---------------- | ------------------------------- | ------------------------------------------------------------ | -| **fluiddexlite** | `RPC_URL` | `DEX_LITE_RESOLVER_ADDRESS` (default mainnet resolver) | -| **fluiddext1** | `RPC_URL`, `FLUID_DEX_T1_RESOLVER` (or `FLUID_DEX_RESOLVER`) | `FLUID_DEX_FACTORY`, `FACTORY_ADDRESS`, `RPS`, `CONCURRENCY` | -| **stableswapng** | `RPC_URL` | `FACTORY_ADDRESS`, `RPS`, `CONCURRENCY` | +| **fluiddexlite** | `RPC_URL` | `FLUID_DEX_LITE_RESOLVER` (default mainnet resolver) | +| **fluiddext1** | `RPC_URL`, `FLUID_DEX_T1_RESOLVER` | `FLUID_DEX_T1_FACTORY`, `RPS`, `CONCURRENCY` | +| **stableswapng** | `RPC_URL` | `STABLESWAPNG_FACTORY`, `RPS`, `CONCURRENCY` | -**Polling scripts** use the same env vars as their historical counterparts (fluiddexlite, fluiddext1, stableswapng), plus `FINALITY_BLOCKS` and `LOOKBACK_BLOCKS` (see Polling env vars above). FluidDexLite polling also requires `DEX_LITE_ADDRESS`. +**Polling scripts** use the same env vars as their historical counterparts (fluiddexlite, fluiddext1, stableswapng), plus `FINALITY_BLOCKS` and `LOOKBACK_BLOCKS` (see Polling env vars above). FluidDexLite polling also requires `FLUID_DEX_LITE`. --- @@ -182,9 +182,9 @@ npx tsx src/createPools.ts detected/1/fluiddext1-pools-curated.json --self-deplo | `ETHERSCAN_API_KEY` or `ETHERSCAN_API_KEY_` | API key for Etherscan verification (required when using `--verify` with Etherscan) | | `BLOCKSCOUT_API_URL` or `BLOCKSCOUT_API_URL_` | Blockscout API URL. If set, `--verify` automatically uses Blockscout. | -**StableSwap (stableswap):** `STABLESWAP_METAREGISTRY_ADDRESS_` — Curve MetaRegistry for meta pool rejection (required for self-deploy). +**StableSwap (stableswap):** `STABLESWAP_METAREGISTRY_` — Curve MetaRegistry for meta pool rejection (required for self-deploy). -**StableSwap-NG (stableswapng):** `STABLESWAPNG_FACTORY_ADDRESS_` or `FACTORY_ADDRESS_` — Curve StableSwap NG factory for meta pool rejection. Defaults to mainnet `0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf` if unset. +**StableSwap-NG (stableswapng):** `STABLESWAPNG_FACTORY_` — Curve StableSwap NG factory for meta pool rejection. Defaults to mainnet `0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf` if unset. ### Security @@ -278,11 +278,12 @@ The `createPools` script and `mine_hook.sh` run from the **contracts/** director > **Note:** When running `createPools` via `npx tsx src/createPools.ts`, run it from **aggregator-hooks/** so it loads `aggregator-hooks/.env`. The forge scripts invoked by createPools run from contracts/ but inherit env vars from the parent process. -1. **v4-hooks-public** (aggregator-hooks-ported branch): Already added as submodule. Ensure it's on the `aggregator-hooks-ported` branch: +1. **v4-hooks-public** (`main` branch): Already added as submodule. Track latest `main` (from **contracts/** root): ```bash - cd lib/v4-hooks-public && git fetch origin aggregator-hooks-ported && git checkout aggregator-hooks-ported + git submodule update --init --recursive + (cd lib/v4-hooks-public && git fetch origin main && git checkout main && git pull origin main) git submodule update --init --recursive ``` -2. **Foundry**: `forge` must be available. The scripts use `script/SelfCreateHook.s.sol` and `lib/v4-hooks-public/script/MineAggregatorHook.s.sol`. +2. **Foundry**: `forge` must be available. The scripts use `lib/v4-hooks-public/script/SelfCreateHook.s.sol` and `lib/v4-hooks-public/script/MineAggregatorHook.s.sol`. diff --git a/aggregator-hooks/creation-modules/FluidDexLite.ts b/aggregator-hooks/creation-modules/FluidDexLite.ts index d70a7cc7..38bf5917 100644 --- a/aggregator-hooks/creation-modules/FluidDexLite.ts +++ b/aggregator-hooks/creation-modules/FluidDexLite.ts @@ -2,7 +2,7 @@ * FluidDex Lite aggregator hook deployment module. */ import { ethers } from "ethers"; -import { getEnvForChain, mustEnvForChain } from "../src/cli.js"; +import { mustEnvForChain } from "../src/cli.js"; import { FLUIDDEXLITE_FACTORY_ABI } from "../abis/index.js"; import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables } from "./types.js"; @@ -54,19 +54,10 @@ export const fluiddexliteModule: CreationModule = { }, getImmutablesFromEnv(chainId: number): FactoryImmutables { - const dexLite = getEnvForChain("FLUID_DEX_LITE", chainId) ?? getEnvForChain("DEX_LITE_ADDRESS", chainId); - const dexLiteResolver = - getEnvForChain("FLUID_DEX_LITE_RESOLVER", chainId) ?? getEnvForChain("DEX_LITE_RESOLVER_ADDRESS", chainId); - if (!dexLite) - throw new Error(`FLUID_DEX_LITE_${chainId} or DEX_LITE_ADDRESS_${chainId} required for fluiddexlite self-deploy`); - if (!dexLiteResolver) - throw new Error( - `FLUID_DEX_LITE_RESOLVER_${chainId} or DEX_LITE_RESOLVER_ADDRESS_${chainId} required for fluiddexlite self-deploy`, - ); return { poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, - fluidDexLite: dexLite as Address, - fluidDexLiteResolver: dexLiteResolver as Address, + fluidDexLite: mustEnvForChain("FLUID_DEX_LITE", chainId) as Address, + fluidDexLiteResolver: mustEnvForChain("FLUID_DEX_LITE_RESOLVER", chainId) as Address, }; }, diff --git a/aggregator-hooks/creation-modules/FluidDexT1.ts b/aggregator-hooks/creation-modules/FluidDexT1.ts index 86f21856..5574ffa4 100644 --- a/aggregator-hooks/creation-modules/FluidDexT1.ts +++ b/aggregator-hooks/creation-modules/FluidDexT1.ts @@ -2,7 +2,7 @@ * FluidDex T1 aggregator hook deployment module. */ import { ethers } from "ethers"; -import { getEnvForChain, mustEnvForChain } from "../src/cli.js"; +import { mustEnvForChain } from "../src/cli.js"; import { FLUIDDEXT1_FACTORY_ABI } from "../abis/index.js"; import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables } from "./types.js"; @@ -54,19 +54,10 @@ export const fluiddext1Module: CreationModule = { }, getImmutablesFromEnv(chainId: number): FactoryImmutables { - const reservesResolver = - getEnvForChain("FLUID_DEX_T1_RESERVES_RESOLVER", chainId) ?? - getEnvForChain("FLUID_DEX_RESERVES_RESOLVER", chainId); - const resolver = getEnvForChain("FLUID_DEX_T1_RESOLVER", chainId) ?? getEnvForChain("FLUID_DEX_RESOLVER", chainId); - if (!reservesResolver) - throw new Error( - `Missing env: FLUID_DEX_T1_RESERVES_RESOLVER_${chainId} or FLUID_DEX_RESERVES_RESOLVER_${chainId}`, - ); - if (!resolver) throw new Error(`Missing env: FLUID_DEX_T1_RESOLVER_${chainId} or FLUID_DEX_RESOLVER_${chainId}`); return { poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, - fluidDexReservesResolver: reservesResolver as Address, - fluidDexResolver: resolver as Address, + fluidDexReservesResolver: mustEnvForChain("FLUID_DEX_T1_RESERVES_RESOLVER", chainId) as Address, + fluidDexResolver: mustEnvForChain("FLUID_DEX_T1_RESOLVER", chainId) as Address, fluidLiquidity: mustEnvForChain("FLUID_LIQUIDITY", chainId) as Address, }; }, diff --git a/aggregator-hooks/creation-modules/StableSwap.ts b/aggregator-hooks/creation-modules/StableSwap.ts index d8e21a9a..b5e267ad 100644 --- a/aggregator-hooks/creation-modules/StableSwap.ts +++ b/aggregator-hooks/creation-modules/StableSwap.ts @@ -65,7 +65,7 @@ export const stableswapModule: CreationModule = { }, getImmutablesFromEnv(chainId: number): FactoryImmutables { - const metaRegistry = mustEnvForChain("STABLESWAP_METAREGISTRY_ADDRESS", chainId) as Address; + const metaRegistry = mustEnvForChain("STABLESWAP_METAREGISTRY", chainId) as Address; return { poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, metaRegistry, @@ -90,7 +90,7 @@ export const stableswapModule: CreationModule = { const metaRegistry = immutables.metaRegistry ?? (immutables as { metaRegistry?: Address }).metaRegistry; if (!metaRegistry) { throw new Error( - "StableSwap requires metaRegistry. Set STABLESWAP_METAREGISTRY_ADDRESS or use StableSwapAggregatorFactory.", + "StableSwap requires metaRegistry. Set STABLESWAP_METAREGISTRY or use StableSwapAggregatorFactory.", ); } const encoded = ethers.AbiCoder.defaultAbiCoder().encode( diff --git a/aggregator-hooks/creation-modules/StableSwapNG.ts b/aggregator-hooks/creation-modules/StableSwapNG.ts index 8303c89f..15e1958c 100644 --- a/aggregator-hooks/creation-modules/StableSwapNG.ts +++ b/aggregator-hooks/creation-modules/StableSwapNG.ts @@ -67,10 +67,7 @@ export const stableswapngModule: CreationModule = { }, getImmutablesFromEnv(chainId: number): FactoryImmutables { - const curveFactory = - getEnvForChain("STABLESWAPNG_FACTORY_ADDRESS", chainId) ?? - getEnvForChain("FACTORY_ADDRESS", chainId) ?? - DEFAULT_STABLESWAPNG_FACTORY; + const curveFactory = getEnvForChain("STABLESWAPNG_FACTORY", chainId) ?? DEFAULT_STABLESWAPNG_FACTORY; return { poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, curveFactory: curveFactory as Address, @@ -90,7 +87,7 @@ export const stableswapngModule: CreationModule = { const curveFactory = immutables.curveFactory ?? (immutables as { curveFactory?: Address }).curveFactory; if (!curveFactory) { throw new Error( - "StableSwapNG requires curveFactory. Set STABLESWAPNG_FACTORY_ADDRESS or use StableSwapNGAggregatorFactory.", + "StableSwapNG requires curveFactory. Set STABLESWAPNG_FACTORY or use StableSwapNGAggregatorFactory.", ); } const encoded = ethers.AbiCoder.defaultAbiCoder().encode( diff --git a/aggregator-hooks/historical/FluidDexLite.ts b/aggregator-hooks/historical/FluidDexLite.ts index e2900607..4f8283a8 100644 --- a/aggregator-hooks/historical/FluidDexLite.ts +++ b/aggregator-hooks/historical/FluidDexLite.ts @@ -9,13 +9,12 @@ * npx tsx historical/fluiddexlite.ts --chain-id 1 * * Options: - * --chain-id (required) Chain ID; loads RPC_URL_, DEX_LITE_RESOLVER_ADDRESS_ + * --chain-id (required) Chain ID; loads RPC_URL_, FLUID_DEX_LITE_RESOLVER_ * --output-dir output directory (default: detected); writes to output-dir/chain-id/fluiddexlite-pools.json * * Env vars (use VAR_ or VAR for single chain): * RPC_URL (required) - * DEX_LITE_RESOLVER_ADDRESS (optional) FluidDexLiteResolver; default mainnet: 0x26b696D0dfDAB6c894Aa9a6575fCD07BB25BbD2C - * DEX_LITE_ADDRESS (optional, legacy) kept for backward compat; resolver is used for discovery + * FLUID_DEX_LITE_RESOLVER (optional) FluidDexLiteResolver; default mainnet: 0x26b696D0dfDAB6c894Aa9a6575fCD07BB25BbD2C * * Output: JSON array in createPools.ts FluidDexLitePoolConfig format. * Fees are fetched via getDexState() and converted from Fluid 1e4 to Uniswap v4 1e6 format. @@ -84,7 +83,7 @@ async function main() { } const rpcUrl = getEnvForChain("RPC_URL", chainId); - const resolverRaw = getEnvForChain("DEX_LITE_RESOLVER_ADDRESS", chainId) ?? DEFAULT_RESOLVER; + const resolverRaw = getEnvForChain("FLUID_DEX_LITE_RESOLVER", chainId) ?? DEFAULT_RESOLVER; if (!rpcUrl) { console.error("Missing env: RPC_URL (or RPC_URL_)"); process.exit(1); diff --git a/aggregator-hooks/historical/FluidDexT1.ts b/aggregator-hooks/historical/FluidDexT1.ts index af7dbb0b..36dab27a 100644 --- a/aggregator-hooks/historical/FluidDexT1.ts +++ b/aggregator-hooks/historical/FluidDexT1.ts @@ -6,7 +6,7 @@ * npx tsx historical/fluiddext1.ts --chain-id 1 * * Options: - * --chain-id (required) Chain ID; loads RPC_URL_, FLUID_DEX_RESOLVER_, etc. + * --chain-id (required) Chain ID; loads RPC_URL_, FLUID_DEX_T1_RESOLVER_, etc. * --output-dir output directory (default: output); writes to output-dir/chain-id/fluiddext1-pools.json * --chunk-blocks block chunk size for getLogs (default: 100000) * --start-block start block for log scan (default: 0) @@ -15,8 +15,8 @@ * * Env vars (use VAR_ or VAR for single chain): * RPC_URL (required) - * FLUID_DEX_T1_RESOLVER (required) IFluidDexResolver for getPoolTokens; FLUID_DEX_RESOLVER fallback - * FLUID_DEX_FACTORY (optional, default mainnet) + * FLUID_DEX_T1_RESOLVER (required) IFluidDexResolver for getPoolTokens + * FLUID_DEX_T1_FACTORY (optional, default mainnet) * RPS (optional, default 80) max RPC requests per second * CONCURRENCY (optional, default 8) max concurrent RPC calls * @@ -111,13 +111,12 @@ async function main() { } const rpcUrl = getEnvForChain("RPC_URL", chainId); - const resolverAddr = - getEnvForChain("FLUID_DEX_T1_RESOLVER", chainId) ?? getEnvForChain("FLUID_DEX_RESOLVER", chainId); + const resolverAddr = getEnvForChain("FLUID_DEX_T1_RESOLVER", chainId); const factoryAddrRaw = - getEnvForChain("FLUID_DEX_FACTORY", chainId) ?? getEnvForChain("FACTORY_ADDRESS", chainId) ?? DEFAULT_FACTORY; + getEnvForChain("FLUID_DEX_T1_FACTORY", chainId) ?? DEFAULT_FACTORY; if (!rpcUrl || !resolverAddr) { - console.error("Missing env: RPC_URL and (FLUID_DEX_T1_RESOLVER or FLUID_DEX_RESOLVER)"); + console.error("Missing env: RPC_URL and FLUID_DEX_T1_RESOLVER"); process.exit(1); } diff --git a/aggregator-hooks/historical/StableSwapNG.ts b/aggregator-hooks/historical/StableSwapNG.ts index 9f78d720..dfa45362 100644 --- a/aggregator-hooks/historical/StableSwapNG.ts +++ b/aggregator-hooks/historical/StableSwapNG.ts @@ -6,14 +6,14 @@ * npx tsx historical/stableswapng.ts --chain-id 1 * * Options: - * --chain-id (required) Chain ID; loads RPC_URL_, FACTORY_ADDRESS_ + * --chain-id (required) Chain ID; loads RPC_URL_, STABLESWAPNG_FACTORY_ * --output-dir output directory (default: output); writes to output-dir/chain-id/stableswapng-pools.json * --chunk chunk size for pool_list reads (default: 500) * --start-index start at pool_list index n (default: 0) * * Env vars (use VAR_ or VAR for single chain): * RPC_URL (required) - * FACTORY_ADDRESS (optional, default mainnet Curve StableSwap-NG factory) + * STABLESWAPNG_FACTORY (optional, default mainnet Curve StableSwap-NG factory) * RPS (optional, default 80) max RPC requests per second * CONCURRENCY (optional, default 8) max concurrent RPC calls * @@ -121,7 +121,7 @@ async function main() { } const rpcUrl = getEnvForChain("RPC_URL", chainId); - const factoryAddrRaw = getEnvForChain("FACTORY_ADDRESS", chainId) ?? DEFAULT_FACTORY; + const factoryAddrRaw = getEnvForChain("STABLESWAPNG_FACTORY", chainId) ?? DEFAULT_FACTORY; if (!rpcUrl) { console.error("Missing env: RPC_URL (or RPC_URL_)"); diff --git a/aggregator-hooks/polling/FluidDexLite.ts b/aggregator-hooks/polling/FluidDexLite.ts index 1d63c416..41e75089 100644 --- a/aggregator-hooks/polling/FluidDexLite.ts +++ b/aggregator-hooks/polling/FluidDexLite.ts @@ -9,7 +9,7 @@ * npx tsx polling/fluiddexlite.ts --chain-id 1 * * Options: - * --chain-id (required) Chain ID; loads RPC_URL_, DEX_LITE_ADDRESS_ + * --chain-id (required) Chain ID; loads RPC_URL_, FLUID_DEX_LITE_ * --output-dir output directory (default: detected); writes to output-dir/chain-id/fluiddexlite-pools.json * --checkpoint-dir checkpoint directory (default: checkpoints); writes to checkpoint-dir/chain-id/dexlite_checkpoint.json * --chunk-blocks block chunk size for getLogs (default: 10000) @@ -17,7 +17,7 @@ * * Env vars (use VAR_ or VAR for single chain): * RPC_URL (required) - * DEX_LITE_ADDRESS (required) FluidDexLite singleton + * FLUID_DEX_LITE (required) FluidDexLite singleton * FINALITY_BLOCKS (optional, default 10) subtract from latest; checkpoint = last scanned block * LOOKBACK_BLOCKS (optional, default 200000) used when checkpoint missing and no --start-block */ @@ -130,10 +130,10 @@ async function main() { } const rpcUrl = getEnvForChain("RPC_URL", chainId); - const dexLiteRaw = getEnvForChain("DEX_LITE_ADDRESS", chainId); + const dexLiteRaw = getEnvForChain("FLUID_DEX_LITE", chainId); if (!rpcUrl || !dexLiteRaw) { throw new Error( - "Missing required env: RPC_URL and DEX_LITE_ADDRESS (or RPC_URL_, DEX_LITE_ADDRESS_)", + "Missing required env: RPC_URL and FLUID_DEX_LITE (or RPC_URL_, FLUID_DEX_LITE_)", ); } const dexLite = ethers.getAddress(dexLiteRaw); diff --git a/aggregator-hooks/polling/FluidDexT1.ts b/aggregator-hooks/polling/FluidDexT1.ts index ab381653..ef54b24f 100644 --- a/aggregator-hooks/polling/FluidDexT1.ts +++ b/aggregator-hooks/polling/FluidDexT1.ts @@ -9,7 +9,7 @@ * npx tsx polling/fluiddext1.ts --chain-id 1 * * Options: - * --chain-id (required) Chain ID; loads RPC_URL_, FLUID_DEX_RESOLVER_, etc. + * --chain-id (required) Chain ID; loads RPC_URL_, FLUID_DEX_T1_RESOLVER_, etc. * --output-dir output directory (default: detected); writes to output-dir/chain-id/fluiddext1-pools.json * --checkpoint-dir checkpoint directory (default: checkpoints) * --chunk-blocks block chunk size for getLogs (default: 10000) @@ -17,8 +17,8 @@ * * Env vars (use VAR_ or VAR for single chain): * RPC_URL (required) - * FLUID_DEX_T1_RESOLVER (required) IFluidDexResolver for getPoolTokens; FLUID_DEX_RESOLVER fallback - * FLUID_DEX_FACTORY (optional, default mainnet) + * FLUID_DEX_T1_RESOLVER (required) IFluidDexResolver for getPoolTokens + * FLUID_DEX_T1_FACTORY (optional, default mainnet) * FINALITY_BLOCKS (optional, default 10) subtract from latest; checkpoint = last scanned block * LOOKBACK_BLOCKS (optional, default 200000) used when checkpoint missing and no --start-block */ @@ -125,13 +125,12 @@ async function main() { } const rpcUrl = getEnvForChain("RPC_URL", chainId); - const resolverAddr = - getEnvForChain("FLUID_DEX_T1_RESOLVER", chainId) ?? getEnvForChain("FLUID_DEX_RESOLVER", chainId); + const resolverAddr = getEnvForChain("FLUID_DEX_T1_RESOLVER", chainId); const factoryRaw = - getEnvForChain("FLUID_DEX_FACTORY", chainId) ?? getEnvForChain("FACTORY_ADDRESS", chainId) ?? DEFAULT_FACTORY; + getEnvForChain("FLUID_DEX_T1_FACTORY", chainId) ?? DEFAULT_FACTORY; if (!rpcUrl || !resolverAddr) { - throw new Error("Missing required env: RPC_URL and (FLUID_DEX_T1_RESOLVER or FLUID_DEX_RESOLVER)"); + throw new Error("Missing required env: RPC_URL and FLUID_DEX_T1_RESOLVER"); } const factory = ethers.getAddress(factoryRaw); diff --git a/aggregator-hooks/polling/StableSwapNG.ts b/aggregator-hooks/polling/StableSwapNG.ts index 6f475d94..87804d5b 100644 --- a/aggregator-hooks/polling/StableSwapNG.ts +++ b/aggregator-hooks/polling/StableSwapNG.ts @@ -10,7 +10,7 @@ * npx tsx polling/stableswapng.ts --chain-id 1 * * Options: - * --chain-id (required) Chain ID; loads RPC_URL_, FACTORY_ADDRESS_ + * --chain-id (required) Chain ID; loads RPC_URL_, STABLESWAPNG_FACTORY_ * --output-dir output directory (default: detected) * --checkpoint-dir checkpoint directory (default: checkpoints) * --chunk-blocks block chunk size for getLogs (default: 10000) @@ -18,7 +18,7 @@ * * Env vars (use VAR_ or VAR for single chain): * RPC_URL (required) - * FACTORY_ADDRESS (optional, default mainnet Curve StableSwap-NG factory) + * STABLESWAPNG_FACTORY (optional, default mainnet Curve StableSwap-NG factory) * FINALITY_BLOCKS (optional, default 10) subtract from latest; checkpoint = last scanned block * LOOKBACK_BLOCKS (optional, default 200000) used when checkpoint missing and no --start-block * RPS (optional, default 80) max RPC requests per second @@ -154,7 +154,7 @@ async function main() { } const rpcUrl = getEnvForChain("RPC_URL", chainId); - const factoryRaw = getEnvForChain("FACTORY_ADDRESS", chainId) ?? DEFAULT_FACTORY; + const factoryRaw = getEnvForChain("STABLESWAPNG_FACTORY", chainId) ?? DEFAULT_FACTORY; if (!rpcUrl) { throw new Error("Missing required env: RPC_URL (or RPC_URL_)"); diff --git a/foundry.lock b/foundry.lock index 4915ae00..17449e57 100644 --- a/foundry.lock +++ b/foundry.lock @@ -24,10 +24,7 @@ } }, "lib/v4-hooks-public": { - "branch": { - "name": "aggregator-hooks", - "rev": "f7b6196ad6677e296850ef06b183e2eed6bb833e" - } + "rev": "a187a29aaefa716cf2fed38d4bc934c67d36c0f0" }, "src/pkgs/calibur": { "rev": "69d5eb61498ffac7740530310b270459f2ae2a20" diff --git a/lib/forge-chronicles b/lib/forge-chronicles index ee772636..000cd249 160000 --- a/lib/forge-chronicles +++ b/lib/forge-chronicles @@ -1 +1 @@ -Subproject commit ee77263652129d7492a79a7c4db3fb5d36a3f079 +Subproject commit 000cd249d0bad6caaa907c12f2d4a8a7608ac5f0 diff --git a/lib/v4-hooks-public b/lib/v4-hooks-public index a824e322..a187a29a 160000 --- a/lib/v4-hooks-public +++ b/lib/v4-hooks-public @@ -1 +1 @@ -Subproject commit a824e322384e296c6da392b4285452c81ca4ec64 +Subproject commit a187a29aaefa716cf2fed38d4bc934c67d36c0f0 diff --git a/mine_hook.sh b/mine_hook.sh index 04169ba9..d72235ba 100644 --- a/mine_hook.sh +++ b/mine_hook.sh @@ -81,9 +81,9 @@ cd "$SCRIPT_DIR" if [ ! -f "lib/v4-hooks-public/script/MineAggregatorHook.s.sol" ]; then echo "Error: MineAggregatorHook.s.sol not found at lib/v4-hooks-public/script/MineAggregatorHook.s.sol" - echo "Add v4-hooks-public and checkout aggregator-hooks-ported branch:" - echo " git submodule add https://github.com/Uniswap/v4-hooks-public lib/v4-hooks-public" - echo " cd lib/v4-hooks-public && git fetch origin aggregator-hooks-ported && git checkout aggregator-hooks-ported" + echo "Add v4-hooks-public and checkout main:" + echo " git submodule add -b main https://github.com/Uniswap/v4-hooks-public lib/v4-hooks-public" + echo " cd lib/v4-hooks-public && git fetch origin main && git checkout main" echo " git submodule update --init --recursive" exit 1 fi From 8e04471dc9388576c521b2feda2f9dd28dbf3598 Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Fri, 1 May 2026 10:18:59 -0400 Subject: [PATCH 17/21] prod deployments --- aggregator-hooks/.env.example | 2 +- aggregator-hooks/abis/FluidDexT1Resolver.json | 2 +- aggregator-hooks/creation-modules/index.ts | 1 + aggregator-hooks/creation-modules/types.ts | 8 +++- aggregator-hooks/historical/FluidDexT1.ts | 6 +-- aggregator-hooks/polling/FluidDexT1.ts | 8 ++-- aggregator-hooks/src/createPools.ts | 42 +++++++------------ foundry.lock | 2 +- 8 files changed, 34 insertions(+), 37 deletions(-) diff --git a/aggregator-hooks/.env.example b/aggregator-hooks/.env.example index 6fd2b366..3779e055 100644 --- a/aggregator-hooks/.env.example +++ b/aggregator-hooks/.env.example @@ -17,7 +17,7 @@ #FLUID_DEX_LITE_RESOLVER_= # discovery + createPools self-deploy (fluiddexlite) #FLUID_DEX_T1_FACTORY_= # FluidDexT1: both resolvers required for pool creation; resolver only for discovery (historical/polling) -#FLUID_DEX_T1_RESOLVER_= # IFluidDexResolver (getPoolTokens, estimateSwap) +#FLUID_DEX_T1_RESOLVER_= # IFluidDexResolver (getDexTokens, estimateSwap) #FLUID_DEX_T1_RESERVES_RESOLVER_= # IFluidDexReservesResolver (getPoolReserves) # StableSwap (legacy Curve): MetaRegistry for meta pool rejection (createPools self-deploy) diff --git a/aggregator-hooks/abis/FluidDexT1Resolver.json b/aggregator-hooks/abis/FluidDexT1Resolver.json index 627bd185..3a24aeab 100644 --- a/aggregator-hooks/abis/FluidDexT1Resolver.json +++ b/aggregator-hooks/abis/FluidDexT1Resolver.json @@ -1 +1 @@ -["function getPoolTokens(address pool) external view returns (address token0, address token1)"] +["function getDexTokens(address dex_) external view returns (address token0_, address token1_)"] diff --git a/aggregator-hooks/creation-modules/index.ts b/aggregator-hooks/creation-modules/index.ts index 56703504..2db43d85 100644 --- a/aggregator-hooks/creation-modules/index.ts +++ b/aggregator-hooks/creation-modules/index.ts @@ -11,6 +11,7 @@ export type { Address, CreationModule, PoolKeyRecord, + PoolEntry, PoolDeployedEntry, FactoryImmutables, HookParams, diff --git a/aggregator-hooks/creation-modules/types.ts b/aggregator-hooks/creation-modules/types.ts index 7a87e6aa..368dba41 100644 --- a/aggregator-hooks/creation-modules/types.ts +++ b/aggregator-hooks/creation-modules/types.ts @@ -16,9 +16,15 @@ export interface PoolKeyRecord { hooks: Address; } +/** A single pool: its PoolKey paired with the computed PoolId */ +export interface PoolEntry { + poolKey: PoolKeyRecord; + poolId: string; +} + /** Entry appended to pool-deployed registry */ export interface PoolDeployedEntry { - poolKeys: PoolKeyRecord[]; + pools: PoolEntry[]; metadata: { externalPool: string; hookAddress: Address; diff --git a/aggregator-hooks/historical/FluidDexT1.ts b/aggregator-hooks/historical/FluidDexT1.ts index 36dab27a..fb277c81 100644 --- a/aggregator-hooks/historical/FluidDexT1.ts +++ b/aggregator-hooks/historical/FluidDexT1.ts @@ -15,7 +15,7 @@ * * Env vars (use VAR_ or VAR for single chain): * RPC_URL (required) - * FLUID_DEX_T1_RESOLVER (required) IFluidDexResolver for getPoolTokens + * FLUID_DEX_T1_RESOLVER (required) IFluidDexResolver for getDexTokens * FLUID_DEX_T1_FACTORY (optional, default mainnet) * RPS (optional, default 80) max RPC requests per second * CONCURRENCY (optional, default 8) max concurrent RPC calls @@ -200,7 +200,7 @@ async function main() { let token0: Address; let token1: Address; try { - [token0, token1] = (await resolver.getPoolTokens(fluidPool)) as [Address, Address]; + [token0, token1] = (await resolver.getDexTokens(fluidPool)) as [Address, Address]; } catch { return null; } @@ -222,7 +222,7 @@ async function main() { } if (skipped > 0) { - console.error(`Skipped ${skipped} pools (getPoolTokens reverted - may be VaultT1 or deprecated)`); + console.error(`Skipped ${skipped} pools (getDexTokens reverted - may be VaultT1 or deprecated)`); } const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); diff --git a/aggregator-hooks/polling/FluidDexT1.ts b/aggregator-hooks/polling/FluidDexT1.ts index ef54b24f..a0ce7d5b 100644 --- a/aggregator-hooks/polling/FluidDexT1.ts +++ b/aggregator-hooks/polling/FluidDexT1.ts @@ -17,7 +17,7 @@ * * Env vars (use VAR_ or VAR for single chain): * RPC_URL (required) - * FLUID_DEX_T1_RESOLVER (required) IFluidDexResolver for getPoolTokens + * FLUID_DEX_T1_RESOLVER (required) IFluidDexResolver for getDexTokens * FLUID_DEX_T1_FACTORY (optional, default mainnet) * FINALITY_BLOCKS (optional, default 10) subtract from latest; checkpoint = last scanned block * LOOKBACK_BLOCKS (optional, default 200000) used when checkpoint missing and no --start-block @@ -61,7 +61,7 @@ type CreatePoolsFluidDexT1Config = { const FACTORY_ABI = ["event LogDexDeployed(address indexed dex, uint256 indexed dexId)"]; -const RESOLVER_ABI = ["function getPoolTokens(address pool) external view returns (address token0, address token1)"]; +const RESOLVER_ABI = ["function getDexTokens(address dex_) external view returns (address token0_, address token1_)"]; function ensureDirForFile(filePath: string) { fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true }); @@ -209,9 +209,9 @@ async function main() { let token0: string; let token1: string; try { - [token0, token1] = await resolver.getPoolTokens(dex); + [token0, token1] = await resolver.getDexTokens(dex); } catch { - console.error(`Failed getPoolTokens for ${dex}`); + console.error(`Failed getDexTokens for ${dex}`); continue; } diff --git a/aggregator-hooks/src/createPools.ts b/aggregator-hooks/src/createPools.ts index 0899074b..ce9a627e 100644 --- a/aggregator-hooks/src/createPools.ts +++ b/aggregator-hooks/src/createPools.ts @@ -15,6 +15,7 @@ import { type Address, type PoolConfig, type PoolDeployedEntry, + type PoolEntry, type PoolKeyRecord, type FactoryImmutables, } from "../creation-modules/index.js"; @@ -425,8 +426,7 @@ async function verifyDeploymentOnForgeFailure( hookDeployed: boolean; poolsInitialized: number; blockNumber?: number; - poolKeysFromEvent?: PoolKeyRecord[]; - poolIdFromEvent?: string; + poolEntriesFromEvent?: PoolEntry[]; } | null> { const module = CREATION_MODULES[poolType]; if (!module) return null; @@ -441,9 +441,8 @@ async function verifyDeploymentOnForgeFailure( if (!hookDeployed) return { hookAddress, hookDeployed: false, poolsInitialized: 0 }; const poolKeys = module.buildPoolKeys(poolConfig, hookAddress); - const poolKeysFromEvent: PoolKeyRecord[] = []; + const poolEntriesFromEvent: PoolEntry[] = []; let initializeBlockNumber: number | undefined; - let poolIdFromEvent: string | undefined; try { const blockNumber = await provider.getBlockNumber(); @@ -475,14 +474,8 @@ async function verifyDeploymentOnForgeFailure( ) ) { if (initializeBlockNumber === undefined) initializeBlockNumber = Number(log.blockNumber); - if (poolIdFromEvent === undefined) poolIdFromEvent = log.topics[1]; - poolKeysFromEvent.push({ - currency0, - currency1, - fee, - tickSpacing, - hooks: hookAddress, - }); + const poolKey: PoolKeyRecord = { currency0, currency1, fee, tickSpacing, hooks: hookAddress }; + poolEntriesFromEvent.push({ poolKey, poolId: log.topics[1] }); } } } catch { @@ -492,10 +485,9 @@ async function verifyDeploymentOnForgeFailure( return { hookAddress, hookDeployed, - poolsInitialized: poolKeysFromEvent.length, + poolsInitialized: poolEntriesFromEvent.length, blockNumber: initializeBlockNumber, - poolKeysFromEvent: poolKeysFromEvent.length > 0 ? poolKeysFromEvent : undefined, - poolIdFromEvent, + poolEntriesFromEvent: poolEntriesFromEvent.length > 0 ? poolEntriesFromEvent : undefined, }; } @@ -859,11 +851,10 @@ async function main() { registryDir, poolType, { - poolKeys, + pools: poolKeys.map((poolKey) => ({ poolKey, poolId: computePoolId(poolKey) })), metadata: { externalPool: module.getExternalPool(poolConfig), hookAddress, - poolId: computePoolId(poolKeys[0]), blockNumber, }, }, @@ -901,21 +892,21 @@ async function main() { log.error(" Check block explorer to confirm."); if (registryDir && !dryRun) { - const poolKeys = - verification.poolKeysFromEvent ?? module.buildPoolKeys(poolConfig, verification.hookAddress); - const poolId = - verification.poolIdFromEvent ?? (poolKeys.length > 0 ? computePoolId(poolKeys[0]) : undefined); + const pools: PoolEntry[] = + verification.poolEntriesFromEvent ?? + module + .buildPoolKeys(poolConfig, verification.hookAddress) + .map((poolKey) => ({ poolKey, poolId: computePoolId(poolKey) })); const blockNumber = verification.blockNumber ?? Number(await provider.getBlockNumber()); - if (poolKeys.length > 0 && poolId) { + if (pools.length > 0) { appendToRegistryFile( registryDir, poolType, { - poolKeys, + pools, metadata: { externalPool: module.getExternalPool(poolConfig), hookAddress: verification.hookAddress, - poolId, blockNumber, }, }, @@ -974,11 +965,10 @@ async function main() { registryDir, poolType, { - poolKeys, + pools: poolKeys.map((poolKey) => ({ poolKey, poolId: computePoolId(poolKey) })), metadata: { externalPool: module.getExternalPool(poolConfig), hookAddress: result.hookAddress, - poolId: computePoolId(poolKeys[0]), txHash: result.txHash, blockNumber: result.blockNumber, }, diff --git a/foundry.lock b/foundry.lock index 17449e57..fe8bed99 100644 --- a/foundry.lock +++ b/foundry.lock @@ -24,7 +24,7 @@ } }, "lib/v4-hooks-public": { - "rev": "a187a29aaefa716cf2fed38d4bc934c67d36c0f0" + "rev": "d68c16abdc16f5e5319865739457ef17081ea789" }, "src/pkgs/calibur": { "rev": "69d5eb61498ffac7740530310b270459f2ae2a20" From df8a56c21dbbaed1f52d6e541b306f7cdc5f51c8 Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Fri, 1 May 2026 17:20:41 -0400 Subject: [PATCH 18/21] add new agg hook types --- aggregator-hooks/.env.example | 17 +- .../creation-modules/PancakeSwapV3.ts | 97 ++++++ .../creation-modules/Slipstream.ts | 97 ++++++ .../creation-modules/UniswapV2.ts | 97 ++++++ .../creation-modules/UniswapV3.ts | 97 ++++++ aggregator-hooks/creation-modules/index.ts | 19 +- aggregator-hooks/creation-modules/types.ts | 27 +- aggregator-hooks/historical/PancakeSwapV3.ts | 168 ++++++++++ aggregator-hooks/historical/Slipstream.ts | 172 +++++++++++ aggregator-hooks/historical/UniswapV2.ts | 168 ++++++++++ aggregator-hooks/historical/UniswapV3.ts | 172 +++++++++++ aggregator-hooks/polling/PancakeSwapV3.ts | 183 +++++++++++ aggregator-hooks/polling/Slipstream.ts | 182 +++++++++++ aggregator-hooks/polling/UniswapV2.ts | 180 +++++++++++ aggregator-hooks/polling/UniswapV3.ts | 183 +++++++++++ aggregator-hooks/src/createPools.ts | 290 ++++++++++++++---- 16 files changed, 2079 insertions(+), 70 deletions(-) create mode 100644 aggregator-hooks/creation-modules/PancakeSwapV3.ts create mode 100644 aggregator-hooks/creation-modules/Slipstream.ts create mode 100644 aggregator-hooks/creation-modules/UniswapV2.ts create mode 100644 aggregator-hooks/creation-modules/UniswapV3.ts create mode 100644 aggregator-hooks/historical/PancakeSwapV3.ts create mode 100644 aggregator-hooks/historical/Slipstream.ts create mode 100644 aggregator-hooks/historical/UniswapV2.ts create mode 100644 aggregator-hooks/historical/UniswapV3.ts create mode 100644 aggregator-hooks/polling/PancakeSwapV3.ts create mode 100644 aggregator-hooks/polling/Slipstream.ts create mode 100644 aggregator-hooks/polling/UniswapV2.ts create mode 100644 aggregator-hooks/polling/UniswapV3.ts diff --git a/aggregator-hooks/.env.example b/aggregator-hooks/.env.example index 3779e055..cafe37a6 100644 --- a/aggregator-hooks/.env.example +++ b/aggregator-hooks/.env.example @@ -26,5 +26,18 @@ # StableSwap-NG: Curve factory (meta pool rejection). Defaults to mainnet if unset. #STABLESWAPNG_FACTORY_= - - +# ── Singleton aggregator types (UniswapV3, UniswapV2, Slipstream, PancakeSwapV3) ── +# One aggregator contract is deployed per chain and reused for all pools of that type. +# The address is written automatically to .env after the first deploy; pre-set it to +# skip re-deployment on subsequent runs. + +#UNISWAP_V3_AGGREGATOR_= # auto-written on first deploy (uniswapv3) +#UNISWAP_V2_AGGREGATOR_= # auto-written on first deploy (uniswapv2) +#SLIPSTREAM_AGGREGATOR_= # auto-written on first deploy (slipstream) +#PANCAKESWAP_V3_AGGREGATOR_= # auto-written on first deploy (pancakeswapv3) + +# External DEX factory addresses (required for singleton aggregator deploy) +#UNISWAP_V3_FACTORY_= +#UNISWAP_V2_FACTORY_= +#SLIPSTREAM_FACTORY_= +#PANCAKESWAP_V3_FACTORY_= diff --git a/aggregator-hooks/creation-modules/PancakeSwapV3.ts b/aggregator-hooks/creation-modules/PancakeSwapV3.ts new file mode 100644 index 00000000..e27080fa --- /dev/null +++ b/aggregator-hooks/creation-modules/PancakeSwapV3.ts @@ -0,0 +1,97 @@ +/** + * PancakeSwap V3 singleton aggregator hook deployment module. + * + * One PancakeSwapV3Aggregator is deployed per chain. Identical to + * UniswapV3Aggregator in routing logic (fee-tier factory lookup) but + * implements the PancakeSwap V3 swap callback ABI instead of Uniswap V3. + */ +import { ethers } from "ethers"; +import { mustEnvForChain } from "../src/cli.js"; +import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables, type PoolKeyRecord } from "./types.js"; + +export interface PancakeSwapV3PoolConfig { + poolType: "pancakeswapv3"; + /** The existing PancakeSwap V3 pool address being wrapped */ + v3Pool: Address; + currency0: Address; + currency1: Address; + /** PancakeSwap V3 fee tier (e.g. 100, 500, 2500, 10000) */ + fee: number; + tickSpacing?: number | null; + sqrtPriceX96?: bigint | null; +} + +const PROTOCOL_ID = 0x93; + +const AGGREGATOR_ABI = [ + "function poolManager() view returns (address)", + "function factory() view returns (address)", +]; + +const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); + +export const pancakeswapv3Module: CreationModule = { + poolType: "pancakeswapv3", + protocolId: PROTOCOL_ID, + factoryAbi: [], + contractIdentifier: + "lib/v4-hooks-public/src/aggregator-hooks/PancakeSwapV3/PancakeSwapV3Aggregator.sol:PancakeSwapV3Aggregator", + isSingleton: true, + aggregatorEnvKey: "PANCAKESWAP_V3_AGGREGATOR", + + getHookParams(config) { + return { + fee: config.fee, + tickSpacing: config.tickSpacing ?? 60, + sqrtPriceX96: config.sqrtPriceX96 ?? DEFAULT_SQRT_PRICE_X96, + }; + }, + + buildPoolKeys(config, hookAddress) { + const params = this.getHookParams(config); + const [c0, c1] = orderPair(config.currency0, config.currency1); + return [{ currency0: c0, currency1: c1, fee: params.fee, tickSpacing: params.tickSpacing, hooks: hookAddress }]; + }, + + getExternalPool(config) { + return config.v3Pool; + }, + + getImmutablesFromEnv(chainId) { + return { + poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, + externalFactory: mustEnvForChain("PANCAKESWAP_V3_FACTORY", chainId) as Address, + }; + }, + + async readFactoryImmutables(provider, aggregatorAddress) { + const aggregator = new ethers.Contract(aggregatorAddress, AGGREGATOR_ABI, provider); + const [poolManager, externalFactory] = await Promise.all([aggregator.poolManager(), aggregator.factory()]); + return { poolManager: poolManager as Address, externalFactory: externalFactory as Address }; + }, + + encodeConstructorArgs(_config, immutables) { + const encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "string"], + [immutables.poolManager, immutables.externalFactory, "PancakeSwapV3Aggregator v1.0"], + ); + return encoded.startsWith("0x") ? encoded : `0x${encoded}`; + }, + + buildSelfDeployEnvVars(_config, immutables) { + return { + EXTERNAL_FACTORY: immutables.externalFactory!, + HOOK_VERSION: "PancakeSwapV3Aggregator v1.0", + }; + }, + + buildCreatePoolArgs(_config, _salt) { + throw new Error("PancakeSwapV3 uses singleton self-deploy mode; factory mode (createPool) is not supported."); + }, + + buildInitializeArgs(config, hookAddress): [PoolKeyRecord, bigint] { + const [poolKey] = this.buildPoolKeys(config, hookAddress); + const sqrtPriceX96 = config.sqrtPriceX96 ?? DEFAULT_SQRT_PRICE_X96; + return [poolKey, sqrtPriceX96]; + }, +}; diff --git a/aggregator-hooks/creation-modules/Slipstream.ts b/aggregator-hooks/creation-modules/Slipstream.ts new file mode 100644 index 00000000..9b1a4f48 --- /dev/null +++ b/aggregator-hooks/creation-modules/Slipstream.ts @@ -0,0 +1,97 @@ +/** + * Slipstream singleton aggregator hook deployment module. + * + * One SlipstreamAggregator is deployed per chain. Each pool config entry + * registers a new V4 pool backed by an existing Slipstream CL pool (resolved + * by tickSpacing via the Slipstream factory, rather than fee tier as in V3). + */ +import { ethers } from "ethers"; +import { mustEnvForChain } from "../src/cli.js"; +import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables, type PoolKeyRecord } from "./types.js"; + +export interface SlipstreamPoolConfig { + poolType: "slipstream"; + /** The existing Slipstream pool address being wrapped */ + slipstreamPool: Address; + currency0: Address; + currency1: Address; + /** Slipstream tickSpacing — used for both pool lookup and V4 PoolKey (fee is always 0) */ + tickSpacing: number; + sqrtPriceX96?: bigint | null; +} + +const PROTOCOL_ID = 0xA1; + +const AGGREGATOR_ABI = [ + "function poolManager() view returns (address)", + "function factory() view returns (address)", +]; + +const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); + +export const slipstreamModule: CreationModule = { + poolType: "slipstream", + protocolId: PROTOCOL_ID, + factoryAbi: [], + contractIdentifier: + "lib/v4-hooks-public/src/aggregator-hooks/implementations/Slipstream/SlipstreamAggregator.sol:SlipstreamAggregator", + isSingleton: true, + aggregatorEnvKey: "SLIPSTREAM_AGGREGATOR", + + getHookParams(config) { + return { + fee: 0, + tickSpacing: config.tickSpacing, + sqrtPriceX96: config.sqrtPriceX96 ?? DEFAULT_SQRT_PRICE_X96, + }; + }, + + buildPoolKeys(config, hookAddress) { + const params = this.getHookParams(config); + const [c0, c1] = orderPair(config.currency0, config.currency1); + return [{ currency0: c0, currency1: c1, fee: params.fee, tickSpacing: params.tickSpacing, hooks: hookAddress }]; + }, + + getExternalPool(config) { + return config.slipstreamPool; + }, + + getImmutablesFromEnv(chainId) { + return { + poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, + externalFactory: mustEnvForChain("SLIPSTREAM_FACTORY", chainId) as Address, + }; + }, + + async readFactoryImmutables(provider, aggregatorAddress) { + const aggregator = new ethers.Contract(aggregatorAddress, AGGREGATOR_ABI, provider); + const [poolManager, externalFactory] = await Promise.all([aggregator.poolManager(), aggregator.factory()]); + return { poolManager: poolManager as Address, externalFactory: externalFactory as Address }; + }, + + encodeConstructorArgs(_config, immutables) { + // SlipstreamAggregator constructor: (IPoolManager manager, address slipstreamFactory) + // hookVersion is hardcoded in the contract; not a constructor arg. + const encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address"], + [immutables.poolManager, immutables.externalFactory], + ); + return encoded.startsWith("0x") ? encoded : `0x${encoded}`; + }, + + buildSelfDeployEnvVars(_config, immutables) { + return { + EXTERNAL_FACTORY: immutables.externalFactory!, + }; + }, + + buildCreatePoolArgs(_config, _salt) { + throw new Error("Slipstream uses singleton self-deploy mode; factory mode (createPool) is not supported."); + }, + + buildInitializeArgs(config, hookAddress): [PoolKeyRecord, bigint] { + const [poolKey] = this.buildPoolKeys(config, hookAddress); + const sqrtPriceX96 = config.sqrtPriceX96 ?? DEFAULT_SQRT_PRICE_X96; + return [poolKey, sqrtPriceX96]; + }, +}; diff --git a/aggregator-hooks/creation-modules/UniswapV2.ts b/aggregator-hooks/creation-modules/UniswapV2.ts new file mode 100644 index 00000000..da180eca --- /dev/null +++ b/aggregator-hooks/creation-modules/UniswapV2.ts @@ -0,0 +1,97 @@ +/** + * UniswapV2 singleton aggregator hook deployment module. + * + * One UniswapV2Aggregator is deployed per chain. Each pool config entry + * registers a new V4 pool backed by an existing Uniswap V2 pair (resolved + * by currency pair via the V2 factory). Fee and tickSpacing on the V4 + * PoolKey do not affect routing — the pair is keyed by currency pair only. + */ +import { ethers } from "ethers"; +import { mustEnvForChain } from "../src/cli.js"; +import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables, type PoolKeyRecord } from "./types.js"; + +export interface UniswapV2PoolConfig { + poolType: "uniswapv2"; + /** The existing Uniswap V2 pair address being wrapped */ + v2Pair: Address; + currency0: Address; + currency1: Address; + /** V4 PoolKey tickSpacing (fee is always 0 for V2 pools) */ + tickSpacing?: number | null; + sqrtPriceX96?: bigint | null; +} + +const PROTOCOL_ID = 0x02; + +const AGGREGATOR_ABI = [ + "function poolManager() view returns (address)", + "function factory() view returns (address)", +]; + +const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); + +export const uniswapv2Module: CreationModule = { + poolType: "uniswapv2", + protocolId: PROTOCOL_ID, + factoryAbi: [], + contractIdentifier: + "lib/v4-hooks-public/src/aggregator-hooks/implementations/UniswapV2/UniswapV2Aggregator.sol:UniswapV2Aggregator", + isSingleton: true, + aggregatorEnvKey: "UNISWAP_V2_AGGREGATOR", + + getHookParams(config) { + return { + fee: 0, + tickSpacing: config.tickSpacing ?? 1, + sqrtPriceX96: config.sqrtPriceX96 ?? DEFAULT_SQRT_PRICE_X96, + }; + }, + + buildPoolKeys(config, hookAddress) { + const params = this.getHookParams(config); + const [c0, c1] = orderPair(config.currency0, config.currency1); + return [{ currency0: c0, currency1: c1, fee: params.fee, tickSpacing: params.tickSpacing, hooks: hookAddress }]; + }, + + getExternalPool(config) { + return config.v2Pair; + }, + + getImmutablesFromEnv(chainId) { + return { + poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, + externalFactory: mustEnvForChain("UNISWAP_V2_FACTORY", chainId) as Address, + }; + }, + + async readFactoryImmutables(provider, aggregatorAddress) { + const aggregator = new ethers.Contract(aggregatorAddress, AGGREGATOR_ABI, provider); + const [poolManager, externalFactory] = await Promise.all([aggregator.poolManager(), aggregator.factory()]); + return { poolManager: poolManager as Address, externalFactory: externalFactory as Address }; + }, + + encodeConstructorArgs(_config, immutables) { + const encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "string"], + [immutables.poolManager, immutables.externalFactory, "UniswapV2Aggregator v1.0"], + ); + return encoded.startsWith("0x") ? encoded : `0x${encoded}`; + }, + + buildSelfDeployEnvVars(_config, immutables) { + return { + EXTERNAL_FACTORY: immutables.externalFactory!, + HOOK_VERSION: "UniswapV2Aggregator v1.0", + }; + }, + + buildCreatePoolArgs(_config, _salt) { + throw new Error("UniswapV2 uses singleton self-deploy mode; factory mode (createPool) is not supported."); + }, + + buildInitializeArgs(config, hookAddress): [PoolKeyRecord, bigint] { + const [poolKey] = this.buildPoolKeys(config, hookAddress); + const sqrtPriceX96 = config.sqrtPriceX96 ?? DEFAULT_SQRT_PRICE_X96; + return [poolKey, sqrtPriceX96]; + }, +}; diff --git a/aggregator-hooks/creation-modules/UniswapV3.ts b/aggregator-hooks/creation-modules/UniswapV3.ts new file mode 100644 index 00000000..9728b246 --- /dev/null +++ b/aggregator-hooks/creation-modules/UniswapV3.ts @@ -0,0 +1,97 @@ +/** + * UniswapV3 singleton aggregator hook deployment module. + * + * One UniswapV3Aggregator is deployed per chain. Each pool config entry + * registers a new V4 pool backed by an existing Uniswap V3 pool (resolved + * by fee tier via the V3 factory). + */ +import { ethers } from "ethers"; +import { mustEnvForChain } from "../src/cli.js"; +import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables, type PoolKeyRecord } from "./types.js"; + +export interface UniswapV3PoolConfig { + poolType: "uniswapv3"; + /** The existing Uniswap V3 pool address being wrapped */ + v3Pool: Address; + currency0: Address; + currency1: Address; + /** V3 fee tier (e.g. 500, 3000, 10000) — used for both V3 pool lookup and V4 PoolKey */ + fee: number; + tickSpacing?: number | null; + sqrtPriceX96?: bigint | null; +} + +const PROTOCOL_ID = 0x03; + +const AGGREGATOR_ABI = [ + "function poolManager() view returns (address)", + "function factory() view returns (address)", +]; + +const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); + +export const uniswapv3Module: CreationModule = { + poolType: "uniswapv3", + protocolId: PROTOCOL_ID, + factoryAbi: [], + contractIdentifier: + "lib/v4-hooks-public/src/aggregator-hooks/implementations/UniswapV3/UniswapV3Aggregator.sol:UniswapV3Aggregator", + isSingleton: true, + aggregatorEnvKey: "UNISWAP_V3_AGGREGATOR", + + getHookParams(config) { + return { + fee: config.fee, + tickSpacing: config.tickSpacing ?? 60, + sqrtPriceX96: config.sqrtPriceX96 ?? DEFAULT_SQRT_PRICE_X96, + }; + }, + + buildPoolKeys(config, hookAddress) { + const params = this.getHookParams(config); + const [c0, c1] = orderPair(config.currency0, config.currency1); + return [{ currency0: c0, currency1: c1, fee: params.fee, tickSpacing: params.tickSpacing, hooks: hookAddress }]; + }, + + getExternalPool(config) { + return config.v3Pool; + }, + + getImmutablesFromEnv(chainId) { + return { + poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, + externalFactory: mustEnvForChain("UNISWAP_V3_FACTORY", chainId) as Address, + }; + }, + + async readFactoryImmutables(provider, aggregatorAddress) { + const aggregator = new ethers.Contract(aggregatorAddress, AGGREGATOR_ABI, provider); + const [poolManager, externalFactory] = await Promise.all([aggregator.poolManager(), aggregator.factory()]); + return { poolManager: poolManager as Address, externalFactory: externalFactory as Address }; + }, + + encodeConstructorArgs(_config, immutables) { + const encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address", "string"], + [immutables.poolManager, immutables.externalFactory, "UniswapV3Aggregator v1.0"], + ); + return encoded.startsWith("0x") ? encoded : `0x${encoded}`; + }, + + buildSelfDeployEnvVars(_config, immutables) { + return { + EXTERNAL_FACTORY: immutables.externalFactory!, + HOOK_VERSION: "UniswapV3Aggregator v1.0", + }; + }, + + buildCreatePoolArgs(_config, _salt) { + throw new Error("UniswapV3 uses singleton self-deploy mode; factory mode (createPool) is not supported."); + }, + + buildInitializeArgs(config, hookAddress): [PoolKeyRecord, bigint] { + const [poolKey] = this.buildPoolKeys(config, hookAddress); + const sqrtPriceX96 = config.sqrtPriceX96 ?? DEFAULT_SQRT_PRICE_X96; + return [poolKey, sqrtPriceX96]; + }, +}; diff --git a/aggregator-hooks/creation-modules/index.ts b/aggregator-hooks/creation-modules/index.ts index 2db43d85..94c1c8f3 100644 --- a/aggregator-hooks/creation-modules/index.ts +++ b/aggregator-hooks/creation-modules/index.ts @@ -5,6 +5,10 @@ import { stableswapModule } from "./StableSwap.js"; import { stableswapngModule } from "./StableSwapNG.js"; import { fluiddext1Module } from "./FluidDexT1.js"; import { fluiddexliteModule } from "./FluidDexLite.js"; +import { uniswapv3Module } from "./UniswapV3.js"; +import { uniswapv2Module } from "./UniswapV2.js"; +import { slipstreamModule } from "./Slipstream.js"; +import { pancakeswapv3Module } from "./PancakeSwapV3.js"; import type { CreationModule } from "./types.js"; export type { @@ -20,14 +24,23 @@ export type { StableSwapPoolConfig } from "./StableSwap.js"; export type { StableSwapNGPoolConfig } from "./StableSwapNG.js"; export type { FluidDexT1PoolConfig } from "./FluidDexT1.js"; export type { FluidDexLitePoolConfig } from "./FluidDexLite.js"; +export type { UniswapV3PoolConfig } from "./UniswapV3.js"; +export type { UniswapV2PoolConfig } from "./UniswapV2.js"; +export type { SlipstreamPoolConfig } from "./Slipstream.js"; +export type { PancakeSwapV3PoolConfig } from "./PancakeSwapV3.js"; export { stableswapModule, stableswapngModule, fluiddext1Module, fluiddexliteModule }; +export { uniswapv3Module, uniswapv2Module, slipstreamModule, pancakeswapv3Module }; export type PoolConfig = | import("./StableSwap.js").StableSwapPoolConfig | import("./StableSwapNG.js").StableSwapNGPoolConfig | import("./FluidDexT1.js").FluidDexT1PoolConfig - | import("./FluidDexLite.js").FluidDexLitePoolConfig; + | import("./FluidDexLite.js").FluidDexLitePoolConfig + | import("./UniswapV3.js").UniswapV3PoolConfig + | import("./UniswapV2.js").UniswapV2PoolConfig + | import("./Slipstream.js").SlipstreamPoolConfig + | import("./PancakeSwapV3.js").PancakeSwapV3PoolConfig; /** Registry of all creation modules by pool type */ export const CREATION_MODULES: Record = { @@ -35,6 +48,10 @@ export const CREATION_MODULES: Record = { stableswapng: stableswapngModule, fluiddext1: fluiddext1Module, fluiddexlite: fluiddexliteModule, + uniswapv3: uniswapv3Module, + uniswapv2: uniswapv2Module, + slipstream: slipstreamModule, + pancakeswapv3: pancakeswapv3Module, }; export const POOL_TYPES = Object.keys(CREATION_MODULES) as string[]; diff --git a/aggregator-hooks/creation-modules/types.ts b/aggregator-hooks/creation-modules/types.ts index 368dba41..db8c3370 100644 --- a/aggregator-hooks/creation-modules/types.ts +++ b/aggregator-hooks/creation-modules/types.ts @@ -59,6 +59,11 @@ export interface HookParams { /** * Creation module interface. Each pool type implements this to provide * type-specific deployment logic. + * + * Singleton types (UniswapV3, UniswapV2, Slipstream, PancakeSwapV3) set + * `isSingleton: true`. For these, createPools.ts deploys the aggregator once + * per chain (writing the address to .env), then calls poolManager.initialize + * for each pool config entry rather than deploying a new hook per pool. */ export interface CreationModule { /** Pool type identifier (e.g. "stableswap", "fluiddext1") */ @@ -73,6 +78,18 @@ export interface CreationModule { /** Solidity contract identifier for forge verify-contract (path:ContractName) */ contractIdentifier: string; + /** + * If true, one aggregator is deployed per chain and reused for all pools. + * Pools are registered by calling poolManager.initialize rather than a factory. + */ + isSingleton?: boolean; + + /** + * Env var key (without chain suffix) that persists the deployed singleton + * address, e.g. "UNISWAP_V3_AGGREGATOR". Required when isSingleton is true. + */ + aggregatorEnvKey?: string; + /** Resolve hook params with defaults */ getHookParams(config: TConfig): HookParams; @@ -85,7 +102,7 @@ export interface CreationModule { /** Read immutables from env for self-deploy */ getImmutablesFromEnv(chainId: number): FactoryImmutables; - /** Read immutables from factory contract */ + /** Read immutables from factory contract (or deployed singleton aggregator) */ readFactoryImmutables(provider: Provider, factoryAddress: Address): Promise; /** Encode constructor args for salt mining */ @@ -94,6 +111,12 @@ export interface CreationModule { /** Build env vars for SelfCreateHook.s.sol self-deploy */ buildSelfDeployEnvVars(config: TConfig, immutables: FactoryImmutables): Record; - /** Build createPool call args for factory contract */ + /** Build createPool call args for factory contract. Throws for singleton types. */ buildCreatePoolArgs(config: TConfig, salt: string): unknown[]; + + /** + * Build args for poolManager.initialize — singleton types only. + * Returns [poolKey, sqrtPriceX96] to pass to the PoolManager initialize function. + */ + buildInitializeArgs?(config: TConfig, hookAddress: Address): [PoolKeyRecord, bigint]; } diff --git a/aggregator-hooks/historical/PancakeSwapV3.ts b/aggregator-hooks/historical/PancakeSwapV3.ts new file mode 100644 index 00000000..d711318b --- /dev/null +++ b/aggregator-hooks/historical/PancakeSwapV3.ts @@ -0,0 +1,168 @@ +/** + * PancakeSwap V3 historical pool discovery. + * Scans PoolCreated events from the PancakeSwap V3 factory and outputs pool + * configs in createPools.ts PancakeSwapV3PoolConfig format. + * + * Usage: + * npx tsx historical/PancakeSwapV3.ts --chain-id 1 + * + * Options: + * --chain-id (required) Chain ID + * --output-dir output directory (default: detected) + * --chunk-blocks block chunk size for getLogs (default: 10000; auto-halved on range errors) + * --start-block start block (default: 0) + * --end-block end block (default: latest) + * + * Env vars (VAR_ or VAR): + * RPC_URL (required) + * PANCAKESWAP_V3_FACTORY (optional, default mainnet factory) + * RPS (optional, default 80) + * + * Output: JSON array in PancakeSwapV3PoolConfig format. + */ + +import "dotenv/config"; +import fs from "node:fs"; +import path from "node:path"; +import { ethers } from "ethers"; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; +import type { Address } from "../creation-modules/types.js"; + +const OUTPUT_FILE = "pancakeswapv3-pools.json"; +const DEFAULT_FACTORY: Address = "0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865"; +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + +const FACTORY_ABI = [ + "event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)", +]; + +type PancakeSwapV3PoolConfig = { + poolType: "pancakeswapv3"; + v3Pool: Address; + currency0: Address; + currency1: Address; + fee: number; + tickSpacing: number; + sqrtPriceX96: null; +}; + +function pRateLimit(rps: number): () => Promise { + if (rps <= 0) return async () => {}; + const minGapMs = 1000 / rps; + let nextAllowed = 0; + return async function acquire(): Promise { + const now = Date.now(); + if (now < nextAllowed) await new Promise((r) => setTimeout(r, nextAllowed - now)); + nextAllowed = Math.max(now, nextAllowed) + minGapMs; + }; +} + +function isRangeLimitError(err: unknown): boolean { + const msg = String((err as { error?: { message?: string }; message?: string })?.error?.message ?? (err as { message?: string })?.message ?? ""); + return msg.toLowerCase().includes("range") || msg.toLowerCase().includes("limit") || msg.includes("-32614"); +} + +async function getLogsWithRetry( + provider: ethers.JsonRpcProvider, + filter: { address: string; topics: string[]; fromBlock: bigint; toBlock: bigint }, +): Promise { + try { + return await provider.getLogs({ + address: filter.address, + topics: filter.topics, + fromBlock: filter.fromBlock, + toBlock: filter.toBlock, + }); + } catch (err) { + if (!isRangeLimitError(err) || filter.toBlock <= filter.fromBlock) throw err; + const mid = filter.fromBlock + (filter.toBlock - filter.fromBlock) / 2n; + const [lo, hi] = await Promise.all([ + getLogsWithRetry(provider, { ...filter, toBlock: mid }), + getLogsWithRetry(provider, { ...filter, fromBlock: mid + 1n }), + ]); + return lo.concat(hi); + } +} + +function isNative(addr: string): boolean { + return addr.toLowerCase() === ZERO_ADDRESS; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + const chainIdRaw = args["chain-id"]; + if (chainIdRaw == null) { console.error("Missing required --chain-id "); process.exit(1); } + const chainId = toInt(chainIdRaw, 0); + if (chainId <= 0) { console.error("--chain-id must be a positive integer"); process.exit(1); } + + const rpcUrl = getEnvForChain("RPC_URL", chainId); + if (!rpcUrl) { console.error("Missing env: RPC_URL"); process.exit(1); } + + const factoryRaw = getEnvForChain("PANCAKESWAP_V3_FACTORY", chainId) ?? DEFAULT_FACTORY; + const factory = ethers.getAddress(factoryRaw) as Address; + + const outputDir = (args["output-dir"] as string) ?? "detected"; + const chunkSize = BigInt(Math.max(1, toInt(args["chunk-blocks"], 10_000))); + const startBlock = BigInt(Math.max(0, toInt(args["start-block"], 0))); + const rps = toInt(getEnvForChain("RPS", chainId) ?? "80", 80); + const rateLimitAcquire = pRateLimit(rps); + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const iface = new ethers.Interface(FACTORY_ABI); + const topic0 = iface.getEvent("PoolCreated")!.topicHash; + + const latest = BigInt(await provider.getBlockNumber()); + const endBlockRaw = args["end-block"]; + const endBlock = endBlockRaw ? BigInt(String(endBlockRaw)) : latest; + + console.error(`Factory: ${factory}`); + console.error(`Scanning blocks ${startBlock}..${endBlock} (chunk=${chunkSize})`); + + const seen = new Set
(); + const configs: PancakeSwapV3PoolConfig[] = []; + + for (let from = startBlock; from <= endBlock; from += chunkSize) { + const to = from + chunkSize - 1n > endBlock ? endBlock : from + chunkSize - 1n; + await rateLimitAcquire(); + + const logs = await getLogsWithRetry(provider, { address: factory, topics: [topic0], fromBlock: from, toBlock: to }); + + for (const log of logs) { + const parsed = iface.parseLog(log); + if (!parsed) continue; + + const token0 = ethers.getAddress(parsed.args.token0 as string) as Address; + const token1 = ethers.getAddress(parsed.args.token1 as string) as Address; + const pool = ethers.getAddress(parsed.args.pool as string) as Address; + const fee = Number(parsed.args.fee); + const tickSpacing = Number(parsed.args.tickSpacing); + + if (isNative(token0) || isNative(token1)) continue; + if (seen.has(pool)) continue; + seen.add(pool); + + configs.push({ + poolType: "pancakeswapv3", + v3Pool: pool, + currency0: token0, + currency1: token1, + fee, + tickSpacing, + sqrtPriceX96: null, + }); + } + + if (to === endBlock || (to - startBlock) % (chunkSize * 10n) === 0n) { + console.error(`[scan] ${from}..${to} — found ${configs.length} pools so far`); + } + console.log(`[scan] ${from}..${to} — found ${configs.length} pools so far`); + } + + const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + "\n"); + console.log(JSON.stringify({ ok: true, chainId, factory, poolsFound: configs.length, outFile: outPath }, null, 2)); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/aggregator-hooks/historical/Slipstream.ts b/aggregator-hooks/historical/Slipstream.ts new file mode 100644 index 00000000..14301384 --- /dev/null +++ b/aggregator-hooks/historical/Slipstream.ts @@ -0,0 +1,172 @@ +/** + * Slipstream historical pool discovery. + * Scans PoolCreated events from the Slipstream (Velodrome/Aerodrome CL) factory + * and outputs pool configs in createPools.ts SlipstreamPoolConfig format. + * + * Usage: + * npx tsx historical/Slipstream.ts --chain-id 10 + * + * Options: + * --chain-id (required) Chain ID + * --output-dir output directory (default: detected) + * --chunk-blocks block chunk size for getLogs (default: 10000; auto-halved on range errors) + * --start-block start block (default: 0) + * --end-block end block (default: latest) + * + * Env vars (VAR_ or VAR): + * RPC_URL (required) + * SLIPSTREAM_FACTORY (required — no chain-agnostic default; set SLIPSTREAM_FACTORY_) + * RPS (optional, default 80) + * + * Output: JSON array in SlipstreamPoolConfig format. + * Fee is always 0 (Slipstream pools key by tickSpacing, not fee). + */ + +import "dotenv/config"; +import fs from "node:fs"; +import path from "node:path"; +import { ethers } from "ethers"; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; +import type { Address } from "../creation-modules/types.js"; + +const OUTPUT_FILE = "slipstream-pools.json"; +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + +// Slipstream CLFactory emits tickSpacing (not fee) as the third indexed param +const FACTORY_ABI = [ + "event PoolCreated(address indexed token0, address indexed token1, int24 indexed tickSpacing, address pool)", +]; + +type SlipstreamPoolConfig = { + poolType: "slipstream"; + slipstreamPool: Address; + currency0: Address; + currency1: Address; + tickSpacing: number; + sqrtPriceX96: null; +}; + +function pRateLimit(rps: number): () => Promise { + if (rps <= 0) return async () => {}; + const minGapMs = 1000 / rps; + let nextAllowed = 0; + return async function acquire(): Promise { + const now = Date.now(); + if (now < nextAllowed) await new Promise((r) => setTimeout(r, nextAllowed - now)); + nextAllowed = Math.max(now, nextAllowed) + minGapMs; + }; +} + +function isRangeLimitError(err: unknown): boolean { + const msg = String((err as { error?: { message?: string }; message?: string })?.error?.message ?? (err as { message?: string })?.message ?? ""); + return msg.toLowerCase().includes("range") || msg.toLowerCase().includes("limit") || msg.includes("-32614"); +} + +async function getLogsWithRetry( + provider: ethers.JsonRpcProvider, + filter: { address: string; topics: string[]; fromBlock: bigint; toBlock: bigint }, +): Promise { + try { + return await provider.getLogs({ + address: filter.address, + topics: filter.topics, + fromBlock: filter.fromBlock, + toBlock: filter.toBlock, + }); + } catch (err) { + if (!isRangeLimitError(err) || filter.toBlock <= filter.fromBlock) throw err; + const mid = filter.fromBlock + (filter.toBlock - filter.fromBlock) / 2n; + const [lo, hi] = await Promise.all([ + getLogsWithRetry(provider, { ...filter, toBlock: mid }), + getLogsWithRetry(provider, { ...filter, fromBlock: mid + 1n }), + ]); + return lo.concat(hi); + } +} + +function isNative(addr: string): boolean { + return addr.toLowerCase() === ZERO_ADDRESS; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + const chainIdRaw = args["chain-id"]; + if (chainIdRaw == null) { console.error("Missing required --chain-id "); process.exit(1); } + const chainId = toInt(chainIdRaw, 0); + if (chainId <= 0) { console.error("--chain-id must be a positive integer"); process.exit(1); } + + const rpcUrl = getEnvForChain("RPC_URL", chainId); + if (!rpcUrl) { console.error("Missing env: RPC_URL"); process.exit(1); } + + const factoryRaw = getEnvForChain("SLIPSTREAM_FACTORY", chainId); + if (!factoryRaw) { + console.error("Missing env: SLIPSTREAM_FACTORY (or SLIPSTREAM_FACTORY_)"); + process.exit(1); + } + const factory = ethers.getAddress(factoryRaw) as Address; + + const outputDir = (args["output-dir"] as string) ?? "detected"; + const chunkSize = BigInt(Math.max(1, toInt(args["chunk-blocks"], 10_000))); + const startBlock = BigInt(Math.max(0, toInt(args["start-block"], 0))); + const rps = toInt(getEnvForChain("RPS", chainId) ?? "80", 80); + const rateLimitAcquire = pRateLimit(rps); + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const iface = new ethers.Interface(FACTORY_ABI); + const topic0 = iface.getEvent("PoolCreated")!.topicHash; + + const latest = BigInt(await provider.getBlockNumber()); + const endBlockRaw = args["end-block"]; + const endBlock = endBlockRaw ? BigInt(String(endBlockRaw)) : latest; + + console.error(`Factory: ${factory}`); + console.error(`Scanning blocks ${startBlock}..${endBlock} (chunk=${chunkSize})`); + + const seen = new Set
(); + const configs: SlipstreamPoolConfig[] = []; + + for (let from = startBlock; from <= endBlock; from += chunkSize) { + const to = from + chunkSize - 1n > endBlock ? endBlock : from + chunkSize - 1n; + await rateLimitAcquire(); + + const logs = await getLogsWithRetry(provider, { address: factory, topics: [topic0], fromBlock: from, toBlock: to }); + + for (const log of logs) { + const parsed = iface.parseLog(log); + if (!parsed) continue; + + const token0 = ethers.getAddress(parsed.args.token0 as string) as Address; + const token1 = ethers.getAddress(parsed.args.token1 as string) as Address; + const pool = ethers.getAddress(parsed.args.pool as string) as Address; + const tickSpacing = Number(parsed.args.tickSpacing); + + // Skip native currency — SlipstreamAggregator reverts on address(0) + if (isNative(token0) || isNative(token1)) continue; + if (seen.has(pool)) continue; + seen.add(pool); + + // token0 < token1 guaranteed by the factory + configs.push({ + poolType: "slipstream", + slipstreamPool: pool, + currency0: token0, + currency1: token1, + tickSpacing, + sqrtPriceX96: null, + }); + } + + if (to === endBlock || (to - startBlock) % (chunkSize * 10n) === 0n) { + console.error(`[scan] ${from}..${to} — found ${configs.length} pools so far`); + } + console.log(`[scan] ${from}..${to} — found ${configs.length} pools so far`); + } + + const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + "\n"); + console.log(JSON.stringify({ ok: true, chainId, factory, poolsFound: configs.length, outFile: outPath }, null, 2)); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/aggregator-hooks/historical/UniswapV2.ts b/aggregator-hooks/historical/UniswapV2.ts new file mode 100644 index 00000000..436bb358 --- /dev/null +++ b/aggregator-hooks/historical/UniswapV2.ts @@ -0,0 +1,168 @@ +/** + * Uniswap V2 historical pool discovery. + * Scans PairCreated events from the Uniswap V2 factory and outputs pool + * configs in createPools.ts UniswapV2PoolConfig format. + * + * Usage: + * npx tsx historical/UniswapV2.ts --chain-id 1 + * + * Options: + * --chain-id (required) Chain ID + * --output-dir output directory (default: detected) + * --chunk-blocks block chunk size for getLogs (default: 10000; auto-halved on range errors) + * --start-block start block (default: 0) + * --end-block end block (default: latest) + * + * Env vars (VAR_ or VAR): + * RPC_URL (required) + * UNISWAP_V2_FACTORY (optional, default mainnet factory) + * RPS (optional, default 80) + * + * Output: JSON array in UniswapV2PoolConfig format. + * Fee is always 0 (30 bps taken by V2 internally, not a V4 PoolKey param). + * tickSpacing defaults to 1 (minimum valid). + */ + +import "dotenv/config"; +import fs from "node:fs"; +import path from "node:path"; +import { ethers } from "ethers"; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; +import type { Address } from "../creation-modules/types.js"; + +const OUTPUT_FILE = "uniswapv2-pools.json"; +const DEFAULT_FACTORY: Address = "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"; +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + +const FACTORY_ABI = [ + "event PairCreated(address indexed token0, address indexed token1, address pair, uint)", +]; + +type UniswapV2PoolConfig = { + poolType: "uniswapv2"; + v2Pair: Address; + currency0: Address; + currency1: Address; + tickSpacing: number; + sqrtPriceX96: null; +}; + +function pRateLimit(rps: number): () => Promise { + if (rps <= 0) return async () => {}; + const minGapMs = 1000 / rps; + let nextAllowed = 0; + return async function acquire(): Promise { + const now = Date.now(); + if (now < nextAllowed) await new Promise((r) => setTimeout(r, nextAllowed - now)); + nextAllowed = Math.max(now, nextAllowed) + minGapMs; + }; +} + +function isRangeLimitError(err: unknown): boolean { + const msg = String((err as { error?: { message?: string }; message?: string })?.error?.message ?? (err as { message?: string })?.message ?? ""); + return msg.toLowerCase().includes("range") || msg.toLowerCase().includes("limit") || msg.includes("-32614"); +} + +async function getLogsWithRetry( + provider: ethers.JsonRpcProvider, + filter: { address: string; topics: string[]; fromBlock: bigint; toBlock: bigint }, +): Promise { + try { + return await provider.getLogs({ + address: filter.address, + topics: filter.topics, + fromBlock: filter.fromBlock, + toBlock: filter.toBlock, + }); + } catch (err) { + if (!isRangeLimitError(err) || filter.toBlock <= filter.fromBlock) throw err; + const mid = filter.fromBlock + (filter.toBlock - filter.fromBlock) / 2n; + const [lo, hi] = await Promise.all([ + getLogsWithRetry(provider, { ...filter, toBlock: mid }), + getLogsWithRetry(provider, { ...filter, fromBlock: mid + 1n }), + ]); + return lo.concat(hi); + } +} + +function isNative(addr: string): boolean { + return addr.toLowerCase() === ZERO_ADDRESS; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + const chainIdRaw = args["chain-id"]; + if (chainIdRaw == null) { console.error("Missing required --chain-id "); process.exit(1); } + const chainId = toInt(chainIdRaw, 0); + if (chainId <= 0) { console.error("--chain-id must be a positive integer"); process.exit(1); } + + const rpcUrl = getEnvForChain("RPC_URL", chainId); + if (!rpcUrl) { console.error("Missing env: RPC_URL"); process.exit(1); } + + const factoryRaw = getEnvForChain("UNISWAP_V2_FACTORY", chainId) ?? DEFAULT_FACTORY; + const factory = ethers.getAddress(factoryRaw) as Address; + + const outputDir = (args["output-dir"] as string) ?? "detected"; + const chunkSize = BigInt(Math.max(1, toInt(args["chunk-blocks"], 10_000))); + const startBlock = BigInt(Math.max(0, toInt(args["start-block"], 0))); + const rps = toInt(getEnvForChain("RPS", chainId) ?? "80", 80); + const rateLimitAcquire = pRateLimit(rps); + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const iface = new ethers.Interface(FACTORY_ABI); + const topic0 = iface.getEvent("PairCreated")!.topicHash; + + const latest = BigInt(await provider.getBlockNumber()); + const endBlockRaw = args["end-block"]; + const endBlock = endBlockRaw ? BigInt(String(endBlockRaw)) : latest; + + console.error(`Factory: ${factory}`); + console.error(`Scanning blocks ${startBlock}..${endBlock} (chunk=${chunkSize})`); + + const seen = new Set
(); + const configs: UniswapV2PoolConfig[] = []; + + for (let from = startBlock; from <= endBlock; from += chunkSize) { + const to = from + chunkSize - 1n > endBlock ? endBlock : from + chunkSize - 1n; + await rateLimitAcquire(); + + const logs = await getLogsWithRetry(provider, { address: factory, topics: [topic0], fromBlock: from, toBlock: to }); + + for (const log of logs) { + const parsed = iface.parseLog(log); + if (!parsed) continue; + + const token0 = ethers.getAddress(parsed.args.token0 as string) as Address; + const token1 = ethers.getAddress(parsed.args.token1 as string) as Address; + const pair = ethers.getAddress(parsed.args.pair as string) as Address; + + // Skip native currency — UniswapV2Aggregator reverts on address(0) + if (isNative(token0) || isNative(token1)) continue; + if (seen.has(pair)) continue; + seen.add(pair); + + // token0 < token1 is guaranteed by the V2 factory + configs.push({ + poolType: "uniswapv2", + v2Pair: pair, + currency0: token0, + currency1: token1, + tickSpacing: 1, + sqrtPriceX96: null, + }); + } + + if (to === endBlock || (to - startBlock) % (chunkSize * 10n) === 0n) { + console.error(`[scan] ${from}..${to} — found ${configs.length} pools so far`); + } + console.log(`[scan] ${from}..${to} — found ${configs.length} pools so far`); + } + + const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + "\n"); + console.log(JSON.stringify({ ok: true, chainId, factory, poolsFound: configs.length, outFile: outPath }, null, 2)); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/aggregator-hooks/historical/UniswapV3.ts b/aggregator-hooks/historical/UniswapV3.ts new file mode 100644 index 00000000..fc572b5c --- /dev/null +++ b/aggregator-hooks/historical/UniswapV3.ts @@ -0,0 +1,172 @@ +/** + * Uniswap V3 historical pool discovery. + * Scans PoolCreated events from the Uniswap V3 factory and outputs pool + * configs in createPools.ts UniswapV3PoolConfig format. + * + * Usage: + * npx tsx historical/UniswapV3.ts --chain-id 1 + * + * Options: + * --chain-id (required) Chain ID + * --output-dir output directory (default: detected) + * --chunk-blocks block chunk size for getLogs (default: 10000; auto-halved on range errors) + * --start-block start block (default: 0) + * --end-block end block (default: latest) + * + * Env vars (VAR_ or VAR): + * RPC_URL (required) + * UNISWAP_V3_FACTORY (optional, default mainnet factory) + * RPS (optional, default 80) + * CONCURRENCY (optional, default 8) + * + * Output: JSON array in UniswapV3PoolConfig format (fee + tickSpacing from event). + */ + +import "dotenv/config"; +import fs from "node:fs"; +import path from "node:path"; +import { ethers } from "ethers"; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; +import type { Address } from "../creation-modules/types.js"; + +const OUTPUT_FILE = "uniswapv3-pools.json"; +const DEFAULT_FACTORY: Address = "0x1F98431c8aD98523631AE4a59f267346ea31F984"; +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + +const FACTORY_ABI = [ + "event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)", +]; + +type UniswapV3PoolConfig = { + poolType: "uniswapv3"; + v3Pool: Address; + currency0: Address; + currency1: Address; + fee: number; + tickSpacing: number; + sqrtPriceX96: null; +}; + +function pRateLimit(rps: number): () => Promise { + if (rps <= 0) return async () => {}; + const minGapMs = 1000 / rps; + let nextAllowed = 0; + return async function acquire(): Promise { + const now = Date.now(); + if (now < nextAllowed) await new Promise((r) => setTimeout(r, nextAllowed - now)); + nextAllowed = Math.max(now, nextAllowed) + minGapMs; + }; +} + +function isRangeLimitError(err: unknown): boolean { + const msg = String((err as { error?: { message?: string }; message?: string })?.error?.message ?? (err as { message?: string })?.message ?? ""); + return msg.toLowerCase().includes("range") || msg.toLowerCase().includes("limit") || msg.includes("-32614"); +} + +/** getLogs with automatic chunk-halving on range-limit errors. */ +async function getLogsWithRetry( + provider: ethers.JsonRpcProvider, + filter: { address: string; topics: string[]; fromBlock: bigint; toBlock: bigint }, +): Promise { + try { + return await provider.getLogs({ + address: filter.address, + topics: filter.topics, + fromBlock: filter.fromBlock, + toBlock: filter.toBlock, + }); + } catch (err) { + if (!isRangeLimitError(err) || filter.toBlock <= filter.fromBlock) throw err; + const mid = filter.fromBlock + (filter.toBlock - filter.fromBlock) / 2n; + const [lo, hi] = await Promise.all([ + getLogsWithRetry(provider, { ...filter, toBlock: mid }), + getLogsWithRetry(provider, { ...filter, fromBlock: mid + 1n }), + ]); + return lo.concat(hi); + } +} + +function isNative(addr: string): boolean { + return addr.toLowerCase() === ZERO_ADDRESS; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + const chainIdRaw = args["chain-id"]; + if (chainIdRaw == null) { console.error("Missing required --chain-id "); process.exit(1); } + const chainId = toInt(chainIdRaw, 0); + if (chainId <= 0) { console.error("--chain-id must be a positive integer"); process.exit(1); } + + const rpcUrl = getEnvForChain("RPC_URL", chainId); + if (!rpcUrl) { console.error("Missing env: RPC_URL"); process.exit(1); } + + const factoryRaw = getEnvForChain("UNISWAP_V3_FACTORY", chainId) ?? DEFAULT_FACTORY; + const factory = ethers.getAddress(factoryRaw) as Address; + + const outputDir = (args["output-dir"] as string) ?? "detected"; + const chunkSize = BigInt(Math.max(1, toInt(args["chunk-blocks"], 10_000))); + const startBlock = BigInt(Math.max(0, toInt(args["start-block"], 0))); + const rps = toInt(getEnvForChain("RPS", chainId) ?? "80", 80); + const rateLimitAcquire = pRateLimit(rps); + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const iface = new ethers.Interface(FACTORY_ABI); + const topic0 = iface.getEvent("PoolCreated")!.topicHash; + + const latest = BigInt(await provider.getBlockNumber()); + const endBlockRaw = args["end-block"]; + const endBlock = endBlockRaw ? BigInt(String(endBlockRaw)) : latest; + + console.error(`Factory: ${factory}`); + console.error(`Scanning blocks ${startBlock}..${endBlock} (chunk=${chunkSize})`); + + const seen = new Set
(); + const configs: UniswapV3PoolConfig[] = []; + + for (let from = startBlock; from <= endBlock; from += chunkSize) { + const to = from + chunkSize - 1n > endBlock ? endBlock : from + chunkSize - 1n; + await rateLimitAcquire(); + + const logs = await getLogsWithRetry(provider, { address: factory, topics: [topic0], fromBlock: from, toBlock: to }); + + for (const log of logs) { + const parsed = iface.parseLog(log); + if (!parsed) continue; + + const token0 = ethers.getAddress(parsed.args.token0 as string) as Address; + const token1 = ethers.getAddress(parsed.args.token1 as string) as Address; + const pool = ethers.getAddress(parsed.args.pool as string) as Address; + const fee = Number(parsed.args.fee); + const tickSpacing = Number(parsed.args.tickSpacing); + + // Skip native currency — UniswapV3Aggregator reverts on address(0) + if (isNative(token0) || isNative(token1)) continue; + if (seen.has(pool)) continue; + seen.add(pool); + + // token0 < token1 is guaranteed by the V3 factory; currency ordering is already correct + configs.push({ + poolType: "uniswapv3", + v3Pool: pool, + currency0: token0, + currency1: token1, + fee, + tickSpacing, + sqrtPriceX96: null, + }); + } + + if (to === endBlock || (to - startBlock) % (chunkSize * 10n) === 0n) { + console.error(`[scan] ${from}..${to} — found ${configs.length} pools so far`); + } + console.log(`[scan] ${from}..${to} — found ${configs.length} pools so far`); + } + + const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + "\n"); + console.log(JSON.stringify({ ok: true, chainId, factory, poolsFound: configs.length, outFile: outPath }, null, 2)); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/aggregator-hooks/polling/PancakeSwapV3.ts b/aggregator-hooks/polling/PancakeSwapV3.ts new file mode 100644 index 00000000..0f93fba6 --- /dev/null +++ b/aggregator-hooks/polling/PancakeSwapV3.ts @@ -0,0 +1,183 @@ +/** + * PancakeSwap V3 pool discovery (checkpoint-based polling). + * Scans new PoolCreated events from the PancakeSwap V3 factory since the last + * checkpoint and appends new pools to the output file. + * + * Usage: + * npx tsx polling/PancakeSwapV3.ts --chain-id 1 + * + * Options: + * --chain-id (required) Chain ID + * --output-dir output directory (default: detected) + * --checkpoint-dir checkpoint directory (default: checkpoints) + * --chunk-blocks block chunk size (default: 10000) + * --start-block (optional) override checkpoint start block + * + * Env vars (VAR_ or VAR): + * RPC_URL (required) + * PANCAKESWAP_V3_FACTORY (optional, default mainnet factory) + * FINALITY_BLOCKS (optional, default 10) + * LOOKBACK_BLOCKS (optional, default 200000) + */ + +import "dotenv/config"; +import fs from "node:fs"; +import path from "node:path"; +import { ethers } from "ethers"; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath, resolveCheckpointPath } from "@src/cli"; + +const OUTPUT_FILE = "pancakeswapv3-pools.json"; +const CHECKPOINT_FILE = "pancakeswapv3_checkpoint.json"; +const DEFAULT_FACTORY = "0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865"; +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + +const FACTORY_ABI = [ + "event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)", +]; + +type Checkpoint = { chainId: number; factory: string; lastProcessedBlock: number; updatedAt: string }; + +type PancakeSwapV3PoolConfig = { + poolType: "pancakeswapv3"; + v3Pool: string; + currency0: string; + currency1: string; + fee: number; + tickSpacing: number; + sqrtPriceX96: null; +}; + +function isNative(addr: string): boolean { + return addr.toLowerCase() === ZERO_ADDRESS; +} + +function ensureDirForFile(filePath: string) { + fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true }); +} + +function safeReadJson(filePath: string): T | null { + try { return JSON.parse(fs.readFileSync(filePath, "utf8")) as T; } catch { return null; } +} + +function atomicWriteFile(filePath: string, contents: string) { + ensureDirForFile(filePath); + const abs = path.resolve(filePath); + fs.writeFileSync(abs + ".tmp", contents); + fs.renameSync(abs + ".tmp", abs); +} + +function loadExistingKeys(outFile: string): Set { + const keys = new Set(); + if (!fs.existsSync(outFile)) return keys; + try { + const arr = safeReadJson(outFile); + if (!Array.isArray(arr)) return keys; + for (const x of arr) keys.add(x.v3Pool.toLowerCase()); + } catch { /* ignore */ } + return keys; +} + +function getEnvInt(name: string, chainId: number, def: number): number { + const v = getEnvForChain(name, chainId); + if (!v) return def; + const n = Number(v); + return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : def; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + const chainIdRaw = args["chain-id"]; + if (chainIdRaw == null) { console.error("Missing required --chain-id "); process.exit(1); } + const chainId = toInt(chainIdRaw, 0); + if (chainId <= 0) { console.error("--chain-id must be a positive integer"); process.exit(1); } + + const rpcUrl = getEnvForChain("RPC_URL", chainId); + if (!rpcUrl) throw new Error("Missing required env: RPC_URL"); + + const factoryRaw = getEnvForChain("PANCAKESWAP_V3_FACTORY", chainId) ?? DEFAULT_FACTORY; + const factory = ethers.getAddress(factoryRaw); + + const outputDir = (args["output-dir"] as string) ?? "detected"; + const checkpointDir = (args["checkpoint-dir"] as string) ?? "checkpoints"; + const chunkBlocks = Math.max(1, toInt(args["chunk-blocks"], 10_000)); + const finality = getEnvInt("FINALITY_BLOCKS", chainId, 10); + const lookbackBlocks = getEnvInt("LOOKBACK_BLOCKS", chainId, 200_000); + const startBlockArg = args["start-block"]; + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const iface = new ethers.Interface(FACTORY_ABI); + const topic0 = iface.getEvent("PoolCreated")!.topicHash; + + const latestBlock = await provider.getBlockNumber(); + const toBlock = Math.max(0, latestBlock - finality); + + const cpPath = resolveCheckpointPath(checkpointDir, chainId, CHECKPOINT_FILE); + const cp = safeReadJson(cpPath); + + let fromBlock: number; + if (startBlockArg != null) { + fromBlock = Math.max(0, toInt(startBlockArg, 0)); + } else if (cp?.chainId === chainId && cp?.factory?.toLowerCase() === factory.toLowerCase()) { + fromBlock = cp.lastProcessedBlock + 1; + } else { + fromBlock = Math.max(0, toBlock - lookbackBlocks); + } + + if (fromBlock > toBlock) { + const newCp: Checkpoint = { chainId, factory, lastProcessedBlock: toBlock, updatedAt: new Date().toISOString() }; + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); + console.log(JSON.stringify({ ok: true, note: "No new blocks to scan", fromBlock, toBlock }, null, 2)); + return; + } + + const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); + const seenKeys = loadExistingKeys(outPath); + let allRecords = safeReadJson(outPath) ?? []; + let totalLogs = 0; + let newPools = 0; + + for (let start = fromBlock; start <= toBlock; start += chunkBlocks) { + const end = Math.min(toBlock, start + chunkBlocks - 1); + + const logs = await provider.getLogs({ address: factory, topics: [topic0], fromBlock: start, toBlock: end }); + totalLogs += logs.length; + const newRecords: PancakeSwapV3PoolConfig[] = []; + + for (const log of logs) { + let parsed: ethers.LogDescription | null; + try { parsed = iface.parseLog(log); } catch { continue; } + if (!parsed) continue; + + const token0 = ethers.getAddress(parsed.args.token0 as string); + const token1 = ethers.getAddress(parsed.args.token1 as string); + const pool = ethers.getAddress(parsed.args.pool as string); + const fee = Number(parsed.args.fee); + const tickSpacing = Number(parsed.args.tickSpacing); + + if (isNative(token0) || isNative(token1)) continue; + if (seenKeys.has(pool.toLowerCase())) continue; + seenKeys.add(pool.toLowerCase()); + + newPools++; + newRecords.push({ poolType: "pancakeswapv3", v3Pool: pool, currency0: token0, currency1: token1, fee, tickSpacing, sqrtPriceX96: null }); + } + + if (newRecords.length > 0) { + allRecords = allRecords.concat(newRecords); + atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + "\n"); + } + + const interimCp: Checkpoint = { chainId, factory, lastProcessedBlock: end, updatedAt: new Date().toISOString() }; + atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + "\n"); + console.error(`[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`); + } + + console.log(JSON.stringify({ + ok: true, chainId, factory, + scanned: { fromBlock, toBlock, latestBlock, finality, chunkBlocks }, + logsFound: totalLogs, newPools, outFile: outPath, checkpointFile: cpPath, + }, null, 2)); +} + +main().catch((err) => { console.error(err); process.exitCode = 1; }); diff --git a/aggregator-hooks/polling/Slipstream.ts b/aggregator-hooks/polling/Slipstream.ts new file mode 100644 index 00000000..66e1232a --- /dev/null +++ b/aggregator-hooks/polling/Slipstream.ts @@ -0,0 +1,182 @@ +/** + * Slipstream pool discovery (checkpoint-based polling). + * Scans new PoolCreated events from the Slipstream (Velodrome/Aerodrome CL) + * factory since the last checkpoint and appends new pools to the output file. + * + * Usage: + * npx tsx polling/Slipstream.ts --chain-id 10 + * + * Options: + * --chain-id (required) Chain ID + * --output-dir output directory (default: detected) + * --checkpoint-dir checkpoint directory (default: checkpoints) + * --chunk-blocks block chunk size (default: 10000) + * --start-block (optional) override checkpoint start block + * + * Env vars (VAR_ or VAR): + * RPC_URL (required) + * SLIPSTREAM_FACTORY (required — no chain-agnostic default) + * FINALITY_BLOCKS (optional, default 10) + * LOOKBACK_BLOCKS (optional, default 200000) + */ + +import "dotenv/config"; +import fs from "node:fs"; +import path from "node:path"; +import { ethers } from "ethers"; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath, resolveCheckpointPath } from "@src/cli"; + +const OUTPUT_FILE = "slipstream-pools.json"; +const CHECKPOINT_FILE = "slipstream_checkpoint.json"; +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + +// Slipstream CLFactory: tickSpacing is the third indexed param (not fee) +const FACTORY_ABI = [ + "event PoolCreated(address indexed token0, address indexed token1, int24 indexed tickSpacing, address pool)", +]; + +type Checkpoint = { chainId: number; factory: string; lastProcessedBlock: number; updatedAt: string }; + +type SlipstreamPoolConfig = { + poolType: "slipstream"; + slipstreamPool: string; + currency0: string; + currency1: string; + tickSpacing: number; + sqrtPriceX96: null; +}; + +function isNative(addr: string): boolean { + return addr.toLowerCase() === ZERO_ADDRESS; +} + +function ensureDirForFile(filePath: string) { + fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true }); +} + +function safeReadJson(filePath: string): T | null { + try { return JSON.parse(fs.readFileSync(filePath, "utf8")) as T; } catch { return null; } +} + +function atomicWriteFile(filePath: string, contents: string) { + ensureDirForFile(filePath); + const abs = path.resolve(filePath); + fs.writeFileSync(abs + ".tmp", contents); + fs.renameSync(abs + ".tmp", abs); +} + +function loadExistingKeys(outFile: string): Set { + const keys = new Set(); + if (!fs.existsSync(outFile)) return keys; + try { + const arr = safeReadJson(outFile); + if (!Array.isArray(arr)) return keys; + for (const x of arr) keys.add(x.slipstreamPool.toLowerCase()); + } catch { /* ignore */ } + return keys; +} + +function getEnvInt(name: string, chainId: number, def: number): number { + const v = getEnvForChain(name, chainId); + if (!v) return def; + const n = Number(v); + return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : def; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + const chainIdRaw = args["chain-id"]; + if (chainIdRaw == null) { console.error("Missing required --chain-id "); process.exit(1); } + const chainId = toInt(chainIdRaw, 0); + if (chainId <= 0) { console.error("--chain-id must be a positive integer"); process.exit(1); } + + const rpcUrl = getEnvForChain("RPC_URL", chainId); + if (!rpcUrl) throw new Error("Missing required env: RPC_URL"); + + const factoryRaw = getEnvForChain("SLIPSTREAM_FACTORY", chainId); + if (!factoryRaw) throw new Error("Missing required env: SLIPSTREAM_FACTORY (or SLIPSTREAM_FACTORY_)"); + const factory = ethers.getAddress(factoryRaw); + + const outputDir = (args["output-dir"] as string) ?? "detected"; + const checkpointDir = (args["checkpoint-dir"] as string) ?? "checkpoints"; + const chunkBlocks = Math.max(1, toInt(args["chunk-blocks"], 10_000)); + const finality = getEnvInt("FINALITY_BLOCKS", chainId, 10); + const lookbackBlocks = getEnvInt("LOOKBACK_BLOCKS", chainId, 200_000); + const startBlockArg = args["start-block"]; + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const iface = new ethers.Interface(FACTORY_ABI); + const topic0 = iface.getEvent("PoolCreated")!.topicHash; + + const latestBlock = await provider.getBlockNumber(); + const toBlock = Math.max(0, latestBlock - finality); + + const cpPath = resolveCheckpointPath(checkpointDir, chainId, CHECKPOINT_FILE); + const cp = safeReadJson(cpPath); + + let fromBlock: number; + if (startBlockArg != null) { + fromBlock = Math.max(0, toInt(startBlockArg, 0)); + } else if (cp?.chainId === chainId && cp?.factory?.toLowerCase() === factory.toLowerCase()) { + fromBlock = cp.lastProcessedBlock + 1; + } else { + fromBlock = Math.max(0, toBlock - lookbackBlocks); + } + + if (fromBlock > toBlock) { + const newCp: Checkpoint = { chainId, factory, lastProcessedBlock: toBlock, updatedAt: new Date().toISOString() }; + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); + console.log(JSON.stringify({ ok: true, note: "No new blocks to scan", fromBlock, toBlock }, null, 2)); + return; + } + + const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); + const seenKeys = loadExistingKeys(outPath); + let allRecords = safeReadJson(outPath) ?? []; + let totalLogs = 0; + let newPools = 0; + + for (let start = fromBlock; start <= toBlock; start += chunkBlocks) { + const end = Math.min(toBlock, start + chunkBlocks - 1); + + const logs = await provider.getLogs({ address: factory, topics: [topic0], fromBlock: start, toBlock: end }); + totalLogs += logs.length; + const newRecords: SlipstreamPoolConfig[] = []; + + for (const log of logs) { + let parsed: ethers.LogDescription | null; + try { parsed = iface.parseLog(log); } catch { continue; } + if (!parsed) continue; + + const token0 = ethers.getAddress(parsed.args.token0 as string); + const token1 = ethers.getAddress(parsed.args.token1 as string); + const pool = ethers.getAddress(parsed.args.pool as string); + const tickSpacing = Number(parsed.args.tickSpacing); + + if (isNative(token0) || isNative(token1)) continue; + if (seenKeys.has(pool.toLowerCase())) continue; + seenKeys.add(pool.toLowerCase()); + + newPools++; + newRecords.push({ poolType: "slipstream", slipstreamPool: pool, currency0: token0, currency1: token1, tickSpacing, sqrtPriceX96: null }); + } + + if (newRecords.length > 0) { + allRecords = allRecords.concat(newRecords); + atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + "\n"); + } + + const interimCp: Checkpoint = { chainId, factory, lastProcessedBlock: end, updatedAt: new Date().toISOString() }; + atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + "\n"); + console.error(`[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`); + } + + console.log(JSON.stringify({ + ok: true, chainId, factory, + scanned: { fromBlock, toBlock, latestBlock, finality, chunkBlocks }, + logsFound: totalLogs, newPools, outFile: outPath, checkpointFile: cpPath, + }, null, 2)); +} + +main().catch((err) => { console.error(err); process.exitCode = 1; }); diff --git a/aggregator-hooks/polling/UniswapV2.ts b/aggregator-hooks/polling/UniswapV2.ts new file mode 100644 index 00000000..87e6aaf2 --- /dev/null +++ b/aggregator-hooks/polling/UniswapV2.ts @@ -0,0 +1,180 @@ +/** + * Uniswap V2 pool discovery (checkpoint-based polling). + * Scans new PairCreated events from the V2 factory since the last checkpoint + * and appends new pools to the output file. + * + * Usage: + * npx tsx polling/UniswapV2.ts --chain-id 1 + * + * Options: + * --chain-id (required) Chain ID + * --output-dir output directory (default: detected) + * --checkpoint-dir checkpoint directory (default: checkpoints) + * --chunk-blocks block chunk size (default: 10000) + * --start-block (optional) override checkpoint start block + * + * Env vars (VAR_ or VAR): + * RPC_URL (required) + * UNISWAP_V2_FACTORY (optional, default mainnet factory) + * FINALITY_BLOCKS (optional, default 10) + * LOOKBACK_BLOCKS (optional, default 200000) + */ + +import "dotenv/config"; +import fs from "node:fs"; +import path from "node:path"; +import { ethers } from "ethers"; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath, resolveCheckpointPath } from "@src/cli"; + +const OUTPUT_FILE = "uniswapv2-pools.json"; +const CHECKPOINT_FILE = "uniswapv2_checkpoint.json"; +const DEFAULT_FACTORY = "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"; +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + +const FACTORY_ABI = [ + "event PairCreated(address indexed token0, address indexed token1, address pair, uint)", +]; + +type Checkpoint = { chainId: number; factory: string; lastProcessedBlock: number; updatedAt: string }; + +type UniswapV2PoolConfig = { + poolType: "uniswapv2"; + v2Pair: string; + currency0: string; + currency1: string; + tickSpacing: number; + sqrtPriceX96: null; +}; + +function isNative(addr: string): boolean { + return addr.toLowerCase() === ZERO_ADDRESS; +} + +function ensureDirForFile(filePath: string) { + fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true }); +} + +function safeReadJson(filePath: string): T | null { + try { return JSON.parse(fs.readFileSync(filePath, "utf8")) as T; } catch { return null; } +} + +function atomicWriteFile(filePath: string, contents: string) { + ensureDirForFile(filePath); + const abs = path.resolve(filePath); + fs.writeFileSync(abs + ".tmp", contents); + fs.renameSync(abs + ".tmp", abs); +} + +function loadExistingKeys(outFile: string): Set { + const keys = new Set(); + if (!fs.existsSync(outFile)) return keys; + try { + const arr = safeReadJson(outFile); + if (!Array.isArray(arr)) return keys; + for (const x of arr) keys.add(x.v2Pair.toLowerCase()); + } catch { /* ignore */ } + return keys; +} + +function getEnvInt(name: string, chainId: number, def: number): number { + const v = getEnvForChain(name, chainId); + if (!v) return def; + const n = Number(v); + return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : def; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + const chainIdRaw = args["chain-id"]; + if (chainIdRaw == null) { console.error("Missing required --chain-id "); process.exit(1); } + const chainId = toInt(chainIdRaw, 0); + if (chainId <= 0) { console.error("--chain-id must be a positive integer"); process.exit(1); } + + const rpcUrl = getEnvForChain("RPC_URL", chainId); + if (!rpcUrl) throw new Error("Missing required env: RPC_URL"); + + const factoryRaw = getEnvForChain("UNISWAP_V2_FACTORY", chainId) ?? DEFAULT_FACTORY; + const factory = ethers.getAddress(factoryRaw); + + const outputDir = (args["output-dir"] as string) ?? "detected"; + const checkpointDir = (args["checkpoint-dir"] as string) ?? "checkpoints"; + const chunkBlocks = Math.max(1, toInt(args["chunk-blocks"], 10_000)); + const finality = getEnvInt("FINALITY_BLOCKS", chainId, 10); + const lookbackBlocks = getEnvInt("LOOKBACK_BLOCKS", chainId, 200_000); + const startBlockArg = args["start-block"]; + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const iface = new ethers.Interface(FACTORY_ABI); + const topic0 = iface.getEvent("PairCreated")!.topicHash; + + const latestBlock = await provider.getBlockNumber(); + const toBlock = Math.max(0, latestBlock - finality); + + const cpPath = resolveCheckpointPath(checkpointDir, chainId, CHECKPOINT_FILE); + const cp = safeReadJson(cpPath); + + let fromBlock: number; + if (startBlockArg != null) { + fromBlock = Math.max(0, toInt(startBlockArg, 0)); + } else if (cp?.chainId === chainId && cp?.factory?.toLowerCase() === factory.toLowerCase()) { + fromBlock = cp.lastProcessedBlock + 1; + } else { + fromBlock = Math.max(0, toBlock - lookbackBlocks); + } + + if (fromBlock > toBlock) { + const newCp: Checkpoint = { chainId, factory, lastProcessedBlock: toBlock, updatedAt: new Date().toISOString() }; + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); + console.log(JSON.stringify({ ok: true, note: "No new blocks to scan", fromBlock, toBlock }, null, 2)); + return; + } + + const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); + const seenKeys = loadExistingKeys(outPath); + let allRecords = safeReadJson(outPath) ?? []; + let totalLogs = 0; + let newPools = 0; + + for (let start = fromBlock; start <= toBlock; start += chunkBlocks) { + const end = Math.min(toBlock, start + chunkBlocks - 1); + + const logs = await provider.getLogs({ address: factory, topics: [topic0], fromBlock: start, toBlock: end }); + totalLogs += logs.length; + const newRecords: UniswapV2PoolConfig[] = []; + + for (const log of logs) { + let parsed: ethers.LogDescription | null; + try { parsed = iface.parseLog(log); } catch { continue; } + if (!parsed) continue; + + const token0 = ethers.getAddress(parsed.args.token0 as string); + const token1 = ethers.getAddress(parsed.args.token1 as string); + const pair = ethers.getAddress(parsed.args.pair as string); + + if (isNative(token0) || isNative(token1)) continue; + if (seenKeys.has(pair.toLowerCase())) continue; + seenKeys.add(pair.toLowerCase()); + + newPools++; + newRecords.push({ poolType: "uniswapv2", v2Pair: pair, currency0: token0, currency1: token1, tickSpacing: 1, sqrtPriceX96: null }); + } + + if (newRecords.length > 0) { + allRecords = allRecords.concat(newRecords); + atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + "\n"); + } + + const interimCp: Checkpoint = { chainId, factory, lastProcessedBlock: end, updatedAt: new Date().toISOString() }; + atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + "\n"); + console.error(`[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`); + } + + console.log(JSON.stringify({ + ok: true, chainId, factory, + scanned: { fromBlock, toBlock, latestBlock, finality, chunkBlocks }, + logsFound: totalLogs, newPools, outFile: outPath, checkpointFile: cpPath, + }, null, 2)); +} + +main().catch((err) => { console.error(err); process.exitCode = 1; }); diff --git a/aggregator-hooks/polling/UniswapV3.ts b/aggregator-hooks/polling/UniswapV3.ts new file mode 100644 index 00000000..7659503a --- /dev/null +++ b/aggregator-hooks/polling/UniswapV3.ts @@ -0,0 +1,183 @@ +/** + * Uniswap V3 pool discovery (checkpoint-based polling). + * Scans new PoolCreated events from the V3 factory since the last checkpoint + * and appends new pools to the output file. + * + * Usage: + * npx tsx polling/UniswapV3.ts --chain-id 1 + * + * Options: + * --chain-id (required) Chain ID + * --output-dir output directory (default: detected) + * --checkpoint-dir checkpoint directory (default: checkpoints) + * --chunk-blocks block chunk size (default: 10000) + * --start-block (optional) override checkpoint start block + * + * Env vars (VAR_ or VAR): + * RPC_URL (required) + * UNISWAP_V3_FACTORY (optional, default mainnet factory) + * FINALITY_BLOCKS (optional, default 10) + * LOOKBACK_BLOCKS (optional, default 200000) + */ + +import "dotenv/config"; +import fs from "node:fs"; +import path from "node:path"; +import { ethers } from "ethers"; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath, resolveCheckpointPath } from "@src/cli"; + +const OUTPUT_FILE = "uniswapv3-pools.json"; +const CHECKPOINT_FILE = "uniswapv3_checkpoint.json"; +const DEFAULT_FACTORY = "0x1F98431c8aD98523631AE4a59f267346ea31F984"; +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + +const FACTORY_ABI = [ + "event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)", +]; + +type Checkpoint = { chainId: number; factory: string; lastProcessedBlock: number; updatedAt: string }; + +type UniswapV3PoolConfig = { + poolType: "uniswapv3"; + v3Pool: string; + currency0: string; + currency1: string; + fee: number; + tickSpacing: number; + sqrtPriceX96: null; +}; + +function isNative(addr: string): boolean { + return addr.toLowerCase() === ZERO_ADDRESS; +} + +function ensureDirForFile(filePath: string) { + fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true }); +} + +function safeReadJson(filePath: string): T | null { + try { return JSON.parse(fs.readFileSync(filePath, "utf8")) as T; } catch { return null; } +} + +function atomicWriteFile(filePath: string, contents: string) { + ensureDirForFile(filePath); + const abs = path.resolve(filePath); + fs.writeFileSync(abs + ".tmp", contents); + fs.renameSync(abs + ".tmp", abs); +} + +function loadExistingKeys(outFile: string): Set { + const keys = new Set(); + if (!fs.existsSync(outFile)) return keys; + try { + const arr = safeReadJson(outFile); + if (!Array.isArray(arr)) return keys; + for (const x of arr) keys.add(x.v3Pool.toLowerCase()); + } catch { /* ignore */ } + return keys; +} + +function getEnvInt(name: string, chainId: number, def: number): number { + const v = getEnvForChain(name, chainId); + if (!v) return def; + const n = Number(v); + return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : def; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + const chainIdRaw = args["chain-id"]; + if (chainIdRaw == null) { console.error("Missing required --chain-id "); process.exit(1); } + const chainId = toInt(chainIdRaw, 0); + if (chainId <= 0) { console.error("--chain-id must be a positive integer"); process.exit(1); } + + const rpcUrl = getEnvForChain("RPC_URL", chainId); + if (!rpcUrl) throw new Error("Missing required env: RPC_URL"); + + const factoryRaw = getEnvForChain("UNISWAP_V3_FACTORY", chainId) ?? DEFAULT_FACTORY; + const factory = ethers.getAddress(factoryRaw); + + const outputDir = (args["output-dir"] as string) ?? "detected"; + const checkpointDir = (args["checkpoint-dir"] as string) ?? "checkpoints"; + const chunkBlocks = Math.max(1, toInt(args["chunk-blocks"], 10_000)); + const finality = getEnvInt("FINALITY_BLOCKS", chainId, 10); + const lookbackBlocks = getEnvInt("LOOKBACK_BLOCKS", chainId, 200_000); + const startBlockArg = args["start-block"]; + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const iface = new ethers.Interface(FACTORY_ABI); + const topic0 = iface.getEvent("PoolCreated")!.topicHash; + + const latestBlock = await provider.getBlockNumber(); + const toBlock = Math.max(0, latestBlock - finality); + + const cpPath = resolveCheckpointPath(checkpointDir, chainId, CHECKPOINT_FILE); + const cp = safeReadJson(cpPath); + + let fromBlock: number; + if (startBlockArg != null) { + fromBlock = Math.max(0, toInt(startBlockArg, 0)); + } else if (cp?.chainId === chainId && cp?.factory?.toLowerCase() === factory.toLowerCase()) { + fromBlock = cp.lastProcessedBlock + 1; + } else { + fromBlock = Math.max(0, toBlock - lookbackBlocks); + } + + if (fromBlock > toBlock) { + const newCp: Checkpoint = { chainId, factory, lastProcessedBlock: toBlock, updatedAt: new Date().toISOString() }; + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); + console.log(JSON.stringify({ ok: true, note: "No new blocks to scan", fromBlock, toBlock }, null, 2)); + return; + } + + const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); + const seenKeys = loadExistingKeys(outPath); + let allRecords = safeReadJson(outPath) ?? []; + let totalLogs = 0; + let newPools = 0; + + for (let start = fromBlock; start <= toBlock; start += chunkBlocks) { + const end = Math.min(toBlock, start + chunkBlocks - 1); + + const logs = await provider.getLogs({ address: factory, topics: [topic0], fromBlock: start, toBlock: end }); + totalLogs += logs.length; + const newRecords: UniswapV3PoolConfig[] = []; + + for (const log of logs) { + let parsed: ethers.LogDescription | null; + try { parsed = iface.parseLog(log); } catch { continue; } + if (!parsed) continue; + + const token0 = ethers.getAddress(parsed.args.token0 as string); + const token1 = ethers.getAddress(parsed.args.token1 as string); + const pool = ethers.getAddress(parsed.args.pool as string); + const fee = Number(parsed.args.fee); + const tickSpacing = Number(parsed.args.tickSpacing); + + if (isNative(token0) || isNative(token1)) continue; + if (seenKeys.has(pool.toLowerCase())) continue; + seenKeys.add(pool.toLowerCase()); + + newPools++; + newRecords.push({ poolType: "uniswapv3", v3Pool: pool, currency0: token0, currency1: token1, fee, tickSpacing, sqrtPriceX96: null }); + } + + if (newRecords.length > 0) { + allRecords = allRecords.concat(newRecords); + atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + "\n"); + } + + const interimCp: Checkpoint = { chainId, factory, lastProcessedBlock: end, updatedAt: new Date().toISOString() }; + atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + "\n"); + console.error(`[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`); + } + + console.log(JSON.stringify({ + ok: true, chainId, factory, + scanned: { fromBlock, toBlock, latestBlock, finality, chunkBlocks }, + logsFound: totalLogs, newPools, outFile: outPath, checkpointFile: cpPath, + }, null, 2)); +} + +main().catch((err) => { console.error(err); process.exitCode = 1; }); diff --git a/aggregator-hooks/src/createPools.ts b/aggregator-hooks/src/createPools.ts index ce9a627e..c720777e 100644 --- a/aggregator-hooks/src/createPools.ts +++ b/aggregator-hooks/src/createPools.ts @@ -268,6 +268,63 @@ function appendToRegistryFile(registryDir: string, poolType: string, entry: Pool log.info(`Appended to registry: ${filePath}`); } +/** + * Upsert a KEY=VALUE line in a .env file. Creates the file if it does not + * exist. Replaces the line if KEY is already present. + */ +function appendToEnvFile(filePath: string, key: string, value: string): void { + let lines: string[] = []; + if (existsSync(filePath)) { + lines = readFileSync(filePath, "utf-8").split("\n"); + } + const prefix = `${key}=`; + const idx = lines.findIndex((l) => l.startsWith(prefix)); + const newLine = `${key}=${value}`; + if (idx !== -1) { + lines[idx] = newLine; + } else { + lines.push(newLine); + } + // Ensure file ends with a newline + if (lines[lines.length - 1] !== "") lines.push(""); + writeFileSync(filePath, lines.join("\n")); +} + +const POOL_MANAGER_INIT_ABI = [ + "function initialize((address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) key, uint160 sqrtPriceX96) returns (int24 tick)", +]; + +async function initializeSingletonPool( + signer: ethers.Signer, + poolManagerAddress: Address, + poolKey: PoolKeyRecord, + sqrtPriceX96: bigint, + log: Logger, + dryRun: boolean, +): Promise<{ blockNumber: number; txHash: string } | null> { + const poolManager = new ethers.Contract(poolManagerAddress, POOL_MANAGER_INIT_ABI, signer); + + log.info(`Calling poolManager.initialize for pool: ${poolKey.currency0} / ${poolKey.currency1}`); + if (dryRun) log.info("(dry run - no broadcast)"); + + try { + if (dryRun) { + await poolManager.initialize.staticCall(poolKey, sqrtPriceX96); + log.success(`Dry run: initialize would succeed`); + return null; + } + + const tx = await poolManager.initialize(poolKey, sqrtPriceX96); + log.success(`Transaction sent: ${tx.hash}`); + const receipt = await tx.wait(); + log.success(`Pool initialized in block ${receipt!.blockNumber}`); + return { blockNumber: Number(receipt!.blockNumber), txHash: tx.hash }; + } catch (error) { + log.error("Error initializing pool:", error); + throw error; + } +} + /** * Read the solc version from a forge build artifact. * The artifact lives at out/.sol/.json and @@ -819,42 +876,92 @@ async function main() { const chainId = parsedChainId ?? Number((await provider.getNetwork()).chainId); - for (let j = 0; j < pools.length; j++) { - const i = startAt - 1 + j; - const poolConfig = pools[j]; - const poolType = poolConfig.poolType; - const module = CREATION_MODULES[poolType]; - const immutables = module.getImmutablesFromEnv(chainId); + // Determine module from first pool (loadJsonFile enforces a single poolType) + const firstPoolType = pools[0].poolType; + const firstModule = CREATION_MODULES[firstPoolType]; - log.section(`Processing Pool ${i + 1}/${allPools.length} (${poolType})`); + if (firstModule.isSingleton) { + // --- Singleton flow: deploy once, then initialize a V4 pool per config --- - try { - const constructorArgs = module.encodeConstructorArgs(poolConfig, immutables); - const salt = await mineSalt(constructorArgs, module.protocolId, log, CREATE2_DEPLOYER, jobs); - const hookAddress = selfDeployPool( - poolConfig, - poolType, - immutables, - salt, - rpcUrl, - dryRun, - log, - priorityGasPrice, - ); + if (!firstModule.aggregatorEnvKey || !firstModule.buildInitializeArgs) { + log.error(`Module ${firstPoolType} is marked as singleton but is missing aggregatorEnvKey or buildInitializeArgs`); + process.exit(1); + } - if (hookAddress && hookAddress !== "deployed") { - if (registryDir) { - const poolKeys = module.buildPoolKeys(poolConfig, hookAddress); + const envKey = `${firstModule.aggregatorEnvKey}_${chainId}`; + let hookAddress = (process.env[envKey] ?? "").trim() || null; + + if (hookAddress) { + log.info(`Reusing existing ${firstPoolType} singleton at ${hookAddress} (${envKey})`); + } else { + log.info(`No ${envKey} found — deploying ${firstPoolType} singleton aggregator...`); + const immutables = firstModule.getImmutablesFromEnv(chainId); + const constructorArgs = firstModule.encodeConstructorArgs(pools[0], immutables); + const salt = await mineSalt(constructorArgs, firstModule.protocolId, log, CREATE2_DEPLOYER, jobs); + const deployed = selfDeployPool(pools[0], firstPoolType, immutables, salt, rpcUrl, dryRun, log, priorityGasPrice); + + if (!deployed || deployed === "deployed") { + log.error("Could not determine deployed aggregator address — aborting"); + process.exit(1); + } + hookAddress = deployed; + + if (!dryRun) { + const envFilePath = join(projectRoot, "aggregator-hooks", ".env"); + appendToEnvFile(envFilePath, envKey, hookAddress); + log.success(`Wrote ${envKey}=${hookAddress} to aggregator-hooks/.env`); + + // Wait for the deployment tx to be confirmed before initializing pools. + // Without this, the first pool's beforeInitialize call hits an unconfirmed contract. + log.info(`Waiting for aggregator contract to be confirmed on-chain...`); + const deadline = Date.now() + 60_000; + while (Date.now() < deadline) { + const code = await provider.getCode(hookAddress); + if (code && code !== "0x") { + log.success(`Aggregator confirmed at ${hookAddress}`); + break; + } + await new Promise((r) => setTimeout(r, 2_000)); + } + } + + if (verify.enabled && !dryRun) { + verifyContract(hookAddress as Address, firstModule.contractIdentifier, constructorArgs, chainId, verify, log); + } + } + + const immutables = firstModule.getImmutablesFromEnv(chainId); + + for (let j = 0; j < pools.length; j++) { + const i = startAt - 1 + j; + const poolConfig = pools[j]; + + log.section(`Initializing Pool ${i + 1}/${allPools.length} (${firstPoolType})`); + + try { + const [poolKey, sqrtPriceX96] = firstModule.buildInitializeArgs!(poolConfig, hookAddress as Address); + const result = await initializeSingletonPool( + signer, + immutables.poolManager as Address, + poolKey, + sqrtPriceX96, + log, + dryRun, + ); + + if (registryDir && !dryRun) { + const poolKeys = firstModule.buildPoolKeys(poolConfig, hookAddress as Address); if (poolKeys.length > 0) { - const blockNumber = Number(await provider.getBlockNumber()); + const blockNumber = result?.blockNumber ?? Number(await provider.getBlockNumber()); appendToRegistryFile( registryDir, - poolType, + firstPoolType, { - pools: poolKeys.map((poolKey) => ({ poolKey, poolId: computePoolId(poolKey) })), + pools: poolKeys.map((pk) => ({ poolKey: pk, poolId: computePoolId(pk) })), metadata: { - externalPool: module.getExternalPool(poolConfig), - hookAddress, + externalPool: firstModule.getExternalPool(poolConfig), + hookAddress: hookAddress as Address, + txHash: result?.txHash, blockNumber, }, }, @@ -862,62 +969,115 @@ async function main() { ); } } - if (verify.enabled && !dryRun) { - verifyContract(hookAddress, module.contractIdentifier, constructorArgs, chainId, verify, log); - } + log.success(`Successfully initialized pool ${i + 1}`); + } catch (error) { + log.error(`Failed to initialize pool ${i + 1}:`, error); + continue; } - log.success(`Successfully created pool ${i + 1}`); - } catch (error) { - log.error(`Failed to create pool ${i + 1}:`, error); - const execErr = error as { stdout?: string; stderr?: string }; - log.dumpForgeOutput({ stdout: execErr.stdout, stderr: execErr.stderr, label: "Forge" }); - const forgeOutput = [execErr.stdout, execErr.stderr].filter(Boolean).join("\n"); - if (forgeOutput) { - const verification = await verifyDeploymentOnForgeFailure( - provider, - immutables.poolManager, + } + } else { + // --- Factory/self-deploy flow: deploy a new hook per pool config --- + for (let j = 0; j < pools.length; j++) { + const i = startAt - 1 + j; + const poolConfig = pools[j]; + const poolType = poolConfig.poolType; + const module = CREATION_MODULES[poolType]; + const immutables = module.getImmutablesFromEnv(chainId); + + log.section(`Processing Pool ${i + 1}/${allPools.length} (${poolType})`); + + try { + const constructorArgs = module.encodeConstructorArgs(poolConfig, immutables); + const salt = await mineSalt(constructorArgs, module.protocolId, log, CREATE2_DEPLOYER, jobs); + const hookAddress = selfDeployPool( poolConfig, poolType, - forgeOutput, + immutables, + salt, + rpcUrl, + dryRun, + log, + priorityGasPrice, ); - if (verification?.hookDeployed) { - log.error(""); - log.error("Verification (possible false positive):"); - log.error(` Hook has code at ${verification.hookAddress} → deployment likely succeeded`); - if (verification.poolsInitialized > 0) { - log.error( - ` Found ${verification.poolsInitialized} Initialize event(s) for this hook → pool(s) initialized on-chain`, - ); - } - log.error(" Check block explorer to confirm."); - - if (registryDir && !dryRun) { - const pools: PoolEntry[] = - verification.poolEntriesFromEvent ?? - module - .buildPoolKeys(poolConfig, verification.hookAddress) - .map((poolKey) => ({ poolKey, poolId: computePoolId(poolKey) })); - const blockNumber = verification.blockNumber ?? Number(await provider.getBlockNumber()); - if (pools.length > 0) { + + if (hookAddress && hookAddress !== "deployed") { + if (registryDir) { + const poolKeys = module.buildPoolKeys(poolConfig, hookAddress); + if (poolKeys.length > 0) { + const blockNumber = Number(await provider.getBlockNumber()); appendToRegistryFile( registryDir, poolType, { - pools, + pools: poolKeys.map((poolKey) => ({ poolKey, poolId: computePoolId(poolKey) })), metadata: { externalPool: module.getExternalPool(poolConfig), - hookAddress: verification.hookAddress, + hookAddress, blockNumber, }, }, log, ); - log.info(" Appended to registry despite forge failure (deployment verified on-chain)."); } } + if (verify.enabled && !dryRun) { + verifyContract(hookAddress, module.contractIdentifier, constructorArgs, chainId, verify, log); + } } + log.success(`Successfully created pool ${i + 1}`); + } catch (error) { + log.error(`Failed to create pool ${i + 1}:`, error); + const execErr = error as { stdout?: string; stderr?: string }; + log.dumpForgeOutput({ stdout: execErr.stdout, stderr: execErr.stderr, label: "Forge" }); + const forgeOutput = [execErr.stdout, execErr.stderr].filter(Boolean).join("\n"); + if (forgeOutput) { + const immutables = CREATION_MODULES[poolConfig.poolType].getImmutablesFromEnv(chainId); + const verification = await verifyDeploymentOnForgeFailure( + provider, + immutables.poolManager, + poolConfig, + poolConfig.poolType, + forgeOutput, + ); + if (verification?.hookDeployed) { + log.error(""); + log.error("Verification (possible false positive):"); + log.error(` Hook has code at ${verification.hookAddress} → deployment likely succeeded`); + if (verification.poolsInitialized > 0) { + log.error( + ` Found ${verification.poolsInitialized} Initialize event(s) for this hook → pool(s) initialized on-chain`, + ); + } + log.error(" Check block explorer to confirm."); + + if (registryDir && !dryRun) { + const recoverPools: PoolEntry[] = + verification.poolEntriesFromEvent ?? + module + .buildPoolKeys(poolConfig, verification.hookAddress) + .map((poolKey) => ({ poolKey, poolId: computePoolId(poolKey) })); + const blockNumber = verification.blockNumber ?? Number(await provider.getBlockNumber()); + if (recoverPools.length > 0) { + appendToRegistryFile( + registryDir, + poolConfig.poolType, + { + pools: recoverPools, + metadata: { + externalPool: module.getExternalPool(poolConfig), + hookAddress: verification.hookAddress, + blockNumber, + }, + }, + log, + ); + log.info(" Appended to registry despite forge failure (deployment verified on-chain)."); + } + } + } + } + continue; } - continue; } } } else { From 8a4a3a309ac8bc1d7ecb5a6e8145a01b52d59089 Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Mon, 1 Jun 2026 13:27:35 -0400 Subject: [PATCH 19/21] format aggregator hooks folder --- .../abis/FluidDexLiteFactory.json | 8 +- .../abis/FluidDexLiteResolver.json | 4 +- aggregator-hooks/abis/FluidDexT1Factory.json | 10 +- .../abis/FluidDexT1HistoricalFactory.json | 8 +- aggregator-hooks/abis/FluidDexT1Resolver.json | 4 +- .../abis/StableSwapNGFactory.json | 6 +- .../abis/StableSwapNGHistoricalFactory.json | 10 +- aggregator-hooks/abis/StableswapFactory.json | 8 +- aggregator-hooks/abis/index.ts | 30 +- .../creation-modules/FluidDexLite.ts | 62 +- .../creation-modules/FluidDexT1.ts | 55 +- .../creation-modules/PancakeSwapV3.ts | 76 +- .../creation-modules/Slipstream.ts | 76 +- .../creation-modules/StableSwap.ts | 62 +- .../creation-modules/StableSwapNG.ts | 64 +- .../creation-modules/UniswapV2.ts | 76 +- .../creation-modules/UniswapV3.ts | 76 +- aggregator-hooks/creation-modules/index.ts | 66 +- aggregator-hooks/creation-modules/types.ts | 17 +- aggregator-hooks/historical/FluidDexLite.ts | 64 +- aggregator-hooks/historical/FluidDexT1.ts | 122 +- aggregator-hooks/historical/PancakeSwapV3.ts | 123 +- aggregator-hooks/historical/Slipstream.ts | 124 +- aggregator-hooks/historical/StableSwapNG.ts | 79 +- aggregator-hooks/historical/UniswapV2.ts | 123 +- aggregator-hooks/historical/UniswapV3.ts | 123 +- aggregator-hooks/package-lock.json | 1433 +++++++++-------- aggregator-hooks/package.json | 45 +- aggregator-hooks/polling/FluidDexLite.ts | 89 +- aggregator-hooks/polling/FluidDexT1.ts | 110 +- aggregator-hooks/polling/PancakeSwapV3.ts | 170 +- aggregator-hooks/polling/Slipstream.ts | 171 +- aggregator-hooks/polling/StableSwapNG.ts | 124 +- aggregator-hooks/polling/UniswapV2.ts | 169 +- aggregator-hooks/polling/UniswapV3.ts | 170 +- aggregator-hooks/src/cli.ts | 33 +- aggregator-hooks/src/createPools.ts | 811 +++++++--- aggregator-hooks/src/logger.ts | 50 +- aggregator-hooks/tsconfig.json | 48 +- foundry.lock | 2 +- 40 files changed, 3076 insertions(+), 1825 deletions(-) diff --git a/aggregator-hooks/abis/FluidDexLiteFactory.json b/aggregator-hooks/abis/FluidDexLiteFactory.json index e45a0331..d7d3869a 100644 --- a/aggregator-hooks/abis/FluidDexLiteFactory.json +++ b/aggregator-hooks/abis/FluidDexLiteFactory.json @@ -1,6 +1,6 @@ [ - "function POOL_MANAGER() external view returns (address)", - "function FLUID_DEX_LITE() external view returns (address)", - "function FLUID_DEX_LITE_RESOLVER() external view returns (address)", - "function createPool(bytes32 salt, bytes32 dexSalt, address currency0, address currency1, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)" + "function POOL_MANAGER() external view returns (address)", + "function FLUID_DEX_LITE() external view returns (address)", + "function FLUID_DEX_LITE_RESOLVER() external view returns (address)", + "function createPool(bytes32 salt, bytes32 dexSalt, address currency0, address currency1, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)" ] diff --git a/aggregator-hooks/abis/FluidDexLiteResolver.json b/aggregator-hooks/abis/FluidDexLiteResolver.json index 7a5011b3..51be0746 100644 --- a/aggregator-hooks/abis/FluidDexLiteResolver.json +++ b/aggregator-hooks/abis/FluidDexLiteResolver.json @@ -1,4 +1,4 @@ [ - "function getAllDexes() external view returns (tuple(address token0, address token1, bytes32 salt)[] memory)", - "function getDexState(tuple(address token0, address token1, bytes32 salt) dexKey) external view returns (tuple(tuple(uint256 fee, uint256 revenueCut, uint256 rebalancingStatus, bool isCenterPriceShiftActive, uint256 centerPrice, address centerPriceAddress, bool isRangePercentShiftActive, uint256 upperRangePercent, uint256 lowerRangePercent, bool isThresholdPercentShiftActive, uint256 upperShiftThresholdPercent, uint256 lowerShiftThresholdPercent, uint256 token0Decimals, uint256 token1Decimals, uint256 totalToken0AdjustedAmount, uint256 totalToken1AdjustedAmount) dexVariables, tuple(uint256 lastInteractionTimestamp, uint256 rebalancingShiftingTime, uint256 maxCenterPrice, uint256 minCenterPrice, uint256 shiftPercentage, uint256 centerPriceShiftingTime, uint256 startTimestamp) centerPriceShift, tuple(uint256 oldUpperRangePercent, uint256 oldLowerRangePercent, uint256 shiftingTime, uint256 startTimestamp) rangeShift, tuple(uint256 oldUpperThresholdPercent, uint256 oldLowerThresholdPercent, uint256 shiftingTime, uint256 startTimestamp) thresholdShift) dexState)" + "function getAllDexes() external view returns (tuple(address token0, address token1, bytes32 salt)[] memory)", + "function getDexState(tuple(address token0, address token1, bytes32 salt) dexKey) external view returns (tuple(tuple(uint256 fee, uint256 revenueCut, uint256 rebalancingStatus, bool isCenterPriceShiftActive, uint256 centerPrice, address centerPriceAddress, bool isRangePercentShiftActive, uint256 upperRangePercent, uint256 lowerRangePercent, bool isThresholdPercentShiftActive, uint256 upperShiftThresholdPercent, uint256 lowerShiftThresholdPercent, uint256 token0Decimals, uint256 token1Decimals, uint256 totalToken0AdjustedAmount, uint256 totalToken1AdjustedAmount) dexVariables, tuple(uint256 lastInteractionTimestamp, uint256 rebalancingShiftingTime, uint256 maxCenterPrice, uint256 minCenterPrice, uint256 shiftPercentage, uint256 centerPriceShiftingTime, uint256 startTimestamp) centerPriceShift, tuple(uint256 oldUpperRangePercent, uint256 oldLowerRangePercent, uint256 shiftingTime, uint256 startTimestamp) rangeShift, tuple(uint256 oldUpperThresholdPercent, uint256 oldLowerThresholdPercent, uint256 shiftingTime, uint256 startTimestamp) thresholdShift) dexState)" ] diff --git a/aggregator-hooks/abis/FluidDexT1Factory.json b/aggregator-hooks/abis/FluidDexT1Factory.json index 4237deb0..80463ddb 100644 --- a/aggregator-hooks/abis/FluidDexT1Factory.json +++ b/aggregator-hooks/abis/FluidDexT1Factory.json @@ -1,7 +1,7 @@ [ - "function POOL_MANAGER() external view returns (address)", - "function fluidDexReservesResolver() external view returns (address)", - "function fluidDexResolver() external view returns (address)", - "function FLUID_LIQUIDITY() external view returns (address)", - "function createPool(bytes32 salt, address fluidPool, address currency0, address currency1, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)" + "function POOL_MANAGER() external view returns (address)", + "function fluidDexReservesResolver() external view returns (address)", + "function fluidDexResolver() external view returns (address)", + "function FLUID_LIQUIDITY() external view returns (address)", + "function createPool(bytes32 salt, address fluidPool, address currency0, address currency1, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)" ] diff --git a/aggregator-hooks/abis/FluidDexT1HistoricalFactory.json b/aggregator-hooks/abis/FluidDexT1HistoricalFactory.json index 97842aaf..f17fd7f9 100644 --- a/aggregator-hooks/abis/FluidDexT1HistoricalFactory.json +++ b/aggregator-hooks/abis/FluidDexT1HistoricalFactory.json @@ -1,6 +1,6 @@ [ - "event LogDexDeployed(address indexed dex, uint256 indexed dexId)", - "function totalDexes() external view returns (uint256)", - "function getDexAddress(uint256 dexId) public view returns (address)", - "function isDex(address dex) public view returns (bool)" + "event LogDexDeployed(address indexed dex, uint256 indexed dexId)", + "function totalDexes() external view returns (uint256)", + "function getDexAddress(uint256 dexId) public view returns (address)", + "function isDex(address dex) public view returns (bool)" ] diff --git a/aggregator-hooks/abis/FluidDexT1Resolver.json b/aggregator-hooks/abis/FluidDexT1Resolver.json index 3a24aeab..edaf9880 100644 --- a/aggregator-hooks/abis/FluidDexT1Resolver.json +++ b/aggregator-hooks/abis/FluidDexT1Resolver.json @@ -1 +1,3 @@ -["function getDexTokens(address dex_) external view returns (address token0_, address token1_)"] +[ + "function getDexTokens(address dex_) external view returns (address token0_, address token1_)" +] diff --git a/aggregator-hooks/abis/StableSwapNGFactory.json b/aggregator-hooks/abis/StableSwapNGFactory.json index a63da0d8..8bb2efec 100644 --- a/aggregator-hooks/abis/StableSwapNGFactory.json +++ b/aggregator-hooks/abis/StableSwapNGFactory.json @@ -1,5 +1,5 @@ [ - "function poolManager() external view returns (address)", - "function curveFactory() external view returns (address)", - "function createPool(bytes32 salt, address curvePool, address[] calldata tokens, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)" + "function poolManager() external view returns (address)", + "function curveFactory() external view returns (address)", + "function createPool(bytes32 salt, address curvePool, address[] calldata tokens, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)" ] diff --git a/aggregator-hooks/abis/StableSwapNGHistoricalFactory.json b/aggregator-hooks/abis/StableSwapNGHistoricalFactory.json index 9848faab..56a96be0 100644 --- a/aggregator-hooks/abis/StableSwapNGHistoricalFactory.json +++ b/aggregator-hooks/abis/StableSwapNGHistoricalFactory.json @@ -1,7 +1,7 @@ [ - "function pool_count() view returns (uint256)", - "function pool_list(uint256) view returns (address)", - "function get_n_coins(address) view returns (uint256)", - "function get_coins(address) view returns (address[])", - "function get_base_pool(address) view returns (address)" + "function pool_count() view returns (uint256)", + "function pool_list(uint256) view returns (address)", + "function get_n_coins(address) view returns (uint256)", + "function get_coins(address) view returns (address[])", + "function get_base_pool(address) view returns (address)" ] diff --git a/aggregator-hooks/abis/StableswapFactory.json b/aggregator-hooks/abis/StableswapFactory.json index 6e88dafb..41ba6727 100644 --- a/aggregator-hooks/abis/StableswapFactory.json +++ b/aggregator-hooks/abis/StableswapFactory.json @@ -1,6 +1,6 @@ [ - "function POOL_MANAGER() external view returns (address)", - "function poolManager() external view returns (address)", - "function metaRegistry() external view returns (address)", - "function createPool(bytes32 salt, address curvePool, address[] calldata tokens, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)" + "function POOL_MANAGER() external view returns (address)", + "function poolManager() external view returns (address)", + "function metaRegistry() external view returns (address)", + "function createPool(bytes32 salt, address curvePool, address[] calldata tokens, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address hook)" ] diff --git a/aggregator-hooks/abis/index.ts b/aggregator-hooks/abis/index.ts index 1c38be67..778b113e 100644 --- a/aggregator-hooks/abis/index.ts +++ b/aggregator-hooks/abis/index.ts @@ -1,15 +1,27 @@ /** * Load ABIs via createRequire (avoids import attributes for Prettier compatibility). */ -import { createRequire } from "node:module"; +import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -export const STABLESWAP_FACTORY_ABI: string[] = [...require("./StableswapFactory.json")]; -export const STABLESWAPNG_FACTORY_ABI: string[] = [...require("./StableSwapNGFactory.json")]; -export const FLUIDDEXT1_FACTORY_ABI: string[] = [...require("./FluidDexT1Factory.json")]; -export const FLUIDDEXLITE_FACTORY_ABI: string[] = [...require("./FluidDexLiteFactory.json")]; -export const FLUIDDEXT1_HISTORICAL_FACTORY_ABI = require("./FluidDexT1HistoricalFactory.json") as readonly string[]; -export const FLUIDDEXT1_RESOLVER_ABI = require("./FluidDexT1Resolver.json") as readonly string[]; -export const STABLESWAPNG_HISTORICAL_FACTORY_ABI = require("./StableSwapNGHistoricalFactory.json") as readonly string[]; -export const FLUIDDEXLITE_RESOLVER_ABI = require("./FluidDexLiteResolver.json") as readonly string[]; +export const STABLESWAP_FACTORY_ABI: string[] = [ + ...require('./StableswapFactory.json'), +]; +export const STABLESWAPNG_FACTORY_ABI: string[] = [ + ...require('./StableSwapNGFactory.json'), +]; +export const FLUIDDEXT1_FACTORY_ABI: string[] = [ + ...require('./FluidDexT1Factory.json'), +]; +export const FLUIDDEXLITE_FACTORY_ABI: string[] = [ + ...require('./FluidDexLiteFactory.json'), +]; +export const FLUIDDEXT1_HISTORICAL_FACTORY_ABI = + require('./FluidDexT1HistoricalFactory.json') as readonly string[]; +export const FLUIDDEXT1_RESOLVER_ABI = + require('./FluidDexT1Resolver.json') as readonly string[]; +export const STABLESWAPNG_HISTORICAL_FACTORY_ABI = + require('./StableSwapNGHistoricalFactory.json') as readonly string[]; +export const FLUIDDEXLITE_RESOLVER_ABI = + require('./FluidDexLiteResolver.json') as readonly string[]; diff --git a/aggregator-hooks/creation-modules/FluidDexLite.ts b/aggregator-hooks/creation-modules/FluidDexLite.ts index 38bf5917..2b784a73 100644 --- a/aggregator-hooks/creation-modules/FluidDexLite.ts +++ b/aggregator-hooks/creation-modules/FluidDexLite.ts @@ -1,13 +1,18 @@ /** * FluidDex Lite aggregator hook deployment module. */ -import { ethers } from "ethers"; -import { mustEnvForChain } from "../src/cli.js"; -import { FLUIDDEXLITE_FACTORY_ABI } from "../abis/index.js"; -import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables } from "./types.js"; +import { ethers } from 'ethers'; +import { mustEnvForChain } from '../src/cli.js'; +import { FLUIDDEXLITE_FACTORY_ABI } from '../abis/index.js'; +import { + DEFAULT_SQRT_PRICE_X96, + type Address, + type CreationModule, + type FactoryImmutables, +} from './types.js'; export interface FluidDexLitePoolConfig { - poolType: "fluiddexlite"; + poolType: 'fluiddexlite'; dexSalt: string; currency0: Address; currency1: Address; @@ -18,14 +23,15 @@ export interface FluidDexLitePoolConfig { const PROTOCOL_ID = 0xf3; -const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); +const orderPair = (a: Address, b: Address): [Address, Address] => + a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]; export const fluiddexliteModule: CreationModule = { - poolType: "fluiddexlite", + poolType: 'fluiddexlite', protocolId: PROTOCOL_ID, factoryAbi: FLUIDDEXLITE_FACTORY_ABI, contractIdentifier: - "lib/v4-hooks-public/src/aggregator-hooks/implementations/FluidDexLite/FluidDexLiteAggregator.sol:FluidDexLiteAggregator", + 'lib/v4-hooks-public/src/aggregator-hooks/implementations/FluidDexLite/FluidDexLiteAggregator.sol:FluidDexLiteAggregator', getHookParams(config) { return { @@ -55,19 +61,28 @@ export const fluiddexliteModule: CreationModule = { getImmutablesFromEnv(chainId: number): FactoryImmutables { return { - poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, - fluidDexLite: mustEnvForChain("FLUID_DEX_LITE", chainId) as Address, - fluidDexLiteResolver: mustEnvForChain("FLUID_DEX_LITE_RESOLVER", chainId) as Address, + poolManager: mustEnvForChain('POOL_MANAGER', chainId) as Address, + fluidDexLite: mustEnvForChain('FLUID_DEX_LITE', chainId) as Address, + fluidDexLiteResolver: mustEnvForChain( + 'FLUID_DEX_LITE_RESOLVER', + chainId, + ) as Address, }; }, async readFactoryImmutables(provider, factoryAddress) { - const factory = new ethers.Contract(factoryAddress, FLUIDDEXLITE_FACTORY_ABI, provider); - const [poolManager, fluidDexLite, fluidDexLiteResolver] = await Promise.all([ - factory.POOL_MANAGER(), - factory.FLUID_DEX_LITE(), - factory.FLUID_DEX_LITE_RESOLVER(), - ]); + const factory = new ethers.Contract( + factoryAddress, + FLUIDDEXLITE_FACTORY_ABI, + provider, + ); + const [poolManager, fluidDexLite, fluidDexLiteResolver] = await Promise.all( + [ + factory.POOL_MANAGER(), + factory.FLUID_DEX_LITE(), + factory.FLUID_DEX_LITE_RESOLVER(), + ], + ); return { poolManager: poolManager as Address, fluidDexLite: fluidDexLite as Address, @@ -77,10 +92,15 @@ export const fluiddexliteModule: CreationModule = { encodeConstructorArgs(config, immutables) { const encoded = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "address", "address", "bytes32"], - [immutables.poolManager, immutables.fluidDexLite, immutables.fluidDexLiteResolver, config.dexSalt], + ['address', 'address', 'address', 'bytes32'], + [ + immutables.poolManager, + immutables.fluidDexLite, + immutables.fluidDexLiteResolver, + config.dexSalt, + ], ); - return encoded.startsWith("0x") ? encoded : `0x${encoded}`; + return encoded.startsWith('0x') ? encoded : `0x${encoded}`; }, buildSelfDeployEnvVars(config, immutables) { @@ -89,7 +109,7 @@ export const fluiddexliteModule: CreationModule = { FLUID_DEX_LITE: immutables.fluidDexLite!, FLUID_DEX_LITE_RESOLVER: immutables.fluidDexLiteResolver!, DEX_SALT: config.dexSalt, - TOKENS: [config.currency0, config.currency1].join(","), + TOKENS: [config.currency0, config.currency1].join(','), FEE: params.fee.toString(), TICK_SPACING: params.tickSpacing.toString(), SQRT_PRICE_X96: params.sqrtPriceX96.toString(), diff --git a/aggregator-hooks/creation-modules/FluidDexT1.ts b/aggregator-hooks/creation-modules/FluidDexT1.ts index 5574ffa4..f1c4b5c9 100644 --- a/aggregator-hooks/creation-modules/FluidDexT1.ts +++ b/aggregator-hooks/creation-modules/FluidDexT1.ts @@ -1,13 +1,18 @@ /** * FluidDex T1 aggregator hook deployment module. */ -import { ethers } from "ethers"; -import { mustEnvForChain } from "../src/cli.js"; -import { FLUIDDEXT1_FACTORY_ABI } from "../abis/index.js"; -import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables } from "./types.js"; +import { ethers } from 'ethers'; +import { mustEnvForChain } from '../src/cli.js'; +import { FLUIDDEXT1_FACTORY_ABI } from '../abis/index.js'; +import { + DEFAULT_SQRT_PRICE_X96, + type Address, + type CreationModule, + type FactoryImmutables, +} from './types.js'; export interface FluidDexT1PoolConfig { - poolType: "fluiddext1"; + poolType: 'fluiddext1'; fluidPool: Address; currency0: Address; currency1: Address; @@ -18,14 +23,15 @@ export interface FluidDexT1PoolConfig { const PROTOCOL_ID = 0xf1; -const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); +const orderPair = (a: Address, b: Address): [Address, Address] => + a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]; export const fluiddext1Module: CreationModule = { - poolType: "fluiddext1", + poolType: 'fluiddext1', protocolId: PROTOCOL_ID, factoryAbi: FLUIDDEXT1_FACTORY_ABI, contractIdentifier: - "lib/v4-hooks-public/src/aggregator-hooks/implementations/FluidDexT1/FluidDexT1Aggregator.sol:FluidDexT1Aggregator", + 'lib/v4-hooks-public/src/aggregator-hooks/implementations/FluidDexT1/FluidDexT1Aggregator.sol:FluidDexT1Aggregator', getHookParams(config) { return { @@ -55,16 +61,31 @@ export const fluiddext1Module: CreationModule = { getImmutablesFromEnv(chainId: number): FactoryImmutables { return { - poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, - fluidDexReservesResolver: mustEnvForChain("FLUID_DEX_T1_RESERVES_RESOLVER", chainId) as Address, - fluidDexResolver: mustEnvForChain("FLUID_DEX_T1_RESOLVER", chainId) as Address, - fluidLiquidity: mustEnvForChain("FLUID_LIQUIDITY", chainId) as Address, + poolManager: mustEnvForChain('POOL_MANAGER', chainId) as Address, + fluidDexReservesResolver: mustEnvForChain( + 'FLUID_DEX_T1_RESERVES_RESOLVER', + chainId, + ) as Address, + fluidDexResolver: mustEnvForChain( + 'FLUID_DEX_T1_RESOLVER', + chainId, + ) as Address, + fluidLiquidity: mustEnvForChain('FLUID_LIQUIDITY', chainId) as Address, }; }, async readFactoryImmutables(provider, factoryAddress) { - const factory = new ethers.Contract(factoryAddress, FLUIDDEXT1_FACTORY_ABI, provider); - const [poolManager, fluidDexReservesResolver, fluidDexResolver, fluidLiquidity] = await Promise.all([ + const factory = new ethers.Contract( + factoryAddress, + FLUIDDEXT1_FACTORY_ABI, + provider, + ); + const [ + poolManager, + fluidDexReservesResolver, + fluidDexResolver, + fluidLiquidity, + ] = await Promise.all([ factory.POOL_MANAGER(), factory.fluidDexReservesResolver(), factory.fluidDexResolver(), @@ -80,7 +101,7 @@ export const fluiddext1Module: CreationModule = { encodeConstructorArgs(config, immutables) { const encoded = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "address", "address", "address", "address"], + ['address', 'address', 'address', 'address', 'address'], [ immutables.poolManager, config.fluidPool, @@ -89,7 +110,7 @@ export const fluiddext1Module: CreationModule = { immutables.fluidLiquidity, ], ); - return encoded.startsWith("0x") ? encoded : `0x${encoded}`; + return encoded.startsWith('0x') ? encoded : `0x${encoded}`; }, buildSelfDeployEnvVars(config, immutables) { @@ -99,7 +120,7 @@ export const fluiddext1Module: CreationModule = { FLUID_DEX_T1_RESERVES_RESOLVER: immutables.fluidDexReservesResolver!, FLUID_DEX_T1_RESOLVER: immutables.fluidDexResolver!, FLUID_LIQUIDITY: immutables.fluidLiquidity!, - TOKENS: [config.currency0, config.currency1].join(","), + TOKENS: [config.currency0, config.currency1].join(','), FEE: params.fee.toString(), TICK_SPACING: params.tickSpacing.toString(), SQRT_PRICE_X96: params.sqrtPriceX96.toString(), diff --git a/aggregator-hooks/creation-modules/PancakeSwapV3.ts b/aggregator-hooks/creation-modules/PancakeSwapV3.ts index e27080fa..457b2dff 100644 --- a/aggregator-hooks/creation-modules/PancakeSwapV3.ts +++ b/aggregator-hooks/creation-modules/PancakeSwapV3.ts @@ -5,12 +5,18 @@ * UniswapV3Aggregator in routing logic (fee-tier factory lookup) but * implements the PancakeSwap V3 swap callback ABI instead of Uniswap V3. */ -import { ethers } from "ethers"; -import { mustEnvForChain } from "../src/cli.js"; -import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables, type PoolKeyRecord } from "./types.js"; +import { ethers } from 'ethers'; +import { mustEnvForChain } from '../src/cli.js'; +import { + DEFAULT_SQRT_PRICE_X96, + type Address, + type CreationModule, + type FactoryImmutables, + type PoolKeyRecord, +} from './types.js'; export interface PancakeSwapV3PoolConfig { - poolType: "pancakeswapv3"; + poolType: 'pancakeswapv3'; /** The existing PancakeSwap V3 pool address being wrapped */ v3Pool: Address; currency0: Address; @@ -24,20 +30,21 @@ export interface PancakeSwapV3PoolConfig { const PROTOCOL_ID = 0x93; const AGGREGATOR_ABI = [ - "function poolManager() view returns (address)", - "function factory() view returns (address)", + 'function poolManager() view returns (address)', + 'function factory() view returns (address)', ]; -const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); +const orderPair = (a: Address, b: Address): [Address, Address] => + a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]; export const pancakeswapv3Module: CreationModule = { - poolType: "pancakeswapv3", + poolType: 'pancakeswapv3', protocolId: PROTOCOL_ID, factoryAbi: [], contractIdentifier: - "lib/v4-hooks-public/src/aggregator-hooks/PancakeSwapV3/PancakeSwapV3Aggregator.sol:PancakeSwapV3Aggregator", + 'lib/v4-hooks-public/src/aggregator-hooks/implementations/PancakeSwapV3/PancakeSwapV3Aggregator.sol:PancakeSwapV3Aggregator', isSingleton: true, - aggregatorEnvKey: "PANCAKESWAP_V3_AGGREGATOR", + aggregatorEnvKey: 'PANCAKESWAP_V3_AGGREGATOR', getHookParams(config) { return { @@ -50,7 +57,15 @@ export const pancakeswapv3Module: CreationModule = { buildPoolKeys(config, hookAddress) { const params = this.getHookParams(config); const [c0, c1] = orderPair(config.currency0, config.currency1); - return [{ currency0: c0, currency1: c1, fee: params.fee, tickSpacing: params.tickSpacing, hooks: hookAddress }]; + return [ + { + currency0: c0, + currency1: c1, + fee: params.fee, + tickSpacing: params.tickSpacing, + hooks: hookAddress, + }, + ]; }, getExternalPool(config) { @@ -59,34 +74,53 @@ export const pancakeswapv3Module: CreationModule = { getImmutablesFromEnv(chainId) { return { - poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, - externalFactory: mustEnvForChain("PANCAKESWAP_V3_FACTORY", chainId) as Address, + poolManager: mustEnvForChain('POOL_MANAGER', chainId) as Address, + externalFactory: mustEnvForChain( + 'PANCAKESWAP_V3_FACTORY', + chainId, + ) as Address, }; }, async readFactoryImmutables(provider, aggregatorAddress) { - const aggregator = new ethers.Contract(aggregatorAddress, AGGREGATOR_ABI, provider); - const [poolManager, externalFactory] = await Promise.all([aggregator.poolManager(), aggregator.factory()]); - return { poolManager: poolManager as Address, externalFactory: externalFactory as Address }; + const aggregator = new ethers.Contract( + aggregatorAddress, + AGGREGATOR_ABI, + provider, + ); + const [poolManager, externalFactory] = await Promise.all([ + aggregator.poolManager(), + aggregator.factory(), + ]); + return { + poolManager: poolManager as Address, + externalFactory: externalFactory as Address, + }; }, encodeConstructorArgs(_config, immutables) { const encoded = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "address", "string"], - [immutables.poolManager, immutables.externalFactory, "PancakeSwapV3Aggregator v1.0"], + ['address', 'address', 'string'], + [ + immutables.poolManager, + immutables.externalFactory, + 'PancakeSwapV3Aggregator v1.0', + ], ); - return encoded.startsWith("0x") ? encoded : `0x${encoded}`; + return encoded.startsWith('0x') ? encoded : `0x${encoded}`; }, buildSelfDeployEnvVars(_config, immutables) { return { EXTERNAL_FACTORY: immutables.externalFactory!, - HOOK_VERSION: "PancakeSwapV3Aggregator v1.0", + HOOK_VERSION: 'PancakeSwapV3Aggregator v1.0', }; }, buildCreatePoolArgs(_config, _salt) { - throw new Error("PancakeSwapV3 uses singleton self-deploy mode; factory mode (createPool) is not supported."); + throw new Error( + 'PancakeSwapV3 uses singleton self-deploy mode; factory mode (createPool) is not supported.', + ); }, buildInitializeArgs(config, hookAddress): [PoolKeyRecord, bigint] { diff --git a/aggregator-hooks/creation-modules/Slipstream.ts b/aggregator-hooks/creation-modules/Slipstream.ts index 9b1a4f48..5ec4e97d 100644 --- a/aggregator-hooks/creation-modules/Slipstream.ts +++ b/aggregator-hooks/creation-modules/Slipstream.ts @@ -5,42 +5,51 @@ * registers a new V4 pool backed by an existing Slipstream CL pool (resolved * by tickSpacing via the Slipstream factory, rather than fee tier as in V3). */ -import { ethers } from "ethers"; -import { mustEnvForChain } from "../src/cli.js"; -import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables, type PoolKeyRecord } from "./types.js"; +import { ethers } from 'ethers'; +import { mustEnvForChain } from '../src/cli.js'; +import { + DEFAULT_SQRT_PRICE_X96, + type Address, + type CreationModule, + type FactoryImmutables, + type PoolKeyRecord, +} from './types.js'; export interface SlipstreamPoolConfig { - poolType: "slipstream"; + poolType: 'slipstream'; /** The existing Slipstream pool address being wrapped */ slipstreamPool: Address; currency0: Address; currency1: Address; - /** Slipstream tickSpacing — used for both pool lookup and V4 PoolKey (fee is always 0) */ + /** Slipstream tickSpacing — used for both pool lookup and V4 PoolKey (fee is always DYNAMIC_FEE_FLAG = 0x800000) */ tickSpacing: number; sqrtPriceX96?: bigint | null; } -const PROTOCOL_ID = 0xA1; +const PROTOCOL_ID = 0xa1; + +const DYNAMIC_FEE_FLAG = 0x800000; const AGGREGATOR_ABI = [ - "function poolManager() view returns (address)", - "function factory() view returns (address)", + 'function poolManager() view returns (address)', + 'function factory() view returns (address)', ]; -const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); +const orderPair = (a: Address, b: Address): [Address, Address] => + a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]; export const slipstreamModule: CreationModule = { - poolType: "slipstream", + poolType: 'slipstream', protocolId: PROTOCOL_ID, factoryAbi: [], contractIdentifier: - "lib/v4-hooks-public/src/aggregator-hooks/implementations/Slipstream/SlipstreamAggregator.sol:SlipstreamAggregator", + 'lib/v4-hooks-public/src/aggregator-hooks/implementations/Slipstream/SlipstreamAggregator.sol:SlipstreamAggregator', isSingleton: true, - aggregatorEnvKey: "SLIPSTREAM_AGGREGATOR", + aggregatorEnvKey: 'SLIPSTREAM_AGGREGATOR', getHookParams(config) { return { - fee: 0, + fee: DYNAMIC_FEE_FLAG, // LPFeeLibrary.DYNAMIC_FEE_FLAG — required by SlipstreamAggregator._resolveExternalPool tickSpacing: config.tickSpacing, sqrtPriceX96: config.sqrtPriceX96 ?? DEFAULT_SQRT_PRICE_X96, }; @@ -49,7 +58,15 @@ export const slipstreamModule: CreationModule = { buildPoolKeys(config, hookAddress) { const params = this.getHookParams(config); const [c0, c1] = orderPair(config.currency0, config.currency1); - return [{ currency0: c0, currency1: c1, fee: params.fee, tickSpacing: params.tickSpacing, hooks: hookAddress }]; + return [ + { + currency0: c0, + currency1: c1, + fee: params.fee, + tickSpacing: params.tickSpacing, + hooks: hookAddress, + }, + ]; }, getExternalPool(config) { @@ -58,25 +75,38 @@ export const slipstreamModule: CreationModule = { getImmutablesFromEnv(chainId) { return { - poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, - externalFactory: mustEnvForChain("SLIPSTREAM_FACTORY", chainId) as Address, + poolManager: mustEnvForChain('POOL_MANAGER', chainId) as Address, + externalFactory: mustEnvForChain( + 'SLIPSTREAM_FACTORY', + chainId, + ) as Address, }; }, async readFactoryImmutables(provider, aggregatorAddress) { - const aggregator = new ethers.Contract(aggregatorAddress, AGGREGATOR_ABI, provider); - const [poolManager, externalFactory] = await Promise.all([aggregator.poolManager(), aggregator.factory()]); - return { poolManager: poolManager as Address, externalFactory: externalFactory as Address }; + const aggregator = new ethers.Contract( + aggregatorAddress, + AGGREGATOR_ABI, + provider, + ); + const [poolManager, externalFactory] = await Promise.all([ + aggregator.poolManager(), + aggregator.factory(), + ]); + return { + poolManager: poolManager as Address, + externalFactory: externalFactory as Address, + }; }, encodeConstructorArgs(_config, immutables) { // SlipstreamAggregator constructor: (IPoolManager manager, address slipstreamFactory) // hookVersion is hardcoded in the contract; not a constructor arg. const encoded = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "address"], + ['address', 'address'], [immutables.poolManager, immutables.externalFactory], ); - return encoded.startsWith("0x") ? encoded : `0x${encoded}`; + return encoded.startsWith('0x') ? encoded : `0x${encoded}`; }, buildSelfDeployEnvVars(_config, immutables) { @@ -86,7 +116,9 @@ export const slipstreamModule: CreationModule = { }, buildCreatePoolArgs(_config, _salt) { - throw new Error("Slipstream uses singleton self-deploy mode; factory mode (createPool) is not supported."); + throw new Error( + 'Slipstream uses singleton self-deploy mode; factory mode (createPool) is not supported.', + ); }, buildInitializeArgs(config, hookAddress): [PoolKeyRecord, bigint] { diff --git a/aggregator-hooks/creation-modules/StableSwap.ts b/aggregator-hooks/creation-modules/StableSwap.ts index b5e267ad..ecfc9785 100644 --- a/aggregator-hooks/creation-modules/StableSwap.ts +++ b/aggregator-hooks/creation-modules/StableSwap.ts @@ -2,19 +2,19 @@ * StableSwap (Curve) aggregator hook deployment module. * Requires Curve MetaRegistry address for meta pool rejection. */ -import { ethers } from "ethers"; -import { getEnvForChain, mustEnvForChain } from "../src/cli.js"; -import { STABLESWAP_FACTORY_ABI } from "../abis/index.js"; +import { ethers } from 'ethers'; +import { getEnvForChain, mustEnvForChain } from '../src/cli.js'; +import { STABLESWAP_FACTORY_ABI } from '../abis/index.js'; import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables, type PoolKeyRecord, -} from "./types.js"; +} from './types.js'; export interface StableSwapPoolConfig { - poolType: "stableswap"; + poolType: 'stableswap'; curvePool: Address; tokens: Address[]; fee: number | null; @@ -24,14 +24,15 @@ export interface StableSwapPoolConfig { const PROTOCOL_ID = 0xc1; -const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); +const orderPair = (a: Address, b: Address): [Address, Address] => + a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]; export const stableswapModule: CreationModule = { - poolType: "stableswap", + poolType: 'stableswap', protocolId: PROTOCOL_ID, factoryAbi: STABLESWAP_FACTORY_ABI, contractIdentifier: - "lib/v4-hooks-public/src/aggregator-hooks/implementations/StableSwap/StableSwapAggregator.sol:StableSwapAggregator", + 'lib/v4-hooks-public/src/aggregator-hooks/implementations/StableSwap/StableSwapAggregator.sol:StableSwapAggregator', getHookParams(config) { return { @@ -65,15 +66,22 @@ export const stableswapModule: CreationModule = { }, getImmutablesFromEnv(chainId: number): FactoryImmutables { - const metaRegistry = mustEnvForChain("STABLESWAP_METAREGISTRY", chainId) as Address; + const metaRegistry = mustEnvForChain( + 'STABLESWAP_METAREGISTRY', + chainId, + ) as Address; return { - poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, + poolManager: mustEnvForChain('POOL_MANAGER', chainId) as Address, metaRegistry, }; }, async readFactoryImmutables(provider, factoryAddress) { - const factory = new ethers.Contract(factoryAddress, STABLESWAP_FACTORY_ABI, provider); + const factory = new ethers.Contract( + factoryAddress, + STABLESWAP_FACTORY_ABI, + provider, + ); let poolManagerRaw: string; try { poolManagerRaw = await factory.poolManager(); @@ -83,30 +91,37 @@ export const stableswapModule: CreationModule = { const metaRegistryRaw = await factory.metaRegistry(); const poolManager = ethers.getAddress(poolManagerRaw); const metaRegistry = ethers.getAddress(metaRegistryRaw); - return { poolManager: poolManager as Address, metaRegistry: metaRegistry as Address }; + return { + poolManager: poolManager as Address, + metaRegistry: metaRegistry as Address, + }; }, encodeConstructorArgs(config, immutables) { - const metaRegistry = immutables.metaRegistry ?? (immutables as { metaRegistry?: Address }).metaRegistry; + const metaRegistry = + immutables.metaRegistry ?? + (immutables as { metaRegistry?: Address }).metaRegistry; if (!metaRegistry) { throw new Error( - "StableSwap requires metaRegistry. Set STABLESWAP_METAREGISTRY or use StableSwapAggregatorFactory.", + 'StableSwap requires metaRegistry. Set STABLESWAP_METAREGISTRY or use StableSwapAggregatorFactory.', ); } const encoded = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "address", "address"], + ['address', 'address', 'address'], [immutables.poolManager, config.curvePool, metaRegistry], ); - return encoded.startsWith("0x") ? encoded : `0x${encoded}`; + return encoded.startsWith('0x') ? encoded : `0x${encoded}`; }, buildSelfDeployEnvVars(config, immutables) { const params = this.getHookParams(config); - const metaRegistry = immutables.metaRegistry ?? (immutables as { metaRegistry?: Address }).metaRegistry; + const metaRegistry = + immutables.metaRegistry ?? + (immutables as { metaRegistry?: Address }).metaRegistry; return { CURVE_POOL: config.curvePool, - METAREGISTRY: metaRegistry ?? "", - TOKENS: config.tokens.join(","), + METAREGISTRY: metaRegistry ?? '', + TOKENS: config.tokens.join(','), FEE: params.fee.toString(), TICK_SPACING: params.tickSpacing.toString(), SQRT_PRICE_X96: params.sqrtPriceX96.toString(), @@ -115,6 +130,13 @@ export const stableswapModule: CreationModule = { buildCreatePoolArgs(config, salt) { const params = this.getHookParams(config); - return [salt, config.curvePool, config.tokens, params.fee, params.tickSpacing, BigInt(params.sqrtPriceX96)]; + return [ + salt, + config.curvePool, + config.tokens, + params.fee, + params.tickSpacing, + BigInt(params.sqrtPriceX96), + ]; }, }; diff --git a/aggregator-hooks/creation-modules/StableSwapNG.ts b/aggregator-hooks/creation-modules/StableSwapNG.ts index 15e1958c..8e7d2f6b 100644 --- a/aggregator-hooks/creation-modules/StableSwapNG.ts +++ b/aggregator-hooks/creation-modules/StableSwapNG.ts @@ -2,19 +2,19 @@ * StableSwap-NG (Curve) aggregator hook deployment module. * Requires Curve StableSwap NG factory address for meta pool rejection. */ -import { ethers } from "ethers"; -import { getEnvForChain, mustEnvForChain } from "../src/cli.js"; -import { STABLESWAPNG_FACTORY_ABI } from "../abis/index.js"; +import { ethers } from 'ethers'; +import { getEnvForChain, mustEnvForChain } from '../src/cli.js'; +import { STABLESWAPNG_FACTORY_ABI } from '../abis/index.js'; import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables, type PoolKeyRecord, -} from "./types.js"; +} from './types.js'; export interface StableSwapNGPoolConfig { - poolType: "stableswapng"; + poolType: 'stableswapng'; curvePool: Address; tokens: Address[]; fee: number | null; @@ -24,16 +24,18 @@ export interface StableSwapNGPoolConfig { const PROTOCOL_ID = 0xc2; -const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); +const orderPair = (a: Address, b: Address): [Address, Address] => + a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]; -const DEFAULT_STABLESWAPNG_FACTORY = "0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf"; +const DEFAULT_STABLESWAPNG_FACTORY = + '0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf'; export const stableswapngModule: CreationModule = { - poolType: "stableswapng", + poolType: 'stableswapng', protocolId: PROTOCOL_ID, factoryAbi: STABLESWAPNG_FACTORY_ABI, contractIdentifier: - "lib/v4-hooks-public/src/aggregator-hooks/implementations/StableSwapNG/StableSwapNGAggregator.sol:StableSwapNGAggregator", + 'lib/v4-hooks-public/src/aggregator-hooks/implementations/StableSwapNG/StableSwapNGAggregator.sol:StableSwapNGAggregator', getHookParams(config) { return { @@ -67,43 +69,56 @@ export const stableswapngModule: CreationModule = { }, getImmutablesFromEnv(chainId: number): FactoryImmutables { - const curveFactory = getEnvForChain("STABLESWAPNG_FACTORY", chainId) ?? DEFAULT_STABLESWAPNG_FACTORY; + const curveFactory = + getEnvForChain('STABLESWAPNG_FACTORY', chainId) ?? + DEFAULT_STABLESWAPNG_FACTORY; return { - poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, + poolManager: mustEnvForChain('POOL_MANAGER', chainId) as Address, curveFactory: curveFactory as Address, }; }, async readFactoryImmutables(provider, factoryAddress) { - const factory = new ethers.Contract(factoryAddress, STABLESWAPNG_FACTORY_ABI, provider); + const factory = new ethers.Contract( + factoryAddress, + STABLESWAPNG_FACTORY_ABI, + provider, + ); const [poolManager, curveFactory] = await Promise.all([ factory.poolManager().then((a: string) => ethers.getAddress(a)), factory.curveFactory().then((a: string) => ethers.getAddress(a)), ]); - return { poolManager: poolManager as Address, curveFactory: curveFactory as Address }; + return { + poolManager: poolManager as Address, + curveFactory: curveFactory as Address, + }; }, encodeConstructorArgs(config, immutables) { - const curveFactory = immutables.curveFactory ?? (immutables as { curveFactory?: Address }).curveFactory; + const curveFactory = + immutables.curveFactory ?? + (immutables as { curveFactory?: Address }).curveFactory; if (!curveFactory) { throw new Error( - "StableSwapNG requires curveFactory. Set STABLESWAPNG_FACTORY or use StableSwapNGAggregatorFactory.", + 'StableSwapNG requires curveFactory. Set STABLESWAPNG_FACTORY or use StableSwapNGAggregatorFactory.', ); } const encoded = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "address", "address"], + ['address', 'address', 'address'], [immutables.poolManager, config.curvePool, curveFactory], ); - return encoded.startsWith("0x") ? encoded : `0x${encoded}`; + return encoded.startsWith('0x') ? encoded : `0x${encoded}`; }, buildSelfDeployEnvVars(config, immutables) { const params = this.getHookParams(config); - const curveFactory = immutables.curveFactory ?? (immutables as { curveFactory?: Address }).curveFactory; + const curveFactory = + immutables.curveFactory ?? + (immutables as { curveFactory?: Address }).curveFactory; return { CURVE_POOL: config.curvePool, - CURVE_FACTORY: curveFactory ?? "", - TOKENS: config.tokens.join(","), + CURVE_FACTORY: curveFactory ?? '', + TOKENS: config.tokens.join(','), FEE: params.fee.toString(), TICK_SPACING: params.tickSpacing.toString(), SQRT_PRICE_X96: params.sqrtPriceX96.toString(), @@ -112,6 +127,13 @@ export const stableswapngModule: CreationModule = { buildCreatePoolArgs(config, salt) { const params = this.getHookParams(config); - return [salt, config.curvePool, config.tokens, params.fee, params.tickSpacing, params.sqrtPriceX96]; + return [ + salt, + config.curvePool, + config.tokens, + params.fee, + params.tickSpacing, + params.sqrtPriceX96, + ]; }, }; diff --git a/aggregator-hooks/creation-modules/UniswapV2.ts b/aggregator-hooks/creation-modules/UniswapV2.ts index da180eca..aee24ab4 100644 --- a/aggregator-hooks/creation-modules/UniswapV2.ts +++ b/aggregator-hooks/creation-modules/UniswapV2.ts @@ -6,12 +6,18 @@ * by currency pair via the V2 factory). Fee and tickSpacing on the V4 * PoolKey do not affect routing — the pair is keyed by currency pair only. */ -import { ethers } from "ethers"; -import { mustEnvForChain } from "../src/cli.js"; -import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables, type PoolKeyRecord } from "./types.js"; +import { ethers } from 'ethers'; +import { mustEnvForChain } from '../src/cli.js'; +import { + DEFAULT_SQRT_PRICE_X96, + type Address, + type CreationModule, + type FactoryImmutables, + type PoolKeyRecord, +} from './types.js'; export interface UniswapV2PoolConfig { - poolType: "uniswapv2"; + poolType: 'uniswapv2'; /** The existing Uniswap V2 pair address being wrapped */ v2Pair: Address; currency0: Address; @@ -24,20 +30,21 @@ export interface UniswapV2PoolConfig { const PROTOCOL_ID = 0x02; const AGGREGATOR_ABI = [ - "function poolManager() view returns (address)", - "function factory() view returns (address)", + 'function poolManager() view returns (address)', + 'function factory() view returns (address)', ]; -const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); +const orderPair = (a: Address, b: Address): [Address, Address] => + a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]; export const uniswapv2Module: CreationModule = { - poolType: "uniswapv2", + poolType: 'uniswapv2', protocolId: PROTOCOL_ID, factoryAbi: [], contractIdentifier: - "lib/v4-hooks-public/src/aggregator-hooks/implementations/UniswapV2/UniswapV2Aggregator.sol:UniswapV2Aggregator", + 'lib/v4-hooks-public/src/aggregator-hooks/implementations/UniswapV2/UniswapV2Aggregator.sol:UniswapV2Aggregator', isSingleton: true, - aggregatorEnvKey: "UNISWAP_V2_AGGREGATOR", + aggregatorEnvKey: 'UNISWAP_V2_AGGREGATOR', getHookParams(config) { return { @@ -50,7 +57,15 @@ export const uniswapv2Module: CreationModule = { buildPoolKeys(config, hookAddress) { const params = this.getHookParams(config); const [c0, c1] = orderPair(config.currency0, config.currency1); - return [{ currency0: c0, currency1: c1, fee: params.fee, tickSpacing: params.tickSpacing, hooks: hookAddress }]; + return [ + { + currency0: c0, + currency1: c1, + fee: params.fee, + tickSpacing: params.tickSpacing, + hooks: hookAddress, + }, + ]; }, getExternalPool(config) { @@ -59,34 +74,53 @@ export const uniswapv2Module: CreationModule = { getImmutablesFromEnv(chainId) { return { - poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, - externalFactory: mustEnvForChain("UNISWAP_V2_FACTORY", chainId) as Address, + poolManager: mustEnvForChain('POOL_MANAGER', chainId) as Address, + externalFactory: mustEnvForChain( + 'UNISWAP_V2_FACTORY', + chainId, + ) as Address, }; }, async readFactoryImmutables(provider, aggregatorAddress) { - const aggregator = new ethers.Contract(aggregatorAddress, AGGREGATOR_ABI, provider); - const [poolManager, externalFactory] = await Promise.all([aggregator.poolManager(), aggregator.factory()]); - return { poolManager: poolManager as Address, externalFactory: externalFactory as Address }; + const aggregator = new ethers.Contract( + aggregatorAddress, + AGGREGATOR_ABI, + provider, + ); + const [poolManager, externalFactory] = await Promise.all([ + aggregator.poolManager(), + aggregator.factory(), + ]); + return { + poolManager: poolManager as Address, + externalFactory: externalFactory as Address, + }; }, encodeConstructorArgs(_config, immutables) { const encoded = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "address", "string"], - [immutables.poolManager, immutables.externalFactory, "UniswapV2Aggregator v1.0"], + ['address', 'address', 'string'], + [ + immutables.poolManager, + immutables.externalFactory, + 'UniswapV2Aggregator v1.0', + ], ); - return encoded.startsWith("0x") ? encoded : `0x${encoded}`; + return encoded.startsWith('0x') ? encoded : `0x${encoded}`; }, buildSelfDeployEnvVars(_config, immutables) { return { EXTERNAL_FACTORY: immutables.externalFactory!, - HOOK_VERSION: "UniswapV2Aggregator v1.0", + HOOK_VERSION: 'UniswapV2Aggregator v1.0', }; }, buildCreatePoolArgs(_config, _salt) { - throw new Error("UniswapV2 uses singleton self-deploy mode; factory mode (createPool) is not supported."); + throw new Error( + 'UniswapV2 uses singleton self-deploy mode; factory mode (createPool) is not supported.', + ); }, buildInitializeArgs(config, hookAddress): [PoolKeyRecord, bigint] { diff --git a/aggregator-hooks/creation-modules/UniswapV3.ts b/aggregator-hooks/creation-modules/UniswapV3.ts index 9728b246..13295277 100644 --- a/aggregator-hooks/creation-modules/UniswapV3.ts +++ b/aggregator-hooks/creation-modules/UniswapV3.ts @@ -5,12 +5,18 @@ * registers a new V4 pool backed by an existing Uniswap V3 pool (resolved * by fee tier via the V3 factory). */ -import { ethers } from "ethers"; -import { mustEnvForChain } from "../src/cli.js"; -import { DEFAULT_SQRT_PRICE_X96, type Address, type CreationModule, type FactoryImmutables, type PoolKeyRecord } from "./types.js"; +import { ethers } from 'ethers'; +import { mustEnvForChain } from '../src/cli.js'; +import { + DEFAULT_SQRT_PRICE_X96, + type Address, + type CreationModule, + type FactoryImmutables, + type PoolKeyRecord, +} from './types.js'; export interface UniswapV3PoolConfig { - poolType: "uniswapv3"; + poolType: 'uniswapv3'; /** The existing Uniswap V3 pool address being wrapped */ v3Pool: Address; currency0: Address; @@ -24,20 +30,21 @@ export interface UniswapV3PoolConfig { const PROTOCOL_ID = 0x03; const AGGREGATOR_ABI = [ - "function poolManager() view returns (address)", - "function factory() view returns (address)", + 'function poolManager() view returns (address)', + 'function factory() view returns (address)', ]; -const orderPair = (a: Address, b: Address): [Address, Address] => (a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]); +const orderPair = (a: Address, b: Address): [Address, Address] => + a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]; export const uniswapv3Module: CreationModule = { - poolType: "uniswapv3", + poolType: 'uniswapv3', protocolId: PROTOCOL_ID, factoryAbi: [], contractIdentifier: - "lib/v4-hooks-public/src/aggregator-hooks/implementations/UniswapV3/UniswapV3Aggregator.sol:UniswapV3Aggregator", + 'lib/v4-hooks-public/src/aggregator-hooks/implementations/UniswapV3/UniswapV3Aggregator.sol:UniswapV3Aggregator', isSingleton: true, - aggregatorEnvKey: "UNISWAP_V3_AGGREGATOR", + aggregatorEnvKey: 'UNISWAP_V3_AGGREGATOR', getHookParams(config) { return { @@ -50,7 +57,15 @@ export const uniswapv3Module: CreationModule = { buildPoolKeys(config, hookAddress) { const params = this.getHookParams(config); const [c0, c1] = orderPair(config.currency0, config.currency1); - return [{ currency0: c0, currency1: c1, fee: params.fee, tickSpacing: params.tickSpacing, hooks: hookAddress }]; + return [ + { + currency0: c0, + currency1: c1, + fee: params.fee, + tickSpacing: params.tickSpacing, + hooks: hookAddress, + }, + ]; }, getExternalPool(config) { @@ -59,34 +74,53 @@ export const uniswapv3Module: CreationModule = { getImmutablesFromEnv(chainId) { return { - poolManager: mustEnvForChain("POOL_MANAGER", chainId) as Address, - externalFactory: mustEnvForChain("UNISWAP_V3_FACTORY", chainId) as Address, + poolManager: mustEnvForChain('POOL_MANAGER', chainId) as Address, + externalFactory: mustEnvForChain( + 'UNISWAP_V3_FACTORY', + chainId, + ) as Address, }; }, async readFactoryImmutables(provider, aggregatorAddress) { - const aggregator = new ethers.Contract(aggregatorAddress, AGGREGATOR_ABI, provider); - const [poolManager, externalFactory] = await Promise.all([aggregator.poolManager(), aggregator.factory()]); - return { poolManager: poolManager as Address, externalFactory: externalFactory as Address }; + const aggregator = new ethers.Contract( + aggregatorAddress, + AGGREGATOR_ABI, + provider, + ); + const [poolManager, externalFactory] = await Promise.all([ + aggregator.poolManager(), + aggregator.factory(), + ]); + return { + poolManager: poolManager as Address, + externalFactory: externalFactory as Address, + }; }, encodeConstructorArgs(_config, immutables) { const encoded = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "address", "string"], - [immutables.poolManager, immutables.externalFactory, "UniswapV3Aggregator v1.0"], + ['address', 'address', 'string'], + [ + immutables.poolManager, + immutables.externalFactory, + 'UniswapV3Aggregator v1.0', + ], ); - return encoded.startsWith("0x") ? encoded : `0x${encoded}`; + return encoded.startsWith('0x') ? encoded : `0x${encoded}`; }, buildSelfDeployEnvVars(_config, immutables) { return { EXTERNAL_FACTORY: immutables.externalFactory!, - HOOK_VERSION: "UniswapV3Aggregator v1.0", + HOOK_VERSION: 'UniswapV3Aggregator v1.0', }; }, buildCreatePoolArgs(_config, _salt) { - throw new Error("UniswapV3 uses singleton self-deploy mode; factory mode (createPool) is not supported."); + throw new Error( + 'UniswapV3 uses singleton self-deploy mode; factory mode (createPool) is not supported.', + ); }, buildInitializeArgs(config, hookAddress): [PoolKeyRecord, bigint] { diff --git a/aggregator-hooks/creation-modules/index.ts b/aggregator-hooks/creation-modules/index.ts index 94c1c8f3..0071f0a3 100644 --- a/aggregator-hooks/creation-modules/index.ts +++ b/aggregator-hooks/creation-modules/index.ts @@ -1,15 +1,15 @@ /** * Creation modules registry. Import this to get all supported pool types. */ -import { stableswapModule } from "./StableSwap.js"; -import { stableswapngModule } from "./StableSwapNG.js"; -import { fluiddext1Module } from "./FluidDexT1.js"; -import { fluiddexliteModule } from "./FluidDexLite.js"; -import { uniswapv3Module } from "./UniswapV3.js"; -import { uniswapv2Module } from "./UniswapV2.js"; -import { slipstreamModule } from "./Slipstream.js"; -import { pancakeswapv3Module } from "./PancakeSwapV3.js"; -import type { CreationModule } from "./types.js"; +import { stableswapModule } from './StableSwap.js'; +import { stableswapngModule } from './StableSwapNG.js'; +import { fluiddext1Module } from './FluidDexT1.js'; +import { fluiddexliteModule } from './FluidDexLite.js'; +import { uniswapv3Module } from './UniswapV3.js'; +import { uniswapv2Module } from './UniswapV2.js'; +import { slipstreamModule } from './Slipstream.js'; +import { pancakeswapv3Module } from './PancakeSwapV3.js'; +import type { CreationModule } from './types.js'; export type { Address, @@ -19,28 +19,38 @@ export type { PoolDeployedEntry, FactoryImmutables, HookParams, -} from "./types.js"; -export type { StableSwapPoolConfig } from "./StableSwap.js"; -export type { StableSwapNGPoolConfig } from "./StableSwapNG.js"; -export type { FluidDexT1PoolConfig } from "./FluidDexT1.js"; -export type { FluidDexLitePoolConfig } from "./FluidDexLite.js"; -export type { UniswapV3PoolConfig } from "./UniswapV3.js"; -export type { UniswapV2PoolConfig } from "./UniswapV2.js"; -export type { SlipstreamPoolConfig } from "./Slipstream.js"; -export type { PancakeSwapV3PoolConfig } from "./PancakeSwapV3.js"; +} from './types.js'; +export type { StableSwapPoolConfig } from './StableSwap.js'; +export type { StableSwapNGPoolConfig } from './StableSwapNG.js'; +export type { FluidDexT1PoolConfig } from './FluidDexT1.js'; +export type { FluidDexLitePoolConfig } from './FluidDexLite.js'; +export type { UniswapV3PoolConfig } from './UniswapV3.js'; +export type { UniswapV2PoolConfig } from './UniswapV2.js'; +export type { SlipstreamPoolConfig } from './Slipstream.js'; +export type { PancakeSwapV3PoolConfig } from './PancakeSwapV3.js'; -export { stableswapModule, stableswapngModule, fluiddext1Module, fluiddexliteModule }; -export { uniswapv3Module, uniswapv2Module, slipstreamModule, pancakeswapv3Module }; +export { + stableswapModule, + stableswapngModule, + fluiddext1Module, + fluiddexliteModule, +}; +export { + uniswapv3Module, + uniswapv2Module, + slipstreamModule, + pancakeswapv3Module, +}; export type PoolConfig = - | import("./StableSwap.js").StableSwapPoolConfig - | import("./StableSwapNG.js").StableSwapNGPoolConfig - | import("./FluidDexT1.js").FluidDexT1PoolConfig - | import("./FluidDexLite.js").FluidDexLitePoolConfig - | import("./UniswapV3.js").UniswapV3PoolConfig - | import("./UniswapV2.js").UniswapV2PoolConfig - | import("./Slipstream.js").SlipstreamPoolConfig - | import("./PancakeSwapV3.js").PancakeSwapV3PoolConfig; + | import('./StableSwap.js').StableSwapPoolConfig + | import('./StableSwapNG.js').StableSwapNGPoolConfig + | import('./FluidDexT1.js').FluidDexT1PoolConfig + | import('./FluidDexLite.js').FluidDexLitePoolConfig + | import('./UniswapV3.js').UniswapV3PoolConfig + | import('./UniswapV2.js').UniswapV2PoolConfig + | import('./Slipstream.js').SlipstreamPoolConfig + | import('./PancakeSwapV3.js').PancakeSwapV3PoolConfig; /** Registry of all creation modules by pool type */ export const CREATION_MODULES: Record = { diff --git a/aggregator-hooks/creation-modules/types.ts b/aggregator-hooks/creation-modules/types.ts index db8c3370..65418cc5 100644 --- a/aggregator-hooks/creation-modules/types.ts +++ b/aggregator-hooks/creation-modules/types.ts @@ -2,7 +2,7 @@ * Shared types and CreationModule interface for pool deployment. * Each protocol (stableswap, stableswapng, fluiddext1, fluiddexlite) implements this interface. */ -import type { Provider } from "ethers"; +import type { Provider } from 'ethers'; /** Ethereum address: 0x-prefixed hex string */ export type Address = `0x${string}`; @@ -103,13 +103,19 @@ export interface CreationModule { getImmutablesFromEnv(chainId: number): FactoryImmutables; /** Read immutables from factory contract (or deployed singleton aggregator) */ - readFactoryImmutables(provider: Provider, factoryAddress: Address): Promise; + readFactoryImmutables( + provider: Provider, + factoryAddress: Address, + ): Promise; /** Encode constructor args for salt mining */ encodeConstructorArgs(config: TConfig, immutables: FactoryImmutables): string; /** Build env vars for SelfCreateHook.s.sol self-deploy */ - buildSelfDeployEnvVars(config: TConfig, immutables: FactoryImmutables): Record; + buildSelfDeployEnvVars( + config: TConfig, + immutables: FactoryImmutables, + ): Record; /** Build createPool call args for factory contract. Throws for singleton types. */ buildCreatePoolArgs(config: TConfig, salt: string): unknown[]; @@ -118,5 +124,8 @@ export interface CreationModule { * Build args for poolManager.initialize — singleton types only. * Returns [poolKey, sqrtPriceX96] to pass to the PoolManager initialize function. */ - buildInitializeArgs?(config: TConfig, hookAddress: Address): [PoolKeyRecord, bigint]; + buildInitializeArgs?( + config: TConfig, + hookAddress: Address, + ): [PoolKeyRecord, bigint]; } diff --git a/aggregator-hooks/historical/FluidDexLite.ts b/aggregator-hooks/historical/FluidDexLite.ts index 4f8283a8..c3eefba3 100644 --- a/aggregator-hooks/historical/FluidDexLite.ts +++ b/aggregator-hooks/historical/FluidDexLite.ts @@ -20,28 +20,30 @@ * Fees are fetched via getDexState() and converted from Fluid 1e4 to Uniswap v4 1e6 format. */ -import "dotenv/config"; -import fs from "node:fs"; -import path from "node:path"; -import { ethers } from "ethers"; -import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; -import { FLUIDDEXLITE_RESOLVER_ABI } from "../abis/index.js"; -import type { Address } from "../creation-modules/types.js"; +import 'dotenv/config'; +import fs from 'node:fs'; +import path from 'node:path'; +import { ethers } from 'ethers'; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from '@src/cli'; +import { FLUIDDEXLITE_RESOLVER_ABI } from '../abis/index.js'; +import type { Address } from '../creation-modules/types.js'; -const OUTPUT_FILE = "fluiddexlite-pools.json"; -const DEFAULT_RESOLVER: Address = "0x26b696D0dfDAB6c894Aa9a6575fCD07BB25BbD2C"; +const OUTPUT_FILE = 'fluiddexlite-pools.json'; +const DEFAULT_RESOLVER: Address = '0x26b696D0dfDAB6c894Aa9a6575fCD07BB25BbD2C'; /** Fluid native token; map to address(0) for Uniswap v4 pool init */ -const FLUID_NATIVE: Address = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; -const ZERO_ADDRESS: Address = "0x0000000000000000000000000000000000000000"; +const FLUID_NATIVE: Address = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; +const ZERO_ADDRESS: Address = '0x0000000000000000000000000000000000000000'; function toUniswapV4Currency(addr: string): Address { - return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() ? ZERO_ADDRESS : (ethers.getAddress(addr) as Address); + return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() + ? ZERO_ADDRESS + : (ethers.getAddress(addr) as Address); } /** Same shape as createPools.ts FluidDexLitePoolConfig */ type CreatePoolsFluidLiteConfig = { - poolType: "fluiddexlite"; + poolType: 'fluiddexlite'; dexSalt: string; currency0: Address; currency1: Address; @@ -71,29 +73,34 @@ function ensureDirForFile(filePath: string): void { async function main() { const args = parseArgs(process.argv.slice(2)); - const chainIdRaw = args["chain-id"]; + const chainIdRaw = args['chain-id']; if (chainIdRaw == null) { - console.error("Missing required --chain-id "); + console.error('Missing required --chain-id '); process.exit(1); } const chainId = toInt(chainIdRaw, 0); if (chainId <= 0) { - console.error("--chain-id must be a positive integer"); + console.error('--chain-id must be a positive integer'); process.exit(1); } - const rpcUrl = getEnvForChain("RPC_URL", chainId); - const resolverRaw = getEnvForChain("FLUID_DEX_LITE_RESOLVER", chainId) ?? DEFAULT_RESOLVER; + const rpcUrl = getEnvForChain('RPC_URL', chainId); + const resolverRaw = + getEnvForChain('FLUID_DEX_LITE_RESOLVER', chainId) ?? DEFAULT_RESOLVER; if (!rpcUrl) { - console.error("Missing env: RPC_URL (or RPC_URL_)"); + console.error('Missing env: RPC_URL (or RPC_URL_)'); process.exit(1); } const resolver = ethers.getAddress(resolverRaw) as Address; - const outputDir = (args["output-dir"] as string) ?? "detected"; + const outputDir = (args['output-dir'] as string) ?? 'detected'; const provider = new ethers.JsonRpcProvider(rpcUrl); - const resolverContract = new ethers.Contract(resolver, FLUIDDEXLITE_RESOLVER_ABI as unknown as string[], provider); + const resolverContract = new ethers.Contract( + resolver, + FLUIDDEXLITE_RESOLVER_ABI as unknown as string[], + provider, + ); console.error(`[enum] FluidDexLiteResolver at ${resolver}`); const dexKeys = (await resolverContract.getAllDexes()) as Array<{ @@ -104,9 +111,10 @@ async function main() { const configs: CreatePoolsFluidLiteConfig[] = []; for (const dk of dexKeys) { - const mapped = [toUniswapV4Currency(dk.token0), toUniswapV4Currency(dk.token1)].sort((a, b) => - a.toLowerCase().localeCompare(b.toLowerCase()), - ); + const mapped = [ + toUniswapV4Currency(dk.token0), + toUniswapV4Currency(dk.token1), + ].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); const [currency0, currency1] = [mapped[0], mapped[1]]; let fee: number | null = null; @@ -121,11 +129,13 @@ async function main() { } } catch { // getDexState reverted; keep fee null - console.error(`getDexState reverted for dexKey: ${JSON.stringify(dexKey)}`); + console.error( + `getDexState reverted for dexKey: ${JSON.stringify(dexKey)}`, + ); } configs.push({ - poolType: "fluiddexlite", + poolType: 'fluiddexlite', dexSalt: dk.salt as string, currency0, currency1, @@ -137,7 +147,7 @@ async function main() { const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); ensureDirForFile(outPath); - fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + "\n"); + fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + '\n'); console.log( JSON.stringify( diff --git a/aggregator-hooks/historical/FluidDexT1.ts b/aggregator-hooks/historical/FluidDexT1.ts index fb277c81..a4a84ba2 100644 --- a/aggregator-hooks/historical/FluidDexT1.ts +++ b/aggregator-hooks/historical/FluidDexT1.ts @@ -23,20 +23,23 @@ * Output: JSON array in createPools.ts FluidDexT1PoolConfig format. */ -import "dotenv/config"; -import fs from "node:fs"; -import path from "node:path"; -import { JsonRpcProvider, Contract, Interface, getAddress } from "ethers"; -import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; -import { FLUIDDEXT1_HISTORICAL_FACTORY_ABI, FLUIDDEXT1_RESOLVER_ABI } from "../abis/index.js"; -import type { Address } from "../creation-modules/types.js"; - -const OUTPUT_FILE = "fluiddext1-pools.json"; -const DEFAULT_FACTORY: Address = "0x91716c4eDA1fB55e84Bf8b4c7085f84285c19085"; +import 'dotenv/config'; +import fs from 'node:fs'; +import path from 'node:path'; +import { JsonRpcProvider, Contract, Interface, getAddress } from 'ethers'; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from '@src/cli'; +import { + FLUIDDEXT1_HISTORICAL_FACTORY_ABI, + FLUIDDEXT1_RESOLVER_ABI, +} from '../abis/index.js'; +import type { Address } from '../creation-modules/types.js'; + +const OUTPUT_FILE = 'fluiddext1-pools.json'; +const DEFAULT_FACTORY: Address = '0x91716c4eDA1fB55e84Bf8b4c7085f84285c19085'; /** Same shape as createPools.ts FluidDexT1PoolConfig */ type CreatePoolsFluidDexT1Config = { - poolType: "fluiddext1"; + poolType: 'fluiddext1'; fluidPool: Address; currency0: Address; currency1: Address; @@ -46,11 +49,13 @@ type CreatePoolsFluidDexT1Config = { }; /** Fluid native token; map to address(0) for Uniswap v4 pool init */ -const FLUID_NATIVE: Address = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; -const ZERO_ADDRESS: Address = "0x0000000000000000000000000000000000000000"; +const FLUID_NATIVE: Address = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; +const ZERO_ADDRESS: Address = '0x0000000000000000000000000000000000000000'; function toUniswapV4Currency(addr: string): Address { - return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() ? ZERO_ADDRESS : (getAddress(addr) as Address); + return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() + ? ZERO_ADDRESS + : (getAddress(addr) as Address); } function pRateLimit(rps: number): () => Promise { @@ -90,66 +95,84 @@ function pLimit(concurrency: number) { } function orderCurrencies(token0: Address, token1: Address): [Address, Address] { - const mapped = [toUniswapV4Currency(token0), toUniswapV4Currency(token1)].sort((a, b) => - a.toLowerCase().localeCompare(b.toLowerCase()), - ); + const mapped = [ + toUniswapV4Currency(token0), + toUniswapV4Currency(token1), + ].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); return [mapped[0]!, mapped[1]!]; } async function main() { const args = parseArgs(process.argv.slice(2)); - const chainIdRaw = args["chain-id"]; + const chainIdRaw = args['chain-id']; if (chainIdRaw == null) { - console.error("Missing required --chain-id "); + console.error('Missing required --chain-id '); process.exit(1); } const chainId = toInt(chainIdRaw, 0); if (chainId <= 0) { - console.error("--chain-id must be a positive integer"); + console.error('--chain-id must be a positive integer'); process.exit(1); } - const rpcUrl = getEnvForChain("RPC_URL", chainId); - const resolverAddr = getEnvForChain("FLUID_DEX_T1_RESOLVER", chainId); + const rpcUrl = getEnvForChain('RPC_URL', chainId); + const resolverAddr = getEnvForChain('FLUID_DEX_T1_RESOLVER', chainId); const factoryAddrRaw = - getEnvForChain("FLUID_DEX_T1_FACTORY", chainId) ?? DEFAULT_FACTORY; + getEnvForChain('FLUID_DEX_T1_FACTORY', chainId) ?? DEFAULT_FACTORY; if (!rpcUrl || !resolverAddr) { - console.error("Missing env: RPC_URL and FLUID_DEX_T1_RESOLVER"); + console.error('Missing env: RPC_URL and FLUID_DEX_T1_RESOLVER'); process.exit(1); } const factoryAddr = getAddress(factoryAddrRaw.toLowerCase()) as Address; - const outputDir = (args["output-dir"] as string) ?? "detected"; - const chunkSize = BigInt(Math.max(1, toInt(args["chunk-blocks"], 100_000))); - const startBlock = BigInt(Math.max(0, toInt(args["start-block"], 0))); - const endBlockRaw = args["end-block"]; - const mode = ((args["mode"] as string | undefined) ?? "enumerate").toLowerCase(); - const rps = toInt(args["rps"] ?? getEnvForChain("RPS", chainId), 80); - const concurrency = Math.max(1, toInt(getEnvForChain("CONCURRENCY", chainId), 8)); + const outputDir = (args['output-dir'] as string) ?? 'detected'; + const chunkSize = BigInt(Math.max(1, toInt(args['chunk-blocks'], 100_000))); + const startBlock = BigInt(Math.max(0, toInt(args['start-block'], 0))); + const endBlockRaw = args['end-block']; + const mode = ( + (args['mode'] as string | undefined) ?? 'enumerate' + ).toLowerCase(); + const rps = toInt(args['rps'] ?? getEnvForChain('RPS', chainId), 80); + const concurrency = Math.max( + 1, + toInt(getEnvForChain('CONCURRENCY', chainId), 8), + ); const rateLimitAcquire = pRateLimit(rps); const limit = pLimit(concurrency); const provider = new JsonRpcProvider(rpcUrl); - const factory = new Contract(factoryAddr, FLUIDDEXT1_HISTORICAL_FACTORY_ABI, provider); - const resolver = new Contract(getAddress(resolverAddr.toLowerCase()), FLUIDDEXT1_RESOLVER_ABI, provider); + const factory = new Contract( + factoryAddr, + FLUIDDEXT1_HISTORICAL_FACTORY_ABI, + provider, + ); + const resolver = new Contract( + getAddress(resolverAddr.toLowerCase()), + FLUIDDEXT1_RESOLVER_ABI, + provider, + ); const latest = BigInt(await provider.getBlockNumber()); const endBlock = endBlockRaw ? BigInt(String(endBlockRaw)) : latest; - if (startBlock < 0n) throw new Error("start-block must be >= 0"); - if (endBlock < startBlock) throw new Error("end-block must be >= start-block"); + if (startBlock < 0n) throw new Error('start-block must be >= 0'); + if (endBlock < startBlock) + throw new Error('end-block must be >= start-block'); const byDexAddr = new Map(); - if (mode === "logs" || mode === "both") { - const iface = new Interface(FLUIDDEXT1_HISTORICAL_FACTORY_ABI as unknown as string[]); - const topic0 = iface.getEvent("LogDexDeployed")!.topicHash; + if (mode === 'logs' || mode === 'both') { + const iface = new Interface( + FLUIDDEXT1_HISTORICAL_FACTORY_ABI as unknown as string[], + ); + const topic0 = iface.getEvent('LogDexDeployed')!.topicHash; for (let from = startBlock; from <= endBlock; from += chunkSize) { - const to = from + chunkSize - 1n > endBlock ? endBlock : from + chunkSize - 1n; + const to = + from + chunkSize - 1n > endBlock ? endBlock : from + chunkSize - 1n; const logs = await provider.getLogs({ address: factoryAddr, @@ -171,7 +194,7 @@ async function main() { } } - if (mode === "enumerate" || mode === "both") { + if (mode === 'enumerate' || mode === 'both') { const total = await factory.totalDexes(); console.error(`[enumerate] totalDexes() = ${total}`); @@ -189,7 +212,9 @@ async function main() { } const uniqueDexes = Array.from(byDexAddr.keys()); - console.log(`Found ${uniqueDexes.length} unique Fluid Dex T1 pools. Fetching tokens via resolver...`); + console.log( + `Found ${uniqueDexes.length} unique Fluid Dex T1 pools. Fetching tokens via resolver...`, + ); const configs: CreatePoolsFluidDexT1Config[] = []; let skipped = 0; @@ -200,14 +225,17 @@ async function main() { let token0: Address; let token1: Address; try { - [token0, token1] = (await resolver.getDexTokens(fluidPool)) as [Address, Address]; + [token0, token1] = (await resolver.getDexTokens(fluidPool)) as [ + Address, + Address, + ]; } catch { return null; } const [currency0, currency1] = orderCurrencies(token0, token1); return { - poolType: "fluiddext1" as const, + poolType: 'fluiddext1' as const, fluidPool, currency0, currency1, @@ -222,13 +250,17 @@ async function main() { } if (skipped > 0) { - console.error(`Skipped ${skipped} pools (getDexTokens reverted - may be VaultT1 or deprecated)`); + console.error( + `Skipped ${skipped} pools (getDexTokens reverted - may be VaultT1 or deprecated)`, + ); } const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); fs.mkdirSync(path.dirname(outPath), { recursive: true }); fs.writeFileSync(outPath, JSON.stringify(configs, null, 2)); - console.log(`Wrote ${outPath} (${configs.length} pools in createPools.ts format)`); + console.log( + `Wrote ${outPath} (${configs.length} pools in createPools.ts format)`, + ); } main().catch((e) => { diff --git a/aggregator-hooks/historical/PancakeSwapV3.ts b/aggregator-hooks/historical/PancakeSwapV3.ts index d711318b..9275e553 100644 --- a/aggregator-hooks/historical/PancakeSwapV3.ts +++ b/aggregator-hooks/historical/PancakeSwapV3.ts @@ -21,23 +21,23 @@ * Output: JSON array in PancakeSwapV3PoolConfig format. */ -import "dotenv/config"; -import fs from "node:fs"; -import path from "node:path"; -import { ethers } from "ethers"; -import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; -import type { Address } from "../creation-modules/types.js"; +import 'dotenv/config'; +import fs from 'node:fs'; +import path from 'node:path'; +import { ethers } from 'ethers'; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from '@src/cli'; +import type { Address } from '../creation-modules/types.js'; -const OUTPUT_FILE = "pancakeswapv3-pools.json"; -const DEFAULT_FACTORY: Address = "0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865"; -const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +const OUTPUT_FILE = 'pancakeswapv3-pools.json'; +const DEFAULT_FACTORY: Address = '0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865'; +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; const FACTORY_ABI = [ - "event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)", + 'event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)', ]; type PancakeSwapV3PoolConfig = { - poolType: "pancakeswapv3"; + poolType: 'pancakeswapv3'; v3Pool: Address; currency0: Address; currency1: Address; @@ -52,19 +52,34 @@ function pRateLimit(rps: number): () => Promise { let nextAllowed = 0; return async function acquire(): Promise { const now = Date.now(); - if (now < nextAllowed) await new Promise((r) => setTimeout(r, nextAllowed - now)); + if (now < nextAllowed) + await new Promise((r) => setTimeout(r, nextAllowed - now)); nextAllowed = Math.max(now, nextAllowed) + minGapMs; }; } function isRangeLimitError(err: unknown): boolean { - const msg = String((err as { error?: { message?: string }; message?: string })?.error?.message ?? (err as { message?: string })?.message ?? ""); - return msg.toLowerCase().includes("range") || msg.toLowerCase().includes("limit") || msg.includes("-32614"); + const msg = String( + (err as { error?: { message?: string }; message?: string })?.error + ?.message ?? + (err as { message?: string })?.message ?? + '', + ); + return ( + msg.toLowerCase().includes('range') || + msg.toLowerCase().includes('limit') || + msg.includes('-32614') + ); } async function getLogsWithRetry( provider: ethers.JsonRpcProvider, - filter: { address: string; topics: string[]; fromBlock: bigint; toBlock: bigint }, + filter: { + address: string; + topics: string[]; + fromBlock: bigint; + toBlock: bigint; + }, ): Promise { try { return await provider.getLogs({ @@ -74,7 +89,8 @@ async function getLogsWithRetry( toBlock: filter.toBlock, }); } catch (err) { - if (!isRangeLimitError(err) || filter.toBlock <= filter.fromBlock) throw err; + if (!isRangeLimitError(err) || filter.toBlock <= filter.fromBlock) + throw err; const mid = filter.fromBlock + (filter.toBlock - filter.fromBlock) / 2n; const [lo, hi] = await Promise.all([ getLogsWithRetry(provider, { ...filter, toBlock: mid }), @@ -91,42 +107,60 @@ function isNative(addr: string): boolean { async function main() { const args = parseArgs(process.argv.slice(2)); - const chainIdRaw = args["chain-id"]; - if (chainIdRaw == null) { console.error("Missing required --chain-id "); process.exit(1); } + const chainIdRaw = args['chain-id']; + if (chainIdRaw == null) { + console.error('Missing required --chain-id '); + process.exit(1); + } const chainId = toInt(chainIdRaw, 0); - if (chainId <= 0) { console.error("--chain-id must be a positive integer"); process.exit(1); } + if (chainId <= 0) { + console.error('--chain-id must be a positive integer'); + process.exit(1); + } - const rpcUrl = getEnvForChain("RPC_URL", chainId); - if (!rpcUrl) { console.error("Missing env: RPC_URL"); process.exit(1); } + const rpcUrl = getEnvForChain('RPC_URL', chainId); + if (!rpcUrl) { + console.error('Missing env: RPC_URL'); + process.exit(1); + } - const factoryRaw = getEnvForChain("PANCAKESWAP_V3_FACTORY", chainId) ?? DEFAULT_FACTORY; + const factoryRaw = + getEnvForChain('PANCAKESWAP_V3_FACTORY', chainId) ?? DEFAULT_FACTORY; const factory = ethers.getAddress(factoryRaw) as Address; - const outputDir = (args["output-dir"] as string) ?? "detected"; - const chunkSize = BigInt(Math.max(1, toInt(args["chunk-blocks"], 10_000))); - const startBlock = BigInt(Math.max(0, toInt(args["start-block"], 0))); - const rps = toInt(getEnvForChain("RPS", chainId) ?? "80", 80); + const outputDir = (args['output-dir'] as string) ?? 'detected'; + const chunkSize = BigInt(Math.max(1, toInt(args['chunk-blocks'], 10_000))); + const startBlock = BigInt(Math.max(0, toInt(args['start-block'], 0))); + const rps = toInt(getEnvForChain('RPS', chainId) ?? '80', 80); const rateLimitAcquire = pRateLimit(rps); const provider = new ethers.JsonRpcProvider(rpcUrl); const iface = new ethers.Interface(FACTORY_ABI); - const topic0 = iface.getEvent("PoolCreated")!.topicHash; + const topic0 = iface.getEvent('PoolCreated')!.topicHash; const latest = BigInt(await provider.getBlockNumber()); - const endBlockRaw = args["end-block"]; + const endBlockRaw = args['end-block']; const endBlock = endBlockRaw ? BigInt(String(endBlockRaw)) : latest; console.error(`Factory: ${factory}`); - console.error(`Scanning blocks ${startBlock}..${endBlock} (chunk=${chunkSize})`); + console.error( + `Scanning blocks ${startBlock}..${endBlock} (chunk=${chunkSize})`, + ); const seen = new Set
(); const configs: PancakeSwapV3PoolConfig[] = []; for (let from = startBlock; from <= endBlock; from += chunkSize) { - const to = from + chunkSize - 1n > endBlock ? endBlock : from + chunkSize - 1n; + const to = + from + chunkSize - 1n > endBlock ? endBlock : from + chunkSize - 1n; await rateLimitAcquire(); - const logs = await getLogsWithRetry(provider, { address: factory, topics: [topic0], fromBlock: from, toBlock: to }); + const logs = await getLogsWithRetry(provider, { + address: factory, + topics: [topic0], + fromBlock: from, + toBlock: to, + }); for (const log of logs) { const parsed = iface.parseLog(log); @@ -143,7 +177,7 @@ async function main() { seen.add(pool); configs.push({ - poolType: "pancakeswapv3", + poolType: 'pancakeswapv3', v3Pool: pool, currency0: token0, currency1: token1, @@ -154,15 +188,32 @@ async function main() { } if (to === endBlock || (to - startBlock) % (chunkSize * 10n) === 0n) { - console.error(`[scan] ${from}..${to} — found ${configs.length} pools so far`); + console.error( + `[scan] ${from}..${to} — found ${configs.length} pools so far`, + ); } console.log(`[scan] ${from}..${to} — found ${configs.length} pools so far`); } const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); fs.mkdirSync(path.dirname(outPath), { recursive: true }); - fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + "\n"); - console.log(JSON.stringify({ ok: true, chainId, factory, poolsFound: configs.length, outFile: outPath }, null, 2)); + fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + '\n'); + console.log( + JSON.stringify( + { + ok: true, + chainId, + factory, + poolsFound: configs.length, + outFile: outPath, + }, + null, + 2, + ), + ); } -main().catch((e) => { console.error(e); process.exit(1); }); +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/aggregator-hooks/historical/Slipstream.ts b/aggregator-hooks/historical/Slipstream.ts index 14301384..38161b10 100644 --- a/aggregator-hooks/historical/Slipstream.ts +++ b/aggregator-hooks/historical/Slipstream.ts @@ -22,23 +22,23 @@ * Fee is always 0 (Slipstream pools key by tickSpacing, not fee). */ -import "dotenv/config"; -import fs from "node:fs"; -import path from "node:path"; -import { ethers } from "ethers"; -import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; -import type { Address } from "../creation-modules/types.js"; +import 'dotenv/config'; +import fs from 'node:fs'; +import path from 'node:path'; +import { ethers } from 'ethers'; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from '@src/cli'; +import type { Address } from '../creation-modules/types.js'; -const OUTPUT_FILE = "slipstream-pools.json"; -const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +const OUTPUT_FILE = 'slipstream-pools.json'; +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; // Slipstream CLFactory emits tickSpacing (not fee) as the third indexed param const FACTORY_ABI = [ - "event PoolCreated(address indexed token0, address indexed token1, int24 indexed tickSpacing, address pool)", + 'event PoolCreated(address indexed token0, address indexed token1, int24 indexed tickSpacing, address pool)', ]; type SlipstreamPoolConfig = { - poolType: "slipstream"; + poolType: 'slipstream'; slipstreamPool: Address; currency0: Address; currency1: Address; @@ -52,19 +52,34 @@ function pRateLimit(rps: number): () => Promise { let nextAllowed = 0; return async function acquire(): Promise { const now = Date.now(); - if (now < nextAllowed) await new Promise((r) => setTimeout(r, nextAllowed - now)); + if (now < nextAllowed) + await new Promise((r) => setTimeout(r, nextAllowed - now)); nextAllowed = Math.max(now, nextAllowed) + minGapMs; }; } function isRangeLimitError(err: unknown): boolean { - const msg = String((err as { error?: { message?: string }; message?: string })?.error?.message ?? (err as { message?: string })?.message ?? ""); - return msg.toLowerCase().includes("range") || msg.toLowerCase().includes("limit") || msg.includes("-32614"); + const msg = String( + (err as { error?: { message?: string }; message?: string })?.error + ?.message ?? + (err as { message?: string })?.message ?? + '', + ); + return ( + msg.toLowerCase().includes('range') || + msg.toLowerCase().includes('limit') || + msg.includes('-32614') + ); } async function getLogsWithRetry( provider: ethers.JsonRpcProvider, - filter: { address: string; topics: string[]; fromBlock: bigint; toBlock: bigint }, + filter: { + address: string; + topics: string[]; + fromBlock: bigint; + toBlock: bigint; + }, ): Promise { try { return await provider.getLogs({ @@ -74,7 +89,8 @@ async function getLogsWithRetry( toBlock: filter.toBlock, }); } catch (err) { - if (!isRangeLimitError(err) || filter.toBlock <= filter.fromBlock) throw err; + if (!isRangeLimitError(err) || filter.toBlock <= filter.fromBlock) + throw err; const mid = filter.fromBlock + (filter.toBlock - filter.fromBlock) / 2n; const [lo, hi] = await Promise.all([ getLogsWithRetry(provider, { ...filter, toBlock: mid }), @@ -91,46 +107,65 @@ function isNative(addr: string): boolean { async function main() { const args = parseArgs(process.argv.slice(2)); - const chainIdRaw = args["chain-id"]; - if (chainIdRaw == null) { console.error("Missing required --chain-id "); process.exit(1); } + const chainIdRaw = args['chain-id']; + if (chainIdRaw == null) { + console.error('Missing required --chain-id '); + process.exit(1); + } const chainId = toInt(chainIdRaw, 0); - if (chainId <= 0) { console.error("--chain-id must be a positive integer"); process.exit(1); } + if (chainId <= 0) { + console.error('--chain-id must be a positive integer'); + process.exit(1); + } - const rpcUrl = getEnvForChain("RPC_URL", chainId); - if (!rpcUrl) { console.error("Missing env: RPC_URL"); process.exit(1); } + const rpcUrl = getEnvForChain('RPC_URL', chainId); + if (!rpcUrl) { + console.error('Missing env: RPC_URL'); + process.exit(1); + } - const factoryRaw = getEnvForChain("SLIPSTREAM_FACTORY", chainId); + const factoryRaw = getEnvForChain('SLIPSTREAM_FACTORY', chainId); if (!factoryRaw) { - console.error("Missing env: SLIPSTREAM_FACTORY (or SLIPSTREAM_FACTORY_)"); + console.error( + 'Missing env: SLIPSTREAM_FACTORY (or SLIPSTREAM_FACTORY_)', + ); process.exit(1); } const factory = ethers.getAddress(factoryRaw) as Address; - const outputDir = (args["output-dir"] as string) ?? "detected"; - const chunkSize = BigInt(Math.max(1, toInt(args["chunk-blocks"], 10_000))); - const startBlock = BigInt(Math.max(0, toInt(args["start-block"], 0))); - const rps = toInt(getEnvForChain("RPS", chainId) ?? "80", 80); + const outputDir = (args['output-dir'] as string) ?? 'detected'; + const chunkSize = BigInt(Math.max(1, toInt(args['chunk-blocks'], 10_000))); + const startBlock = BigInt(Math.max(0, toInt(args['start-block'], 0))); + const rps = toInt(getEnvForChain('RPS', chainId) ?? '80', 80); const rateLimitAcquire = pRateLimit(rps); const provider = new ethers.JsonRpcProvider(rpcUrl); const iface = new ethers.Interface(FACTORY_ABI); - const topic0 = iface.getEvent("PoolCreated")!.topicHash; + const topic0 = iface.getEvent('PoolCreated')!.topicHash; const latest = BigInt(await provider.getBlockNumber()); - const endBlockRaw = args["end-block"]; + const endBlockRaw = args['end-block']; const endBlock = endBlockRaw ? BigInt(String(endBlockRaw)) : latest; console.error(`Factory: ${factory}`); - console.error(`Scanning blocks ${startBlock}..${endBlock} (chunk=${chunkSize})`); + console.error( + `Scanning blocks ${startBlock}..${endBlock} (chunk=${chunkSize})`, + ); const seen = new Set
(); const configs: SlipstreamPoolConfig[] = []; for (let from = startBlock; from <= endBlock; from += chunkSize) { - const to = from + chunkSize - 1n > endBlock ? endBlock : from + chunkSize - 1n; + const to = + from + chunkSize - 1n > endBlock ? endBlock : from + chunkSize - 1n; await rateLimitAcquire(); - const logs = await getLogsWithRetry(provider, { address: factory, topics: [topic0], fromBlock: from, toBlock: to }); + const logs = await getLogsWithRetry(provider, { + address: factory, + topics: [topic0], + fromBlock: from, + toBlock: to, + }); for (const log of logs) { const parsed = iface.parseLog(log); @@ -148,7 +183,7 @@ async function main() { // token0 < token1 guaranteed by the factory configs.push({ - poolType: "slipstream", + poolType: 'slipstream', slipstreamPool: pool, currency0: token0, currency1: token1, @@ -158,15 +193,32 @@ async function main() { } if (to === endBlock || (to - startBlock) % (chunkSize * 10n) === 0n) { - console.error(`[scan] ${from}..${to} — found ${configs.length} pools so far`); + console.error( + `[scan] ${from}..${to} — found ${configs.length} pools so far`, + ); } console.log(`[scan] ${from}..${to} — found ${configs.length} pools so far`); } const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); fs.mkdirSync(path.dirname(outPath), { recursive: true }); - fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + "\n"); - console.log(JSON.stringify({ ok: true, chainId, factory, poolsFound: configs.length, outFile: outPath }, null, 2)); + fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + '\n'); + console.log( + JSON.stringify( + { + ok: true, + chainId, + factory, + poolsFound: configs.length, + outFile: outPath, + }, + null, + 2, + ), + ); } -main().catch((e) => { console.error(e); process.exit(1); }); +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/aggregator-hooks/historical/StableSwapNG.ts b/aggregator-hooks/historical/StableSwapNG.ts index dfa45362..5967e7e6 100644 --- a/aggregator-hooks/historical/StableSwapNG.ts +++ b/aggregator-hooks/historical/StableSwapNG.ts @@ -20,20 +20,20 @@ * Output: JSON array in createPools.ts StableSwapPoolConfig format. */ -import "dotenv/config"; -import fs from "node:fs"; -import path from "node:path"; -import { JsonRpcProvider, Contract, getAddress, ZeroAddress } from "ethers"; -import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; -import { STABLESWAPNG_HISTORICAL_FACTORY_ABI } from "../abis/index.js"; -import type { Address } from "../creation-modules/types.js"; +import 'dotenv/config'; +import fs from 'node:fs'; +import path from 'node:path'; +import { JsonRpcProvider, Contract, getAddress, ZeroAddress } from 'ethers'; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from '@src/cli'; +import { STABLESWAPNG_HISTORICAL_FACTORY_ABI } from '../abis/index.js'; +import type { Address } from '../creation-modules/types.js'; -const OUTPUT_FILE = "stableswapng-pools.json"; -const DEFAULT_FACTORY: Address = "0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf"; +const OUTPUT_FILE = 'stableswapng-pools.json'; +const DEFAULT_FACTORY: Address = '0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf'; type PoolMeta = { pool: Address; - kind: "plain" | "meta"; + kind: 'plain' | 'meta'; nCoins?: number; coins?: Address[]; basePool?: Address; @@ -41,7 +41,7 @@ type PoolMeta = { /** Same shape as createPools.ts StableSwapPoolConfig for stableswapng pool type */ type CreatePoolsStableSwapConfig = { - poolType: "stableswapng"; + poolType: 'stableswapng'; curvePool: Address; tokens: Address[]; fee: number | null; @@ -109,35 +109,43 @@ function uniqAddresses(addrs: string[]): Address[] { async function main() { const args = parseArgs(process.argv.slice(2)); - const chainIdRaw = args["chain-id"]; + const chainIdRaw = args['chain-id']; if (chainIdRaw == null) { - console.error("Missing required --chain-id "); + console.error('Missing required --chain-id '); process.exit(1); } const chainId = toInt(chainIdRaw, 0); if (chainId <= 0) { - console.error("--chain-id must be a positive integer"); + console.error('--chain-id must be a positive integer'); process.exit(1); } - const rpcUrl = getEnvForChain("RPC_URL", chainId); - const factoryAddrRaw = getEnvForChain("STABLESWAPNG_FACTORY", chainId) ?? DEFAULT_FACTORY; + const rpcUrl = getEnvForChain('RPC_URL', chainId); + const factoryAddrRaw = + getEnvForChain('STABLESWAPNG_FACTORY', chainId) ?? DEFAULT_FACTORY; if (!rpcUrl) { - console.error("Missing env: RPC_URL (or RPC_URL_)"); + console.error('Missing env: RPC_URL (or RPC_URL_)'); process.exit(1); } const factoryAddress = getAddress(factoryAddrRaw) as Address; - const outputDir = (args["output-dir"] as string) ?? "detected"; - const chunkSize = toInt(args["chunk"], 500); - const concurrency = Math.max(1, toInt(getEnvForChain("CONCURRENCY", chainId), 8)); - const rps = toInt(getEnvForChain("RPS", chainId), 80); + const outputDir = (args['output-dir'] as string) ?? 'detected'; + const chunkSize = toInt(args['chunk'], 500); + const concurrency = Math.max( + 1, + toInt(getEnvForChain('CONCURRENCY', chainId), 8), + ); + const rps = toInt(getEnvForChain('RPS', chainId), 80); const rateLimitAcquire = rps > 0 ? pRateLimit(rps) : async () => {}; - const startIndex = toInt(args["start-index"], 0); + const startIndex = toInt(args['start-index'], 0); const provider = new JsonRpcProvider(rpcUrl); - const factory = new Contract(factoryAddress, STABLESWAPNG_HISTORICAL_FACTORY_ABI, provider); + const factory = new Contract( + factoryAddress, + STABLESWAPNG_HISTORICAL_FACTORY_ABI, + provider, + ); const limit = pLimit(concurrency); const poolCountBn: bigint = await factory.pool_count(); @@ -149,7 +157,9 @@ async function main() { console.log(`Factory: ${factoryAddress}`); console.log(`pool_count: ${poolCount}`); console.log(`Starting at index: ${startIndex}`); - console.log(`chunkSize=${chunkSize} concurrency=${concurrency} rps=${rps > 0 ? rps : "unlimited"}`); + console.log( + `chunkSize=${chunkSize} concurrency=${concurrency} rps=${rps > 0 ? rps : 'unlimited'}`, + ); const pools: Address[] = []; const metas: PoolMeta[] = []; @@ -185,29 +195,36 @@ async function main() { return { pool, - kind: basePool.toLowerCase() === ZeroAddress.toLowerCase() ? "plain" : "meta", + kind: + basePool.toLowerCase() === ZeroAddress.toLowerCase() + ? 'plain' + : 'meta', nCoins: Number(nCoinsBn), coins, - basePool: basePool.toLowerCase() === ZeroAddress.toLowerCase() ? undefined : basePool, + basePool: + basePool.toLowerCase() === ZeroAddress.toLowerCase() + ? undefined + : basePool, } satisfies PoolMeta; }), ), ); metas.push(...chunkMetas); - for (let j = 0; j < addrs.length; j++) metaByPool.set(addrs[j], chunkMetas[j]); + for (let j = 0; j < addrs.length; j++) + metaByPool.set(addrs[j], chunkMetas[j]); console.log(`Fetched [${i}, ${end}) / ${poolCount}`); } const uniquePools = uniqAddresses(pools); const createPoolsConfigs: CreatePoolsStableSwapConfig[] = uniquePools - .filter((curvePool) => metaByPool.get(curvePool)?.kind === "plain") + .filter((curvePool) => metaByPool.get(curvePool)?.kind === 'plain') .map((curvePool) => { const meta = metaByPool.get(curvePool)!; const tokens = meta?.coins ?? []; return { - poolType: "stableswapng" as const, + poolType: 'stableswapng' as const, curvePool, tokens, fee: CREATE_POOLS_DEFAULTS.fee, @@ -219,7 +236,9 @@ async function main() { const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); fs.mkdirSync(path.dirname(outPath), { recursive: true }); saveJson(outPath, createPoolsConfigs); - console.log(`Wrote ${outPath} (${createPoolsConfigs.length} pools in createPools.ts format)`); + console.log( + `Wrote ${outPath} (${createPoolsConfigs.length} pools in createPools.ts format)`, + ); } main().catch((e) => { diff --git a/aggregator-hooks/historical/UniswapV2.ts b/aggregator-hooks/historical/UniswapV2.ts index 436bb358..b7fed514 100644 --- a/aggregator-hooks/historical/UniswapV2.ts +++ b/aggregator-hooks/historical/UniswapV2.ts @@ -23,23 +23,23 @@ * tickSpacing defaults to 1 (minimum valid). */ -import "dotenv/config"; -import fs from "node:fs"; -import path from "node:path"; -import { ethers } from "ethers"; -import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; -import type { Address } from "../creation-modules/types.js"; +import 'dotenv/config'; +import fs from 'node:fs'; +import path from 'node:path'; +import { ethers } from 'ethers'; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from '@src/cli'; +import type { Address } from '../creation-modules/types.js'; -const OUTPUT_FILE = "uniswapv2-pools.json"; -const DEFAULT_FACTORY: Address = "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"; -const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +const OUTPUT_FILE = 'uniswapv2-pools.json'; +const DEFAULT_FACTORY: Address = '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f'; +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; const FACTORY_ABI = [ - "event PairCreated(address indexed token0, address indexed token1, address pair, uint)", + 'event PairCreated(address indexed token0, address indexed token1, address pair, uint)', ]; type UniswapV2PoolConfig = { - poolType: "uniswapv2"; + poolType: 'uniswapv2'; v2Pair: Address; currency0: Address; currency1: Address; @@ -53,19 +53,34 @@ function pRateLimit(rps: number): () => Promise { let nextAllowed = 0; return async function acquire(): Promise { const now = Date.now(); - if (now < nextAllowed) await new Promise((r) => setTimeout(r, nextAllowed - now)); + if (now < nextAllowed) + await new Promise((r) => setTimeout(r, nextAllowed - now)); nextAllowed = Math.max(now, nextAllowed) + minGapMs; }; } function isRangeLimitError(err: unknown): boolean { - const msg = String((err as { error?: { message?: string }; message?: string })?.error?.message ?? (err as { message?: string })?.message ?? ""); - return msg.toLowerCase().includes("range") || msg.toLowerCase().includes("limit") || msg.includes("-32614"); + const msg = String( + (err as { error?: { message?: string }; message?: string })?.error + ?.message ?? + (err as { message?: string })?.message ?? + '', + ); + return ( + msg.toLowerCase().includes('range') || + msg.toLowerCase().includes('limit') || + msg.includes('-32614') + ); } async function getLogsWithRetry( provider: ethers.JsonRpcProvider, - filter: { address: string; topics: string[]; fromBlock: bigint; toBlock: bigint }, + filter: { + address: string; + topics: string[]; + fromBlock: bigint; + toBlock: bigint; + }, ): Promise { try { return await provider.getLogs({ @@ -75,7 +90,8 @@ async function getLogsWithRetry( toBlock: filter.toBlock, }); } catch (err) { - if (!isRangeLimitError(err) || filter.toBlock <= filter.fromBlock) throw err; + if (!isRangeLimitError(err) || filter.toBlock <= filter.fromBlock) + throw err; const mid = filter.fromBlock + (filter.toBlock - filter.fromBlock) / 2n; const [lo, hi] = await Promise.all([ getLogsWithRetry(provider, { ...filter, toBlock: mid }), @@ -92,42 +108,60 @@ function isNative(addr: string): boolean { async function main() { const args = parseArgs(process.argv.slice(2)); - const chainIdRaw = args["chain-id"]; - if (chainIdRaw == null) { console.error("Missing required --chain-id "); process.exit(1); } + const chainIdRaw = args['chain-id']; + if (chainIdRaw == null) { + console.error('Missing required --chain-id '); + process.exit(1); + } const chainId = toInt(chainIdRaw, 0); - if (chainId <= 0) { console.error("--chain-id must be a positive integer"); process.exit(1); } + if (chainId <= 0) { + console.error('--chain-id must be a positive integer'); + process.exit(1); + } - const rpcUrl = getEnvForChain("RPC_URL", chainId); - if (!rpcUrl) { console.error("Missing env: RPC_URL"); process.exit(1); } + const rpcUrl = getEnvForChain('RPC_URL', chainId); + if (!rpcUrl) { + console.error('Missing env: RPC_URL'); + process.exit(1); + } - const factoryRaw = getEnvForChain("UNISWAP_V2_FACTORY", chainId) ?? DEFAULT_FACTORY; + const factoryRaw = + getEnvForChain('UNISWAP_V2_FACTORY', chainId) ?? DEFAULT_FACTORY; const factory = ethers.getAddress(factoryRaw) as Address; - const outputDir = (args["output-dir"] as string) ?? "detected"; - const chunkSize = BigInt(Math.max(1, toInt(args["chunk-blocks"], 10_000))); - const startBlock = BigInt(Math.max(0, toInt(args["start-block"], 0))); - const rps = toInt(getEnvForChain("RPS", chainId) ?? "80", 80); + const outputDir = (args['output-dir'] as string) ?? 'detected'; + const chunkSize = BigInt(Math.max(1, toInt(args['chunk-blocks'], 10_000))); + const startBlock = BigInt(Math.max(0, toInt(args['start-block'], 0))); + const rps = toInt(getEnvForChain('RPS', chainId) ?? '80', 80); const rateLimitAcquire = pRateLimit(rps); const provider = new ethers.JsonRpcProvider(rpcUrl); const iface = new ethers.Interface(FACTORY_ABI); - const topic0 = iface.getEvent("PairCreated")!.topicHash; + const topic0 = iface.getEvent('PairCreated')!.topicHash; const latest = BigInt(await provider.getBlockNumber()); - const endBlockRaw = args["end-block"]; + const endBlockRaw = args['end-block']; const endBlock = endBlockRaw ? BigInt(String(endBlockRaw)) : latest; console.error(`Factory: ${factory}`); - console.error(`Scanning blocks ${startBlock}..${endBlock} (chunk=${chunkSize})`); + console.error( + `Scanning blocks ${startBlock}..${endBlock} (chunk=${chunkSize})`, + ); const seen = new Set
(); const configs: UniswapV2PoolConfig[] = []; for (let from = startBlock; from <= endBlock; from += chunkSize) { - const to = from + chunkSize - 1n > endBlock ? endBlock : from + chunkSize - 1n; + const to = + from + chunkSize - 1n > endBlock ? endBlock : from + chunkSize - 1n; await rateLimitAcquire(); - const logs = await getLogsWithRetry(provider, { address: factory, topics: [topic0], fromBlock: from, toBlock: to }); + const logs = await getLogsWithRetry(provider, { + address: factory, + topics: [topic0], + fromBlock: from, + toBlock: to, + }); for (const log of logs) { const parsed = iface.parseLog(log); @@ -144,7 +178,7 @@ async function main() { // token0 < token1 is guaranteed by the V2 factory configs.push({ - poolType: "uniswapv2", + poolType: 'uniswapv2', v2Pair: pair, currency0: token0, currency1: token1, @@ -154,15 +188,32 @@ async function main() { } if (to === endBlock || (to - startBlock) % (chunkSize * 10n) === 0n) { - console.error(`[scan] ${from}..${to} — found ${configs.length} pools so far`); + console.error( + `[scan] ${from}..${to} — found ${configs.length} pools so far`, + ); } console.log(`[scan] ${from}..${to} — found ${configs.length} pools so far`); } const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); fs.mkdirSync(path.dirname(outPath), { recursive: true }); - fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + "\n"); - console.log(JSON.stringify({ ok: true, chainId, factory, poolsFound: configs.length, outFile: outPath }, null, 2)); + fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + '\n'); + console.log( + JSON.stringify( + { + ok: true, + chainId, + factory, + poolsFound: configs.length, + outFile: outPath, + }, + null, + 2, + ), + ); } -main().catch((e) => { console.error(e); process.exit(1); }); +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/aggregator-hooks/historical/UniswapV3.ts b/aggregator-hooks/historical/UniswapV3.ts index fc572b5c..303c225d 100644 --- a/aggregator-hooks/historical/UniswapV3.ts +++ b/aggregator-hooks/historical/UniswapV3.ts @@ -22,23 +22,23 @@ * Output: JSON array in UniswapV3PoolConfig format (fee + tickSpacing from event). */ -import "dotenv/config"; -import fs from "node:fs"; -import path from "node:path"; -import { ethers } from "ethers"; -import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from "@src/cli"; -import type { Address } from "../creation-modules/types.js"; +import 'dotenv/config'; +import fs from 'node:fs'; +import path from 'node:path'; +import { ethers } from 'ethers'; +import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from '@src/cli'; +import type { Address } from '../creation-modules/types.js'; -const OUTPUT_FILE = "uniswapv3-pools.json"; -const DEFAULT_FACTORY: Address = "0x1F98431c8aD98523631AE4a59f267346ea31F984"; -const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +const OUTPUT_FILE = 'uniswapv3-pools.json'; +const DEFAULT_FACTORY: Address = '0x1F98431c8aD98523631AE4a59f267346ea31F984'; +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; const FACTORY_ABI = [ - "event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)", + 'event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)', ]; type UniswapV3PoolConfig = { - poolType: "uniswapv3"; + poolType: 'uniswapv3'; v3Pool: Address; currency0: Address; currency1: Address; @@ -53,20 +53,35 @@ function pRateLimit(rps: number): () => Promise { let nextAllowed = 0; return async function acquire(): Promise { const now = Date.now(); - if (now < nextAllowed) await new Promise((r) => setTimeout(r, nextAllowed - now)); + if (now < nextAllowed) + await new Promise((r) => setTimeout(r, nextAllowed - now)); nextAllowed = Math.max(now, nextAllowed) + minGapMs; }; } function isRangeLimitError(err: unknown): boolean { - const msg = String((err as { error?: { message?: string }; message?: string })?.error?.message ?? (err as { message?: string })?.message ?? ""); - return msg.toLowerCase().includes("range") || msg.toLowerCase().includes("limit") || msg.includes("-32614"); + const msg = String( + (err as { error?: { message?: string }; message?: string })?.error + ?.message ?? + (err as { message?: string })?.message ?? + '', + ); + return ( + msg.toLowerCase().includes('range') || + msg.toLowerCase().includes('limit') || + msg.includes('-32614') + ); } /** getLogs with automatic chunk-halving on range-limit errors. */ async function getLogsWithRetry( provider: ethers.JsonRpcProvider, - filter: { address: string; topics: string[]; fromBlock: bigint; toBlock: bigint }, + filter: { + address: string; + topics: string[]; + fromBlock: bigint; + toBlock: bigint; + }, ): Promise { try { return await provider.getLogs({ @@ -76,7 +91,8 @@ async function getLogsWithRetry( toBlock: filter.toBlock, }); } catch (err) { - if (!isRangeLimitError(err) || filter.toBlock <= filter.fromBlock) throw err; + if (!isRangeLimitError(err) || filter.toBlock <= filter.fromBlock) + throw err; const mid = filter.fromBlock + (filter.toBlock - filter.fromBlock) / 2n; const [lo, hi] = await Promise.all([ getLogsWithRetry(provider, { ...filter, toBlock: mid }), @@ -93,42 +109,60 @@ function isNative(addr: string): boolean { async function main() { const args = parseArgs(process.argv.slice(2)); - const chainIdRaw = args["chain-id"]; - if (chainIdRaw == null) { console.error("Missing required --chain-id "); process.exit(1); } + const chainIdRaw = args['chain-id']; + if (chainIdRaw == null) { + console.error('Missing required --chain-id '); + process.exit(1); + } const chainId = toInt(chainIdRaw, 0); - if (chainId <= 0) { console.error("--chain-id must be a positive integer"); process.exit(1); } + if (chainId <= 0) { + console.error('--chain-id must be a positive integer'); + process.exit(1); + } - const rpcUrl = getEnvForChain("RPC_URL", chainId); - if (!rpcUrl) { console.error("Missing env: RPC_URL"); process.exit(1); } + const rpcUrl = getEnvForChain('RPC_URL', chainId); + if (!rpcUrl) { + console.error('Missing env: RPC_URL'); + process.exit(1); + } - const factoryRaw = getEnvForChain("UNISWAP_V3_FACTORY", chainId) ?? DEFAULT_FACTORY; + const factoryRaw = + getEnvForChain('UNISWAP_V3_FACTORY', chainId) ?? DEFAULT_FACTORY; const factory = ethers.getAddress(factoryRaw) as Address; - const outputDir = (args["output-dir"] as string) ?? "detected"; - const chunkSize = BigInt(Math.max(1, toInt(args["chunk-blocks"], 10_000))); - const startBlock = BigInt(Math.max(0, toInt(args["start-block"], 0))); - const rps = toInt(getEnvForChain("RPS", chainId) ?? "80", 80); + const outputDir = (args['output-dir'] as string) ?? 'detected'; + const chunkSize = BigInt(Math.max(1, toInt(args['chunk-blocks'], 10_000))); + const startBlock = BigInt(Math.max(0, toInt(args['start-block'], 0))); + const rps = toInt(getEnvForChain('RPS', chainId) ?? '80', 80); const rateLimitAcquire = pRateLimit(rps); const provider = new ethers.JsonRpcProvider(rpcUrl); const iface = new ethers.Interface(FACTORY_ABI); - const topic0 = iface.getEvent("PoolCreated")!.topicHash; + const topic0 = iface.getEvent('PoolCreated')!.topicHash; const latest = BigInt(await provider.getBlockNumber()); - const endBlockRaw = args["end-block"]; + const endBlockRaw = args['end-block']; const endBlock = endBlockRaw ? BigInt(String(endBlockRaw)) : latest; console.error(`Factory: ${factory}`); - console.error(`Scanning blocks ${startBlock}..${endBlock} (chunk=${chunkSize})`); + console.error( + `Scanning blocks ${startBlock}..${endBlock} (chunk=${chunkSize})`, + ); const seen = new Set
(); const configs: UniswapV3PoolConfig[] = []; for (let from = startBlock; from <= endBlock; from += chunkSize) { - const to = from + chunkSize - 1n > endBlock ? endBlock : from + chunkSize - 1n; + const to = + from + chunkSize - 1n > endBlock ? endBlock : from + chunkSize - 1n; await rateLimitAcquire(); - const logs = await getLogsWithRetry(provider, { address: factory, topics: [topic0], fromBlock: from, toBlock: to }); + const logs = await getLogsWithRetry(provider, { + address: factory, + topics: [topic0], + fromBlock: from, + toBlock: to, + }); for (const log of logs) { const parsed = iface.parseLog(log); @@ -147,7 +181,7 @@ async function main() { // token0 < token1 is guaranteed by the V3 factory; currency ordering is already correct configs.push({ - poolType: "uniswapv3", + poolType: 'uniswapv3', v3Pool: pool, currency0: token0, currency1: token1, @@ -158,15 +192,32 @@ async function main() { } if (to === endBlock || (to - startBlock) % (chunkSize * 10n) === 0n) { - console.error(`[scan] ${from}..${to} — found ${configs.length} pools so far`); + console.error( + `[scan] ${from}..${to} — found ${configs.length} pools so far`, + ); } console.log(`[scan] ${from}..${to} — found ${configs.length} pools so far`); } const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); fs.mkdirSync(path.dirname(outPath), { recursive: true }); - fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + "\n"); - console.log(JSON.stringify({ ok: true, chainId, factory, poolsFound: configs.length, outFile: outPath }, null, 2)); + fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + '\n'); + console.log( + JSON.stringify( + { + ok: true, + chainId, + factory, + poolsFound: configs.length, + outFile: outPath, + }, + null, + 2, + ), + ); } -main().catch((e) => { console.error(e); process.exit(1); }); +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/aggregator-hooks/package-lock.json b/aggregator-hooks/package-lock.json index 15fb1861..bf94ed29 100644 --- a/aggregator-hooks/package-lock.json +++ b/aggregator-hooks/package-lock.json @@ -1,712 +1,729 @@ { - "name": "agg-hook-scripts", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "agg-hook-scripts", - "version": "1.0.0", - "dependencies": { - "dotenv": "^17.3.1", - "ethers": "^6.13.0" - }, - "devDependencies": { - "@types/node": "^22.10.0", - "tsx": "^4.19.2", - "typescript": "^5.7.0" - } - }, - "node_modules/@adraffy/ens-normalize": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", - "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", - "license": "MIT" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.3.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@types/node": { - "version": "22.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", - "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/aes-js": { - "version": "4.0.0-beta.5", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", - "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", - "license": "MIT" - }, - "node_modules/dotenv": { - "version": "17.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", - "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "node_modules/ethers": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", - "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/ethers-io/" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "1.10.1", - "@noble/curves": "1.2.0", - "@noble/hashes": "1.3.2", - "@types/node": "22.7.5", - "aes-js": "4.0.0-beta.5", - "tslib": "2.7.0", - "ws": "8.17.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/ethers/node_modules/@types/node": { - "version": "22.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", - "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/ethers/node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "license": "MIT" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "license": "0BSD" - }, - "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } + "name": "agg-hook-scripts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agg-hook-scripts", + "version": "1.0.0", + "dependencies": { + "dotenv": "^17.3.1", + "ethers": "^6.13.0" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "prettier": "^3.8.3", + "tsx": "^4.19.2", + "typescript": "^5.7.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true } + } } + } } diff --git a/aggregator-hooks/package.json b/aggregator-hooks/package.json index 530065d1..2d937024 100644 --- a/aggregator-hooks/package.json +++ b/aggregator-hooks/package.json @@ -1,22 +1,27 @@ { - "name": "agg-hook-scripts", - "version": "1.0.0", - "description": "Aggregator hook pool discovery and creation scripts", - "type": "module", - "scripts": { - "build": "tsc && cp abis/*.json dist/abis/", - "stableswapng": "tsx historical/StableSwapNG.ts", - "fluiddext1": "tsx historical/FluidDexT1.ts", - "fluiddexlite": "tsx historical/FluidDexLite.ts", - "create-pools": "tsx src/createPools.ts" - }, - "devDependencies": { - "@types/node": "^22.10.0", - "tsx": "^4.19.2", - "typescript": "^5.7.0" - }, - "dependencies": { - "dotenv": "^17.3.1", - "ethers": "^6.13.0" - } + "name": "agg-hook-scripts", + "version": "1.0.0", + "description": "Aggregator hook pool discovery and creation scripts", + "type": "module", + "scripts": { + "build": "tsc && cp abis/*.json dist/abis/", + "stableswapng": "tsx historical/StableSwapNG.ts", + "fluiddext1": "tsx historical/FluidDexT1.ts", + "fluiddexlite": "tsx historical/FluidDexLite.ts", + "create-pools": "tsx src/createPools.ts", + "format": "prettier --write \"**/*.{ts,json}\"" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "prettier": "^3.8.3", + "tsx": "^4.19.2", + "typescript": "^5.7.0" + }, + "prettier": { + "singleQuote": true + }, + "dependencies": { + "dotenv": "^17.3.1", + "ethers": "^6.13.0" + } } diff --git a/aggregator-hooks/polling/FluidDexLite.ts b/aggregator-hooks/polling/FluidDexLite.ts index 41e75089..34c6ea73 100644 --- a/aggregator-hooks/polling/FluidDexLite.ts +++ b/aggregator-hooks/polling/FluidDexLite.ts @@ -22,21 +22,29 @@ * LOOKBACK_BLOCKS (optional, default 200000) used when checkpoint missing and no --start-block */ -import "dotenv/config"; -import fs from "node:fs"; -import path from "node:path"; -import { ethers } from "ethers"; -import { parseArgs, getEnvForChain, toInt, resolveOutputPath, resolveCheckpointPath } from "@src/cli"; - -const OUTPUT_FILE = "fluiddexlite-pools.json"; -const CHECKPOINT_FILE = "dexlite_checkpoint.json"; +import 'dotenv/config'; +import fs from 'node:fs'; +import path from 'node:path'; +import { ethers } from 'ethers'; +import { + parseArgs, + getEnvForChain, + toInt, + resolveOutputPath, + resolveCheckpointPath, +} from '@src/cli'; + +const OUTPUT_FILE = 'fluiddexlite-pools.json'; +const CHECKPOINT_FILE = 'dexlite_checkpoint.json'; /** Fluid native token; map to address(0) for Uniswap v4 pool init */ -const FLUID_NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; -const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +const FLUID_NATIVE = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; function toUniswapV4Currency(addr: string): string { - return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() ? ZERO_ADDRESS : ethers.getAddress(addr); + return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() + ? ZERO_ADDRESS + : ethers.getAddress(addr); } type Checkpoint = { @@ -48,7 +56,7 @@ type Checkpoint = { /** Same shape as createPools.ts FluidDexLitePoolConfig */ type CreatePoolsFluidLiteConfig = { - poolType: "fluiddexlite"; + poolType: 'fluiddexlite'; dexSalt: string; currency0: string; currency1: string; @@ -58,7 +66,7 @@ type CreatePoolsFluidLiteConfig = { }; const LOG_INITIALIZE_ABI = [ - "event LogInitialize(uint256 dexId, tuple(address token0,address token1,bytes32 salt) dexKey, tuple(tuple(address token0,address token1,bytes32 salt) dexKey,uint256 fee,uint256 revenueCut,bool rebalancingStatus,uint256 centerPrice,uint256 centerPriceContract,uint256 upperPercent,uint256 lowerPercent,uint256 upperShiftThreshold,uint256 lowerShiftThreshold,uint256 shiftTime,uint256 minCenterPrice,uint256 maxCenterPrice,uint256 token0Amount,uint256 token1Amount) params, uint256 time)", + 'event LogInitialize(uint256 dexId, tuple(address token0,address token1,bytes32 salt) dexKey, tuple(tuple(address token0,address token1,bytes32 salt) dexKey,uint256 fee,uint256 revenueCut,bool rebalancingStatus,uint256 centerPrice,uint256 centerPriceContract,uint256 upperPercent,uint256 lowerPercent,uint256 upperShiftThreshold,uint256 lowerShiftThreshold,uint256 shiftTime,uint256 minCenterPrice,uint256 maxCenterPrice,uint256 token0Amount,uint256 token1Amount) params, uint256 time)', ] as const; function ensureDirForFile(filePath: string) { @@ -68,7 +76,7 @@ function ensureDirForFile(filePath: string) { function safeReadJson(filePath: string): T | null { try { - const raw = fs.readFileSync(filePath, "utf8"); + const raw = fs.readFileSync(filePath, 'utf8'); return JSON.parse(raw) as T; } catch { return null; @@ -78,7 +86,7 @@ function safeReadJson(filePath: string): T | null { function atomicWriteFile(filePath: string, contents: string) { ensureDirForFile(filePath); const abs = path.resolve(filePath); - const tmp = abs + ".tmp"; + const tmp = abs + '.tmp'; fs.writeFileSync(tmp, contents); fs.renameSync(tmp, abs); } @@ -86,7 +94,7 @@ function atomicWriteFile(filePath: string, contents: string) { function loadExistingKeys(outFile: string): Set { const keys = new Set(); if (!fs.existsSync(outFile)) return keys; - const raw = fs.readFileSync(outFile, "utf8").trim(); + const raw = fs.readFileSync(outFile, 'utf8').trim(); if (!raw) return keys; try { const arr = JSON.parse(raw) as CreatePoolsFluidLiteConfig[]; @@ -118,37 +126,37 @@ function getEnvInt(name: string, chainId: number, def: number): number { async function main() { const args = parseArgs(process.argv.slice(2)); - const chainIdRaw = args["chain-id"]; + const chainIdRaw = args['chain-id']; if (chainIdRaw == null) { - console.error("Missing required --chain-id "); + console.error('Missing required --chain-id '); process.exit(1); } const chainId = toInt(chainIdRaw, 0); if (chainId <= 0) { - console.error("--chain-id must be a positive integer"); + console.error('--chain-id must be a positive integer'); process.exit(1); } - const rpcUrl = getEnvForChain("RPC_URL", chainId); - const dexLiteRaw = getEnvForChain("FLUID_DEX_LITE", chainId); + const rpcUrl = getEnvForChain('RPC_URL', chainId); + const dexLiteRaw = getEnvForChain('FLUID_DEX_LITE', chainId); if (!rpcUrl || !dexLiteRaw) { throw new Error( - "Missing required env: RPC_URL and FLUID_DEX_LITE (or RPC_URL_, FLUID_DEX_LITE_)", + 'Missing required env: RPC_URL and FLUID_DEX_LITE (or RPC_URL_, FLUID_DEX_LITE_)', ); } const dexLite = ethers.getAddress(dexLiteRaw); - const outputDir = (args["output-dir"] as string) ?? "detected"; - const checkpointDir = (args["checkpoint-dir"] as string) ?? "checkpoints"; - const chunkBlocks = Math.max(1, toInt(args["chunk-blocks"], 10000)); + const outputDir = (args['output-dir'] as string) ?? 'detected'; + const checkpointDir = (args['checkpoint-dir'] as string) ?? 'checkpoints'; + const chunkBlocks = Math.max(1, toInt(args['chunk-blocks'], 10000)); - const finality = getEnvInt("FINALITY_BLOCKS", chainId, 10); - const lookbackBlocks = getEnvInt("LOOKBACK_BLOCKS", chainId, 200000); - const startBlockArg = args["start-block"]; + const finality = getEnvInt('FINALITY_BLOCKS', chainId, 10); + const lookbackBlocks = getEnvInt('LOOKBACK_BLOCKS', chainId, 200000); + const startBlockArg = args['start-block']; const provider = new ethers.JsonRpcProvider(rpcUrl); const iface = new ethers.Interface(LOG_INITIALIZE_ABI as unknown as string[]); - const topic0 = iface.getEvent("LogInitialize")!.topicHash; + const topic0 = iface.getEvent('LogInitialize')!.topicHash; const latestBlock = await provider.getBlockNumber(); const toBlock = Math.max(0, latestBlock - finality); @@ -159,7 +167,11 @@ async function main() { let fromBlock: number; if (startBlockArg != null) { fromBlock = Math.max(0, toInt(startBlockArg, 0)); - } else if (cp && cp.chainId === chainId && cp.dexLite.toLowerCase() === dexLite.toLowerCase()) { + } else if ( + cp && + cp.chainId === chainId && + cp.dexLite.toLowerCase() === dexLite.toLowerCase() + ) { fromBlock = cp.lastProcessedBlock + 1; } else { fromBlock = Math.max(0, toBlock - lookbackBlocks); @@ -172,12 +184,12 @@ async function main() { lastProcessedBlock: toBlock, updatedAt: new Date().toISOString(), }; - atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + '\n'); console.log( JSON.stringify( { ok: true, - note: "No new blocks to scan", + note: 'No new blocks to scan', chainId, dexLite, fromBlock, @@ -238,7 +250,7 @@ async function main() { newPools++; newRecords.push({ - poolType: "fluiddexlite", + poolType: 'fluiddexlite', dexSalt: salt, currency0, currency1, @@ -249,9 +261,10 @@ async function main() { } if (newRecords.length > 0) { - const existing = safeReadJson(outPath) ?? []; + const existing = + safeReadJson(outPath) ?? []; const merged = existing.concat(newRecords); - atomicWriteFile(outPath, JSON.stringify(merged, null, 2) + "\n"); + atomicWriteFile(outPath, JSON.stringify(merged, null, 2) + '\n'); } const interimCp: Checkpoint = { @@ -260,9 +273,11 @@ async function main() { lastProcessedBlock: end, updatedAt: new Date().toISOString(), }; - atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + "\n"); + atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + '\n'); - console.error(`[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`); + console.error( + `[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`, + ); } console.log( diff --git a/aggregator-hooks/polling/FluidDexT1.ts b/aggregator-hooks/polling/FluidDexT1.ts index a0ce7d5b..5fc8ef56 100644 --- a/aggregator-hooks/polling/FluidDexT1.ts +++ b/aggregator-hooks/polling/FluidDexT1.ts @@ -23,22 +23,30 @@ * LOOKBACK_BLOCKS (optional, default 200000) used when checkpoint missing and no --start-block */ -import "dotenv/config"; -import fs from "node:fs"; -import path from "node:path"; -import { ethers } from "ethers"; -import { parseArgs, getEnvForChain, toInt, resolveOutputPath, resolveCheckpointPath } from "@src/cli"; - -const OUTPUT_FILE = "fluiddext1-pools.json"; -const CHECKPOINT_FILE = "fluiddext1_checkpoint.json"; -const DEFAULT_FACTORY = "0x91716c4eDA1fB55e84Bf8b4c7085f84285c19085"; +import 'dotenv/config'; +import fs from 'node:fs'; +import path from 'node:path'; +import { ethers } from 'ethers'; +import { + parseArgs, + getEnvForChain, + toInt, + resolveOutputPath, + resolveCheckpointPath, +} from '@src/cli'; + +const OUTPUT_FILE = 'fluiddext1-pools.json'; +const CHECKPOINT_FILE = 'fluiddext1_checkpoint.json'; +const DEFAULT_FACTORY = '0x91716c4eDA1fB55e84Bf8b4c7085f84285c19085'; /** Fluid native token; map to address(0) for Uniswap v4 pool init */ -const FLUID_NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; -const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +const FLUID_NATIVE = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; function toUniswapV4Currency(addr: string): string { - return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() ? ZERO_ADDRESS : ethers.getAddress(addr); + return addr.toLowerCase() === FLUID_NATIVE.toLowerCase() + ? ZERO_ADDRESS + : ethers.getAddress(addr); } type Checkpoint = { @@ -50,7 +58,7 @@ type Checkpoint = { /** Same shape as createPools.ts FluidDexT1PoolConfig */ type CreatePoolsFluidDexT1Config = { - poolType: "fluiddext1"; + poolType: 'fluiddext1'; fluidPool: string; currency0: string; currency1: string; @@ -59,9 +67,13 @@ type CreatePoolsFluidDexT1Config = { sqrtPriceX96: string | null; }; -const FACTORY_ABI = ["event LogDexDeployed(address indexed dex, uint256 indexed dexId)"]; +const FACTORY_ABI = [ + 'event LogDexDeployed(address indexed dex, uint256 indexed dexId)', +]; -const RESOLVER_ABI = ["function getDexTokens(address dex_) external view returns (address token0_, address token1_)"]; +const RESOLVER_ABI = [ + 'function getDexTokens(address dex_) external view returns (address token0_, address token1_)', +]; function ensureDirForFile(filePath: string) { fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true }); @@ -69,7 +81,7 @@ function ensureDirForFile(filePath: string) { function safeReadJson(filePath: string): T | null { try { - return JSON.parse(fs.readFileSync(filePath, "utf8")) as T; + return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; } catch { return null; } @@ -78,8 +90,8 @@ function safeReadJson(filePath: string): T | null { function atomicWriteFile(filePath: string, contents: string) { ensureDirForFile(filePath); const abs = path.resolve(filePath); - fs.writeFileSync(abs + ".tmp", contents); - fs.renameSync(abs + ".tmp", abs); + fs.writeFileSync(abs + '.tmp', contents); + fs.renameSync(abs + '.tmp', abs); } function loadExistingKeys(outFile: string): Set { @@ -88,7 +100,8 @@ function loadExistingKeys(outFile: string): Set { try { const arr = safeReadJson(outFile); if (!Array.isArray(arr)) return keys; - for (const x of arr) keys.add(`${x.fluidPool}:${x.currency0}:${x.currency1}`); + for (const x of arr) + keys.add(`${x.fluidPool}:${x.currency0}:${x.currency1}`); } catch { // ignore } @@ -113,41 +126,45 @@ function getEnvInt(name: string, chainId: number, def: number): number { async function main() { const args = parseArgs(process.argv.slice(2)); - const chainIdRaw = args["chain-id"]; + const chainIdRaw = args['chain-id']; if (chainIdRaw == null) { - console.error("Missing required --chain-id "); + console.error('Missing required --chain-id '); process.exit(1); } const chainId = toInt(chainIdRaw, 0); if (chainId <= 0) { - console.error("--chain-id must be a positive integer"); + console.error('--chain-id must be a positive integer'); process.exit(1); } - const rpcUrl = getEnvForChain("RPC_URL", chainId); - const resolverAddr = getEnvForChain("FLUID_DEX_T1_RESOLVER", chainId); + const rpcUrl = getEnvForChain('RPC_URL', chainId); + const resolverAddr = getEnvForChain('FLUID_DEX_T1_RESOLVER', chainId); const factoryRaw = - getEnvForChain("FLUID_DEX_T1_FACTORY", chainId) ?? DEFAULT_FACTORY; + getEnvForChain('FLUID_DEX_T1_FACTORY', chainId) ?? DEFAULT_FACTORY; if (!rpcUrl || !resolverAddr) { - throw new Error("Missing required env: RPC_URL and FLUID_DEX_T1_RESOLVER"); + throw new Error('Missing required env: RPC_URL and FLUID_DEX_T1_RESOLVER'); } const factory = ethers.getAddress(factoryRaw); - const outputDir = (args["output-dir"] as string) ?? "detected"; - const checkpointDir = (args["checkpoint-dir"] as string) ?? "checkpoints"; - const chunkBlocks = Math.max(1, toInt(args["chunk-blocks"], 10000)); + const outputDir = (args['output-dir'] as string) ?? 'detected'; + const checkpointDir = (args['checkpoint-dir'] as string) ?? 'checkpoints'; + const chunkBlocks = Math.max(1, toInt(args['chunk-blocks'], 10000)); - const finality = getEnvInt("FINALITY_BLOCKS", chainId, 10); - const lookbackBlocks = getEnvInt("LOOKBACK_BLOCKS", chainId, 200000); - const startBlockArg = args["start-block"]; + const finality = getEnvInt('FINALITY_BLOCKS', chainId, 10); + const lookbackBlocks = getEnvInt('LOOKBACK_BLOCKS', chainId, 200000); + const startBlockArg = args['start-block']; const provider = new ethers.JsonRpcProvider(rpcUrl); - const resolver = new ethers.Contract(ethers.getAddress(resolverAddr), RESOLVER_ABI, provider); + const resolver = new ethers.Contract( + ethers.getAddress(resolverAddr), + RESOLVER_ABI, + provider, + ); const iface = new ethers.Interface(FACTORY_ABI as unknown as string[]); - const topic0 = iface.getEvent("LogDexDeployed")!.topicHash; + const topic0 = iface.getEvent('LogDexDeployed')!.topicHash; const latestBlock = await provider.getBlockNumber(); const toBlock = Math.max(0, latestBlock - finality); @@ -158,7 +175,10 @@ async function main() { let fromBlock: number; if (startBlockArg != null) { fromBlock = Math.max(0, toInt(startBlockArg, 0)); - } else if (cp?.chainId === chainId && cp?.factory?.toLowerCase() === factory.toLowerCase()) { + } else if ( + cp?.chainId === chainId && + cp?.factory?.toLowerCase() === factory.toLowerCase() + ) { fromBlock = cp.lastProcessedBlock + 1; } else { fromBlock = Math.max(0, toBlock - lookbackBlocks); @@ -171,8 +191,14 @@ async function main() { lastProcessedBlock: toBlock, updatedAt: new Date().toISOString(), }; - atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); - console.log(JSON.stringify({ ok: true, note: "No new blocks to scan", fromBlock, toBlock }, null, 2)); + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + '\n'); + console.log( + JSON.stringify( + { ok: true, note: 'No new blocks to scan', fromBlock, toBlock }, + null, + 2, + ), + ); return; } @@ -222,7 +248,7 @@ async function main() { newPools++; newRecords.push({ - poolType: "fluiddext1", + poolType: 'fluiddext1', fluidPool: dex, currency0, currency1, @@ -234,7 +260,7 @@ async function main() { if (newRecords.length > 0) { allRecords = allRecords.concat(newRecords); - atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + "\n"); + atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + '\n'); } const interimCp: Checkpoint = { @@ -243,8 +269,10 @@ async function main() { lastProcessedBlock: end, updatedAt: new Date().toISOString(), }; - atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + "\n"); - console.error(`[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`); + atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + '\n'); + console.error( + `[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`, + ); } console.log( diff --git a/aggregator-hooks/polling/PancakeSwapV3.ts b/aggregator-hooks/polling/PancakeSwapV3.ts index 0f93fba6..e5afab1c 100644 --- a/aggregator-hooks/polling/PancakeSwapV3.ts +++ b/aggregator-hooks/polling/PancakeSwapV3.ts @@ -20,25 +20,36 @@ * LOOKBACK_BLOCKS (optional, default 200000) */ -import "dotenv/config"; -import fs from "node:fs"; -import path from "node:path"; -import { ethers } from "ethers"; -import { parseArgs, getEnvForChain, toInt, resolveOutputPath, resolveCheckpointPath } from "@src/cli"; - -const OUTPUT_FILE = "pancakeswapv3-pools.json"; -const CHECKPOINT_FILE = "pancakeswapv3_checkpoint.json"; -const DEFAULT_FACTORY = "0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865"; -const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +import 'dotenv/config'; +import fs from 'node:fs'; +import path from 'node:path'; +import { ethers } from 'ethers'; +import { + parseArgs, + getEnvForChain, + toInt, + resolveOutputPath, + resolveCheckpointPath, +} from '@src/cli'; + +const OUTPUT_FILE = 'pancakeswapv3-pools.json'; +const CHECKPOINT_FILE = 'pancakeswapv3_checkpoint.json'; +const DEFAULT_FACTORY = '0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865'; +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; const FACTORY_ABI = [ - "event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)", + 'event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)', ]; -type Checkpoint = { chainId: number; factory: string; lastProcessedBlock: number; updatedAt: string }; +type Checkpoint = { + chainId: number; + factory: string; + lastProcessedBlock: number; + updatedAt: string; +}; type PancakeSwapV3PoolConfig = { - poolType: "pancakeswapv3"; + poolType: 'pancakeswapv3'; v3Pool: string; currency0: string; currency1: string; @@ -56,14 +67,18 @@ function ensureDirForFile(filePath: string) { } function safeReadJson(filePath: string): T | null { - try { return JSON.parse(fs.readFileSync(filePath, "utf8")) as T; } catch { return null; } + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; + } catch { + return null; + } } function atomicWriteFile(filePath: string, contents: string) { ensureDirForFile(filePath); const abs = path.resolve(filePath); - fs.writeFileSync(abs + ".tmp", contents); - fs.renameSync(abs + ".tmp", abs); + fs.writeFileSync(abs + '.tmp', contents); + fs.renameSync(abs + '.tmp', abs); } function loadExistingKeys(outFile: string): Set { @@ -73,7 +88,9 @@ function loadExistingKeys(outFile: string): Set { const arr = safeReadJson(outFile); if (!Array.isArray(arr)) return keys; for (const x of arr) keys.add(x.v3Pool.toLowerCase()); - } catch { /* ignore */ } + } catch { + /* ignore */ + } return keys; } @@ -87,27 +104,34 @@ function getEnvInt(name: string, chainId: number, def: number): number { async function main() { const args = parseArgs(process.argv.slice(2)); - const chainIdRaw = args["chain-id"]; - if (chainIdRaw == null) { console.error("Missing required --chain-id "); process.exit(1); } + const chainIdRaw = args['chain-id']; + if (chainIdRaw == null) { + console.error('Missing required --chain-id '); + process.exit(1); + } const chainId = toInt(chainIdRaw, 0); - if (chainId <= 0) { console.error("--chain-id must be a positive integer"); process.exit(1); } + if (chainId <= 0) { + console.error('--chain-id must be a positive integer'); + process.exit(1); + } - const rpcUrl = getEnvForChain("RPC_URL", chainId); - if (!rpcUrl) throw new Error("Missing required env: RPC_URL"); + const rpcUrl = getEnvForChain('RPC_URL', chainId); + if (!rpcUrl) throw new Error('Missing required env: RPC_URL'); - const factoryRaw = getEnvForChain("PANCAKESWAP_V3_FACTORY", chainId) ?? DEFAULT_FACTORY; + const factoryRaw = + getEnvForChain('PANCAKESWAP_V3_FACTORY', chainId) ?? DEFAULT_FACTORY; const factory = ethers.getAddress(factoryRaw); - const outputDir = (args["output-dir"] as string) ?? "detected"; - const checkpointDir = (args["checkpoint-dir"] as string) ?? "checkpoints"; - const chunkBlocks = Math.max(1, toInt(args["chunk-blocks"], 10_000)); - const finality = getEnvInt("FINALITY_BLOCKS", chainId, 10); - const lookbackBlocks = getEnvInt("LOOKBACK_BLOCKS", chainId, 200_000); - const startBlockArg = args["start-block"]; + const outputDir = (args['output-dir'] as string) ?? 'detected'; + const checkpointDir = (args['checkpoint-dir'] as string) ?? 'checkpoints'; + const chunkBlocks = Math.max(1, toInt(args['chunk-blocks'], 10_000)); + const finality = getEnvInt('FINALITY_BLOCKS', chainId, 10); + const lookbackBlocks = getEnvInt('LOOKBACK_BLOCKS', chainId, 200_000); + const startBlockArg = args['start-block']; const provider = new ethers.JsonRpcProvider(rpcUrl); const iface = new ethers.Interface(FACTORY_ABI); - const topic0 = iface.getEvent("PoolCreated")!.topicHash; + const topic0 = iface.getEvent('PoolCreated')!.topicHash; const latestBlock = await provider.getBlockNumber(); const toBlock = Math.max(0, latestBlock - finality); @@ -118,16 +142,30 @@ async function main() { let fromBlock: number; if (startBlockArg != null) { fromBlock = Math.max(0, toInt(startBlockArg, 0)); - } else if (cp?.chainId === chainId && cp?.factory?.toLowerCase() === factory.toLowerCase()) { + } else if ( + cp?.chainId === chainId && + cp?.factory?.toLowerCase() === factory.toLowerCase() + ) { fromBlock = cp.lastProcessedBlock + 1; } else { fromBlock = Math.max(0, toBlock - lookbackBlocks); } if (fromBlock > toBlock) { - const newCp: Checkpoint = { chainId, factory, lastProcessedBlock: toBlock, updatedAt: new Date().toISOString() }; - atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); - console.log(JSON.stringify({ ok: true, note: "No new blocks to scan", fromBlock, toBlock }, null, 2)); + const newCp: Checkpoint = { + chainId, + factory, + lastProcessedBlock: toBlock, + updatedAt: new Date().toISOString(), + }; + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + '\n'); + console.log( + JSON.stringify( + { ok: true, note: 'No new blocks to scan', fromBlock, toBlock }, + null, + 2, + ), + ); return; } @@ -140,13 +178,22 @@ async function main() { for (let start = fromBlock; start <= toBlock; start += chunkBlocks) { const end = Math.min(toBlock, start + chunkBlocks - 1); - const logs = await provider.getLogs({ address: factory, topics: [topic0], fromBlock: start, toBlock: end }); + const logs = await provider.getLogs({ + address: factory, + topics: [topic0], + fromBlock: start, + toBlock: end, + }); totalLogs += logs.length; const newRecords: PancakeSwapV3PoolConfig[] = []; for (const log of logs) { let parsed: ethers.LogDescription | null; - try { parsed = iface.parseLog(log); } catch { continue; } + try { + parsed = iface.parseLog(log); + } catch { + continue; + } if (!parsed) continue; const token0 = ethers.getAddress(parsed.args.token0 as string); @@ -160,24 +207,53 @@ async function main() { seenKeys.add(pool.toLowerCase()); newPools++; - newRecords.push({ poolType: "pancakeswapv3", v3Pool: pool, currency0: token0, currency1: token1, fee, tickSpacing, sqrtPriceX96: null }); + newRecords.push({ + poolType: 'pancakeswapv3', + v3Pool: pool, + currency0: token0, + currency1: token1, + fee, + tickSpacing, + sqrtPriceX96: null, + }); } if (newRecords.length > 0) { allRecords = allRecords.concat(newRecords); - atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + "\n"); + atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + '\n'); } - const interimCp: Checkpoint = { chainId, factory, lastProcessedBlock: end, updatedAt: new Date().toISOString() }; - atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + "\n"); - console.error(`[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`); + const interimCp: Checkpoint = { + chainId, + factory, + lastProcessedBlock: end, + updatedAt: new Date().toISOString(), + }; + atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + '\n'); + console.error( + `[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`, + ); } - console.log(JSON.stringify({ - ok: true, chainId, factory, - scanned: { fromBlock, toBlock, latestBlock, finality, chunkBlocks }, - logsFound: totalLogs, newPools, outFile: outPath, checkpointFile: cpPath, - }, null, 2)); + console.log( + JSON.stringify( + { + ok: true, + chainId, + factory, + scanned: { fromBlock, toBlock, latestBlock, finality, chunkBlocks }, + logsFound: totalLogs, + newPools, + outFile: outPath, + checkpointFile: cpPath, + }, + null, + 2, + ), + ); } -main().catch((err) => { console.error(err); process.exitCode = 1; }); +main().catch((err) => { + console.error(err); + process.exitCode = 1; +}); diff --git a/aggregator-hooks/polling/Slipstream.ts b/aggregator-hooks/polling/Slipstream.ts index 66e1232a..d3df4a3e 100644 --- a/aggregator-hooks/polling/Slipstream.ts +++ b/aggregator-hooks/polling/Slipstream.ts @@ -20,25 +20,36 @@ * LOOKBACK_BLOCKS (optional, default 200000) */ -import "dotenv/config"; -import fs from "node:fs"; -import path from "node:path"; -import { ethers } from "ethers"; -import { parseArgs, getEnvForChain, toInt, resolveOutputPath, resolveCheckpointPath } from "@src/cli"; - -const OUTPUT_FILE = "slipstream-pools.json"; -const CHECKPOINT_FILE = "slipstream_checkpoint.json"; -const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +import 'dotenv/config'; +import fs from 'node:fs'; +import path from 'node:path'; +import { ethers } from 'ethers'; +import { + parseArgs, + getEnvForChain, + toInt, + resolveOutputPath, + resolveCheckpointPath, +} from '@src/cli'; + +const OUTPUT_FILE = 'slipstream-pools.json'; +const CHECKPOINT_FILE = 'slipstream_checkpoint.json'; +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; // Slipstream CLFactory: tickSpacing is the third indexed param (not fee) const FACTORY_ABI = [ - "event PoolCreated(address indexed token0, address indexed token1, int24 indexed tickSpacing, address pool)", + 'event PoolCreated(address indexed token0, address indexed token1, int24 indexed tickSpacing, address pool)', ]; -type Checkpoint = { chainId: number; factory: string; lastProcessedBlock: number; updatedAt: string }; +type Checkpoint = { + chainId: number; + factory: string; + lastProcessedBlock: number; + updatedAt: string; +}; type SlipstreamPoolConfig = { - poolType: "slipstream"; + poolType: 'slipstream'; slipstreamPool: string; currency0: string; currency1: string; @@ -55,14 +66,18 @@ function ensureDirForFile(filePath: string) { } function safeReadJson(filePath: string): T | null { - try { return JSON.parse(fs.readFileSync(filePath, "utf8")) as T; } catch { return null; } + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; + } catch { + return null; + } } function atomicWriteFile(filePath: string, contents: string) { ensureDirForFile(filePath); const abs = path.resolve(filePath); - fs.writeFileSync(abs + ".tmp", contents); - fs.renameSync(abs + ".tmp", abs); + fs.writeFileSync(abs + '.tmp', contents); + fs.renameSync(abs + '.tmp', abs); } function loadExistingKeys(outFile: string): Set { @@ -72,7 +87,9 @@ function loadExistingKeys(outFile: string): Set { const arr = safeReadJson(outFile); if (!Array.isArray(arr)) return keys; for (const x of arr) keys.add(x.slipstreamPool.toLowerCase()); - } catch { /* ignore */ } + } catch { + /* ignore */ + } return keys; } @@ -86,28 +103,37 @@ function getEnvInt(name: string, chainId: number, def: number): number { async function main() { const args = parseArgs(process.argv.slice(2)); - const chainIdRaw = args["chain-id"]; - if (chainIdRaw == null) { console.error("Missing required --chain-id "); process.exit(1); } + const chainIdRaw = args['chain-id']; + if (chainIdRaw == null) { + console.error('Missing required --chain-id '); + process.exit(1); + } const chainId = toInt(chainIdRaw, 0); - if (chainId <= 0) { console.error("--chain-id must be a positive integer"); process.exit(1); } + if (chainId <= 0) { + console.error('--chain-id must be a positive integer'); + process.exit(1); + } - const rpcUrl = getEnvForChain("RPC_URL", chainId); - if (!rpcUrl) throw new Error("Missing required env: RPC_URL"); + const rpcUrl = getEnvForChain('RPC_URL', chainId); + if (!rpcUrl) throw new Error('Missing required env: RPC_URL'); - const factoryRaw = getEnvForChain("SLIPSTREAM_FACTORY", chainId); - if (!factoryRaw) throw new Error("Missing required env: SLIPSTREAM_FACTORY (or SLIPSTREAM_FACTORY_)"); + const factoryRaw = getEnvForChain('SLIPSTREAM_FACTORY', chainId); + if (!factoryRaw) + throw new Error( + 'Missing required env: SLIPSTREAM_FACTORY (or SLIPSTREAM_FACTORY_)', + ); const factory = ethers.getAddress(factoryRaw); - const outputDir = (args["output-dir"] as string) ?? "detected"; - const checkpointDir = (args["checkpoint-dir"] as string) ?? "checkpoints"; - const chunkBlocks = Math.max(1, toInt(args["chunk-blocks"], 10_000)); - const finality = getEnvInt("FINALITY_BLOCKS", chainId, 10); - const lookbackBlocks = getEnvInt("LOOKBACK_BLOCKS", chainId, 200_000); - const startBlockArg = args["start-block"]; + const outputDir = (args['output-dir'] as string) ?? 'detected'; + const checkpointDir = (args['checkpoint-dir'] as string) ?? 'checkpoints'; + const chunkBlocks = Math.max(1, toInt(args['chunk-blocks'], 10_000)); + const finality = getEnvInt('FINALITY_BLOCKS', chainId, 10); + const lookbackBlocks = getEnvInt('LOOKBACK_BLOCKS', chainId, 200_000); + const startBlockArg = args['start-block']; const provider = new ethers.JsonRpcProvider(rpcUrl); const iface = new ethers.Interface(FACTORY_ABI); - const topic0 = iface.getEvent("PoolCreated")!.topicHash; + const topic0 = iface.getEvent('PoolCreated')!.topicHash; const latestBlock = await provider.getBlockNumber(); const toBlock = Math.max(0, latestBlock - finality); @@ -118,16 +144,30 @@ async function main() { let fromBlock: number; if (startBlockArg != null) { fromBlock = Math.max(0, toInt(startBlockArg, 0)); - } else if (cp?.chainId === chainId && cp?.factory?.toLowerCase() === factory.toLowerCase()) { + } else if ( + cp?.chainId === chainId && + cp?.factory?.toLowerCase() === factory.toLowerCase() + ) { fromBlock = cp.lastProcessedBlock + 1; } else { fromBlock = Math.max(0, toBlock - lookbackBlocks); } if (fromBlock > toBlock) { - const newCp: Checkpoint = { chainId, factory, lastProcessedBlock: toBlock, updatedAt: new Date().toISOString() }; - atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); - console.log(JSON.stringify({ ok: true, note: "No new blocks to scan", fromBlock, toBlock }, null, 2)); + const newCp: Checkpoint = { + chainId, + factory, + lastProcessedBlock: toBlock, + updatedAt: new Date().toISOString(), + }; + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + '\n'); + console.log( + JSON.stringify( + { ok: true, note: 'No new blocks to scan', fromBlock, toBlock }, + null, + 2, + ), + ); return; } @@ -140,13 +180,22 @@ async function main() { for (let start = fromBlock; start <= toBlock; start += chunkBlocks) { const end = Math.min(toBlock, start + chunkBlocks - 1); - const logs = await provider.getLogs({ address: factory, topics: [topic0], fromBlock: start, toBlock: end }); + const logs = await provider.getLogs({ + address: factory, + topics: [topic0], + fromBlock: start, + toBlock: end, + }); totalLogs += logs.length; const newRecords: SlipstreamPoolConfig[] = []; for (const log of logs) { let parsed: ethers.LogDescription | null; - try { parsed = iface.parseLog(log); } catch { continue; } + try { + parsed = iface.parseLog(log); + } catch { + continue; + } if (!parsed) continue; const token0 = ethers.getAddress(parsed.args.token0 as string); @@ -159,24 +208,52 @@ async function main() { seenKeys.add(pool.toLowerCase()); newPools++; - newRecords.push({ poolType: "slipstream", slipstreamPool: pool, currency0: token0, currency1: token1, tickSpacing, sqrtPriceX96: null }); + newRecords.push({ + poolType: 'slipstream', + slipstreamPool: pool, + currency0: token0, + currency1: token1, + tickSpacing, + sqrtPriceX96: null, + }); } if (newRecords.length > 0) { allRecords = allRecords.concat(newRecords); - atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + "\n"); + atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + '\n'); } - const interimCp: Checkpoint = { chainId, factory, lastProcessedBlock: end, updatedAt: new Date().toISOString() }; - atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + "\n"); - console.error(`[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`); + const interimCp: Checkpoint = { + chainId, + factory, + lastProcessedBlock: end, + updatedAt: new Date().toISOString(), + }; + atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + '\n'); + console.error( + `[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`, + ); } - console.log(JSON.stringify({ - ok: true, chainId, factory, - scanned: { fromBlock, toBlock, latestBlock, finality, chunkBlocks }, - logsFound: totalLogs, newPools, outFile: outPath, checkpointFile: cpPath, - }, null, 2)); + console.log( + JSON.stringify( + { + ok: true, + chainId, + factory, + scanned: { fromBlock, toBlock, latestBlock, finality, chunkBlocks }, + logsFound: totalLogs, + newPools, + outFile: outPath, + checkpointFile: cpPath, + }, + null, + 2, + ), + ); } -main().catch((err) => { console.error(err); process.exitCode = 1; }); +main().catch((err) => { + console.error(err); + process.exitCode = 1; +}); diff --git a/aggregator-hooks/polling/StableSwapNG.ts b/aggregator-hooks/polling/StableSwapNG.ts index 87804d5b..3f47e6f1 100644 --- a/aggregator-hooks/polling/StableSwapNG.ts +++ b/aggregator-hooks/polling/StableSwapNG.ts @@ -25,15 +25,21 @@ * CONCURRENCY (optional, default 8) max concurrent RPC calls */ -import "dotenv/config"; -import fs from "node:fs"; -import path from "node:path"; -import { ethers } from "ethers"; -import { parseArgs, getEnvForChain, toInt, resolveOutputPath, resolveCheckpointPath } from "@src/cli"; - -const OUTPUT_FILE = "stableswapng-pools.json"; -const CHECKPOINT_FILE = "stableswapng_checkpoint.json"; -const DEFAULT_FACTORY = "0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf"; +import 'dotenv/config'; +import fs from 'node:fs'; +import path from 'node:path'; +import { ethers } from 'ethers'; +import { + parseArgs, + getEnvForChain, + toInt, + resolveOutputPath, + resolveCheckpointPath, +} from '@src/cli'; + +const OUTPUT_FILE = 'stableswapng-pools.json'; +const CHECKPOINT_FILE = 'stableswapng_checkpoint.json'; +const DEFAULT_FACTORY = '0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf'; type Checkpoint = { chainId: number; @@ -45,7 +51,7 @@ type Checkpoint = { /** Same shape as createPools.ts StableSwapPoolConfig for stableswapng */ type CreatePoolsStableSwapConfig = { - poolType: "stableswapng"; + poolType: 'stableswapng'; curvePool: string; tokens: string[]; fee: number | null; @@ -54,13 +60,13 @@ type CreatePoolsStableSwapConfig = { }; const FACTORY_ABI = [ - "function pool_count() view returns (uint256)", - "function pool_list(uint256) view returns (address)", - "function get_n_coins(address) view returns (uint256)", - "function get_coins(address) view returns (address[])", - "function get_base_pool(address) view returns (address)", - "event PlainPoolDeployed(address[] coins, uint256 A, uint256 fee, address deployer)", - "event MetaPoolDeployed(address coin, address base_pool, uint256 A, uint256 fee, address deployer)", + 'function pool_count() view returns (uint256)', + 'function pool_list(uint256) view returns (address)', + 'function get_n_coins(address) view returns (uint256)', + 'function get_coins(address) view returns (address[])', + 'function get_base_pool(address) view returns (address)', + 'event PlainPoolDeployed(address[] coins, uint256 A, uint256 fee, address deployer)', + 'event MetaPoolDeployed(address coin, address base_pool, uint256 A, uint256 fee, address deployer)', ]; function ensureDirForFile(filePath: string) { @@ -69,7 +75,7 @@ function ensureDirForFile(filePath: string) { function safeReadJson(filePath: string): T | null { try { - return JSON.parse(fs.readFileSync(filePath, "utf8")) as T; + return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; } catch { return null; } @@ -78,8 +84,8 @@ function safeReadJson(filePath: string): T | null { function atomicWriteFile(filePath: string, contents: string) { ensureDirForFile(filePath); const abs = path.resolve(filePath); - fs.writeFileSync(abs + ".tmp", contents); - fs.renameSync(abs + ".tmp", abs); + fs.writeFileSync(abs + '.tmp', contents); + fs.renameSync(abs + '.tmp', abs); } function loadExistingPoolAddrs(outFile: string): Set { @@ -101,7 +107,8 @@ function pRateLimit(rps: number): () => Promise { let nextAllowed = 0; return async function acquire() { const now = Date.now(); - if (now < nextAllowed) await new Promise((r) => setTimeout(r, nextAllowed - now)); + if (now < nextAllowed) + await new Promise((r) => setTimeout(r, nextAllowed - now)); nextAllowed = Math.max(now, nextAllowed) + minGapMs; }; } @@ -142,36 +149,40 @@ function getEnvInt(name: string, chainId: number, def: number): number { async function main() { const args = parseArgs(process.argv.slice(2)); - const chainIdRaw = args["chain-id"]; + const chainIdRaw = args['chain-id']; if (chainIdRaw == null) { - console.error("Missing required --chain-id "); + console.error('Missing required --chain-id '); process.exit(1); } const chainId = toInt(chainIdRaw, 0); if (chainId <= 0) { - console.error("--chain-id must be a positive integer"); + console.error('--chain-id must be a positive integer'); process.exit(1); } - const rpcUrl = getEnvForChain("RPC_URL", chainId); - const factoryRaw = getEnvForChain("STABLESWAPNG_FACTORY", chainId) ?? DEFAULT_FACTORY; + const rpcUrl = getEnvForChain('RPC_URL', chainId); + const factoryRaw = + getEnvForChain('STABLESWAPNG_FACTORY', chainId) ?? DEFAULT_FACTORY; if (!rpcUrl) { - throw new Error("Missing required env: RPC_URL (or RPC_URL_)"); + throw new Error('Missing required env: RPC_URL (or RPC_URL_)'); } const factory = ethers.getAddress(factoryRaw); - const outputDir = (args["output-dir"] as string) ?? "detected"; - const checkpointDir = (args["checkpoint-dir"] as string) ?? "checkpoints"; - const chunkBlocks = Math.max(1, toInt(args["chunk-blocks"], 10000)); + const outputDir = (args['output-dir'] as string) ?? 'detected'; + const checkpointDir = (args['checkpoint-dir'] as string) ?? 'checkpoints'; + const chunkBlocks = Math.max(1, toInt(args['chunk-blocks'], 10000)); - const finality = getEnvInt("FINALITY_BLOCKS", chainId, 10); - const lookbackBlocks = getEnvInt("LOOKBACK_BLOCKS", chainId, 200000); - const startBlockArg = args["start-block"]; + const finality = getEnvInt('FINALITY_BLOCKS', chainId, 10); + const lookbackBlocks = getEnvInt('LOOKBACK_BLOCKS', chainId, 200000); + const startBlockArg = args['start-block']; - const concurrency = Math.max(1, toInt(getEnvForChain("CONCURRENCY", chainId), 8)); - const rps = toInt(getEnvForChain("RPS", chainId), 80); + const concurrency = Math.max( + 1, + toInt(getEnvForChain('CONCURRENCY', chainId), 8), + ); + const rps = toInt(getEnvForChain('RPS', chainId), 80); const rateLimit = pRateLimit(rps); const limit = pLimit(concurrency); @@ -179,8 +190,8 @@ async function main() { const contract = new ethers.Contract(factory, FACTORY_ABI, provider); const iface = new ethers.Interface(FACTORY_ABI as unknown as string[]); - const plainTopic = iface.getEvent("PlainPoolDeployed")!.topicHash; - const metaTopic = iface.getEvent("MetaPoolDeployed")!.topicHash; + const plainTopic = iface.getEvent('PlainPoolDeployed')!.topicHash; + const metaTopic = iface.getEvent('MetaPoolDeployed')!.topicHash; const latestBlock = await provider.getBlockNumber(); const toBlock = Math.max(0, latestBlock - finality); @@ -191,7 +202,10 @@ async function main() { let fromBlock: number; if (startBlockArg != null) { fromBlock = Math.max(0, toInt(startBlockArg, 0)); - } else if (cp?.chainId === chainId && cp?.factory?.toLowerCase() === factory.toLowerCase()) { + } else if ( + cp?.chainId === chainId && + cp?.factory?.toLowerCase() === factory.toLowerCase() + ) { fromBlock = cp.lastProcessedBlock + 1; } else { fromBlock = Math.max(0, toBlock - lookbackBlocks); @@ -204,7 +218,10 @@ async function main() { } const lastKnownPoolCount = - cp?.chainId === chainId && cp?.factory?.toLowerCase() === factory.toLowerCase() ? cp.lastKnownPoolCount ?? 0 : 0; + cp?.chainId === chainId && + cp?.factory?.toLowerCase() === factory.toLowerCase() + ? (cp.lastKnownPoolCount ?? 0) + : 0; if (fromBlock > toBlock && poolCount <= lastKnownPoolCount) { const newCp: Checkpoint = { @@ -214,9 +231,19 @@ async function main() { lastKnownPoolCount: poolCount, updatedAt: new Date().toISOString(), }; - atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + '\n'); console.log( - JSON.stringify({ ok: true, note: "No new blocks or pools to process", fromBlock, toBlock, poolCount }, null, 2), + JSON.stringify( + { + ok: true, + note: 'No new blocks or pools to process', + fromBlock, + toBlock, + poolCount, + }, + null, + 2, + ), ); return; } @@ -234,7 +261,9 @@ async function main() { }); eventCount += logs.length; - console.error(`[scan] ${start}..${end} events=${logs.length} total=${eventCount}`); + console.error( + `[scan] ${start}..${end} events=${logs.length} total=${eventCount}`, + ); } } @@ -249,7 +278,7 @@ async function main() { lastKnownPoolCount: poolCount, updatedAt: new Date().toISOString(), }; - atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + '\n'); console.log( JSON.stringify( { @@ -291,7 +320,8 @@ async function main() { contract.get_base_pool(curvePool) as Promise, ]); const basePool = ethers.getAddress(basePoolRaw); - const isPlain = basePool.toLowerCase() === ethers.ZeroAddress.toLowerCase(); + const isPlain = + basePool.toLowerCase() === ethers.ZeroAddress.toLowerCase(); const coins = uniqAddresses(coinsRaw as string[]); return { nCoins: Number(nCoinsBn), coins, isPlain }; }); @@ -299,7 +329,7 @@ async function main() { if (!meta.isPlain) continue; allRecords.push({ - poolType: "stableswapng", + poolType: 'stableswapng', curvePool, tokens: meta.coins, fee: null, @@ -310,7 +340,7 @@ async function main() { } if (newCount > 0) { - atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + "\n"); + atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + '\n'); } const newCp: Checkpoint = { @@ -320,7 +350,7 @@ async function main() { lastKnownPoolCount: poolCount, updatedAt: new Date().toISOString(), }; - atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + '\n'); console.log( JSON.stringify( diff --git a/aggregator-hooks/polling/UniswapV2.ts b/aggregator-hooks/polling/UniswapV2.ts index 87e6aaf2..199abc18 100644 --- a/aggregator-hooks/polling/UniswapV2.ts +++ b/aggregator-hooks/polling/UniswapV2.ts @@ -20,25 +20,36 @@ * LOOKBACK_BLOCKS (optional, default 200000) */ -import "dotenv/config"; -import fs from "node:fs"; -import path from "node:path"; -import { ethers } from "ethers"; -import { parseArgs, getEnvForChain, toInt, resolveOutputPath, resolveCheckpointPath } from "@src/cli"; - -const OUTPUT_FILE = "uniswapv2-pools.json"; -const CHECKPOINT_FILE = "uniswapv2_checkpoint.json"; -const DEFAULT_FACTORY = "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"; -const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +import 'dotenv/config'; +import fs from 'node:fs'; +import path from 'node:path'; +import { ethers } from 'ethers'; +import { + parseArgs, + getEnvForChain, + toInt, + resolveOutputPath, + resolveCheckpointPath, +} from '@src/cli'; + +const OUTPUT_FILE = 'uniswapv2-pools.json'; +const CHECKPOINT_FILE = 'uniswapv2_checkpoint.json'; +const DEFAULT_FACTORY = '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f'; +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; const FACTORY_ABI = [ - "event PairCreated(address indexed token0, address indexed token1, address pair, uint)", + 'event PairCreated(address indexed token0, address indexed token1, address pair, uint)', ]; -type Checkpoint = { chainId: number; factory: string; lastProcessedBlock: number; updatedAt: string }; +type Checkpoint = { + chainId: number; + factory: string; + lastProcessedBlock: number; + updatedAt: string; +}; type UniswapV2PoolConfig = { - poolType: "uniswapv2"; + poolType: 'uniswapv2'; v2Pair: string; currency0: string; currency1: string; @@ -55,14 +66,18 @@ function ensureDirForFile(filePath: string) { } function safeReadJson(filePath: string): T | null { - try { return JSON.parse(fs.readFileSync(filePath, "utf8")) as T; } catch { return null; } + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; + } catch { + return null; + } } function atomicWriteFile(filePath: string, contents: string) { ensureDirForFile(filePath); const abs = path.resolve(filePath); - fs.writeFileSync(abs + ".tmp", contents); - fs.renameSync(abs + ".tmp", abs); + fs.writeFileSync(abs + '.tmp', contents); + fs.renameSync(abs + '.tmp', abs); } function loadExistingKeys(outFile: string): Set { @@ -72,7 +87,9 @@ function loadExistingKeys(outFile: string): Set { const arr = safeReadJson(outFile); if (!Array.isArray(arr)) return keys; for (const x of arr) keys.add(x.v2Pair.toLowerCase()); - } catch { /* ignore */ } + } catch { + /* ignore */ + } return keys; } @@ -86,27 +103,34 @@ function getEnvInt(name: string, chainId: number, def: number): number { async function main() { const args = parseArgs(process.argv.slice(2)); - const chainIdRaw = args["chain-id"]; - if (chainIdRaw == null) { console.error("Missing required --chain-id "); process.exit(1); } + const chainIdRaw = args['chain-id']; + if (chainIdRaw == null) { + console.error('Missing required --chain-id '); + process.exit(1); + } const chainId = toInt(chainIdRaw, 0); - if (chainId <= 0) { console.error("--chain-id must be a positive integer"); process.exit(1); } + if (chainId <= 0) { + console.error('--chain-id must be a positive integer'); + process.exit(1); + } - const rpcUrl = getEnvForChain("RPC_URL", chainId); - if (!rpcUrl) throw new Error("Missing required env: RPC_URL"); + const rpcUrl = getEnvForChain('RPC_URL', chainId); + if (!rpcUrl) throw new Error('Missing required env: RPC_URL'); - const factoryRaw = getEnvForChain("UNISWAP_V2_FACTORY", chainId) ?? DEFAULT_FACTORY; + const factoryRaw = + getEnvForChain('UNISWAP_V2_FACTORY', chainId) ?? DEFAULT_FACTORY; const factory = ethers.getAddress(factoryRaw); - const outputDir = (args["output-dir"] as string) ?? "detected"; - const checkpointDir = (args["checkpoint-dir"] as string) ?? "checkpoints"; - const chunkBlocks = Math.max(1, toInt(args["chunk-blocks"], 10_000)); - const finality = getEnvInt("FINALITY_BLOCKS", chainId, 10); - const lookbackBlocks = getEnvInt("LOOKBACK_BLOCKS", chainId, 200_000); - const startBlockArg = args["start-block"]; + const outputDir = (args['output-dir'] as string) ?? 'detected'; + const checkpointDir = (args['checkpoint-dir'] as string) ?? 'checkpoints'; + const chunkBlocks = Math.max(1, toInt(args['chunk-blocks'], 10_000)); + const finality = getEnvInt('FINALITY_BLOCKS', chainId, 10); + const lookbackBlocks = getEnvInt('LOOKBACK_BLOCKS', chainId, 200_000); + const startBlockArg = args['start-block']; const provider = new ethers.JsonRpcProvider(rpcUrl); const iface = new ethers.Interface(FACTORY_ABI); - const topic0 = iface.getEvent("PairCreated")!.topicHash; + const topic0 = iface.getEvent('PairCreated')!.topicHash; const latestBlock = await provider.getBlockNumber(); const toBlock = Math.max(0, latestBlock - finality); @@ -117,16 +141,30 @@ async function main() { let fromBlock: number; if (startBlockArg != null) { fromBlock = Math.max(0, toInt(startBlockArg, 0)); - } else if (cp?.chainId === chainId && cp?.factory?.toLowerCase() === factory.toLowerCase()) { + } else if ( + cp?.chainId === chainId && + cp?.factory?.toLowerCase() === factory.toLowerCase() + ) { fromBlock = cp.lastProcessedBlock + 1; } else { fromBlock = Math.max(0, toBlock - lookbackBlocks); } if (fromBlock > toBlock) { - const newCp: Checkpoint = { chainId, factory, lastProcessedBlock: toBlock, updatedAt: new Date().toISOString() }; - atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); - console.log(JSON.stringify({ ok: true, note: "No new blocks to scan", fromBlock, toBlock }, null, 2)); + const newCp: Checkpoint = { + chainId, + factory, + lastProcessedBlock: toBlock, + updatedAt: new Date().toISOString(), + }; + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + '\n'); + console.log( + JSON.stringify( + { ok: true, note: 'No new blocks to scan', fromBlock, toBlock }, + null, + 2, + ), + ); return; } @@ -139,13 +177,22 @@ async function main() { for (let start = fromBlock; start <= toBlock; start += chunkBlocks) { const end = Math.min(toBlock, start + chunkBlocks - 1); - const logs = await provider.getLogs({ address: factory, topics: [topic0], fromBlock: start, toBlock: end }); + const logs = await provider.getLogs({ + address: factory, + topics: [topic0], + fromBlock: start, + toBlock: end, + }); totalLogs += logs.length; const newRecords: UniswapV2PoolConfig[] = []; for (const log of logs) { let parsed: ethers.LogDescription | null; - try { parsed = iface.parseLog(log); } catch { continue; } + try { + parsed = iface.parseLog(log); + } catch { + continue; + } if (!parsed) continue; const token0 = ethers.getAddress(parsed.args.token0 as string); @@ -157,24 +204,52 @@ async function main() { seenKeys.add(pair.toLowerCase()); newPools++; - newRecords.push({ poolType: "uniswapv2", v2Pair: pair, currency0: token0, currency1: token1, tickSpacing: 1, sqrtPriceX96: null }); + newRecords.push({ + poolType: 'uniswapv2', + v2Pair: pair, + currency0: token0, + currency1: token1, + tickSpacing: 1, + sqrtPriceX96: null, + }); } if (newRecords.length > 0) { allRecords = allRecords.concat(newRecords); - atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + "\n"); + atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + '\n'); } - const interimCp: Checkpoint = { chainId, factory, lastProcessedBlock: end, updatedAt: new Date().toISOString() }; - atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + "\n"); - console.error(`[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`); + const interimCp: Checkpoint = { + chainId, + factory, + lastProcessedBlock: end, + updatedAt: new Date().toISOString(), + }; + atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + '\n'); + console.error( + `[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`, + ); } - console.log(JSON.stringify({ - ok: true, chainId, factory, - scanned: { fromBlock, toBlock, latestBlock, finality, chunkBlocks }, - logsFound: totalLogs, newPools, outFile: outPath, checkpointFile: cpPath, - }, null, 2)); + console.log( + JSON.stringify( + { + ok: true, + chainId, + factory, + scanned: { fromBlock, toBlock, latestBlock, finality, chunkBlocks }, + logsFound: totalLogs, + newPools, + outFile: outPath, + checkpointFile: cpPath, + }, + null, + 2, + ), + ); } -main().catch((err) => { console.error(err); process.exitCode = 1; }); +main().catch((err) => { + console.error(err); + process.exitCode = 1; +}); diff --git a/aggregator-hooks/polling/UniswapV3.ts b/aggregator-hooks/polling/UniswapV3.ts index 7659503a..57a0e0d8 100644 --- a/aggregator-hooks/polling/UniswapV3.ts +++ b/aggregator-hooks/polling/UniswapV3.ts @@ -20,25 +20,36 @@ * LOOKBACK_BLOCKS (optional, default 200000) */ -import "dotenv/config"; -import fs from "node:fs"; -import path from "node:path"; -import { ethers } from "ethers"; -import { parseArgs, getEnvForChain, toInt, resolveOutputPath, resolveCheckpointPath } from "@src/cli"; - -const OUTPUT_FILE = "uniswapv3-pools.json"; -const CHECKPOINT_FILE = "uniswapv3_checkpoint.json"; -const DEFAULT_FACTORY = "0x1F98431c8aD98523631AE4a59f267346ea31F984"; -const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +import 'dotenv/config'; +import fs from 'node:fs'; +import path from 'node:path'; +import { ethers } from 'ethers'; +import { + parseArgs, + getEnvForChain, + toInt, + resolveOutputPath, + resolveCheckpointPath, +} from '@src/cli'; + +const OUTPUT_FILE = 'uniswapv3-pools.json'; +const CHECKPOINT_FILE = 'uniswapv3_checkpoint.json'; +const DEFAULT_FACTORY = '0x1F98431c8aD98523631AE4a59f267346ea31F984'; +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; const FACTORY_ABI = [ - "event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)", + 'event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)', ]; -type Checkpoint = { chainId: number; factory: string; lastProcessedBlock: number; updatedAt: string }; +type Checkpoint = { + chainId: number; + factory: string; + lastProcessedBlock: number; + updatedAt: string; +}; type UniswapV3PoolConfig = { - poolType: "uniswapv3"; + poolType: 'uniswapv3'; v3Pool: string; currency0: string; currency1: string; @@ -56,14 +67,18 @@ function ensureDirForFile(filePath: string) { } function safeReadJson(filePath: string): T | null { - try { return JSON.parse(fs.readFileSync(filePath, "utf8")) as T; } catch { return null; } + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; + } catch { + return null; + } } function atomicWriteFile(filePath: string, contents: string) { ensureDirForFile(filePath); const abs = path.resolve(filePath); - fs.writeFileSync(abs + ".tmp", contents); - fs.renameSync(abs + ".tmp", abs); + fs.writeFileSync(abs + '.tmp', contents); + fs.renameSync(abs + '.tmp', abs); } function loadExistingKeys(outFile: string): Set { @@ -73,7 +88,9 @@ function loadExistingKeys(outFile: string): Set { const arr = safeReadJson(outFile); if (!Array.isArray(arr)) return keys; for (const x of arr) keys.add(x.v3Pool.toLowerCase()); - } catch { /* ignore */ } + } catch { + /* ignore */ + } return keys; } @@ -87,27 +104,34 @@ function getEnvInt(name: string, chainId: number, def: number): number { async function main() { const args = parseArgs(process.argv.slice(2)); - const chainIdRaw = args["chain-id"]; - if (chainIdRaw == null) { console.error("Missing required --chain-id "); process.exit(1); } + const chainIdRaw = args['chain-id']; + if (chainIdRaw == null) { + console.error('Missing required --chain-id '); + process.exit(1); + } const chainId = toInt(chainIdRaw, 0); - if (chainId <= 0) { console.error("--chain-id must be a positive integer"); process.exit(1); } + if (chainId <= 0) { + console.error('--chain-id must be a positive integer'); + process.exit(1); + } - const rpcUrl = getEnvForChain("RPC_URL", chainId); - if (!rpcUrl) throw new Error("Missing required env: RPC_URL"); + const rpcUrl = getEnvForChain('RPC_URL', chainId); + if (!rpcUrl) throw new Error('Missing required env: RPC_URL'); - const factoryRaw = getEnvForChain("UNISWAP_V3_FACTORY", chainId) ?? DEFAULT_FACTORY; + const factoryRaw = + getEnvForChain('UNISWAP_V3_FACTORY', chainId) ?? DEFAULT_FACTORY; const factory = ethers.getAddress(factoryRaw); - const outputDir = (args["output-dir"] as string) ?? "detected"; - const checkpointDir = (args["checkpoint-dir"] as string) ?? "checkpoints"; - const chunkBlocks = Math.max(1, toInt(args["chunk-blocks"], 10_000)); - const finality = getEnvInt("FINALITY_BLOCKS", chainId, 10); - const lookbackBlocks = getEnvInt("LOOKBACK_BLOCKS", chainId, 200_000); - const startBlockArg = args["start-block"]; + const outputDir = (args['output-dir'] as string) ?? 'detected'; + const checkpointDir = (args['checkpoint-dir'] as string) ?? 'checkpoints'; + const chunkBlocks = Math.max(1, toInt(args['chunk-blocks'], 10_000)); + const finality = getEnvInt('FINALITY_BLOCKS', chainId, 10); + const lookbackBlocks = getEnvInt('LOOKBACK_BLOCKS', chainId, 200_000); + const startBlockArg = args['start-block']; const provider = new ethers.JsonRpcProvider(rpcUrl); const iface = new ethers.Interface(FACTORY_ABI); - const topic0 = iface.getEvent("PoolCreated")!.topicHash; + const topic0 = iface.getEvent('PoolCreated')!.topicHash; const latestBlock = await provider.getBlockNumber(); const toBlock = Math.max(0, latestBlock - finality); @@ -118,16 +142,30 @@ async function main() { let fromBlock: number; if (startBlockArg != null) { fromBlock = Math.max(0, toInt(startBlockArg, 0)); - } else if (cp?.chainId === chainId && cp?.factory?.toLowerCase() === factory.toLowerCase()) { + } else if ( + cp?.chainId === chainId && + cp?.factory?.toLowerCase() === factory.toLowerCase() + ) { fromBlock = cp.lastProcessedBlock + 1; } else { fromBlock = Math.max(0, toBlock - lookbackBlocks); } if (fromBlock > toBlock) { - const newCp: Checkpoint = { chainId, factory, lastProcessedBlock: toBlock, updatedAt: new Date().toISOString() }; - atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + "\n"); - console.log(JSON.stringify({ ok: true, note: "No new blocks to scan", fromBlock, toBlock }, null, 2)); + const newCp: Checkpoint = { + chainId, + factory, + lastProcessedBlock: toBlock, + updatedAt: new Date().toISOString(), + }; + atomicWriteFile(cpPath, JSON.stringify(newCp, null, 2) + '\n'); + console.log( + JSON.stringify( + { ok: true, note: 'No new blocks to scan', fromBlock, toBlock }, + null, + 2, + ), + ); return; } @@ -140,13 +178,22 @@ async function main() { for (let start = fromBlock; start <= toBlock; start += chunkBlocks) { const end = Math.min(toBlock, start + chunkBlocks - 1); - const logs = await provider.getLogs({ address: factory, topics: [topic0], fromBlock: start, toBlock: end }); + const logs = await provider.getLogs({ + address: factory, + topics: [topic0], + fromBlock: start, + toBlock: end, + }); totalLogs += logs.length; const newRecords: UniswapV3PoolConfig[] = []; for (const log of logs) { let parsed: ethers.LogDescription | null; - try { parsed = iface.parseLog(log); } catch { continue; } + try { + parsed = iface.parseLog(log); + } catch { + continue; + } if (!parsed) continue; const token0 = ethers.getAddress(parsed.args.token0 as string); @@ -160,24 +207,53 @@ async function main() { seenKeys.add(pool.toLowerCase()); newPools++; - newRecords.push({ poolType: "uniswapv3", v3Pool: pool, currency0: token0, currency1: token1, fee, tickSpacing, sqrtPriceX96: null }); + newRecords.push({ + poolType: 'uniswapv3', + v3Pool: pool, + currency0: token0, + currency1: token1, + fee, + tickSpacing, + sqrtPriceX96: null, + }); } if (newRecords.length > 0) { allRecords = allRecords.concat(newRecords); - atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + "\n"); + atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + '\n'); } - const interimCp: Checkpoint = { chainId, factory, lastProcessedBlock: end, updatedAt: new Date().toISOString() }; - atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + "\n"); - console.error(`[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`); + const interimCp: Checkpoint = { + chainId, + factory, + lastProcessedBlock: end, + updatedAt: new Date().toISOString(), + }; + atomicWriteFile(cpPath, JSON.stringify(interimCp, null, 2) + '\n'); + console.error( + `[scan] ${start}..${end} logs=${logs.length} new=${newRecords.length}`, + ); } - console.log(JSON.stringify({ - ok: true, chainId, factory, - scanned: { fromBlock, toBlock, latestBlock, finality, chunkBlocks }, - logsFound: totalLogs, newPools, outFile: outPath, checkpointFile: cpPath, - }, null, 2)); + console.log( + JSON.stringify( + { + ok: true, + chainId, + factory, + scanned: { fromBlock, toBlock, latestBlock, finality, chunkBlocks }, + logsFound: totalLogs, + newPools, + outFile: outPath, + checkpointFile: cpPath, + }, + null, + 2, + ), + ); } -main().catch((err) => { console.error(err); process.exitCode = 1; }); +main().catch((err) => { + console.error(err); + process.exitCode = 1; +}); diff --git a/aggregator-hooks/src/cli.ts b/aggregator-hooks/src/cli.ts index d0b6347e..2adbcf45 100644 --- a/aggregator-hooks/src/cli.ts +++ b/aggregator-hooks/src/cli.ts @@ -2,17 +2,17 @@ * Shared CLI and env utilities for discovery scripts. * All scripts use chain-ID-suffixed env vars and consistent CLI args. */ -import path from "node:path"; +import path from 'node:path'; /** Parse generic --key value args. Keys with no value get true. */ export function parseArgs(argv: string[]): Record { const out: Record = {}; for (let i = 0; i < argv.length; i++) { const a = argv[i]; - if (!a.startsWith("--")) continue; + if (!a.startsWith('--')) continue; const key = a.slice(2); const next = argv[i + 1]; - if (!next || next.startsWith("--")) { + if (!next || next.startsWith('--')) { out[key] = true; } else { out[key] = next; @@ -23,25 +23,30 @@ export function parseArgs(argv: string[]): Record { } /** Get env var: VAR_${chainId} first, fallback to VAR for single-chain usage. */ -export function getEnvForChain(name: string, chainId: number): string | undefined { +export function getEnvForChain( + name: string, + chainId: number, +): string | undefined { const v = process.env[`${name}_${chainId}`]; if (v != null && String(v).trim()) return String(v).trim(); const fallback = process.env[name]; - if (fallback != null && String(fallback).trim()) return String(fallback).trim(); + if (fallback != null && String(fallback).trim()) + return String(fallback).trim(); return undefined; } /** Require env var; throws if missing. */ export function mustEnvForChain(name: string, chainId: number): string { const v = getEnvForChain(name, chainId); - if (!v) throw new Error(`Missing required env var: ${name}_${chainId} or ${name}`); + if (!v) + throw new Error(`Missing required env var: ${name}_${chainId} or ${name}`); return v; } /** Parse int from string or number; return default if invalid. */ export function toInt(v: unknown, def: number): number { - if (typeof v === "number") return Number.isFinite(v) ? Math.floor(v) : def; - if (typeof v === "string") { + if (typeof v === 'number') return Number.isFinite(v) ? Math.floor(v) : def; + if (typeof v === 'string') { const n = Number(v); return Number.isFinite(n) ? Math.floor(n) : def; } @@ -49,12 +54,20 @@ export function toInt(v: unknown, def: number): number { } /** Resolve output path: outputDir/chainId/filename */ -export function resolveOutputPath(outputDir: string, chainId: number, filename: string): string { +export function resolveOutputPath( + outputDir: string, + chainId: number, + filename: string, +): string { return path.resolve(outputDir, String(chainId), filename); } /** Resolve checkpoint path: checkpointDir/chainId/filename */ -export function resolveCheckpointPath(checkpointDir: string, chainId: number, filename: string): string { +export function resolveCheckpointPath( + checkpointDir: string, + chainId: number, + filename: string, +): string { return path.resolve(checkpointDir, String(chainId), filename); } diff --git a/aggregator-hooks/src/createPools.ts b/aggregator-hooks/src/createPools.ts index c720777e..023bcd50 100644 --- a/aggregator-hooks/src/createPools.ts +++ b/aggregator-hooks/src/createPools.ts @@ -1,14 +1,14 @@ #!/usr/bin/env node -import "dotenv/config"; -import { ethers } from "ethers"; -import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"; -import { execFileSync, spawn } from "child_process"; -import { join, dirname } from "path"; -import { fileURLToPath } from "url"; - -import { getEnvForChain, toInt } from "./cli.js"; -import { createLogger, type Logger } from "./logger.js"; +import 'dotenv/config'; +import { ethers } from 'ethers'; +import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; +import { execFileSync, spawn } from 'child_process'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +import { getEnvForChain, toInt } from './cli.js'; +import { createLogger, type Logger } from './logger.js'; import { CREATION_MODULES, POOL_TYPES, @@ -18,22 +18,28 @@ import { type PoolEntry, type PoolKeyRecord, type FactoryImmutables, -} from "../creation-modules/index.js"; +} from '../creation-modules/index.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); -const projectRoot = join(__dirname, "..", ".."); +const projectRoot = join(__dirname, '..', '..'); /** Compute PoolId = keccak256(abi.encode(poolKey)) matching Uniswap v4 PoolIdLibrary.toId() */ function computePoolId(poolKey: PoolKeyRecord): string { const encoded = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "address", "uint24", "int24", "address"], - [poolKey.currency0, poolKey.currency1, poolKey.fee, poolKey.tickSpacing, poolKey.hooks], + ['address', 'address', 'uint24', 'int24', 'address'], + [ + poolKey.currency0, + poolKey.currency1, + poolKey.fee, + poolKey.tickSpacing, + poolKey.hooks, + ], ); return ethers.keccak256(encoded); } // Foundry's default CREATE2 deployer -const CREATE2_DEPLOYER = "0x4e59b44847b379578588920cA78FbF26c0B4956C"; +const CREATE2_DEPLOYER = '0x4e59b44847b379578588920cA78FbF26c0B4956C'; interface VerifyOptions { enabled: boolean; @@ -59,44 +65,44 @@ interface ParsedArgs { } function isPoolType(s: unknown): s is string { - return typeof s === "string" && POOL_TYPES.includes(s); + return typeof s === 'string' && POOL_TYPES.includes(s); } function parseArgs(): ParsedArgs { const args = process.argv.slice(2); - const selfDeployIndex = args.indexOf("--self-deploy"); + const selfDeployIndex = args.indexOf('--self-deploy'); const selfDeploy = selfDeployIndex !== -1; const flagNames = [ - "--self-deploy", - "--chain-id", - "--registry-dir", - "--dry-run", - "--verbose", - "-v", - "--start-at", - "--jobs", - "-j", - "--priority-gas-price", - "--verify", - "--verifier-url", - "--verifier", - "--compiler-version", + '--self-deploy', + '--chain-id', + '--registry-dir', + '--dry-run', + '--verbose', + '-v', + '--start-at', + '--jobs', + '-j', + '--priority-gas-price', + '--verify', + '--verifier-url', + '--verifier', + '--compiler-version', ]; const positionalArgs: string[] = []; for (let i = 0; i < args.length; i++) { const a = args[i]; if (flagNames.includes(a)) { if ( - a === "--chain-id" || - a === "--registry-dir" || - a === "--start-at" || - a === "--jobs" || - a === "-j" || - a === "--priority-gas-price" || - a === "--verifier-url" || - a === "--verifier" || - a === "--compiler-version" + a === '--chain-id' || + a === '--registry-dir' || + a === '--start-at' || + a === '--jobs' || + a === '-j' || + a === '--priority-gas-price' || + a === '--verifier-url' || + a === '--verifier' || + a === '--compiler-version' ) i++; continue; @@ -106,44 +112,70 @@ function parseArgs(): ParsedArgs { const minArgs = 1; if (positionalArgs.length < minArgs) { - console.error("Usage: ts-node createPools.ts [factoryAddress] [--self-deploy] [--chain-id ]"); + console.error( + 'Usage: ts-node createPools.ts [factoryAddress] [--self-deploy] [--chain-id ]', + ); console.error( " jsonFile: Path to JSON file containing pool configurations (each config must have a 'poolType' field)", ); - console.error(" factoryAddress: Factory contract address (required unless --self-deploy)"); - console.error(" --self-deploy: Deploy hooks directly from wallet instead of via factory"); - console.error(" --chain-id : Chain ID; selects RPC_URL_ from env (e.g. RPC_URL_1 for mainnet)"); - console.error(""); - console.error("Modes:"); console.error( - " Factory mode: ts-node createPools.ts pools.json 0xFactoryAddr [--chain-id 1] [--registry-dir ./deployed-pools]", + ' factoryAddress: Factory contract address (required unless --self-deploy)', + ); + console.error( + ' --self-deploy: Deploy hooks directly from wallet instead of via factory', + ); + console.error( + ' --chain-id : Chain ID; selects RPC_URL_ from env (e.g. RPC_URL_1 for mainnet)', + ); + console.error(''); + console.error('Modes:'); + console.error( + ' Factory mode: ts-node createPools.ts pools.json 0xFactoryAddr [--chain-id 1] [--registry-dir ./deployed-pools]', + ); + console.error( + ' Self-deploy: ts-node createPools.ts pools.json --self-deploy [--chain-id 1] [--registry-dir ./deployed-pools]', + ); + console.error( + ' --dry-run: Simulate without broadcasting (self-deploy: forge script without --broadcast; factory: staticCall only)', + ); + console.error( + ' --verbose, -v: Run forge scripts verbosely and log full output on errors', + ); + console.error( + ' --start-at : Start at 1-based pool index (skip earlier pools). e.g. --start-at 3 to resume from pool 3.', + ); + console.error( + ' --jobs , -j : Run N parallel salt mining workers (default 1). Speeds up mining.', + ); + console.error( + ' --priority-gas-price : Max priority fee per gas for EIP1559 (e.g. 3gwei). Speeds up tx inclusion.', + ); + console.error( + ' --verify: Submit hook contract for block explorer verification after deployment', + ); + console.error( + ' --verifier : Verifier backend (etherscan|blockscout|sourcify). Default: etherscan', + ); + console.error( + ' --verifier-url : Custom verifier API URL (e.g. for blockscout)', ); console.error( - " Self-deploy: ts-node createPools.ts pools.json --self-deploy [--chain-id 1] [--registry-dir ./deployed-pools]", + ' --compiler-version : Solc version used to compile the hook (e.g. 0.8.24). Required when the factory', ); console.error( - " --dry-run: Simulate without broadcasting (self-deploy: forge script without --broadcast; factory: staticCall only)", + ' was deployed with a different solc than the current local environment.', ); - console.error(" --verbose, -v: Run forge scripts verbosely and log full output on errors"); + console.error(''); + console.error('Environment variables:'); console.error( - " --start-at : Start at 1-based pool index (skip earlier pools). e.g. --start-at 3 to resume from pool 3.", + ' RPC_URL_: RPC endpoint (required when --chain-id set)', ); - console.error(" --jobs , -j : Run N parallel salt mining workers (default 1). Speeds up mining."); console.error( - " --priority-gas-price : Max priority fee per gas for EIP1559 (e.g. 3gwei). Speeds up tx inclusion.", + ' PRIVATE_KEY: Private key for signing transactions (required)', ); - console.error(" --verify: Submit hook contract for block explorer verification after deployment"); - console.error(" --verifier : Verifier backend (etherscan|blockscout|sourcify). Default: etherscan"); - console.error(" --verifier-url : Custom verifier API URL (e.g. for blockscout)"); console.error( - " --compiler-version : Solc version used to compile the hook (e.g. 0.8.24). Required when the factory", + ' ETHERSCAN_API_KEY or ETHERSCAN_API_KEY_: API key for block explorer verification', ); - console.error(" was deployed with a different solc than the current local environment."); - console.error(""); - console.error("Environment variables:"); - console.error(" RPC_URL_: RPC endpoint (required when --chain-id set)"); - console.error(" PRIVATE_KEY: Private key for signing transactions (required)"); - console.error(" ETHERSCAN_API_KEY or ETHERSCAN_API_KEY_: API key for block explorer verification"); process.exit(1); } @@ -154,76 +186,103 @@ function parseArgs(): ParsedArgs { ? (ethers.getAddress(positionalArgs[1]) as Address) : null; - if (selfDeploy && positionalArgs.length >= 2 && positionalArgs[1].startsWith("0x")) { - console.error("Error: --self-deploy and factoryAddress are mutually exclusive"); + if ( + selfDeploy && + positionalArgs.length >= 2 && + positionalArgs[1].startsWith('0x') + ) { + console.error( + 'Error: --self-deploy and factoryAddress are mutually exclusive', + ); process.exit(1); } if (!selfDeploy && (!factoryAddress || positionalArgs.length < 2)) { - console.error("Error: In factory mode, factoryAddress is required (e.g. createPools.ts pools.json 0xFactoryAddr)"); + console.error( + 'Error: In factory mode, factoryAddress is required (e.g. createPools.ts pools.json 0xFactoryAddr)', + ); process.exit(1); } - const chainIdIndex = args.indexOf("--chain-id"); - const chainIdRaw = chainIdIndex !== -1 && args[chainIdIndex + 1] ? args[chainIdIndex + 1] : null; + const chainIdIndex = args.indexOf('--chain-id'); + const chainIdRaw = + chainIdIndex !== -1 && args[chainIdIndex + 1] + ? args[chainIdIndex + 1] + : null; const chainId = chainIdRaw != null ? toInt(String(chainIdRaw), 0) : null; const rpcUrl = chainId != null && chainId > 0 - ? getEnvForChain("RPC_URL", chainId) - : (process.env.RPC_URL ?? "").trim() || undefined; + ? getEnvForChain('RPC_URL', chainId) + : (process.env.RPC_URL ?? '').trim() || undefined; if (!rpcUrl) { console.error( chainId != null && chainId > 0 ? `Error: RPC_URL_${chainId} environment variable is required when using --chain-id ${chainId}` - : "Error: RPC_URL environment variable is required", + : 'Error: RPC_URL environment variable is required', ); process.exit(1); } if (!process.env.PRIVATE_KEY) { - console.error("Error: PRIVATE_KEY environment variable is required"); + console.error('Error: PRIVATE_KEY environment variable is required'); process.exit(1); } - const registryDirIndex = args.indexOf("--registry-dir"); + const registryDirIndex = args.indexOf('--registry-dir'); const registryDir = - registryDirIndex !== -1 && args[registryDirIndex + 1] ? args[registryDirIndex + 1] : "created-pools"; + registryDirIndex !== -1 && args[registryDirIndex + 1] + ? args[registryDirIndex + 1] + : 'created-pools'; - const dryRun = args.includes("--dry-run"); - const verbose = args.includes("--verbose") || args.includes("-v"); + const dryRun = args.includes('--dry-run'); + const verbose = args.includes('--verbose') || args.includes('-v'); - const startAtIndex = args.indexOf("--start-at"); - const startAtRaw = startAtIndex !== -1 && args[startAtIndex + 1] ? args[startAtIndex + 1] : null; + const startAtIndex = args.indexOf('--start-at'); + const startAtRaw = + startAtIndex !== -1 && args[startAtIndex + 1] + ? args[startAtIndex + 1] + : null; const startAt = startAtRaw != null ? toInt(String(startAtRaw), 1) : 1; if (startAt < 1) { - console.error("Error: --start-at must be >= 1"); + console.error('Error: --start-at must be >= 1'); process.exit(1); } - const jobsIndex = args.indexOf("--jobs"); - const jobsIndexShort = args.indexOf("-j"); + const jobsIndex = args.indexOf('--jobs'); + const jobsIndexShort = args.indexOf('-j'); const jobsIdx = jobsIndex !== -1 ? jobsIndex : jobsIndexShort; - const jobsRaw = jobsIdx !== -1 && args[jobsIdx + 1] ? args[jobsIdx + 1] : null; + const jobsRaw = + jobsIdx !== -1 && args[jobsIdx + 1] ? args[jobsIdx + 1] : null; const jobs = jobsRaw != null ? toInt(String(jobsRaw), 1) : 1; if (jobs < 1 || jobs > 16) { - console.error("Error: --jobs must be between 1 and 16"); + console.error('Error: --jobs must be between 1 and 16'); process.exit(1); } - const priorityGasPriceIndex = args.indexOf("--priority-gas-price"); + const priorityGasPriceIndex = args.indexOf('--priority-gas-price'); const priorityGasPriceRaw = - priorityGasPriceIndex !== -1 && args[priorityGasPriceIndex + 1] ? args[priorityGasPriceIndex + 1] : null; + priorityGasPriceIndex !== -1 && args[priorityGasPriceIndex + 1] + ? args[priorityGasPriceIndex + 1] + : null; const priorityGasPrice = priorityGasPriceRaw?.trim() || null; - const verifyEnabled = args.includes("--verify"); - const verifierIndex = args.indexOf("--verifier"); - const verifier = verifierIndex !== -1 && args[verifierIndex + 1] ? args[verifierIndex + 1] : null; - const verifierUrlIndex = args.indexOf("--verifier-url"); - const verifierUrl = verifierUrlIndex !== -1 && args[verifierUrlIndex + 1] ? args[verifierUrlIndex + 1] : null; - const compilerVersionIndex = args.indexOf("--compiler-version"); + const verifyEnabled = args.includes('--verify'); + const verifierIndex = args.indexOf('--verifier'); + const verifier = + verifierIndex !== -1 && args[verifierIndex + 1] + ? args[verifierIndex + 1] + : null; + const verifierUrlIndex = args.indexOf('--verifier-url'); + const verifierUrl = + verifierUrlIndex !== -1 && args[verifierUrlIndex + 1] + ? args[verifierUrlIndex + 1] + : null; + const compilerVersionIndex = args.indexOf('--compiler-version'); const compilerVersion = - compilerVersionIndex !== -1 && args[compilerVersionIndex + 1] ? args[compilerVersionIndex + 1] : null; + compilerVersionIndex !== -1 && args[compilerVersionIndex + 1] + ? args[compilerVersionIndex + 1] + : null; return { jsonFile, @@ -247,13 +306,18 @@ function parseArgs(): ParsedArgs { }; } -function appendToRegistryFile(registryDir: string, poolType: string, entry: PoolDeployedEntry, log: Logger): void { +function appendToRegistryFile( + registryDir: string, + poolType: string, + entry: PoolDeployedEntry, + log: Logger, +): void { const fileName = `deployed-${poolType}.json`; const filePath = join(registryDir, fileName); let poolsDeployed: PoolDeployedEntry[]; if (existsSync(filePath)) { - const content = readFileSync(filePath, "utf-8"); + const content = readFileSync(filePath, 'utf-8'); poolsDeployed = JSON.parse(content) as PoolDeployedEntry[]; } else { poolsDeployed = []; @@ -275,7 +339,7 @@ function appendToRegistryFile(registryDir: string, poolType: string, entry: Pool function appendToEnvFile(filePath: string, key: string, value: string): void { let lines: string[] = []; if (existsSync(filePath)) { - lines = readFileSync(filePath, "utf-8").split("\n"); + lines = readFileSync(filePath, 'utf-8').split('\n'); } const prefix = `${key}=`; const idx = lines.findIndex((l) => l.startsWith(prefix)); @@ -286,12 +350,12 @@ function appendToEnvFile(filePath: string, key: string, value: string): void { lines.push(newLine); } // Ensure file ends with a newline - if (lines[lines.length - 1] !== "") lines.push(""); - writeFileSync(filePath, lines.join("\n")); + if (lines[lines.length - 1] !== '') lines.push(''); + writeFileSync(filePath, lines.join('\n')); } const POOL_MANAGER_INIT_ABI = [ - "function initialize((address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) key, uint160 sqrtPriceX96) returns (int24 tick)", + 'function initialize((address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) key, uint160 sqrtPriceX96) returns (int24 tick)', ]; async function initializeSingletonPool( @@ -302,10 +366,16 @@ async function initializeSingletonPool( log: Logger, dryRun: boolean, ): Promise<{ blockNumber: number; txHash: string } | null> { - const poolManager = new ethers.Contract(poolManagerAddress, POOL_MANAGER_INIT_ABI, signer); + const poolManager = new ethers.Contract( + poolManagerAddress, + POOL_MANAGER_INIT_ABI, + signer, + ); - log.info(`Calling poolManager.initialize for pool: ${poolKey.currency0} / ${poolKey.currency1}`); - if (dryRun) log.info("(dry run - no broadcast)"); + log.info( + `Calling poolManager.initialize for pool: ${poolKey.currency0} / ${poolKey.currency1}`, + ); + if (dryRun) log.info('(dry run - no broadcast)'); try { if (dryRun) { @@ -320,7 +390,7 @@ async function initializeSingletonPool( log.success(`Pool initialized in block ${receipt!.blockNumber}`); return { blockNumber: Number(receipt!.blockNumber), txHash: tx.hash }; } catch (error) { - log.error("Error initializing pool:", error); + log.error('Error initializing pool:', error); throw error; } } @@ -331,14 +401,21 @@ async function initializeSingletonPool( * contains a "metadata" object with compiler.version (e.g. "0.8.24+commit.e11b9ed9"). * Returns null if the artifact is missing or unparseable. */ -function readCompilerVersionFromArtifact(contractIdentifier: string): string | null { +function readCompilerVersionFromArtifact( + contractIdentifier: string, +): string | null { try { // contractIdentifier: "path/to/Foo.sol:Foo" - const contractName = contractIdentifier.split(":")[1]; - const solFile = contractIdentifier.split(":")[0].split("/").pop()!; // "Foo.sol" - const artifactPath = join(projectRoot, "out", solFile, `${contractName}.json`); + const contractName = contractIdentifier.split(':')[1]; + const solFile = contractIdentifier.split(':')[0].split('/').pop()!; // "Foo.sol" + const artifactPath = join( + projectRoot, + 'out', + solFile, + `${contractName}.json`, + ); if (!existsSync(artifactPath)) return null; - const artifact = JSON.parse(readFileSync(artifactPath, "utf-8")) as { + const artifact = JSON.parse(readFileSync(artifactPath, 'utf-8')) as { metadata?: { compiler?: { version?: string } }; }; return artifact?.metadata?.compiler?.version ?? null; @@ -356,20 +433,30 @@ function verifyContract( log: Logger, ): void { // Resolve verifier URL: explicit flag > BLOCKSCOUT_API_URL env > none - const resolvedVerifierUrl = verifyOptions.verifierUrl ?? getEnvForChain("BLOCKSCOUT_API_URL", chainId) ?? null; + const resolvedVerifierUrl = + verifyOptions.verifierUrl ?? + getEnvForChain('BLOCKSCOUT_API_URL', chainId) ?? + null; // If a Blockscout URL is in play but no verifier was explicitly named, use blockscout const isBlockscout = - verifyOptions.verifier === "blockscout" || (!verifyOptions.verifier && resolvedVerifierUrl != null); - const verifier = verifyOptions.verifier ?? (isBlockscout ? "blockscout" : "etherscan"); + verifyOptions.verifier === 'blockscout' || + (!verifyOptions.verifier && resolvedVerifierUrl != null); + const verifier = + verifyOptions.verifier ?? (isBlockscout ? 'blockscout' : 'etherscan'); // API key: explicit flag > ETHERSCAN_API_KEY env (blockscout public instances don't require one) - const apiKey = verifyOptions.etherscanApiKey ?? getEnvForChain("ETHERSCAN_API_KEY", chainId) ?? null; + const apiKey = + verifyOptions.etherscanApiKey ?? + getEnvForChain('ETHERSCAN_API_KEY', chainId) ?? + null; // Compiler version: explicit flag > build artifact > let forge auto-detect // In factory mode this matters: the factory embeds the hook bytecode at its own compile time, // so the solc version used then must match what forge verify-contract uses now. - const compilerVersion = verifyOptions.compilerVersion ?? readCompilerVersionFromArtifact(contractIdentifier); + const compilerVersion = + verifyOptions.compilerVersion ?? + readCompilerVersionFromArtifact(contractIdentifier); if (!compilerVersion) { log.info( ` Warning: compiler version not found in build artifacts. If the factory was compiled with a different` + @@ -379,69 +466,83 @@ function verifyContract( log.info(` Compiler version: ${compilerVersion}`); } - log.info(`Submitting ${contractIdentifier} at ${hookAddress} for verification (verifier: ${verifier})...`); + log.info( + `Submitting ${contractIdentifier} at ${hookAddress} for verification (verifier: ${verifier})...`, + ); const forgeArgs = [ - "verify-contract", + 'verify-contract', hookAddress, contractIdentifier, - "--constructor-args", + '--constructor-args', constructorArgs, - "--chain-id", + '--chain-id', chainId.toString(), - "--verifier", + '--verifier', verifier, - "--watch", + '--watch', ]; - if (apiKey) forgeArgs.push("--etherscan-api-key", apiKey); - if (resolvedVerifierUrl) forgeArgs.push("--verifier-url", resolvedVerifierUrl); - if (compilerVersion) forgeArgs.push("--compiler-version", compilerVersion); + if (apiKey) forgeArgs.push('--etherscan-api-key', apiKey); + if (resolvedVerifierUrl) + forgeArgs.push('--verifier-url', resolvedVerifierUrl); + if (compilerVersion) forgeArgs.push('--compiler-version', compilerVersion); try { - const output = execFileSync("forge", forgeArgs, { - encoding: "utf-8", + const output = execFileSync('forge', forgeArgs, { + encoding: 'utf-8', cwd: projectRoot, }); log.success(`Verification submitted successfully for ${hookAddress}`); - if (output.trim()) log.verbose(`\n--- forge verify-contract output ---\n${output}`); + if (output.trim()) + log.verbose(`\n--- forge verify-contract output ---\n${output}`); } catch (error) { - const execErr = error as { stdout?: string; stderr?: string; message?: string }; + const execErr = error as { + stdout?: string; + stderr?: string; + message?: string; + }; log.error( - `Verification failed for ${hookAddress} (non-fatal): ${execErr.message ?? "unknown error"}`, + `Verification failed for ${hookAddress} (non-fatal): ${execErr.message ?? 'unknown error'}`, error instanceof Error ? error : new Error(String(error)), ); if (execErr.stdout || execErr.stderr) { - log.dumpForgeOutput({ stdout: execErr.stdout, stderr: execErr.stderr, label: "forge verify-contract" }); + log.dumpForgeOutput({ + stdout: execErr.stdout, + stderr: execErr.stderr, + label: 'forge verify-contract', + }); } } } function parseSqrtPriceX96(v: unknown): bigint | null { if (v == null) return null; - if (typeof v === "bigint") return v; - if (typeof v === "string") return BigInt(v); - if (typeof v === "number") return BigInt(Math.floor(v)); + if (typeof v === 'bigint') return v; + if (typeof v === 'string') return BigInt(v); + if (typeof v === 'number') return BigInt(Math.floor(v)); return null; } function loadJsonFile(filePath: string, log: Logger): PoolConfig[] { try { - const content = readFileSync(filePath, "utf-8"); + const content = readFileSync(filePath, 'utf-8'); const pools = JSON.parse(content); if (!Array.isArray(pools)) { - throw new Error("JSON file must contain an array of pool configurations"); + throw new Error('JSON file must contain an array of pool configurations'); } if (pools.length === 0) { - throw new Error("JSON file must contain at least one pool configuration"); + throw new Error('JSON file must contain at least one pool configuration'); } for (let i = 0; i < pools.length; i++) { const p = pools[i]; if (!isPoolType(p?.poolType)) { - throw new Error(`Pool ${i + 1} missing or invalid 'poolType'. Must be one of: ${POOL_TYPES.join(", ")}`); + throw new Error( + `Pool ${i + 1} missing or invalid 'poolType'. Must be one of: ${POOL_TYPES.join(', ')}`, + ); } } @@ -462,15 +563,18 @@ function loadJsonFile(filePath: string, log: Logger): PoolConfig[] { })) as PoolConfig[]; } catch (error) { log.error( - "Error loading JSON file:", - error instanceof Error ? error : new Error("Unknown error loading JSON file"), + 'Error loading JSON file:', + error instanceof Error + ? error + : new Error('Unknown error loading JSON file'), ); process.exit(1); } } /** PoolManager Initialize event for verification */ -const INITIALIZE_TOPIC = "0xdd466e674ea557f56295e2d0218a125ea4b4f0f6f3307b95f85e6110838d6438"; +const INITIALIZE_TOPIC = + '0xdd466e674ea557f56295e2d0218a125ea4b4f0f6f3307b95f85e6110838d6438'; async function verifyDeploymentOnForgeFailure( provider: ethers.Provider, @@ -493,9 +597,10 @@ async function verifyDeploymentOnForgeFailure( const hookAddress = ethers.getAddress(hookMatch[1]) as Address; const code = await provider.getCode(hookAddress); - const hookDeployed = !!code && code !== "0x" && code.length > 2; + const hookDeployed = !!code && code !== '0x' && code.length > 2; - if (!hookDeployed) return { hookAddress, hookDeployed: false, poolsInitialized: 0 }; + if (!hookDeployed) + return { hookAddress, hookDeployed: false, poolsInitialized: 0 }; const poolKeys = module.buildPoolKeys(poolConfig, hookAddress); const poolEntriesFromEvent: PoolEntry[] = []; @@ -513,10 +618,14 @@ async function verifyDeploymentOnForgeFailure( for (const log of logs) { if (log.topics.length < 4) continue; - const currency0 = ethers.getAddress("0x" + log.topics[2].slice(26)) as Address; - const currency1 = ethers.getAddress("0x" + log.topics[3].slice(26)) as Address; + const currency0 = ethers.getAddress( + '0x' + log.topics[2].slice(26), + ) as Address; + const currency1 = ethers.getAddress( + '0x' + log.topics[3].slice(26), + ) as Address; const data = ethers.AbiCoder.defaultAbiCoder().decode( - ["uint24", "int24", "address", "uint160", "int24"], + ['uint24', 'int24', 'address', 'uint160', 'int24'], log.data, ); const fee = Number(data[0]); @@ -530,8 +639,15 @@ async function verifyDeploymentOnForgeFailure( k.currency1.toLowerCase() === currency1.toLowerCase(), ) ) { - if (initializeBlockNumber === undefined) initializeBlockNumber = Number(log.blockNumber); - const poolKey: PoolKeyRecord = { currency0, currency1, fee, tickSpacing, hooks: hookAddress }; + if (initializeBlockNumber === undefined) + initializeBlockNumber = Number(log.blockNumber); + const poolKey: PoolKeyRecord = { + currency0, + currency1, + fee, + tickSpacing, + hooks: hookAddress, + }; poolEntriesFromEvent.push({ poolKey, poolId: log.topics[1] }); } } @@ -544,7 +660,8 @@ async function verifyDeploymentOnForgeFailure( hookDeployed, poolsInitialized: poolEntriesFromEvent.length, blockNumber: initializeBlockNumber, - poolEntriesFromEvent: poolEntriesFromEvent.length > 0 ? poolEntriesFromEvent : undefined, + poolEntriesFromEvent: + poolEntriesFromEvent.length > 0 ? poolEntriesFromEvent : undefined, }; } @@ -556,31 +673,37 @@ function runMineHookWorker( streamOutput: boolean, ): Promise<{ salt: string; output: string }> { return new Promise((resolve, reject) => { - const child = spawn("bash", args, { + const child = spawn('bash', args, { cwd: projectRoot, env: execEnv, - stdio: ["inherit", "pipe", streamOutput ? "inherit" : "pipe"], + stdio: ['inherit', 'pipe', streamOutput ? 'inherit' : 'pipe'], }); - let output = ""; - child.stdout!.on("data", (chunk) => { + let output = ''; + child.stdout!.on('data', (chunk) => { const str = chunk.toString(); output += str; if (streamOutput) process.stdout.write(chunk); }); - child.on("close", (code) => { + child.on('close', (code) => { if (code === 0) { - const saltMatch = output.match(/Salt \(bytes32\):\s*(0x[a-fA-F0-9]{64})/); + const saltMatch = output.match( + /Salt \(bytes32\):\s*(0x[a-fA-F0-9]{64})/, + ); if (saltMatch) resolve({ salt: saltMatch[1], output }); - else reject(new Error("Could not parse salt from mine_hook.sh output")); + else reject(new Error('Could not parse salt from mine_hook.sh output')); } else { - const err = new Error(`mine_hook.sh exited with code ${code}`) as Error & { stdout?: string }; + const err = new Error( + `mine_hook.sh exited with code ${code}`, + ) as Error & { + stdout?: string; + }; err.stdout = output; reject(err); } }); - child.on("error", reject); + child.on('error', reject); }); } @@ -591,7 +714,7 @@ async function mineSalt( deployerAddress?: Address, jobs = 1, ): Promise { - const scriptPath = join(projectRoot, "mine_hook.sh"); + const scriptPath = join(projectRoot, 'mine_hook.sh'); const protocolIdHex = `0x${protocolId.toString(16).toUpperCase()}`; log.info(`Mining salt for protocol ${protocolIdHex}...`); @@ -600,47 +723,64 @@ async function mineSalt( if (jobs > 1) log.info(`Running ${jobs} parallel mining workers...`); const baseArgs = [scriptPath, constructorArgs, protocolIdHex]; - if (deployerAddress) baseArgs.push("500", deployerAddress); + if (deployerAddress) baseArgs.push('500', deployerAddress); - const execEnv = { ...process.env, ...(log.verboseEnabled && { FORGE_VERBOSE: "1" }) }; + const execEnv = { + ...process.env, + ...(log.verboseEnabled && { FORGE_VERBOSE: '1' }), + }; try { if (jobs === 1) { - const result = await runMineHookWorker(scriptPath, baseArgs, execEnv, projectRoot, true); + const result = await runMineHookWorker( + scriptPath, + baseArgs, + execEnv, + projectRoot, + true, + ); log.success(`Found salt: ${result.salt}`); return result.salt; } const children: ReturnType[] = []; - const workerPromises: Promise<{ salt: string } | { failed: true; output: string }>[] = []; + const workerPromises: Promise< + { salt: string } | { failed: true; output: string } + >[] = []; for (let i = 0; i < jobs; i++) { - const child = spawn("bash", baseArgs, { + const child = spawn('bash', baseArgs, { cwd: projectRoot, env: execEnv, - stdio: ["inherit", "pipe", "pipe"], + stdio: ['inherit', 'pipe', 'pipe'], }); children.push(child); - let output = ""; - child.stdout!.on("data", (chunk) => { + let output = ''; + child.stdout!.on('data', (chunk) => { output += chunk.toString(); }); - child.stderr!.on("data", (chunk) => { + child.stderr!.on('data', (chunk) => { output += chunk.toString(); }); - const promise = new Promise<{ salt: string } | { failed: true; output: string }>((resolve) => { - child.on("close", (code) => { + const promise = new Promise< + { salt: string } | { failed: true; output: string } + >((resolve) => { + child.on('close', (code) => { if (code === 0) { - const saltMatch = output.match(/Salt \(bytes32\):\s*(0x[a-fA-F0-9]{64})/); + const saltMatch = output.match( + /Salt \(bytes32\):\s*(0x[a-fA-F0-9]{64})/, + ); if (saltMatch) resolve({ salt: saltMatch[1] }); else resolve({ failed: true, output }); } else { resolve({ failed: true, output }); } }); - child.on("error", (err) => resolve({ failed: true, output: err.message })); + child.on('error', (err) => + resolve({ failed: true, output: err.message }), + ); }); workerPromises.push(promise); } @@ -648,7 +788,7 @@ async function mineSalt( const killAll = () => { children.forEach((c) => { try { - c.kill("SIGTERM"); + c.kill('SIGTERM'); } catch { /* ignore */ } @@ -657,17 +797,19 @@ async function mineSalt( const salt = await new Promise((resolve, reject) => { let failedCount = 0; - let lastFailedOutput = ""; + let lastFailedOutput = ''; workerPromises.forEach((p) => { p.then((result) => { - if ("salt" in result) { + if ('salt' in result) { killAll(); resolve(result.salt); } else { failedCount++; lastFailedOutput = result.output; if (failedCount === jobs) { - const err = new Error("All mining workers failed") as Error & { stdout?: string }; + const err = new Error('All mining workers failed') as Error & { + stdout?: string; + }; err.stdout = lastFailedOutput; reject(err); } @@ -679,12 +821,12 @@ async function mineSalt( log.success(`Found salt: ${salt}`); return salt; } catch (error) { - log.error("Error mining salt:", error); + log.error('Error mining salt:', error); const execErr = error as { stdout?: string; stderr?: string }; log.dumpForgeOutput({ stdout: execErr.stdout, stderr: execErr.stderr, - label: "Forge/mine_hook (from failed worker)", + label: 'Forge/mine_hook (from failed worker)', }); throw error; } @@ -699,12 +841,12 @@ function selfDeployPool( dryRun: boolean, log: Logger, priorityGasPrice: string | null = null, -): Address | "deployed" { +): Address | 'deployed' { const module = CREATION_MODULES[poolType]; if (!module) throw new Error(`Unknown pool type: ${poolType}`); - const rawKey = (process.env.PRIVATE_KEY ?? "").trim(); - const privateKey = rawKey.startsWith("0x") ? rawKey : `0x${rawKey}`; + const rawKey = (process.env.PRIVATE_KEY ?? '').trim(); + const privateKey = rawKey.startsWith('0x') ? rawKey : `0x${rawKey}`; const envVars: Record = { PROTOCOL_ID: module.protocolId.toString(), @@ -715,29 +857,30 @@ function selfDeployPool( }; log.info(`Self-deploying hook via SelfCreateHook.s.sol...`); - if (dryRun) log.info("(dry run - no broadcast)"); + if (dryRun) log.info('(dry run - no broadcast)'); log.info(`Protocol: ${poolType}, Salt: ${salt.substring(0, 18)}...`); try { const output = execFileSync( - "forge", + 'forge', [ - "script", - "lib/v4-hooks-public/script/SelfCreateHook.s.sol:SelfCreateHookScript", - "--rpc-url", + 'script', + 'lib/v4-hooks-public/script/SelfCreateHook.s.sol:SelfCreateHookScript', + '--rpc-url', rpcUrl, - ...(dryRun ? [] : ["--broadcast"]), - ...(priorityGasPrice ? ["--priority-gas-price", priorityGasPrice] : []), - ...(log.verboseEnabled ? ["-vvvv"] : []), + ...(dryRun ? [] : ['--broadcast']), + ...(priorityGasPrice ? ['--priority-gas-price', priorityGasPrice] : []), + ...(log.verboseEnabled ? ['-vvvv'] : []), ], { - encoding: "utf-8", + encoding: 'utf-8', cwd: projectRoot, env: { ...process.env, ...envVars }, }, ); - if (log.verboseEnabled) log.verbose(`\n--- Forge script output ---\n${output}`); + if (log.verboseEnabled) + log.verbose(`\n--- Forge script output ---\n${output}`); const hookMatch = output.match(/Hook Address:\s*(0x[a-fA-F0-9]{40})/); if (hookMatch) { @@ -747,11 +890,15 @@ function selfDeployPool( } log.success(`Self-deploy completed`); - return "deployed"; + return 'deployed'; } catch (error) { - log.error("Error in self-deploy:", error); + log.error('Error in self-deploy:', error); const execErr = error as { stdout?: string; stderr?: string }; - log.dumpForgeOutput({ stdout: execErr.stdout, stderr: execErr.stderr, label: "Forge" }); + log.dumpForgeOutput({ + stdout: execErr.stdout, + stderr: execErr.stderr, + label: 'Forge', + }); throw error; } } @@ -764,22 +911,28 @@ async function createPool( salt: string, log: Logger, dryRun: boolean, -): Promise<{ hookAddress: Address | ""; blockNumber: number; txHash: string }> { +): Promise<{ hookAddress: Address | ''; blockNumber: number; txHash: string }> { const module = CREATION_MODULES[poolType]; if (!module) throw new Error(`Unknown pool type: ${poolType}`); const args = module.buildCreatePoolArgs(poolConfig, salt); - const factory = new ethers.Contract(factoryAddress, module.factoryAbi, signer); + const factory = new ethers.Contract( + factoryAddress, + module.factoryAbi, + signer, + ); log.info(`Calling createPool on factory ${factoryAddress}...`); - if (dryRun) log.info("(dry run - no broadcast)"); - log.info(`Args: ${args.map((a, i) => `${i}: ${String(a).substring(0, 66)}...`).join(", ")}`); + if (dryRun) log.info('(dry run - no broadcast)'); + log.info( + `Args: ${args.map((a, i) => `${i}: ${String(a).substring(0, 66)}...`).join(', ')}`, + ); try { if (dryRun) { await factory.createPool.staticCall(...args); log.success(`Dry run: createPool would succeed (simulation passed)`); - return { hookAddress: "", blockNumber: 0, txHash: "" }; + return { hookAddress: '', blockNumber: 0, txHash: '' }; } const tx = await factory.createPool(...args); @@ -790,8 +943,11 @@ async function createPool( const hookDeployedEvent = receipt!.logs.find((l: ethers.Log) => { try { - const parsed = factory.interface.parseLog({ topics: l.topics as string[], data: l.data }); - return parsed?.name === "HookDeployed"; + const parsed = factory.interface.parseLog({ + topics: l.topics as string[], + data: l.data, + }); + return parsed?.name === 'HookDeployed'; } catch { return false; } @@ -802,7 +958,9 @@ async function createPool( topics: hookDeployedEvent.topics as string[], data: hookDeployedEvent.data, }); - const hookAddress = ethers.getAddress((parsed?.args.hook || parsed?.args[0]) as string) as Address; + const hookAddress = ethers.getAddress( + (parsed?.args.hook || parsed?.args[0]) as string, + ) as Address; log.success(`Hook deployed at: ${hookAddress}`); return { hookAddress, @@ -812,12 +970,12 @@ async function createPool( } return { - hookAddress: "", + hookAddress: '', blockNumber: Number(receipt!.blockNumber), txHash: tx.hash, }; } catch (error) { - log.error("Error creating pool:", error); + log.error('Error creating pool:', error); throw error; } } @@ -845,9 +1003,9 @@ async function main() { const signer = new ethers.Wallet(privateKey, provider); log.banner({ - title: "Pool Creation Script", + title: 'Pool Creation Script', jsonFile, - mode: selfDeploy ? "Self-Deploy" : "Factory", + mode: selfDeploy ? 'Self-Deploy' : 'Factory', factoryAddress: factoryAddress ?? undefined, rpcUrl, registryDir, @@ -864,17 +1022,22 @@ async function main() { const allPools = loadJsonFile(jsonFile, log); const pools = startAt > 1 ? allPools.slice(startAt - 1) : allPools; if (startAt > 1 && pools.length === 0) { - log.error(`Error: --start-at ${startAt} exceeds pool count (${allPools.length})`); + log.error( + `Error: --start-at ${startAt} exceeds pool count (${allPools.length})`, + ); process.exit(1); } log.info( `Loaded ${allPools.length} pool configuration(s)${ - startAt > 1 ? `, processing from index ${startAt} (${pools.length} remaining)` : "" + startAt > 1 + ? `, processing from index ${startAt} (${pools.length} remaining)` + : '' }`, ); - log.info(""); + log.info(''); - const chainId = parsedChainId ?? Number((await provider.getNetwork()).chainId); + const chainId = + parsedChainId ?? Number((await provider.getNetwork()).chainId); // Determine module from first pool (loadJsonFile enforces a single poolType) const firstPoolType = pools[0].poolType; @@ -884,40 +1047,70 @@ async function main() { // --- Singleton flow: deploy once, then initialize a V4 pool per config --- if (!firstModule.aggregatorEnvKey || !firstModule.buildInitializeArgs) { - log.error(`Module ${firstPoolType} is marked as singleton but is missing aggregatorEnvKey or buildInitializeArgs`); + log.error( + `Module ${firstPoolType} is marked as singleton but is missing aggregatorEnvKey or buildInitializeArgs`, + ); process.exit(1); } const envKey = `${firstModule.aggregatorEnvKey}_${chainId}`; - let hookAddress = (process.env[envKey] ?? "").trim() || null; + let hookAddress = (process.env[envKey] ?? '').trim() || null; if (hookAddress) { - log.info(`Reusing existing ${firstPoolType} singleton at ${hookAddress} (${envKey})`); + log.info( + `Reusing existing ${firstPoolType} singleton at ${hookAddress} (${envKey})`, + ); } else { - log.info(`No ${envKey} found — deploying ${firstPoolType} singleton aggregator...`); + log.info( + `No ${envKey} found — deploying ${firstPoolType} singleton aggregator...`, + ); const immutables = firstModule.getImmutablesFromEnv(chainId); - const constructorArgs = firstModule.encodeConstructorArgs(pools[0], immutables); - const salt = await mineSalt(constructorArgs, firstModule.protocolId, log, CREATE2_DEPLOYER, jobs); - const deployed = selfDeployPool(pools[0], firstPoolType, immutables, salt, rpcUrl, dryRun, log, priorityGasPrice); + const constructorArgs = firstModule.encodeConstructorArgs( + pools[0], + immutables, + ); + const salt = await mineSalt( + constructorArgs, + firstModule.protocolId, + log, + CREATE2_DEPLOYER, + jobs, + ); + const deployed = selfDeployPool( + pools[0], + firstPoolType, + immutables, + salt, + rpcUrl, + dryRun, + log, + priorityGasPrice, + ); - if (!deployed || deployed === "deployed") { - log.error("Could not determine deployed aggregator address — aborting"); + if (!deployed || deployed === 'deployed') { + log.error( + 'Could not determine deployed aggregator address — aborting', + ); process.exit(1); } hookAddress = deployed; if (!dryRun) { - const envFilePath = join(projectRoot, "aggregator-hooks", ".env"); + const envFilePath = join(projectRoot, 'aggregator-hooks', '.env'); appendToEnvFile(envFilePath, envKey, hookAddress); - log.success(`Wrote ${envKey}=${hookAddress} to aggregator-hooks/.env`); + log.success( + `Wrote ${envKey}=${hookAddress} to aggregator-hooks/.env`, + ); // Wait for the deployment tx to be confirmed before initializing pools. // Without this, the first pool's beforeInitialize call hits an unconfirmed contract. - log.info(`Waiting for aggregator contract to be confirmed on-chain...`); + log.info( + `Waiting for aggregator contract to be confirmed on-chain...`, + ); const deadline = Date.now() + 60_000; while (Date.now() < deadline) { const code = await provider.getCode(hookAddress); - if (code && code !== "0x") { + if (code && code !== '0x') { log.success(`Aggregator confirmed at ${hookAddress}`); break; } @@ -926,7 +1119,14 @@ async function main() { } if (verify.enabled && !dryRun) { - verifyContract(hookAddress as Address, firstModule.contractIdentifier, constructorArgs, chainId, verify, log); + verifyContract( + hookAddress as Address, + firstModule.contractIdentifier, + constructorArgs, + chainId, + verify, + log, + ); } } @@ -936,10 +1136,15 @@ async function main() { const i = startAt - 1 + j; const poolConfig = pools[j]; - log.section(`Initializing Pool ${i + 1}/${allPools.length} (${firstPoolType})`); + log.section( + `Initializing Pool ${i + 1}/${allPools.length} (${firstPoolType})`, + ); try { - const [poolKey, sqrtPriceX96] = firstModule.buildInitializeArgs!(poolConfig, hookAddress as Address); + const [poolKey, sqrtPriceX96] = firstModule.buildInitializeArgs!( + poolConfig, + hookAddress as Address, + ); const result = await initializeSingletonPool( signer, immutables.poolManager as Address, @@ -950,14 +1155,21 @@ async function main() { ); if (registryDir && !dryRun) { - const poolKeys = firstModule.buildPoolKeys(poolConfig, hookAddress as Address); + const poolKeys = firstModule.buildPoolKeys( + poolConfig, + hookAddress as Address, + ); if (poolKeys.length > 0) { - const blockNumber = result?.blockNumber ?? Number(await provider.getBlockNumber()); + const blockNumber = + result?.blockNumber ?? Number(await provider.getBlockNumber()); appendToRegistryFile( registryDir, firstPoolType, { - pools: poolKeys.map((pk) => ({ poolKey: pk, poolId: computePoolId(pk) })), + pools: poolKeys.map((pk) => ({ + poolKey: pk, + poolId: computePoolId(pk), + })), metadata: { externalPool: firstModule.getExternalPool(poolConfig), hookAddress: hookAddress as Address, @@ -984,11 +1196,22 @@ async function main() { const module = CREATION_MODULES[poolType]; const immutables = module.getImmutablesFromEnv(chainId); - log.section(`Processing Pool ${i + 1}/${allPools.length} (${poolType})`); + log.section( + `Processing Pool ${i + 1}/${allPools.length} (${poolType})`, + ); try { - const constructorArgs = module.encodeConstructorArgs(poolConfig, immutables); - const salt = await mineSalt(constructorArgs, module.protocolId, log, CREATE2_DEPLOYER, jobs); + const constructorArgs = module.encodeConstructorArgs( + poolConfig, + immutables, + ); + const salt = await mineSalt( + constructorArgs, + module.protocolId, + log, + CREATE2_DEPLOYER, + jobs, + ); const hookAddress = selfDeployPool( poolConfig, poolType, @@ -1000,7 +1223,7 @@ async function main() { priorityGasPrice, ); - if (hookAddress && hookAddress !== "deployed") { + if (hookAddress && hookAddress !== 'deployed') { if (registryDir) { const poolKeys = module.buildPoolKeys(poolConfig, hookAddress); if (poolKeys.length > 0) { @@ -1009,7 +1232,10 @@ async function main() { registryDir, poolType, { - pools: poolKeys.map((poolKey) => ({ poolKey, poolId: computePoolId(poolKey) })), + pools: poolKeys.map((poolKey) => ({ + poolKey, + poolId: computePoolId(poolKey), + })), metadata: { externalPool: module.getExternalPool(poolConfig), hookAddress, @@ -1021,17 +1247,33 @@ async function main() { } } if (verify.enabled && !dryRun) { - verifyContract(hookAddress, module.contractIdentifier, constructorArgs, chainId, verify, log); + verifyContract( + hookAddress, + module.contractIdentifier, + constructorArgs, + chainId, + verify, + log, + ); } } log.success(`Successfully created pool ${i + 1}`); } catch (error) { log.error(`Failed to create pool ${i + 1}:`, error); const execErr = error as { stdout?: string; stderr?: string }; - log.dumpForgeOutput({ stdout: execErr.stdout, stderr: execErr.stderr, label: "Forge" }); - const forgeOutput = [execErr.stdout, execErr.stderr].filter(Boolean).join("\n"); + log.dumpForgeOutput({ + stdout: execErr.stdout, + stderr: execErr.stderr, + label: 'Forge', + }); + const forgeOutput = [execErr.stdout, execErr.stderr] + .filter(Boolean) + .join('\n'); if (forgeOutput) { - const immutables = CREATION_MODULES[poolConfig.poolType].getImmutablesFromEnv(chainId); + const immutables = + CREATION_MODULES[poolConfig.poolType].getImmutablesFromEnv( + chainId, + ); const verification = await verifyDeploymentOnForgeFailure( provider, immutables.poolManager, @@ -1040,23 +1282,30 @@ async function main() { forgeOutput, ); if (verification?.hookDeployed) { - log.error(""); - log.error("Verification (possible false positive):"); - log.error(` Hook has code at ${verification.hookAddress} → deployment likely succeeded`); + log.error(''); + log.error('Verification (possible false positive):'); + log.error( + ` Hook has code at ${verification.hookAddress} → deployment likely succeeded`, + ); if (verification.poolsInitialized > 0) { log.error( ` Found ${verification.poolsInitialized} Initialize event(s) for this hook → pool(s) initialized on-chain`, ); } - log.error(" Check block explorer to confirm."); + log.error(' Check block explorer to confirm.'); if (registryDir && !dryRun) { const recoverPools: PoolEntry[] = verification.poolEntriesFromEvent ?? module .buildPoolKeys(poolConfig, verification.hookAddress) - .map((poolKey) => ({ poolKey, poolId: computePoolId(poolKey) })); - const blockNumber = verification.blockNumber ?? Number(await provider.getBlockNumber()); + .map((poolKey) => ({ + poolKey, + poolId: computePoolId(poolKey), + })); + const blockNumber = + verification.blockNumber ?? + Number(await provider.getBlockNumber()); if (recoverPools.length > 0) { appendToRegistryFile( registryDir, @@ -1071,7 +1320,9 @@ async function main() { }, log, ); - log.info(" Appended to registry despite forge failure (deployment verified on-chain)."); + log.info( + ' Appended to registry despite forge failure (deployment verified on-chain).', + ); } } } @@ -1084,7 +1335,9 @@ async function main() { const allPools = loadJsonFile(jsonFile, log); const pools = startAt > 1 ? allPools.slice(startAt - 1) : allPools; if (startAt > 1 && pools.length === 0) { - log.error(`Error: --start-at ${startAt} exceeds pool count (${allPools.length})`); + log.error( + `Error: --start-at ${startAt} exceeds pool count (${allPools.length})`, + ); process.exit(1); } const poolType = pools[0].poolType as string; @@ -1092,19 +1345,25 @@ async function main() { log.info( `Loaded ${allPools.length} pool configuration(s) (poolType: ${poolType})${ - startAt > 1 ? `, processing from index ${startAt} (${pools.length} remaining)` : "" + startAt > 1 + ? `, processing from index ${startAt} (${pools.length} remaining)` + : '' }`, ); - log.info(""); + log.info(''); - log.info("Reading factory immutables..."); - const factoryImmutables = await module.readFactoryImmutables(provider, factoryAddress!); - const chainId = parsedChainId ?? Number((await provider.getNetwork()).chainId); + log.info('Reading factory immutables...'); + const factoryImmutables = await module.readFactoryImmutables( + provider, + factoryAddress!, + ); + const chainId = + parsedChainId ?? Number((await provider.getNetwork()).chainId); log.info(`POOL_MANAGER: ${factoryImmutables.poolManager}`); for (const [key, val] of Object.entries(factoryImmutables)) { - if (key !== "poolManager" && val) log.info(`${key}: ${val}`); + if (key !== 'poolManager' && val) log.info(`${key}: ${val}`); } - log.info(""); + log.info(''); for (let j = 0; j < pools.length; j++) { const i = startAt - 1 + j; @@ -1113,19 +1372,42 @@ async function main() { log.section(`Processing Pool ${i + 1}/${allPools.length}`); try { - const constructorArgs = module.encodeConstructorArgs(poolConfig, factoryImmutables); - const salt = await mineSalt(constructorArgs, module.protocolId, log, factoryAddress!, jobs); - const result = await createPool(signer, factoryAddress!, poolConfig, poolType, salt, log, dryRun); + const constructorArgs = module.encodeConstructorArgs( + poolConfig, + factoryImmutables, + ); + const salt = await mineSalt( + constructorArgs, + module.protocolId, + log, + factoryAddress!, + jobs, + ); + const result = await createPool( + signer, + factoryAddress!, + poolConfig, + poolType, + salt, + log, + dryRun, + ); if (result.hookAddress && !dryRun) { if (registryDir) { - const poolKeys = module.buildPoolKeys(poolConfig, result.hookAddress); + const poolKeys = module.buildPoolKeys( + poolConfig, + result.hookAddress, + ); if (poolKeys.length > 0) { appendToRegistryFile( registryDir, poolType, { - pools: poolKeys.map((poolKey) => ({ poolKey, poolId: computePoolId(poolKey) })), + pools: poolKeys.map((poolKey) => ({ + poolKey, + poolId: computePoolId(poolKey), + })), metadata: { externalPool: module.getExternalPool(poolConfig), hookAddress: result.hookAddress, @@ -1138,23 +1420,36 @@ async function main() { } } if (verify.enabled) { - verifyContract(result.hookAddress, module.contractIdentifier, constructorArgs, chainId, verify, log); + verifyContract( + result.hookAddress, + module.contractIdentifier, + constructorArgs, + chainId, + verify, + log, + ); } } - log.success(`Successfully created pool ${i + 1}${dryRun ? " (dry run)" : ""}`); + log.success( + `Successfully created pool ${i + 1}${dryRun ? ' (dry run)' : ''}`, + ); } catch (error) { log.error(`Failed to create pool ${i + 1}:`, error); const execErr = error as { stdout?: string; stderr?: string }; - log.dumpForgeOutput({ stdout: execErr.stdout, stderr: execErr.stderr, label: "Forge" }); + log.dumpForgeOutput({ + stdout: execErr.stdout, + stderr: execErr.stderr, + label: 'Forge', + }); continue; } } } - log.info("\n=== Done ==="); + log.info('\n=== Done ==='); } main().catch((error) => { - console.error("Fatal error:", error); + console.error('Fatal error:', error); process.exit(1); }); diff --git a/aggregator-hooks/src/logger.ts b/aggregator-hooks/src/logger.ts index 8fda0cbd..d95d1639 100644 --- a/aggregator-hooks/src/logger.ts +++ b/aggregator-hooks/src/logger.ts @@ -29,7 +29,11 @@ export interface Logger { verbose: (msg: string) => void; section: (title: string) => void; banner: (config: BannerConfig) => void; - dumpForgeOutput: (opts: { stdout?: string; stderr?: string; label?: string }) => void; + dumpForgeOutput: (opts: { + stdout?: string; + stderr?: string; + label?: string; + }) => void; } export function createLogger(opts: { verbose: boolean }): Logger { @@ -43,8 +47,8 @@ export function createLogger(opts: { verbose: boolean }): Logger { console.error(msg); if (err instanceof Error) console.error(err.message); const e = err as { data?: unknown; reason?: string }; - if (e?.data) console.error("Error data:", e.data); - if (e?.reason) console.error("Revert reason:", e.reason); + if (e?.data) console.error('Error data:', e.data); + if (e?.reason) console.error('Revert reason:', e.reason); }, verbose: (msg: string) => { if (verbose) console.log(msg); @@ -55,25 +59,41 @@ export function createLogger(opts: { verbose: boolean }): Logger { `=== ${config.title} ===`, `JSON File: ${config.jsonFile}`, `Mode: ${config.mode}`, - ...(config.factoryAddress ? [`Factory Address: ${config.factoryAddress}`] : []), + ...(config.factoryAddress + ? [`Factory Address: ${config.factoryAddress}`] + : []), `RPC URL: ${config.rpcUrl}`, ...(config.registryDir ? [`Registry dir: ${config.registryDir}`] : []), - ...(config.dryRun ? ["DRY RUN: forge scripts will simulate without broadcasting"] : []), - ...(config.verbose ? ["VERBOSE: forge scripts will run with -vvvv"] : []), + ...(config.dryRun + ? ['DRY RUN: forge scripts will simulate without broadcasting'] + : []), + ...(config.verbose + ? ['VERBOSE: forge scripts will run with -vvvv'] + : []), ...(config.startAt && config.startAt > 1 - ? [`Starting at pool index: ${config.startAt} (skipping first ${config.startAt - 1} pool(s))`] + ? [ + `Starting at pool index: ${config.startAt} (skipping first ${config.startAt - 1} pool(s))`, + ] + : []), + ...(config.jobs && config.jobs > 1 + ? [`Salt mining: ${config.jobs} parallel workers`] + : []), + ...(config.priorityGasPrice + ? [`Priority gas price: ${config.priorityGasPrice}`] + : []), + ...(config.verify ? ['Contract verification: enabled (--verify)'] : []), + ...(config.signerAddress + ? [`Using signer: ${config.signerAddress}`] : []), - ...(config.jobs && config.jobs > 1 ? [`Salt mining: ${config.jobs} parallel workers`] : []), - ...(config.priorityGasPrice ? [`Priority gas price: ${config.priorityGasPrice}`] : []), - ...(config.verify ? ["Contract verification: enabled (--verify)"] : []), - ...(config.signerAddress ? [`Using signer: ${config.signerAddress}`] : []), ]; lines.forEach((line) => console.log(line)); - if (lines.length > 0) console.log(""); + if (lines.length > 0) console.log(''); }, - dumpForgeOutput: ({ stdout, stderr, label = "Forge" }) => { - if (stdout != null && stdout !== "") console.error(`\n--- ${label} stdout ---\n`, stdout); - if (stderr != null && stderr !== "") console.error(`\n--- ${label} stderr ---\n`, stderr); + dumpForgeOutput: ({ stdout, stderr, label = 'Forge' }) => { + if (stdout != null && stdout !== '') + console.error(`\n--- ${label} stdout ---\n`, stdout); + if (stderr != null && stderr !== '') + console.error(`\n--- ${label} stderr ---\n`, stderr); }, }; } diff --git a/aggregator-hooks/tsconfig.json b/aggregator-hooks/tsconfig.json index 04f76275..b5cd5d00 100644 --- a/aggregator-hooks/tsconfig.json +++ b/aggregator-hooks/tsconfig.json @@ -1,24 +1,30 @@ { - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "baseUrl": ".", - "paths": { - "@src/cli": ["src/cli.ts"] - }, - "lib": ["ES2022"], - "outDir": "dist", - "rootDir": ".", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "baseUrl": ".", + "paths": { + "@src/cli": ["src/cli.ts"] }, - "include": ["src/**/*.ts", "creation-modules/*.ts", "historical/**/*.ts", "polling/**/*.ts", "abis/**/*.ts"], - "exclude": ["node_modules", "dist"] + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": [ + "src/**/*.ts", + "creation-modules/*.ts", + "historical/**/*.ts", + "polling/**/*.ts", + "abis/**/*.ts" + ], + "exclude": ["node_modules", "dist"] } diff --git a/foundry.lock b/foundry.lock index fe8bed99..0db4b853 100644 --- a/foundry.lock +++ b/foundry.lock @@ -24,7 +24,7 @@ } }, "lib/v4-hooks-public": { - "rev": "d68c16abdc16f5e5319865739457ef17081ea789" + "rev": "0ec65960e213d0bc38be91004c66d550a085c0dc" }, "src/pkgs/calibur": { "rev": "69d5eb61498ffac7740530310b270459f2ae2a20" From 6ff6165d1900c96750626bd259c017425b89990c Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Mon, 1 Jun 2026 13:31:38 -0400 Subject: [PATCH 20/21] submodule update --- lib/v4-hooks-public | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/v4-hooks-public b/lib/v4-hooks-public index a187a29a..0ec65960 160000 --- a/lib/v4-hooks-public +++ b/lib/v4-hooks-public @@ -1 +1 @@ -Subproject commit a187a29aaefa716cf2fed38d4bc934c67d36c0f0 +Subproject commit 0ec65960e213d0bc38be91004c66d550a085c0dc From 3682876991a65c291a5b3ab61491040767873fd1 Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Mon, 1 Jun 2026 15:17:47 -0400 Subject: [PATCH 21/21] create utils file and make more robust --- aggregator-hooks/historical/FluidDexLite.ts | 7 +- aggregator-hooks/historical/FluidDexT1.ts | 40 +----- aggregator-hooks/historical/PancakeSwapV3.ts | 16 +-- aggregator-hooks/historical/Slipstream.ts | 16 +-- aggregator-hooks/historical/StableSwapNG.ts | 40 +----- aggregator-hooks/historical/UniswapV2.ts | 16 +-- aggregator-hooks/historical/UniswapV3.ts | 16 +-- aggregator-hooks/polling/FluidDexLite.ts | 24 +--- aggregator-hooks/polling/FluidDexT1.ts | 130 ++++++++--------- aggregator-hooks/polling/PancakeSwapV3.ts | 21 +-- aggregator-hooks/polling/Slipstream.ts | 21 +-- aggregator-hooks/polling/StableSwapNG.ts | 144 +++++++------------ aggregator-hooks/polling/UniswapV2.ts | 21 +-- aggregator-hooks/polling/UniswapV3.ts | 21 +-- aggregator-hooks/src/cli.ts | 20 +++ aggregator-hooks/src/createPools.ts | 28 +++- aggregator-hooks/src/logger.ts | 20 ++- aggregator-hooks/src/utils.ts | 57 ++++++++ aggregator-hooks/tsconfig.json | 3 +- src/pkgs/v4-core | 2 +- 20 files changed, 258 insertions(+), 405 deletions(-) create mode 100644 aggregator-hooks/src/utils.ts diff --git a/aggregator-hooks/historical/FluidDexLite.ts b/aggregator-hooks/historical/FluidDexLite.ts index c3eefba3..4bd77cbe 100644 --- a/aggregator-hooks/historical/FluidDexLite.ts +++ b/aggregator-hooks/historical/FluidDexLite.ts @@ -22,9 +22,9 @@ import 'dotenv/config'; import fs from 'node:fs'; -import path from 'node:path'; import { ethers } from 'ethers'; import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from '@src/cli'; +import { ensureDirForFile } from '@src/utils'; import { FLUIDDEXLITE_RESOLVER_ABI } from '../abis/index.js'; import type { Address } from '../creation-modules/types.js'; @@ -65,11 +65,6 @@ function fluidFeeToUniswapV4(fluidFee: bigint | number): number { return Math.min(Math.max(0, Math.floor(converted)), MAX_U24); } -function ensureDirForFile(filePath: string): void { - const dir = path.dirname(path.resolve(filePath)); - fs.mkdirSync(dir, { recursive: true }); -} - async function main() { const args = parseArgs(process.argv.slice(2)); diff --git a/aggregator-hooks/historical/FluidDexT1.ts b/aggregator-hooks/historical/FluidDexT1.ts index a4a84ba2..ee65187f 100644 --- a/aggregator-hooks/historical/FluidDexT1.ts +++ b/aggregator-hooks/historical/FluidDexT1.ts @@ -25,9 +25,9 @@ import 'dotenv/config'; import fs from 'node:fs'; -import path from 'node:path'; import { JsonRpcProvider, Contract, Interface, getAddress } from 'ethers'; import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from '@src/cli'; +import { pRateLimit, pLimit, ensureDirForFile } from '@src/utils'; import { FLUIDDEXT1_HISTORICAL_FACTORY_ABI, FLUIDDEXT1_RESOLVER_ABI, @@ -58,42 +58,6 @@ function toUniswapV4Currency(addr: string): Address { : (getAddress(addr) as Address); } -function pRateLimit(rps: number): () => Promise { - if (rps <= 0) return async () => {}; - const minGapMs = 1000 / rps; - let nextAllowed = 0; - return async function acquire(): Promise { - const now = Date.now(); - if (now < nextAllowed) { - await new Promise((r) => setTimeout(r, nextAllowed - now)); - } - nextAllowed = Math.max(now, nextAllowed) + minGapMs; - }; -} - -function pLimit(concurrency: number) { - let active = 0; - const queue: Array<() => void> = []; - - const next = (): void => { - active--; - const fn = queue.shift(); - if (fn) fn(); - }; - - return async function limit(fn: () => Promise): Promise { - if (active >= concurrency) { - await new Promise((resolve) => queue.push(resolve)); - } - active++; - try { - return await fn(); - } finally { - next(); - } - }; -} - function orderCurrencies(token0: Address, token1: Address): [Address, Address] { const mapped = [ toUniswapV4Currency(token0), @@ -256,7 +220,7 @@ async function main() { } const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); - fs.mkdirSync(path.dirname(outPath), { recursive: true }); + ensureDirForFile(outPath); fs.writeFileSync(outPath, JSON.stringify(configs, null, 2)); console.log( `Wrote ${outPath} (${configs.length} pools in createPools.ts format)`, diff --git a/aggregator-hooks/historical/PancakeSwapV3.ts b/aggregator-hooks/historical/PancakeSwapV3.ts index 9275e553..d429fde5 100644 --- a/aggregator-hooks/historical/PancakeSwapV3.ts +++ b/aggregator-hooks/historical/PancakeSwapV3.ts @@ -23,9 +23,9 @@ import 'dotenv/config'; import fs from 'node:fs'; -import path from 'node:path'; import { ethers } from 'ethers'; import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from '@src/cli'; +import { pRateLimit, ensureDirForFile } from '@src/utils'; import type { Address } from '../creation-modules/types.js'; const OUTPUT_FILE = 'pancakeswapv3-pools.json'; @@ -46,18 +46,6 @@ type PancakeSwapV3PoolConfig = { sqrtPriceX96: null; }; -function pRateLimit(rps: number): () => Promise { - if (rps <= 0) return async () => {}; - const minGapMs = 1000 / rps; - let nextAllowed = 0; - return async function acquire(): Promise { - const now = Date.now(); - if (now < nextAllowed) - await new Promise((r) => setTimeout(r, nextAllowed - now)); - nextAllowed = Math.max(now, nextAllowed) + minGapMs; - }; -} - function isRangeLimitError(err: unknown): boolean { const msg = String( (err as { error?: { message?: string }; message?: string })?.error @@ -196,7 +184,7 @@ async function main() { } const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); - fs.mkdirSync(path.dirname(outPath), { recursive: true }); + ensureDirForFile(outPath); fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + '\n'); console.log( JSON.stringify( diff --git a/aggregator-hooks/historical/Slipstream.ts b/aggregator-hooks/historical/Slipstream.ts index 38161b10..469b441c 100644 --- a/aggregator-hooks/historical/Slipstream.ts +++ b/aggregator-hooks/historical/Slipstream.ts @@ -24,9 +24,9 @@ import 'dotenv/config'; import fs from 'node:fs'; -import path from 'node:path'; import { ethers } from 'ethers'; import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from '@src/cli'; +import { pRateLimit, ensureDirForFile } from '@src/utils'; import type { Address } from '../creation-modules/types.js'; const OUTPUT_FILE = 'slipstream-pools.json'; @@ -46,18 +46,6 @@ type SlipstreamPoolConfig = { sqrtPriceX96: null; }; -function pRateLimit(rps: number): () => Promise { - if (rps <= 0) return async () => {}; - const minGapMs = 1000 / rps; - let nextAllowed = 0; - return async function acquire(): Promise { - const now = Date.now(); - if (now < nextAllowed) - await new Promise((r) => setTimeout(r, nextAllowed - now)); - nextAllowed = Math.max(now, nextAllowed) + minGapMs; - }; -} - function isRangeLimitError(err: unknown): boolean { const msg = String( (err as { error?: { message?: string }; message?: string })?.error @@ -201,7 +189,7 @@ async function main() { } const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); - fs.mkdirSync(path.dirname(outPath), { recursive: true }); + ensureDirForFile(outPath); fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + '\n'); console.log( JSON.stringify( diff --git a/aggregator-hooks/historical/StableSwapNG.ts b/aggregator-hooks/historical/StableSwapNG.ts index 5967e7e6..54e3728a 100644 --- a/aggregator-hooks/historical/StableSwapNG.ts +++ b/aggregator-hooks/historical/StableSwapNG.ts @@ -22,9 +22,9 @@ import 'dotenv/config'; import fs from 'node:fs'; -import path from 'node:path'; import { JsonRpcProvider, Contract, getAddress, ZeroAddress } from 'ethers'; import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from '@src/cli'; +import { pRateLimit, pLimit, ensureDirForFile } from '@src/utils'; import { STABLESWAPNG_HISTORICAL_FACTORY_ABI } from '../abis/index.js'; import type { Address } from '../creation-modules/types.js'; @@ -59,42 +59,6 @@ function saveJson(filePath: string, data: unknown): void { fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); } -function pRateLimit(rps: number): () => Promise { - if (rps <= 0) return async () => {}; - const minGapMs = 1000 / rps; - let nextAllowed = 0; - return async function acquire(): Promise { - const now = Date.now(); - if (now < nextAllowed) { - await new Promise((r) => setTimeout(r, nextAllowed - now)); - } - nextAllowed = Math.max(now, nextAllowed) + minGapMs; - }; -} - -function pLimit(concurrency: number) { - let active = 0; - const queue: Array<() => void> = []; - - const next = (): void => { - active--; - const fn = queue.shift(); - if (fn) fn(); - }; - - return async function limit(fn: () => Promise): Promise { - if (active >= concurrency) { - await new Promise((resolve) => queue.push(resolve)); - } - active++; - try { - return await fn(); - } finally { - next(); - } - }; -} - function uniqAddresses(addrs: string[]): Address[] { const s = new Set
(); for (const a of addrs) { @@ -234,7 +198,7 @@ async function main() { }); const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); - fs.mkdirSync(path.dirname(outPath), { recursive: true }); + ensureDirForFile(outPath); saveJson(outPath, createPoolsConfigs); console.log( `Wrote ${outPath} (${createPoolsConfigs.length} pools in createPools.ts format)`, diff --git a/aggregator-hooks/historical/UniswapV2.ts b/aggregator-hooks/historical/UniswapV2.ts index b7fed514..dd8eaa51 100644 --- a/aggregator-hooks/historical/UniswapV2.ts +++ b/aggregator-hooks/historical/UniswapV2.ts @@ -25,9 +25,9 @@ import 'dotenv/config'; import fs from 'node:fs'; -import path from 'node:path'; import { ethers } from 'ethers'; import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from '@src/cli'; +import { pRateLimit, ensureDirForFile } from '@src/utils'; import type { Address } from '../creation-modules/types.js'; const OUTPUT_FILE = 'uniswapv2-pools.json'; @@ -47,18 +47,6 @@ type UniswapV2PoolConfig = { sqrtPriceX96: null; }; -function pRateLimit(rps: number): () => Promise { - if (rps <= 0) return async () => {}; - const minGapMs = 1000 / rps; - let nextAllowed = 0; - return async function acquire(): Promise { - const now = Date.now(); - if (now < nextAllowed) - await new Promise((r) => setTimeout(r, nextAllowed - now)); - nextAllowed = Math.max(now, nextAllowed) + minGapMs; - }; -} - function isRangeLimitError(err: unknown): boolean { const msg = String( (err as { error?: { message?: string }; message?: string })?.error @@ -196,7 +184,7 @@ async function main() { } const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); - fs.mkdirSync(path.dirname(outPath), { recursive: true }); + ensureDirForFile(outPath); fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + '\n'); console.log( JSON.stringify( diff --git a/aggregator-hooks/historical/UniswapV3.ts b/aggregator-hooks/historical/UniswapV3.ts index 303c225d..ee1ff3cb 100644 --- a/aggregator-hooks/historical/UniswapV3.ts +++ b/aggregator-hooks/historical/UniswapV3.ts @@ -24,9 +24,9 @@ import 'dotenv/config'; import fs from 'node:fs'; -import path from 'node:path'; import { ethers } from 'ethers'; import { parseArgs, getEnvForChain, toInt, resolveOutputPath } from '@src/cli'; +import { pRateLimit, ensureDirForFile } from '@src/utils'; import type { Address } from '../creation-modules/types.js'; const OUTPUT_FILE = 'uniswapv3-pools.json'; @@ -47,18 +47,6 @@ type UniswapV3PoolConfig = { sqrtPriceX96: null; }; -function pRateLimit(rps: number): () => Promise { - if (rps <= 0) return async () => {}; - const minGapMs = 1000 / rps; - let nextAllowed = 0; - return async function acquire(): Promise { - const now = Date.now(); - if (now < nextAllowed) - await new Promise((r) => setTimeout(r, nextAllowed - now)); - nextAllowed = Math.max(now, nextAllowed) + minGapMs; - }; -} - function isRangeLimitError(err: unknown): boolean { const msg = String( (err as { error?: { message?: string }; message?: string })?.error @@ -200,7 +188,7 @@ async function main() { } const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); - fs.mkdirSync(path.dirname(outPath), { recursive: true }); + ensureDirForFile(outPath); fs.writeFileSync(outPath, JSON.stringify(configs, null, 2) + '\n'); console.log( JSON.stringify( diff --git a/aggregator-hooks/polling/FluidDexLite.ts b/aggregator-hooks/polling/FluidDexLite.ts index 34c6ea73..0ec82ce7 100644 --- a/aggregator-hooks/polling/FluidDexLite.ts +++ b/aggregator-hooks/polling/FluidDexLite.ts @@ -24,7 +24,6 @@ import 'dotenv/config'; import fs from 'node:fs'; -import path from 'node:path'; import { ethers } from 'ethers'; import { parseArgs, @@ -33,6 +32,7 @@ import { resolveOutputPath, resolveCheckpointPath, } from '@src/cli'; +import { safeReadJson, atomicWriteFile, ensureDirForFile } from '@src/utils'; const OUTPUT_FILE = 'fluiddexlite-pools.json'; const CHECKPOINT_FILE = 'dexlite_checkpoint.json'; @@ -69,28 +69,6 @@ const LOG_INITIALIZE_ABI = [ 'event LogInitialize(uint256 dexId, tuple(address token0,address token1,bytes32 salt) dexKey, tuple(tuple(address token0,address token1,bytes32 salt) dexKey,uint256 fee,uint256 revenueCut,bool rebalancingStatus,uint256 centerPrice,uint256 centerPriceContract,uint256 upperPercent,uint256 lowerPercent,uint256 upperShiftThreshold,uint256 lowerShiftThreshold,uint256 shiftTime,uint256 minCenterPrice,uint256 maxCenterPrice,uint256 token0Amount,uint256 token1Amount) params, uint256 time)', ] as const; -function ensureDirForFile(filePath: string) { - const dir = path.dirname(path.resolve(filePath)); - fs.mkdirSync(dir, { recursive: true }); -} - -function safeReadJson(filePath: string): T | null { - try { - const raw = fs.readFileSync(filePath, 'utf8'); - return JSON.parse(raw) as T; - } catch { - return null; - } -} - -function atomicWriteFile(filePath: string, contents: string) { - ensureDirForFile(filePath); - const abs = path.resolve(filePath); - const tmp = abs + '.tmp'; - fs.writeFileSync(tmp, contents); - fs.renameSync(tmp, abs); -} - function loadExistingKeys(outFile: string): Set { const keys = new Set(); if (!fs.existsSync(outFile)) return keys; diff --git a/aggregator-hooks/polling/FluidDexT1.ts b/aggregator-hooks/polling/FluidDexT1.ts index 5fc8ef56..954a1f43 100644 --- a/aggregator-hooks/polling/FluidDexT1.ts +++ b/aggregator-hooks/polling/FluidDexT1.ts @@ -25,7 +25,6 @@ import 'dotenv/config'; import fs from 'node:fs'; -import path from 'node:path'; import { ethers } from 'ethers'; import { parseArgs, @@ -33,7 +32,9 @@ import { toInt, resolveOutputPath, resolveCheckpointPath, + withRetry, } from '@src/cli'; +import { pRateLimit, pLimit, safeReadJson, atomicWriteFile } from '@src/utils'; const OUTPUT_FILE = 'fluiddext1-pools.json'; const CHECKPOINT_FILE = 'fluiddext1_checkpoint.json'; @@ -75,25 +76,6 @@ const RESOLVER_ABI = [ 'function getDexTokens(address dex_) external view returns (address token0_, address token1_)', ]; -function ensureDirForFile(filePath: string) { - fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true }); -} - -function safeReadJson(filePath: string): T | null { - try { - return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; - } catch { - return null; - } -} - -function atomicWriteFile(filePath: string, contents: string) { - ensureDirForFile(filePath); - const abs = path.resolve(filePath); - fs.writeFileSync(abs + '.tmp', contents); - fs.renameSync(abs + '.tmp', abs); -} - function loadExistingKeys(outFile: string): Set { const keys = new Set(); if (!fs.existsSync(outFile)) return keys; @@ -156,6 +138,14 @@ async function main() { const lookbackBlocks = getEnvInt('LOOKBACK_BLOCKS', chainId, 200000); const startBlockArg = args['start-block']; + const concurrency = Math.max( + 1, + toInt(getEnvForChain('CONCURRENCY', chainId), 8), + ); + const rps = toInt(getEnvForChain('RPS', chainId), 80); + const rateLimit = pRateLimit(rps); + const limit = pLimit(concurrency); + const provider = new ethers.JsonRpcProvider(rpcUrl); const resolver = new ethers.Contract( ethers.getAddress(resolverAddr), @@ -166,7 +156,7 @@ async function main() { const iface = new ethers.Interface(FACTORY_ABI as unknown as string[]); const topic0 = iface.getEvent('LogDexDeployed')!.topicHash; - const latestBlock = await provider.getBlockNumber(); + const latestBlock = await withRetry(() => provider.getBlockNumber()); const toBlock = Math.max(0, latestBlock - finality); const cpPath = resolveCheckpointPath(checkpointDir, chainId, CHECKPOINT_FILE); @@ -211,52 +201,62 @@ async function main() { for (let start = fromBlock; start <= toBlock; start += chunkBlocks) { const end = Math.min(toBlock, start + chunkBlocks - 1); - const logs = await provider.getLogs({ - address: factory, - topics: [topic0], - fromBlock: start, - toBlock: end, - }); + const logs = await withRetry(() => + provider.getLogs({ + address: factory, + topics: [topic0], + fromBlock: start, + toBlock: end, + }), + ); totalLogs += logs.length; - const newRecords: CreatePoolsFluidDexT1Config[] = []; - - for (const log of logs) { - let parsed: ethers.LogDescription | null; - try { - parsed = iface.parseLog(log); - } catch { - continue; - } - if (!parsed) continue; - - const dex = ethers.getAddress(parsed.args.dex as string); - - let token0: string; - let token1: string; - try { - [token0, token1] = await resolver.getDexTokens(dex); - } catch { - console.error(`Failed getDexTokens for ${dex}`); - continue; - } - - const [currency0, currency1] = orderCurrencies(token0, token1); - const key = `${dex}:${currency0}:${currency1}`; - if (seenKeys.has(key)) continue; - seenKeys.add(key); - - newPools++; - newRecords.push({ - poolType: 'fluiddext1', - fluidPool: dex, - currency0, - currency1, - fee: null, - tickSpacing: null, - sqrtPriceX96: null, - }); - } + + const newRecords = ( + await Promise.all( + logs.map((log) => + limit(async () => { + let parsed: ethers.LogDescription | null; + try { + parsed = iface.parseLog(log); + } catch { + return null; + } + if (!parsed) return null; + + const dex = ethers.getAddress(parsed.args.dex as string); + let token0: string; + let token1: string; + try { + await rateLimit(); + [token0, token1] = await withRetry(() => + resolver.getDexTokens(dex), + ); + } catch { + console.error(`Failed getDexTokens for ${dex}`); + return null; + } + + const [currency0, currency1] = orderCurrencies(token0, token1); + const key = `${dex}:${currency0}:${currency1}`; + if (seenKeys.has(key)) return null; + seenKeys.add(key); + + return { + poolType: 'fluiddext1' as const, + fluidPool: dex, + currency0, + currency1, + fee: null, + tickSpacing: null, + sqrtPriceX96: null, + }; + }), + ), + ) + ).filter(Boolean) as CreatePoolsFluidDexT1Config[]; + + newPools += newRecords.length; if (newRecords.length > 0) { allRecords = allRecords.concat(newRecords); diff --git a/aggregator-hooks/polling/PancakeSwapV3.ts b/aggregator-hooks/polling/PancakeSwapV3.ts index e5afab1c..c9fdf140 100644 --- a/aggregator-hooks/polling/PancakeSwapV3.ts +++ b/aggregator-hooks/polling/PancakeSwapV3.ts @@ -22,7 +22,6 @@ import 'dotenv/config'; import fs from 'node:fs'; -import path from 'node:path'; import { ethers } from 'ethers'; import { parseArgs, @@ -31,6 +30,7 @@ import { resolveOutputPath, resolveCheckpointPath, } from '@src/cli'; +import { safeReadJson, atomicWriteFile } from '@src/utils'; const OUTPUT_FILE = 'pancakeswapv3-pools.json'; const CHECKPOINT_FILE = 'pancakeswapv3_checkpoint.json'; @@ -62,25 +62,6 @@ function isNative(addr: string): boolean { return addr.toLowerCase() === ZERO_ADDRESS; } -function ensureDirForFile(filePath: string) { - fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true }); -} - -function safeReadJson(filePath: string): T | null { - try { - return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; - } catch { - return null; - } -} - -function atomicWriteFile(filePath: string, contents: string) { - ensureDirForFile(filePath); - const abs = path.resolve(filePath); - fs.writeFileSync(abs + '.tmp', contents); - fs.renameSync(abs + '.tmp', abs); -} - function loadExistingKeys(outFile: string): Set { const keys = new Set(); if (!fs.existsSync(outFile)) return keys; diff --git a/aggregator-hooks/polling/Slipstream.ts b/aggregator-hooks/polling/Slipstream.ts index d3df4a3e..c2300751 100644 --- a/aggregator-hooks/polling/Slipstream.ts +++ b/aggregator-hooks/polling/Slipstream.ts @@ -22,7 +22,6 @@ import 'dotenv/config'; import fs from 'node:fs'; -import path from 'node:path'; import { ethers } from 'ethers'; import { parseArgs, @@ -31,6 +30,7 @@ import { resolveOutputPath, resolveCheckpointPath, } from '@src/cli'; +import { safeReadJson, atomicWriteFile } from '@src/utils'; const OUTPUT_FILE = 'slipstream-pools.json'; const CHECKPOINT_FILE = 'slipstream_checkpoint.json'; @@ -61,25 +61,6 @@ function isNative(addr: string): boolean { return addr.toLowerCase() === ZERO_ADDRESS; } -function ensureDirForFile(filePath: string) { - fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true }); -} - -function safeReadJson(filePath: string): T | null { - try { - return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; - } catch { - return null; - } -} - -function atomicWriteFile(filePath: string, contents: string) { - ensureDirForFile(filePath); - const abs = path.resolve(filePath); - fs.writeFileSync(abs + '.tmp', contents); - fs.renameSync(abs + '.tmp', abs); -} - function loadExistingKeys(outFile: string): Set { const keys = new Set(); if (!fs.existsSync(outFile)) return keys; diff --git a/aggregator-hooks/polling/StableSwapNG.ts b/aggregator-hooks/polling/StableSwapNG.ts index 3f47e6f1..c2fe23d8 100644 --- a/aggregator-hooks/polling/StableSwapNG.ts +++ b/aggregator-hooks/polling/StableSwapNG.ts @@ -27,7 +27,6 @@ import 'dotenv/config'; import fs from 'node:fs'; -import path from 'node:path'; import { ethers } from 'ethers'; import { parseArgs, @@ -35,7 +34,9 @@ import { toInt, resolveOutputPath, resolveCheckpointPath, + withRetry, } from '@src/cli'; +import { pRateLimit, pLimit, safeReadJson, atomicWriteFile } from '@src/utils'; const OUTPUT_FILE = 'stableswapng-pools.json'; const CHECKPOINT_FILE = 'stableswapng_checkpoint.json'; @@ -69,25 +70,6 @@ const FACTORY_ABI = [ 'event MetaPoolDeployed(address coin, address base_pool, uint256 A, uint256 fee, address deployer)', ]; -function ensureDirForFile(filePath: string) { - fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true }); -} - -function safeReadJson(filePath: string): T | null { - try { - return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; - } catch { - return null; - } -} - -function atomicWriteFile(filePath: string, contents: string) { - ensureDirForFile(filePath); - const abs = path.resolve(filePath); - fs.writeFileSync(abs + '.tmp', contents); - fs.renameSync(abs + '.tmp', abs); -} - function loadExistingPoolAddrs(outFile: string): Set { const keys = new Set(); if (!fs.existsSync(outFile)) return keys; @@ -101,33 +83,6 @@ function loadExistingPoolAddrs(outFile: string): Set { return keys; } -function pRateLimit(rps: number): () => Promise { - if (rps <= 0) return async () => {}; - const minGapMs = 1000 / rps; - let nextAllowed = 0; - return async function acquire() { - const now = Date.now(); - if (now < nextAllowed) - await new Promise((r) => setTimeout(r, nextAllowed - now)); - nextAllowed = Math.max(now, nextAllowed) + minGapMs; - }; -} - -function pLimit(concurrency: number) { - let active = 0; - const queue: (() => void)[] = []; - return async function limit(fn: () => Promise): Promise { - if (active >= concurrency) await new Promise((r) => queue.push(r)); - active++; - try { - return await fn(); - } finally { - active--; - queue.shift()?.(); - } - }; -} - function uniqAddresses(addrs: string[]): string[] { const s = new Set(); for (const a of addrs) { @@ -193,7 +148,7 @@ async function main() { const plainTopic = iface.getEvent('PlainPoolDeployed')!.topicHash; const metaTopic = iface.getEvent('MetaPoolDeployed')!.topicHash; - const latestBlock = await provider.getBlockNumber(); + const latestBlock = await withRetry(() => provider.getBlockNumber()); const toBlock = Math.max(0, latestBlock - finality); const cpPath = resolveCheckpointPath(checkpointDir, chainId, CHECKPOINT_FILE); @@ -253,12 +208,14 @@ async function main() { for (let start = fromBlock; start <= toBlock; start += chunkBlocks) { const end = Math.min(toBlock, start + chunkBlocks - 1); - const logs = await provider.getLogs({ - address: factory, - fromBlock: start, - toBlock: end, - topics: [[plainTopic, metaTopic]], - }); + const logs = await withRetry(() => + provider.getLogs({ + address: factory, + fromBlock: start, + toBlock: end, + topics: [[plainTopic, metaTopic]], + }), + ); eventCount += logs.length; console.error( @@ -300,44 +257,47 @@ async function main() { const outPath = resolveOutputPath(outputDir, chainId, OUTPUT_FILE); const seenAddrs = loadExistingPoolAddrs(outPath); let allRecords = safeReadJson(outPath) ?? []; - let newCount = 0; - - for (let i = startIndex; i < poolCount; i++) { - const curvePool = await limit(async () => { - await rateLimit(); - const addr = await contract.pool_list(i); - return ethers.getAddress(addr); - }); - - if (seenAddrs.has(curvePool.toLowerCase())) continue; - seenAddrs.add(curvePool.toLowerCase()); - - const meta = await limit(async () => { - await rateLimit(); - const [nCoinsBn, coinsRaw, basePoolRaw] = await Promise.all([ - contract.get_n_coins(curvePool) as Promise, - contract.get_coins(curvePool) as Promise, - contract.get_base_pool(curvePool) as Promise, - ]); - const basePool = ethers.getAddress(basePoolRaw); - const isPlain = - basePool.toLowerCase() === ethers.ZeroAddress.toLowerCase(); - const coins = uniqAddresses(coinsRaw as string[]); - return { nCoins: Number(nCoinsBn), coins, isPlain }; - }); - - if (!meta.isPlain) continue; - - allRecords.push({ - poolType: 'stableswapng', - curvePool, - tokens: meta.coins, - fee: null, - tickSpacing: null, - sqrtPriceX96: null, - }); - newCount++; - } + + const newIndices = Array.from( + { length: fetchCount }, + (_, k) => startIndex + k, + ); + const fetched = await Promise.all( + newIndices.map((i) => + limit(async () => { + await rateLimit(); + const addr = await withRetry( + () => contract.pool_list(i) as Promise, + ); + const curvePool = ethers.getAddress(addr); + if (seenAddrs.has(curvePool.toLowerCase())) return null; + + await rateLimit(); + const [nCoinsBn, coinsRaw, basePoolRaw] = await Promise.all([ + withRetry(() => contract.get_n_coins(curvePool) as Promise), + withRetry(() => contract.get_coins(curvePool) as Promise), + withRetry(() => contract.get_base_pool(curvePool) as Promise), + ]); + const basePool = ethers.getAddress(basePoolRaw); + const isPlain = + basePool.toLowerCase() === ethers.ZeroAddress.toLowerCase(); + if (!isPlain) return null; + const coins = uniqAddresses(coinsRaw as string[]); + return { + poolType: 'stableswapng' as const, + curvePool, + tokens: coins, + fee: null, + tickSpacing: null, + sqrtPriceX96: null, + }; + }), + ), + ); + + const newRecords = fetched.filter(Boolean) as CreatePoolsStableSwapConfig[]; + allRecords = allRecords.concat(newRecords); + const newCount = newRecords.length; if (newCount > 0) { atomicWriteFile(outPath, JSON.stringify(allRecords, null, 2) + '\n'); diff --git a/aggregator-hooks/polling/UniswapV2.ts b/aggregator-hooks/polling/UniswapV2.ts index 199abc18..d9267bd4 100644 --- a/aggregator-hooks/polling/UniswapV2.ts +++ b/aggregator-hooks/polling/UniswapV2.ts @@ -22,7 +22,6 @@ import 'dotenv/config'; import fs from 'node:fs'; -import path from 'node:path'; import { ethers } from 'ethers'; import { parseArgs, @@ -31,6 +30,7 @@ import { resolveOutputPath, resolveCheckpointPath, } from '@src/cli'; +import { safeReadJson, atomicWriteFile } from '@src/utils'; const OUTPUT_FILE = 'uniswapv2-pools.json'; const CHECKPOINT_FILE = 'uniswapv2_checkpoint.json'; @@ -61,25 +61,6 @@ function isNative(addr: string): boolean { return addr.toLowerCase() === ZERO_ADDRESS; } -function ensureDirForFile(filePath: string) { - fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true }); -} - -function safeReadJson(filePath: string): T | null { - try { - return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; - } catch { - return null; - } -} - -function atomicWriteFile(filePath: string, contents: string) { - ensureDirForFile(filePath); - const abs = path.resolve(filePath); - fs.writeFileSync(abs + '.tmp', contents); - fs.renameSync(abs + '.tmp', abs); -} - function loadExistingKeys(outFile: string): Set { const keys = new Set(); if (!fs.existsSync(outFile)) return keys; diff --git a/aggregator-hooks/polling/UniswapV3.ts b/aggregator-hooks/polling/UniswapV3.ts index 57a0e0d8..d0385d35 100644 --- a/aggregator-hooks/polling/UniswapV3.ts +++ b/aggregator-hooks/polling/UniswapV3.ts @@ -22,7 +22,6 @@ import 'dotenv/config'; import fs from 'node:fs'; -import path from 'node:path'; import { ethers } from 'ethers'; import { parseArgs, @@ -31,6 +30,7 @@ import { resolveOutputPath, resolveCheckpointPath, } from '@src/cli'; +import { safeReadJson, atomicWriteFile } from '@src/utils'; const OUTPUT_FILE = 'uniswapv3-pools.json'; const CHECKPOINT_FILE = 'uniswapv3_checkpoint.json'; @@ -62,25 +62,6 @@ function isNative(addr: string): boolean { return addr.toLowerCase() === ZERO_ADDRESS; } -function ensureDirForFile(filePath: string) { - fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true }); -} - -function safeReadJson(filePath: string): T | null { - try { - return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; - } catch { - return null; - } -} - -function atomicWriteFile(filePath: string, contents: string) { - ensureDirForFile(filePath); - const abs = path.resolve(filePath); - fs.writeFileSync(abs + '.tmp', contents); - fs.renameSync(abs + '.tmp', abs); -} - function loadExistingKeys(outFile: string): Set { const keys = new Set(); if (!fs.existsSync(outFile)) return keys; diff --git a/aggregator-hooks/src/cli.ts b/aggregator-hooks/src/cli.ts index 2adbcf45..beb04fa5 100644 --- a/aggregator-hooks/src/cli.ts +++ b/aggregator-hooks/src/cli.ts @@ -88,3 +88,23 @@ export interface HistoricalArgs extends CommonArgs { startBlock: number; endBlock: number | null; } + +/** + * Retry wrapper with exponential backoff for transient RPC errors. + * Retries on any thrown error; callers should not use this for on-chain reverts. + */ +export async function withRetry( + fn: () => Promise, + retries = 5, + baseMs = 500, +): Promise { + for (let attempt = 0; ; attempt++) { + try { + return await fn(); + } catch (e) { + if (attempt >= retries) throw e; + const delay = baseMs * 2 ** attempt + Math.random() * 100; + await new Promise((r) => setTimeout(r, delay)); + } + } +} diff --git a/aggregator-hooks/src/createPools.ts b/aggregator-hooks/src/createPools.ts index 023bcd50..0eedd88f 100644 --- a/aggregator-hooks/src/createPools.ts +++ b/aggregator-hooks/src/createPools.ts @@ -2,7 +2,13 @@ import 'dotenv/config'; import { ethers } from 'ethers'; -import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; +import { + readFileSync, + writeFileSync, + renameSync, + existsSync, + mkdirSync, +} from 'fs'; import { execFileSync, spawn } from 'child_process'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; @@ -328,7 +334,9 @@ function appendToRegistryFile( if (!existsSync(registryDir)) { mkdirSync(registryDir, { recursive: true }); } - writeFileSync(filePath, JSON.stringify(poolsDeployed, null, 2)); + const tmp = filePath + '.tmp'; + writeFileSync(tmp, JSON.stringify(poolsDeployed, null, 2)); + renameSync(tmp, filePath); log.info(`Appended to registry: ${filePath}`); } @@ -725,8 +733,10 @@ async function mineSalt( const baseArgs = [scriptPath, constructorArgs, protocolIdHex]; if (deployerAddress) baseArgs.push('500', deployerAddress); + // Strip sensitive credentials — salt mining workers only need PATH and build tooling + const { PRIVATE_KEY: _pk, ...safeEnv } = process.env; const execEnv = { - ...process.env, + ...safeEnv, ...(log.verboseEnabled && { FORGE_VERBOSE: '1' }), }; @@ -875,7 +885,11 @@ function selfDeployPool( { encoding: 'utf-8', cwd: projectRoot, - env: { ...process.env, ...envVars }, + // envVars already contains PRIVATE_KEY; strip it from process.env to avoid duplication + env: (() => { + const { PRIVATE_KEY: _pk, ...rest } = process.env; + return { ...rest, ...envVars }; + })(), }, ); @@ -1088,6 +1102,12 @@ async function main() { ); if (!deployed || deployed === 'deployed') { + if (dryRun) { + log.success( + 'Dry run: singleton self-deploy script completed — skipping pool initialization (no real address without broadcast)', + ); + return; + } log.error( 'Could not determine deployed aggregator address — aborting', ); diff --git a/aggregator-hooks/src/logger.ts b/aggregator-hooks/src/logger.ts index d95d1639..96e9e928 100644 --- a/aggregator-hooks/src/logger.ts +++ b/aggregator-hooks/src/logger.ts @@ -1,3 +1,21 @@ +/** Redact the API key segment from RPC URLs (e.g. Infura/Alchemy path keys). */ +function redactRpcUrl(url: string): string { + try { + const u = new URL(url); + const parts = u.pathname.split('/').filter(Boolean); + if (parts.length > 0) { + const last = parts[parts.length - 1]; + if (last.length >= 16) { + parts[parts.length - 1] = last.slice(0, 4) + '***'; + u.pathname = '/' + parts.join('/'); + } + } + return u.toString(); + } catch { + return url; + } +} + /** * Config passed to banner() for startup output. */ @@ -62,7 +80,7 @@ export function createLogger(opts: { verbose: boolean }): Logger { ...(config.factoryAddress ? [`Factory Address: ${config.factoryAddress}`] : []), - `RPC URL: ${config.rpcUrl}`, + `RPC URL: ${redactRpcUrl(config.rpcUrl)}`, ...(config.registryDir ? [`Registry dir: ${config.registryDir}`] : []), ...(config.dryRun ? ['DRY RUN: forge scripts will simulate without broadcasting'] diff --git a/aggregator-hooks/src/utils.ts b/aggregator-hooks/src/utils.ts new file mode 100644 index 00000000..207a6519 --- /dev/null +++ b/aggregator-hooks/src/utils.ts @@ -0,0 +1,57 @@ +/** + * Shared utilities for polling and historical discovery scripts. + * Centralizes rate limiting, concurrency control, and file I/O helpers. + */ +import fs from 'node:fs'; +import path from 'node:path'; + +/** Global RPC rate limiter: token-bucket style, strictly serialized. */ +export function pRateLimit(rps: number): () => Promise { + if (rps <= 0) return async () => {}; + const minGapMs = 1000 / rps; + let nextAllowed = 0; + return async function acquire(): Promise { + const now = Date.now(); + if (now < nextAllowed) + await new Promise((r) => setTimeout(r, nextAllowed - now)); + nextAllowed = Math.max(now, nextAllowed) + minGapMs; + }; +} + +/** Concurrency limiter: at most `concurrency` async tasks in flight at once. */ +export function pLimit(concurrency: number) { + let active = 0; + const queue: (() => void)[] = []; + return async function limit(fn: () => Promise): Promise { + if (active >= concurrency) await new Promise((r) => queue.push(r)); + active++; + try { + return await fn(); + } finally { + active--; + queue.shift()?.(); + } + }; +} + +/** Ensure the parent directory of a file path exists. */ +export function ensureDirForFile(filePath: string): void { + fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true }); +} + +/** Read and parse a JSON file; returns null on any error. */ +export function safeReadJson(filePath: string): T | null { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; + } catch { + return null; + } +} + +/** Write a file atomically via a .tmp rename to prevent partial writes on crash. */ +export function atomicWriteFile(filePath: string, contents: string): void { + ensureDirForFile(filePath); + const abs = path.resolve(filePath); + fs.writeFileSync(abs + '.tmp', contents); + fs.renameSync(abs + '.tmp', abs); +} diff --git a/aggregator-hooks/tsconfig.json b/aggregator-hooks/tsconfig.json index b5cd5d00..485caec0 100644 --- a/aggregator-hooks/tsconfig.json +++ b/aggregator-hooks/tsconfig.json @@ -5,7 +5,8 @@ "moduleResolution": "NodeNext", "baseUrl": ".", "paths": { - "@src/cli": ["src/cli.ts"] + "@src/cli": ["src/cli.ts"], + "@src/utils": ["src/utils.ts"] }, "lib": ["ES2022"], "outDir": "dist", diff --git a/src/pkgs/v4-core b/src/pkgs/v4-core index 46c68346..d153b048 160000 --- a/src/pkgs/v4-core +++ b/src/pkgs/v4-core @@ -1 +1 @@ -Subproject commit 46c6834698c48bc4a463a86d8420f4eb1d7f3b75 +Subproject commit d153b048868a60c2403a3ef5b2301bb247884d46