chain-insights 0.2.18 → 0.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +54 -12
  2. package/bin/cli.js +2 -3
  3. package/bin/install.cjs +0 -1
  4. package/dist/{app-DxlQE_P5.cjs → app-BxojXjtB.cjs} +1 -1
  5. package/dist/{app-DdWQF_zb.mjs → app-CRd39JJ8.mjs} +2 -2
  6. package/dist/{app-DdWQF_zb.mjs.map → app-CRd39JJ8.mjs.map} +1 -1
  7. package/dist/{artifact-server-4DiMvwhC.mjs → artifact-server-CP6LXQ9d.mjs} +2 -2
  8. package/dist/{artifact-server-4DiMvwhC.mjs.map → artifact-server-CP6LXQ9d.mjs.map} +1 -1
  9. package/dist/{artifact-server-B-3ho4bk.cjs → artifact-server-XbN16DwU.cjs} +1 -1
  10. package/dist/cli.cjs +66 -25
  11. package/dist/cli.mjs +66 -25
  12. package/dist/cli.mjs.map +1 -1
  13. package/dist/{config-BhYbhLDI.cjs → config-BwVx19Og.cjs} +48 -15
  14. package/dist/config-Drgc2HuF.mjs +77 -0
  15. package/dist/config-Drgc2HuF.mjs.map +1 -0
  16. package/dist/frontmatter-D0ccQnUM.mjs.map +1 -1
  17. package/dist/index.cjs +4 -4
  18. package/dist/index.d.cts +3 -3
  19. package/dist/index.d.cts.map +1 -1
  20. package/dist/index.d.mts +3 -3
  21. package/dist/index.d.mts.map +1 -1
  22. package/dist/index.mjs +4 -4
  23. package/dist/{init-CZbZegIW.mjs → init-4tn7jfhN.mjs} +3 -2
  24. package/dist/init-4tn7jfhN.mjs.map +1 -0
  25. package/dist/{init-BvpZtFiT.cjs → init-TCQY5RDJ.cjs} +2 -1
  26. package/dist/mcp-endpoint-BaV8h_lq.cjs +60 -0
  27. package/dist/mcp-endpoint-DHs1cRFH.mjs +39 -0
  28. package/dist/mcp-endpoint-DHs1cRFH.mjs.map +1 -0
  29. package/dist/mcp-proxy.cjs +108 -9
  30. package/dist/mcp-proxy.d.cts.map +1 -1
  31. package/dist/mcp-proxy.d.mts.map +1 -1
  32. package/dist/mcp-proxy.mjs +108 -9
  33. package/dist/mcp-proxy.mjs.map +1 -1
  34. package/dist/{public-tools-D6Q5MTcO.mjs → public-tools-B13J0MJZ.mjs} +465 -70
  35. package/dist/public-tools-B13J0MJZ.mjs.map +1 -0
  36. package/dist/{public-tools-V7ON7goq.cjs → public-tools-BC1fi0DV.cjs} +464 -68
  37. package/dist/resolver-D7VBb0uB.mjs.map +1 -1
  38. package/dist/{runner-BatyCxv7.mjs → runner-DIs04IhN.mjs} +2 -2
  39. package/dist/{runner-BatyCxv7.mjs.map → runner-DIs04IhN.mjs.map} +1 -1
  40. package/dist/{runner-CCA7SJ7X.cjs → runner-ZYowxCVl.cjs} +1 -1
  41. package/dist/schema-BFEWhzg7.mjs +60 -0
  42. package/dist/schema-BFEWhzg7.mjs.map +1 -0
  43. package/dist/{schema-DN-KLkYN.cjs → schema-Vl9yuOFO.cjs} +31 -8
  44. package/dist/{server-BDlbmGbL.mjs → server-BXLX2j_A.mjs} +2 -2
  45. package/dist/{server-BDlbmGbL.mjs.map → server-BXLX2j_A.mjs.map} +1 -1
  46. package/dist/{server-C3y1gQmZ.cjs → server-BqVdWath.cjs} +1 -1
  47. package/dist/{topup-server-6MH7q73X.mjs → topup-server-BJgVw6Jt.mjs} +100 -42
  48. package/dist/topup-server-BJgVw6Jt.mjs.map +1 -0
  49. package/dist/{topup-server-DjUjhNjv.cjs → topup-server-yAaXYkJP.cjs} +98 -40
  50. package/docs/architecture.md +4 -0
  51. package/docs/contributing.md +1 -0
  52. package/docs/debugging.md +10 -14
  53. package/docs/graph-tools.md +60 -2
  54. package/docs/mcp-proxy.md +44 -0
  55. package/package.json +2 -2
  56. package/skills/chain-insights-developer-experience/SKILL.md +4 -2
  57. package/skills/chain-insights-investigation/SKILL.md +1 -1
  58. package/skills/test-chain-insights-graphrag-mcp/SKILL.md +4 -5
  59. package/skills/test-chain-insights-graphrag-mcp/scripts/run-uat.sh +5 -24
  60. package/dist/config-9KYXaAv-.mjs +0 -44
  61. package/dist/config-9KYXaAv-.mjs.map +0 -1
  62. package/dist/init-CZbZegIW.mjs.map +0 -1
  63. package/dist/public-tools-D6Q5MTcO.mjs.map +0 -1
  64. package/dist/schema-BbQVXp36.mjs +0 -37
  65. package/dist/schema-BbQVXp36.mjs.map +0 -1
  66. package/dist/topup-server-6MH7q73X.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"topup-server-BJgVw6Jt.mjs","names":["USDC_ADDRESS","getTopupUrl","startTopupServer","getCopiedTopupUrl","startCopiedTopupServer"],"sources":["../src/wallet/mcp-proxy/qr.ts","../src/wallet/mcp-proxy/tools.ts","../src/wallet/mcp-proxy/topup-server.ts","../src/wallet/topup-server.ts"],"sourcesContent":["/**\n * Minimal QR code generator — produces SVG string server-side.\n * Supports alphanumeric mode (sufficient for Ethereum addresses).\n * No external dependencies.\n */\n\n// Error correction level M (15% recovery)\nconst EC_LEVEL = 0; // L=0, M=1 — using L for simplicity with short data\n\n// QR code version 2 (25x25) is sufficient for 42-char ETH addresses\nconst VERSION = 2;\nconst SIZE = 25; // modules per side for version 2\n\n// Generator polynomial for version 2-L: 10 EC codewords\nconst EC_CODEWORDS = 10;\nconst DATA_CODEWORDS = 34;\n\n// Format info for version 2, mask 0, EC level L\nconst FORMAT_BITS = 0b111011111000100;\n\n// Byte mode indicator\nconst MODE_BYTE = 0b0100;\n\nfunction createMatrix(): number[][] {\n const m: number[][] = [];\n for (let i = 0; i < SIZE; i++) {\n m[i] = new Array(SIZE).fill(-1);\n }\n return m;\n}\n\nfunction addFinderPattern(matrix: number[][], row: number, col: number): void {\n for (let r = -1; r <= 7; r++) {\n for (let c = -1; c <= 7; c++) {\n const mr = row + r;\n const mc = col + c;\n if (mr < 0 || mr >= SIZE || mc < 0 || mc >= SIZE) continue;\n if (r >= 0 && r <= 6 && c >= 0 && c <= 6) {\n if (r === 0 || r === 6 || c === 0 || c === 6 || (r >= 2 && r <= 4 && c >= 2 && c <= 4)) {\n matrix[mr][mc] = 1;\n } else {\n matrix[mr][mc] = 0;\n }\n } else {\n matrix[mr][mc] = 0;\n }\n }\n }\n}\n\nfunction addAlignmentPattern(matrix: number[][], row: number, col: number): void {\n for (let r = -2; r <= 2; r++) {\n for (let c = -2; c <= 2; c++) {\n if (Math.abs(r) === 2 || Math.abs(c) === 2 || (r === 0 && c === 0)) {\n matrix[row + r][col + c] = 1;\n } else {\n matrix[row + r][col + c] = 0;\n }\n }\n }\n}\n\nfunction addTimingPatterns(matrix: number[][]): void {\n for (let i = 8; i < SIZE - 8; i++) {\n if (matrix[6][i] === -1) matrix[6][i] = i % 2 === 0 ? 1 : 0;\n if (matrix[i][6] === -1) matrix[i][6] = i % 2 === 0 ? 1 : 0;\n }\n}\n\nfunction addFormatInfo(matrix: number[][]): void {\n const bits = FORMAT_BITS;\n for (let i = 0; i <= 5; i++) matrix[8][i] = (bits >> (14 - i)) & 1;\n matrix[8][7] = (bits >> 8) & 1;\n matrix[8][8] = (bits >> 7) & 1;\n matrix[7][8] = (bits >> 6) & 1;\n for (let i = 0; i <= 5; i++) matrix[5 - i][8] = (bits >> (i)) & 1;\n\n for (let i = 0; i <= 7; i++) matrix[SIZE - 1 - i][8] = (bits >> (14 - i)) & 1;\n for (let i = 0; i <= 7; i++) matrix[8][SIZE - 8 + i] = (bits >> (7 - i)) & 1;\n\n // Dark module\n matrix[SIZE - 8][8] = 1;\n}\n\nfunction encodeData(text: string): number[] {\n const bytes = new TextEncoder().encode(text);\n const bits: number[] = [];\n\n // Mode indicator (4 bits): byte mode\n for (let i = 3; i >= 0; i--) bits.push((MODE_BYTE >> i) & 1);\n\n // Character count (8 bits for version 1-9 byte mode)\n for (let i = 7; i >= 0; i--) bits.push((bytes.length >> i) & 1);\n\n // Data\n for (const b of bytes) {\n for (let i = 7; i >= 0; i--) bits.push((b >> i) & 1);\n }\n\n // Terminator\n while (bits.length < DATA_CODEWORDS * 8 && bits.length < DATA_CODEWORDS * 8) {\n bits.push(0);\n if (bits.length >= DATA_CODEWORDS * 8) break;\n }\n\n // Pad to byte boundary\n while (bits.length % 8 !== 0) bits.push(0);\n\n // Pad codewords\n const padBytes = [0xec, 0x11];\n let padIdx = 0;\n while (bits.length < DATA_CODEWORDS * 8) {\n const pb = padBytes[padIdx % 2];\n for (let i = 7; i >= 0; i--) bits.push((pb >> i) & 1);\n padIdx++;\n }\n\n // Convert to bytes\n const codewords: number[] = [];\n for (let i = 0; i < bits.length; i += 8) {\n let val = 0;\n for (let j = 0; j < 8; j++) val = (val << 1) | (bits[i + j] || 0);\n codewords.push(val);\n }\n\n return codewords;\n}\n\n// GF(256) arithmetic for Reed-Solomon\nconst GF_EXP = new Array(512).fill(0);\nconst GF_LOG = new Array(256).fill(0);\n\n(function initGF() {\n let x = 1;\n for (let i = 0; i < 255; i++) {\n GF_EXP[i] = x;\n GF_LOG[x] = i;\n x <<= 1;\n if (x & 0x100) x ^= 0x11d;\n }\n for (let i = 255; i < 512; i++) GF_EXP[i] = GF_EXP[i - 255];\n})();\n\nfunction gfMul(a: number, b: number): number {\n if (a === 0 || b === 0) return 0;\n return GF_EXP[GF_LOG[a] + GF_LOG[b]];\n}\n\nfunction rsEncode(data: number[], ecCount: number): number[] {\n // Generate generator polynomial\n let gen = [1];\n for (let i = 0; i < ecCount; i++) {\n const next = new Array(gen.length + 1).fill(0);\n for (let j = 0; j < gen.length; j++) {\n next[j] ^= gen[j];\n next[j + 1] ^= gfMul(gen[j], GF_EXP[i]);\n }\n gen = next;\n }\n\n const msg = [...data, ...new Array(ecCount).fill(0)];\n for (let i = 0; i < data.length; i++) {\n const coef = msg[i];\n if (coef !== 0) {\n for (let j = 0; j < gen.length; j++) {\n msg[i + j] ^= gfMul(gen[j], coef);\n }\n }\n }\n\n return msg.slice(data.length);\n}\n\nfunction placeData(matrix: number[][], dataBits: number[]): void {\n let bitIdx = 0;\n let upward = true;\n\n for (let right = SIZE - 1; right >= 1; right -= 2) {\n if (right === 6) right = 5; // skip timing column\n\n const rows = upward\n ? Array.from({ length: SIZE }, (_, i) => SIZE - 1 - i)\n : Array.from({ length: SIZE }, (_, i) => i);\n\n for (const row of rows) {\n for (let c = 0; c < 2; c++) {\n const col = right - c;\n if (matrix[row][col] !== -1) continue;\n matrix[row][col] = bitIdx < dataBits.length ? dataBits[bitIdx++] : 0;\n }\n }\n upward = !upward;\n }\n}\n\nfunction applyMask0(matrix: number[][], reserved: number[][]): void {\n for (let r = 0; r < SIZE; r++) {\n for (let c = 0; c < SIZE; c++) {\n if (reserved[r][c] !== -1) continue;\n if ((r + c) % 2 === 0) {\n matrix[r][c] ^= 1;\n }\n }\n }\n}\n\nexport interface QrOptions {\n cellSize?: number;\n fgColor?: string;\n bgColor?: string;\n finderColor?: string;\n logoBase64?: string; // data URI for center logo\n logoWidth?: number; // logo width in modules (default: 7)\n logoHeight?: number; // logo height in modules (default: 5)\n}\n\nexport function generateQrSvg(text: string, opts: QrOptions | number = 4): string {\n // Backward compat: accept bare cellSize number\n const options: QrOptions = typeof opts === \"number\" ? { cellSize: opts } : opts;\n const cellSize = options.cellSize ?? 4;\n const fgColor = options.fgColor ?? \"#000\";\n const bgColor = options.bgColor ?? \"#fff\";\n const finderColor = options.finderColor ?? fgColor;\n\n const matrix = createMatrix();\n\n // Finder patterns\n addFinderPattern(matrix, 0, 0);\n addFinderPattern(matrix, 0, SIZE - 7);\n addFinderPattern(matrix, SIZE - 7, 0);\n\n // Alignment pattern (version 2: at position 18)\n addAlignmentPattern(matrix, 18, 18);\n\n // Timing\n addTimingPatterns(matrix);\n\n // Format info placeholder\n addFormatInfo(matrix);\n\n // Save reserved areas\n const reserved = matrix.map(row => [...row]);\n\n // Encode data + EC\n const dataCodewords = encodeData(text);\n const ecCodewords = rsEncode(dataCodewords, EC_CODEWORDS);\n const allCodewords = [...dataCodewords, ...ecCodewords];\n\n // Convert to bits\n const dataBits: number[] = [];\n for (const cw of allCodewords) {\n for (let i = 7; i >= 0; i--) dataBits.push((cw >> i) & 1);\n }\n\n // Place data\n placeData(matrix, dataBits);\n\n // Apply mask 0\n applyMask0(matrix, reserved);\n\n // Re-apply format info (mask may have flipped it)\n addFormatInfo(matrix);\n\n // Logo exclusion zone (center of QR)\n const logoW = options.logoWidth ?? 7;\n const logoH = options.logoHeight ?? 5;\n const logoStartC = Math.floor((SIZE - logoW) / 2);\n const logoStartR = Math.floor((SIZE - logoH) / 2);\n const hasLogo = !!options.logoBase64;\n\n // Generate SVG\n const svgSize = SIZE * cellSize;\n let svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"${svgSize}\" height=\"${svgSize}\" viewBox=\"0 0 ${svgSize} ${svgSize}\">`;\n svg += `<rect width=\"${svgSize}\" height=\"${svgSize}\" fill=\"${bgColor}\" rx=\"4\"/>`;\n\n // Finder pattern regions for coloring\n const isFinderModule = (r: number, c: number): boolean =>\n (r < 7 && c < 7) || (r < 7 && c >= SIZE - 7) || (r >= SIZE - 7 && c < 7);\n\n for (let r = 0; r < SIZE; r++) {\n for (let c = 0; c < SIZE; c++) {\n // Skip logo area\n if (hasLogo && r >= logoStartR && r < logoStartR + logoH && c >= logoStartC && c < logoStartC + logoW) {\n continue;\n }\n if (matrix[r][c] === 1) {\n const color = isFinderModule(r, c) ? finderColor : fgColor;\n svg += `<rect x=\"${c * cellSize}\" y=\"${r * cellSize}\" width=\"${cellSize}\" height=\"${cellSize}\" fill=\"${color}\" rx=\"0.5\"/>`;\n }\n }\n }\n\n // Embed logo in center\n if (hasLogo && options.logoBase64) {\n const lx = logoStartC * cellSize;\n const ly = logoStartR * cellSize;\n const lw = logoW * cellSize;\n const lh = logoH * cellSize;\n // White background behind logo\n svg += `<rect x=\"${lx - 1}\" y=\"${ly - 1}\" width=\"${lw + 2}\" height=\"${lh + 2}\" fill=\"${bgColor}\" rx=\"3\"/>`;\n svg += `<image x=\"${lx + 2}\" y=\"${ly + 2}\" width=\"${lw - 4}\" height=\"${lh - 4}\" href=\"${options.logoBase64}\" xlink:href=\"${options.logoBase64}\" preserveAspectRatio=\"xMidYMid meet\"/>`;\n }\n\n svg += '</svg>';\n return svg;\n}\n","import type { WalletData } from \"./types.js\";\nimport { createPublicClient, http, formatEther, formatUnits } from \"viem\";\nimport { base } from \"viem/chains\";\n\nconst USDC_ADDRESS = \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\" as const;\nconst DEFAULT_BASE_RPC_URL = \"https://mainnet.base.org\";\nconst PUBLIC_BASE_RPC_URLS = [\n DEFAULT_BASE_RPC_URL,\n \"https://base-rpc.publicnode.com\",\n \"https://base.drpc.org\",\n \"https://1rpc.io/base\",\n] as const;\nconst USDC_ABI = [\n {\n name: \"balanceOf\",\n type: \"function\",\n stateMutability: \"view\",\n inputs: [{ name: \"account\", type: \"address\" }],\n outputs: [{ name: \"\", type: \"uint256\" }],\n },\n] as const;\n\nfunction walletAddress(wallet: WalletData | string): `0x${string}` {\n return (typeof wallet === \"string\" ? wallet : wallet.address) as `0x${string}`;\n}\n\nexport async function getBalanceUsdc(wallet: WalletData | string): Promise<string> {\n const envRpcUrl = process.env.BASE_RPC_URL;\n const rpcUrls = [\n ...(envRpcUrl ? [envRpcUrl] : []),\n ...PUBLIC_BASE_RPC_URLS.filter((url) => url !== envRpcUrl),\n ];\n\n for (const rpcUrl of rpcUrls) {\n try {\n const client = createPublicClient({ chain: base, transport: http(rpcUrl) });\n const balance = await client.readContract({\n address: USDC_ADDRESS,\n abi: USDC_ABI,\n functionName: \"balanceOf\",\n args: [walletAddress(wallet)],\n });\n return formatUnits(balance, 6);\n } catch {\n // Try the next public Base RPC endpoint.\n }\n }\n\n return \"unknown\";\n}\n\nexport async function getBalanceEth(wallet: WalletData | string): Promise<string> {\n const envRpcUrl = process.env.BASE_RPC_URL;\n const rpcUrls = [\n ...(envRpcUrl ? [envRpcUrl] : []),\n ...PUBLIC_BASE_RPC_URLS.filter((url) => url !== envRpcUrl),\n ];\n\n for (const rpcUrl of rpcUrls) {\n try {\n const client = createPublicClient({ chain: base, transport: http(rpcUrl) });\n const balance = await client.getBalance({ address: walletAddress(wallet) });\n return formatEther(balance);\n } catch {\n // Try the next public Base RPC endpoint.\n }\n }\n\n return \"unknown\";\n}\n\nexport async function getBalance(wallet: WalletData): Promise<string> {\n const [balanceUsdc, balanceEth] = await Promise.all([\n getBalanceUsdc(wallet),\n getBalanceEth(wallet),\n ]);\n return [\n `Balance: ${balanceUsdc} USDC`,\n `Gas: ${balanceEth} ETH on Base`,\n `Network: Base`,\n `Base ETH is required for one-time USDC Permit2 approval gas.`,\n `Address: ${wallet.address}`,\n ].filter(Boolean).join(\"\\n\");\n}\n\nexport function getTopupInfo(wallet: WalletData): string {\n return JSON.stringify({\n wallet_address: wallet.address,\n network: \"Base (Chain ID 8453)\",\n token: \"USDC\",\n contract: USDC_ADDRESS,\n instructions: [\n `Send USDC on Base network to: ${wallet.address}`,\n ],\n });\n}\n","import { createServer, type Server } from \"node:http\";\nimport { readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { isAddress } from \"viem\";\nimport type { WalletData } from \"./types.js\";\nimport { generateQrSvg } from \"./qr.js\";\nimport { getBalanceEth, getBalanceUsdc } from \"./tools.js\";\n\nconst USDC_ADDRESS = \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\";\nconst BASE_CHAIN_ID = \"0x2105\"; // 8453\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n// Pre-load assets — try dist/assets first (installed), then src/assets (dev)\nfunction loadAsset(name: string): Buffer {\n const paths = [\n join(__dirname, \"assets\", name), // dist/assets/ (global install)\n join(__dirname, \"..\", \"src\", \"assets\", name), // src/assets/ (dev)\n ];\n for (const p of paths) {\n try { return readFileSync(p); } catch { /* try next */ }\n }\n console.error(`[chain-insights] Warning: asset ${name} not found`);\n return Buffer.alloc(0);\n}\n\nconst logoPng = loadAsset(\"logo.png\");\nconst bgPatternPng = loadAsset(\"bg-pattern.png\");\n\nlet server: Server | null = null;\nlet serverPort: number | null = null;\n\nfunction assertWalletAddress(wallet: WalletData | string): string {\n const walletAddress = typeof wallet === \"string\" ? wallet : wallet.address;\n if (!isAddress(walletAddress)) {\n throw new Error(\"Wallet address must be a valid 0x-prefixed 20-byte EVM address\");\n }\n return walletAddress;\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\")\n .replaceAll(\"'\", \"&#39;\");\n}\n\nfunction jsonForScript(value: string): string {\n return JSON.stringify(value)\n .replaceAll(\"<\", \"\\\\u003c\")\n .replaceAll(\">\", \"\\\\u003e\")\n .replaceAll(\"&\", \"\\\\u0026\");\n}\n\nexport function getTopupUrl(): string | null {\n return serverPort ? `http://localhost:${serverPort}` : null;\n}\n\nexport async function startTopupServer(wallet: WalletData | string): Promise<string> {\n const walletAddress = assertWalletAddress(wallet);\n\n if (server && serverPort) {\n return `http://localhost:${serverPort}`;\n }\n\n return new Promise((resolve, reject) => {\n server = createServer((req, res) => {\n if (req.url === \"/api/wallet\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\", \"Access-Control-Allow-Origin\": \"*\" });\n res.end(JSON.stringify({ address: walletAddress }));\n return;\n }\n\n if (req.url === \"/api/balance\") {\n Promise.all([getBalanceUsdc(walletAddress), getBalanceEth(walletAddress)]).then(([balanceUsdc, balanceEth]) => {\n res.writeHead(200, { \"Content-Type\": \"application/json\", \"Access-Control-Allow-Origin\": \"*\" });\n res.end(JSON.stringify({ balance_usdc: balanceUsdc, balance_eth: balanceEth }));\n }).catch(() => {\n res.writeHead(200, { \"Content-Type\": \"application/json\", \"Access-Control-Allow-Origin\": \"*\" });\n res.end(JSON.stringify({ balance_usdc: \"unknown\", balance_eth: \"unknown\" }));\n });\n return;\n }\n\n if (req.url === \"/assets/logo.png\") {\n res.writeHead(200, { \"Content-Type\": \"image/png\", \"Cache-Control\": \"public, max-age=86400\", \"Access-Control-Allow-Origin\": \"*\" });\n res.end(logoPng);\n return;\n }\n\n if (req.url === \"/assets/bg-pattern.png\") {\n res.writeHead(200, { \"Content-Type\": \"image/png\", \"Cache-Control\": \"public, max-age=86400\", \"Access-Control-Allow-Origin\": \"*\" });\n res.end(bgPatternPng);\n return;\n }\n\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(generatePage(walletAddress));\n });\n\n server.listen(0, \"127.0.0.1\", () => {\n const addr = server!.address();\n if (addr && typeof addr === \"object\") {\n serverPort = addr.port;\n const url = `http://localhost:${serverPort}`;\n console.error(`[chain-insights] Topup server running at ${url}`);\n resolve(url);\n } else {\n reject(new Error(\"Failed to start topup server\"));\n }\n });\n\n server.on(\"error\", reject);\n });\n}\n\nexport function generateArtifactHtml(walletAddressInput: string, topupUrl: string): string {\n const walletAddress = assertWalletAddress(walletAddressInput);\n const safeWalletAddress = escapeHtml(walletAddress);\n const safeTopupUrl = escapeHtml(topupUrl);\n const topupUrlJson = jsonForScript(topupUrl);\n\n const LOGO_B64 = \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMEAAAAeCAYAAACVKnpmAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAA2JSURBVHgB7VxNUhtJFn6ZWSCYNkKsJqI3I43t7phVwwksTmCI2djgDsQJLE6AOAFwAuToBnozgTiB5ROgWU1M27TkTUf0Cln0BH9V+ea9rCqpVD+SQMaWHfoiJJVKVZkvf97/KwkYEGf1vYwF02tC47wGyIPADCBkzI8CmkJDgz4bQkLlBsSbudzzBowxxhcA0e+C87eHeVCwiYh5uAUEiqpQWH6QW3kFY4wxwkhkgrP6YdZC2Eva/Agk9Unym2OELDWUje1AQM0WYnmsGcYYVcQyQevdfgGk2G6bOz4QqvReds6vjucW1pvBn85O9jLwzeSSssQaXZfvbhGbCLg1+/DFDowxxoghwgQfTvdLAsRm8BxJ/aa2nfW573+swAA4+3W/IJXYDGsH1MQIj1dLMMYYI4QuJvhwelCkE9vBc2z26Ourxbl/rDfgFjj7z15WTqZeRxgB9MZYI4wxSmgzAfsASmM9fIHj4MLcd6s1uAPO6vvzSouTj9EmO+go8alhKhQZ9kdQipoNeBznb3jXv+bjielUdvrbf76He4QZ642c5+P0d8/LcA9ovTtYAi0oKofN9PcrA2nljwWzP25cM9f532UlbA7fFp96fXpB+gcKXYKCIDu+dFcGYMzlVmtCilL4PPkNR8aHGAA8Wa3fDurehBVJMy2hwLwGLKDWO8y4rdPDvUHbuy9YWj6lSdwzr3sCzeVL077Vra0/BSybhI83PiuT+gE+M85/O3xN614/r//yEoaEYQLjCGPYbGEz6Hro8KYNl7ucR4DuxrNWeqov8eyfmM3v0ca+CX1UzEsYJ91vsKBmUyefmxHG+HSgqGWW3rOgceg1t8y7JEcYu38QgNWgHxBMlgWvI4ncdKTYTQqBzuXWm63TgzIdFoPnyTfg71uQgNa7n5bAc9CZIaWEQjq38iZ4DfsdajLFUnEpwFiJbd4n7Nbl7tRfJ8rwlWLm8fPqxe8/Z/n48o+bD/AVwTK2meGqbpDqLQe/Kz21TRu3oBHIxOlIds4RKK0LJIVzSXaio51jJVUx1EPm7O1P+bnHP1bj7gGpOETbdsxnYxxzj0mXyVxibZFnxiI6dpPoYEaeuJnMopAZgbr5oI+pF7zetqDRK9fh9dnXTr4tDXdBsA/+zhu41/UXZO/btqtte9E0/e2L94P2e5ex3cfcDNKmxc5m3M0zIalL2zFPbzuzj1Y2ujpxpXF9YibF7cSbT5ZdA60ip4kx+J5q+LxxANFfFCj1i0yRk1wiRi6YL1MmtxHZjCb0i+IlkZEx3g7pOmKehmPjctjv4YmTerIYvF5pskPfHdRsJZbJDyGNI5aY9vTD5+ttmoXYJnobM4+eL7rtcLABPOdvMn99cbkhtGkTumi4cTYGDT/HgW1j/nQcvUyh6TzRsOnTbX6nPhD1bjgq50YDxcsbTULQ8w59muig4rSutnyBEhwL9xOcs7j5ao/NcdYta+IHCo8Xg3Nzl/UJ0mBMIX4XWKTxF/hYSVH8Jvfs2L12f5429zb1m++a79MDOhZlR8KWL9QsIcQ8baAuguhrLAfKwHk2V4RQaxyp4eZRQvH83WEBKWrjtC43gtLYM4kaEA6XRpJqHgQ88Q85MQd94DHsm/hfsUmbb93kPjD8E2kxBa+DWsxMtBskyPrXsy9CUakMjY2iXfpEgNxBXoSgH8W2qQKSOJH+swEaon4Q02CpI8qtLNw9COFuCEvJpxzMiBsn0bxNmrfma143HwSb7uag8Ql3bU323x1XUc2k2PRdjPQzIWb9M8ZMxsnXtMHmY+dXqteonTItaszcmIv6r0/9cKGjhUNWi0noun4BOu6nF+k8QW9s9MY1bU3UwFHFeeND0t6j6xa5XdrX3TY+Q/RR69wJCHWk2YwiB5UGQBMvKih0w3QwkzqKuS2ywERQrFPDjOkNsDpsKI5QofbWaDJ2JqZ1Nv1wRTjXVzlwHWzuLWOlJ9f8i0nSbHYccSyRNJybfbgyx/eQ1izx9WR2ZeF2aNNAi5EP0NCWzEqJNRgGnM0XuGT68vsg2l2avT6kajMh0+PTxuNL/31lkV90nHO0drW9oI1CJmuvbkkDkBZ21ys8x52+JUSCIx30Xx+tPWvlsslj4xd9aXgDL/vnbjyBadYQPFO6dZUj62XBjI0+TdvCMEbW1ejsGGPMRhSiAT2gbjQX1TX1+fXibGiTntcPG6SCCtG7RBMirB5fb/RRIQSbVmVa3LYZx+YVSbB1hak8jx/Bje8b5vZoZwaYfbi6FbyHPrZap/tZr83bEFEgEUs0rG6E2tsglV/w1iALw4A2rNZYm320utzuw12bLeojb0pZutfarwDO/Pnr/nzQVp57/GKHnGAjyJw/7EQnmLUAm3fut9jxbdF+4EoBXztFGcHMJVbC98atD1sU4Gl816xh8mVjJvf8TfyUYMZ6MMX3VrvaJs1PQYxZ38G34BZAIbL8mf5utUwfZXcivCSblkvpx8+O8Ua/B9W3OLV3P5QMMp8Aw4c8aRIpORfxVXhCz9lWBmhrQuWQVvRI163r3bjmSEq+IolagFsiHGgI0FcFN7o19FhJI+8k9MEbPB88RXb9lpKSgw9kM4sTYpQmbega2dg1isTVLq/6l8Nb9tQ8SXuvPYidLw6RK5EqJo7PrE/03rj1GRSO1LsKmblIa1OInRlG8BxwwaeUVU6wTn/bGZvsoaa6wRlako7npwdHf1Jyyn/5STbHvvg33BqY1HfDdEn228eI/U89mDqL/0V099+JOTcSzTDLasAdYN/gh4FoGAJC+SZC6HxMHyztSTiwU1/x7OYMJyHpe1FrKLNg+/B2vwQ9QM52tt1egj9jpDf23mMDr8+A4CStc3W1wNoJvb3E/pxJtMYkWKWA6MSRJI5wHzXKE8YOYZN9AQ16iV58cZXtsQEiONloP/FmF4dU/eNBkmomq0wREn6d/ZfzC0Mjm8R8ln03syV5oT8faOOWyQZfNj6PxAWSkkvBjUPaa/PP+kGyryI7m7SnsBIfQaPfErwf0w9X142PQ36A8RvId8WOL1FQ6ZTxHSzaiMTB3ZteiGjewLdh+bj1K2WYFcw7Um5FVKaST3xzJohYB5xUL8QNgCIYpJ6rbuwfSxQ5Oe4ZOVHszHo0azv5uj5wJmSVpIT75S9WHtrOWQcUBRvOgR0BsC2vbqaMsHAmKPhAa8jSE9zgxbH5HVN11g5aizwkhL4ddUXzlTLHXnAhYtZ4eahPygR+jZVD1ssc5Ui8vcsv9h3Yr9szfhqbobSnpaPtmAGKzMXv//pbUifOxHWFRTubQhQWDbwOTpjDuksaPIczzibUqpLYh8AN31SjyMkJq2bTTgA8wVxD0nnwB8u3rXYNwjC0R7uaUNvh/jisaMZH/cAXDGOiWLjNdUC0AY7C4/TgCbJEk9Vtx5svEh0srLoEHbeLppaK5mtQs3tACK89jAsoCBJU1K+iV/zY3KQIemaaZRJZqJrhTWpfXBcgoQSBB09JsgVrcmqtuxhbcEKCogTPuiSCciiaJLqdZVZLs4+fJeYAWDJReG5ZKXXEtLFqJim9SSZPw+0KM0bCdGL5XOs0dMkEm30qlToxITTEOjF31VSskjnnMxtHJPB24aGRg+8YcziabWR/nPwbmbttR52fGe/ZDs2XWzIvOKZ/0jVfOvBUIsYnMe8MISpgzHYskONb8GjNc86ITLstcozznTU8qNE4vTwIzretEoFmbJbxwuuHO/RjKdgHOUnccOKm8kNgMAhiapMGkaZsFjGzteuD3Puy7kegJfJVdCC7OQxMCI369J+FYGcRA/3RBBYBxK0jFqMGdozP3v5MboB82TVOBvrFilhK51bf9GzHna/FHvNVkiR5P7bQ4KiThFQh7rFeI0Dr+4sSxZFLEwQ2vk8X7PghcCuxQeIidoqGfVB+2ApVvz7IfUhn+qkwiSrMGGmM2Ih71JNhW5c1CyjODFzzEl+rbtt6g7Of9uXl+5g+c636L09Bkxaj/hBUTbcuXnFfrPbpvjKF2toRH8e5rloTqXzwnJvc6U0DSeTdcFtJaNMbutZLHoHdvIqN0NnXl6+sqVQ1fJ9hhJO9sspMP+HCSDcJKNgqqOnIvHbGEu4nab6k1DVmovPTw80ITUOsj+nTzRnkzusHT9rXB+jyfBz3d/Jr/ARnh65OWVDnoRrKDHKKu7srrhCVC3d9SL5dghBiAvpeSD8a/wvF1wAyRbYpzp4BidUkgUnXcOKNTawqrfsijBjaD9Ww6RFMsbsQmR6OU0+4EYYoA7B6HDPAVwQyNfgBJ8pW78SFSbmQDXxTVsCdI3f3iUhq90N9f5Myh6XQVQ1HiMVBNYIJi3FUIIYBgqUIY3z56LIg+M/XUJTRAWO+SAVPNHRKaDheP0z07r4QW99ADlPRpNSjF1fQhlfO5FXVs8k693A1IaXRk/6oix1KYoDY1PoYXzZiBWcApkpV6mI692IkLYDEIh/jiE6kjtzS05gbTT2KmzFEkw2OJtgMyA50NG7M3cPDI2OMDrznSl6yecQly3zOhEpBVv2AAowo+la68X8Ixf+hVh/Q5hcKStGHc8YY7QwcLknczqoyTxlb5/ygxd0Z1Z4VZ5uTJkfPMEqh6Amp+1Kv0fxxhhjVPB/tEQMOhIpwbgAAAAASUVORK5CYII=\";\n\n // Generate QR code SVG server-side — standard B&W, 2x size\n const qrSvg = generateQrSvg(walletAddress, { cellSize: 8 });\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;\n background: rgba(10, 12, 17, 1) url('${safeTopupUrl}/assets/bg-pattern.png') top left / contain no-repeat;\n color: rgba(255, 255, 255, 0.9);\n display: flex;\n justify-content: center;\n padding: 24px;\n line-height: 1.3;\n }\n .card {\n max-width: 400px;\n width: 100%;\n background: rgba(19, 19, 24, 1);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 12px;\n padding: 28px 24px;\n text-align: center;\n }\n .logo { margin-bottom: 8px; }\n .logo img { height: 24px; }\n .subtitle { color: rgba(255, 255, 255, 0.5); font-size: 13px; margin-bottom: 20px; }\n .qr { margin-bottom: 16px; }\n .qr svg { border-radius: 12px; background: #fff; padding: 10px; }\n .address-wrap {\n position: relative;\n margin-bottom: 16px;\n }\n .address {\n width: 100%;\n background: rgba(10, 12, 17, 1);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 8px;\n padding: 10px 14px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 11px; color: #f2dda6; word-break: break-all;\n cursor: pointer; text-align: center;\n transition: border-color 0.3s linear;\n -webkit-user-select: all; user-select: all;\n outline: none;\n }\n .address:focus { border-color: #ae9d71; }\n .address-hint {\n font-size: 10px; color: rgba(255,255,255,0.3);\n text-align: center; margin-top: 4px;\n }\n .badge {\n display: inline-flex; align-items: center; gap: 6px;\n background: rgba(255, 255, 255, 0.04);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 20px; padding: 5px 12px;\n font-size: 11px; color: rgba(255, 255, 255, 0.5); margin-bottom: 20px;\n }\n .badge .dot { width: 7px; height: 7px; border-radius: 50%; background: #f2dda6; }\n .balance-line {\n font-size: 13px;\n color: rgba(255, 255, 255, 0.5);\n margin-bottom: 20px;\n line-height: 1.4;\n }\n .balance-line .amount {\n color: #4feb69;\n font-weight: 600;\n }\n .balance-line .gas {\n color: #f2dda6;\n font-weight: 600;\n }\n @keyframes flash { 0%{opacity:0.4} 100%{opacity:1} }\n .balance-line.flash .amount { animation: flash 1s ease-out; }\n .hint {\n margin-top: 14px; font-size: 12px;\n color: rgba(255, 255, 255, 0.3);\n line-height: 1.5;\n }\n</style>\n</head>\n<body>\n<div class=\"card\">\n <div class=\"logo\"><img src=\"${safeTopupUrl}/assets/logo.png\" alt=\"Chain Insights\"></div>\n <p class=\"subtitle\">Fund your wallet with USDC on Base</p>\n <div class=\"qr\">${qrSvg}</div>\n <div class=\"address-wrap\">\n <div class=\"address\" id=\"addr\" onclick=\"selectAndCopy()\">${safeWalletAddress}</div>\n <div class=\"address-hint\" id=\"addrHint\">Click to copy address</div>\n </div>\n <div class=\"badge\"><span class=\"dot\"></span>Base Network &middot; USDC</div>\n <p class=\"balance-line\" id=\"balLine\">Current balance: <span class=\"amount\" id=\"bal\">--</span> USDC<br>Gas balance: <span class=\"gas\" id=\"gas\">--</span> ETH<br>Base ETH is used for one-time approval gas.</p>\n</div>\n<script>\n// MCP Apps protocol handshake (matches @modelcontextprotocol/ext-apps App.connect())\n(function() {\n var initId = 1;\n\n // Listen for host messages\n window.addEventListener('message', function(event) {\n var data = event.data;\n if (!data || data.jsonrpc !== '2.0') return;\n\n // Initialize response\n if (data.id === initId && data.result) {\n // Send initialized notification\n window.parent.postMessage({\n jsonrpc: '2.0',\n method: 'ui/notifications/initialized',\n params: {}\n }, '*');\n\n // Send initial size\n var rect = document.documentElement.getBoundingClientRect();\n window.parent.postMessage({\n jsonrpc: '2.0',\n method: 'ui/notifications/size-changed',\n params: { width: Math.ceil(rect.width), height: Math.ceil(rect.height) }\n }, '*');\n }\n\n // Respond to pings\n if (data.method === 'ping' && data.id != null) {\n window.parent.postMessage({\n jsonrpc: '2.0',\n id: data.id,\n result: {}\n }, '*');\n }\n });\n\n // Send initialize request (must match App class protocol)\n window.parent.postMessage({\n jsonrpc: '2.0',\n id: initId,\n method: 'ui/initialize',\n params: {\n appInfo: { name: 'Chain Insights Topup', version: '1.0.0' },\n appCapabilities: {},\n protocolVersion: '2026-01-26'\n }\n }, '*');\n})();\n\n// Live balance polling\nvar lastBal = null;\nvar TOPUP_URL = ${topupUrlJson};\nfunction fetchBal() {\n fetch(TOPUP_URL + '/api/balance')\n .then(function(r) { return r.json(); })\n .then(function(d) {\n var el = document.getElementById('bal');\n var gas = document.getElementById('gas');\n var line = document.getElementById('balLine');\n var val = parseFloat(d.balance_usdc || '0').toFixed(2);\n var gasVal = d.balance_eth === 'unknown' ? '--' : parseFloat(d.balance_eth || '0').toFixed(6);\n if (d.balance_usdc === 'unknown') { el.textContent = '--'; gas.textContent = gasVal; return; }\n el.textContent = val;\n gas.textContent = gasVal;\n if (lastBal !== null && val !== lastBal) {\n line.classList.remove('flash');\n void line.offsetWidth;\n line.classList.add('flash');\n }\n lastBal = val;\n })\n .catch(function() {});\n}\nfetchBal();\nsetInterval(fetchBal, 10000);\n\nfunction selectAndCopy() {\n var addr = document.getElementById('addr');\n var hint = document.getElementById('addrHint');\n // Select the text\n var r = document.createRange();\n r.selectNodeContents(addr);\n var s = window.getSelection();\n s.removeAllRanges();\n s.addRange(r);\n // Try every clipboard method available\n var copied = false;\n try { copied = document.execCommand('copy'); } catch(e) {}\n if (!copied && navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(addr.textContent).then(function() {\n hint.textContent = 'Copied!'; hint.style.color = '#4feb69';\n setTimeout(function() { hint.textContent = 'Click to copy address'; hint.style.color = ''; }, 2000);\n }).catch(function() {});\n }\n if (copied) {\n hint.textContent = 'Copied!'; hint.style.color = '#4feb69';\n setTimeout(function() { hint.textContent = 'Click to copy address'; hint.style.color = ''; }, 2000);\n } else {\n hint.textContent = 'Selected — press Ctrl+C'; hint.style.color = '#f2dda6';\n setTimeout(function() { hint.textContent = 'Click to copy address'; hint.style.color = ''; }, 3000);\n }\n}\n<\\/script>\n</body>\n</html>`;\n}\n\nfunction generatePage(walletAddressInput: string): string {\n const walletAddress = assertWalletAddress(walletAddressInput);\n const safeWalletAddress = escapeHtml(walletAddress);\n const walletAddressJson = jsonForScript(walletAddress);\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Chain Insights — Fund Wallet</title>\n<script src=\"https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js\"></script>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n body {\n font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;\n background: rgba(10, 12, 17, 1);\n color: rgba(255, 255, 255, 0.9);\n min-height: 100vh;\n display: flex;\n justify-content: center;\n align-items: center;\n line-height: 1.3;\n }\n\n .container {\n max-width: 480px;\n width: 100%;\n padding: 24px;\n }\n\n .logo {\n text-align: center;\n margin-bottom: 32px;\n }\n\n .logo h1 {\n font-size: 24px;\n font-weight: 600;\n color: #f2dda6;\n letter-spacing: -0.5px;\n }\n\n .logo p {\n color: rgba(255, 255, 255, 0.5);\n font-size: 14px;\n margin-top: 4px;\n }\n\n .card {\n background: rgba(19, 19, 24, 1);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 12px;\n padding: 32px 24px;\n }\n\n .qr-container {\n display: flex;\n justify-content: center;\n margin-bottom: 24px;\n }\n\n .qr-container canvas, .qr-container img {\n border-radius: 12px;\n background: #fff;\n padding: 12px;\n }\n\n .address-box {\n background: rgba(10, 12, 17, 1);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 8px;\n padding: 12px 16px;\n display: flex;\n align-items: center;\n gap: 8px;\n margin-bottom: 24px;\n }\n\n .address-box code {\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 12px;\n color: #f2dda6;\n word-break: break-all;\n flex: 1;\n }\n\n .copy-btn {\n background: none;\n border: none;\n color: rgba(255, 255, 255, 0.4);\n cursor: pointer;\n padding: 4px;\n font-size: 18px;\n transition: color 0.3s linear;\n }\n\n .copy-btn:hover { color: #f2dda6; }\n .copy-btn.copied { color: #4feb69; }\n\n .network-badge {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n background: rgba(255, 255, 255, 0.04);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 20px;\n padding: 6px 12px;\n font-size: 12px;\n color: rgba(255, 255, 255, 0.5);\n margin-bottom: 24px;\n }\n\n .network-badge .dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: #f2dda6;\n }\n\n .balance-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 12px 0;\n border-top: 1px solid rgba(255, 255, 255, 0.06);\n margin-bottom: 16px;\n }\n\n .balance-label { color: rgba(255, 255, 255, 0.5); font-size: 14px; }\n\n .balance-value {\n font-size: 20px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n }\n\n .balance-value .currency {\n font-size: 14px;\n color: rgba(255, 255, 255, 0.5);\n font-weight: 400;\n margin-left: 4px;\n }\n\n .gas-note {\n margin-top: -8px;\n margin-bottom: 16px;\n color: rgba(255,255,255,0.45);\n font-size: 12px;\n line-height: 1.4;\n text-align: left;\n }\n\n .metamask-btn {\n width: 100%;\n padding: 14px;\n border: none;\n border-radius: 10px;\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 10px;\n transition: all 0.2s;\n background: #f6851b;\n color: #fff;\n }\n\n .metamask-btn:hover { background: #e2761b; transform: translateY(-1px); }\n .metamask-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }\n\n .metamask-btn svg { width: 22px; height: 22px; }\n\n .amount-input {\n width: 100%;\n padding: 12px 16px;\n background: #0a0c11;\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 8px;\n color: rgba(255, 255, 255, 0.9);\n font-size: 16px;\n margin-bottom: 16px;\n outline: none;\n transition: border-color 0.2s;\n }\n\n .amount-input:focus { border-color: #f2dda6; }\n\n .amount-label {\n font-size: 13px;\n color: rgba(255, 255, 255, 0.5);\n margin-bottom: 6px;\n display: block;\n }\n\n .status {\n text-align: center;\n padding: 12px;\n border-radius: 8px;\n font-size: 14px;\n margin-top: 16px;\n display: none;\n }\n\n .status.success { display: block; background: #0a2e1a; color: #4feb69; border: 1px solid #1a4a2e; }\n .status.error { display: block; background: #2e0a0a; color: #eb4f4f; border: 1px solid #4a1a1a; }\n .status.pending { display: block; background: #1a1a0a; color: #f2dda6; border: 1px solid #4a4a1a; }\n\n .info {\n text-align: center;\n margin-top: 24px;\n font-size: 12px;\n color: rgba(255, 255, 255, 0.3);\n line-height: 1.6;\n }\n</style>\n</head>\n<body>\n<div class=\"container\">\n <div class=\"logo\">\n <h1>Chain Insights</h1>\n <p>Fund your wallet to use blockchain intelligence tools</p>\n </div>\n\n <div class=\"card\">\n <div class=\"qr-container\" id=\"qr\"></div>\n\n <div class=\"address-box\">\n <code id=\"address\">${safeWalletAddress}</code>\n <button class=\"copy-btn\" onclick=\"copyAddress()\" id=\"copyBtn\" title=\"Copy address\">&#x2398;</button>\n </div>\n\n <div class=\"network-badge\">\n <span class=\"dot\"></span>\n Base Network &middot; USDC\n </div>\n\n <div class=\"balance-row\">\n <span class=\"balance-label\">Current balance</span>\n <span class=\"balance-value\" id=\"balance\">—<span class=\"currency\">USDC</span></span>\n </div>\n <div class=\"balance-row\">\n <span class=\"balance-label\">Gas balance</span>\n <span class=\"balance-value\" id=\"gasBalance\">—<span class=\"currency\">ETH</span></span>\n </div>\n <p class=\"gas-note\">Base ETH is used for one-time approval gas.</p>\n\n <div class=\"status\" id=\"status\"></div>\n </div>\n\n <p class=\"info\">Send Base USDC to the wallet address above.</p>\n</div>\n\n<script>\nconst WALLET = ${walletAddressJson};\nconst USDC = '${USDC_ADDRESS}';\nconst CHAIN_ID = '${BASE_CHAIN_ID}';\n\n// QR code\n(function() {\n var qr = qrcode(0, 'M');\n qr.addData(WALLET);\n qr.make();\n document.getElementById('qr').innerHTML = qr.createSvgTag(5, 0);\n var svg = document.querySelector('#qr svg');\n if (svg) { svg.style.borderRadius = '12px'; svg.style.background = '#fff'; svg.style.padding = '12px'; }\n})();\n\n// Copy address — fallback for sandboxed iframes where navigator.clipboard is blocked\nfunction copyAddress() {\n var ok = false;\n // Try modern clipboard API first\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(WALLET).then(function() { ok = true; }).catch(function() {});\n }\n // Fallback: hidden textarea + execCommand\n if (!ok) {\n var ta = document.createElement('textarea');\n ta.value = WALLET;\n ta.style.position = 'fixed';\n ta.style.left = '-9999px';\n document.body.appendChild(ta);\n ta.select();\n try { document.execCommand('copy'); } catch(e) {}\n document.body.removeChild(ta);\n }\n var btn = document.getElementById('copyBtn');\n btn.classList.add('copied');\n btn.innerHTML = '&#x2713;';\n setTimeout(function() { btn.classList.remove('copied'); btn.innerHTML = '&#x2398;'; }, 2000);\n}\n\n// Fetch balance\nasync function fetchBalance() {\n try {\n var resp = await fetch('/api/balance');\n var json = await resp.json();\n if (json.balance_usdc === 'unknown') throw new Error('balance unavailable');\n var balance = Number(json.balance_usdc || 0).toFixed(2);\n document.getElementById('balance').innerHTML = balance + '<span class=\"currency\">USDC</span>';\n var gasBalance = json.balance_eth === 'unknown' ? '—' : Number(json.balance_eth || 0).toFixed(6);\n document.getElementById('gasBalance').innerHTML = gasBalance + '<span class=\"currency\">ETH</span>';\n } catch(e) {\n document.getElementById('balance').innerHTML = '—<span class=\"currency\">USDC</span>';\n document.getElementById('gasBalance').innerHTML = '—<span class=\"currency\">ETH</span>';\n }\n}\nfetchBalance();\nsetInterval(fetchBalance, 15000);\n\n</script>\n</body>\n</html>`;\n}\n","import {\n generateArtifactHtml,\n getTopupUrl as getCopiedTopupUrl,\n startTopupServer as startCopiedTopupServer,\n} from './mcp-proxy/topup-server.js'\nimport { createServer, type Server, type ServerResponse } from 'node:http'\nimport { isAddress } from 'viem'\nimport type { PaymentWalletAccount } from './tools.js'\n\ninterface ArtifactServerState {\n address: string\n assetServerUrl: string\n server: Server\n url: string\n}\n\nlet artifactServerState: ArtifactServerState | null = null\nconst REQUEST_TARGET_BASE = 'http://localhost'\n\nclass ProxyRequestError extends Error {\n status: number\n\n constructor(status: number, message: string) {\n super(message)\n this.name = 'ProxyRequestError'\n this.status = status\n }\n}\n\ninterface NormalizedProxyTarget {\n pathAndSearch: string\n pathname: string\n}\n\nfunction toWalletAddress(account: PaymentWalletAccount | string): string {\n const address = typeof account === 'string' ? account : account.address\n if (!isAddress(address)) {\n throw new Error('Wallet address must be a valid 0x-prefixed 20-byte EVM address')\n }\n return address\n}\n\nfunction send(res: ServerResponse, status: number, body: string | Buffer, contentType: string): void {\n res.writeHead(status, {\n 'content-type': contentType,\n 'cache-control': 'no-store',\n 'access-control-allow-origin': '*',\n })\n res.end(body)\n}\n\nfunction normalizeProxyTarget(reqUrl: string): NormalizedProxyTarget {\n if (!reqUrl.startsWith('/') || reqUrl.startsWith('//')) {\n throw new ProxyRequestError(400, 'Proxy request target must be an origin-form path')\n }\n\n const parsed = new URL(reqUrl, REQUEST_TARGET_BASE)\n if (parsed.origin !== REQUEST_TARGET_BASE) {\n throw new ProxyRequestError(400, 'Absolute and protocol-relative proxy targets are not allowed')\n }\n\n let decodedPathname: string\n try {\n decodedPathname = decodeURIComponent(parsed.pathname)\n } catch {\n throw new ProxyRequestError(400, 'Proxy request target contains invalid encoding')\n }\n\n if (decodedPathname.startsWith('//') || /^\\/[a-z][a-z0-9+.-]*:\\/\\//i.test(decodedPathname)) {\n throw new ProxyRequestError(400, 'Encoded host override targets are not allowed')\n }\n\n return {\n pathAndSearch: `${parsed.pathname}${parsed.search}`,\n pathname: parsed.pathname,\n }\n}\n\nasync function proxyToCopiedServer(\n proxyTarget: NormalizedProxyTarget,\n res: ServerResponse,\n assetServerUrl: string,\n): Promise<void> {\n const allowedOrigin = new URL(assetServerUrl).origin\n const upstreamUrl = new URL(proxyTarget.pathAndSearch, assetServerUrl)\n if (upstreamUrl.origin !== allowedOrigin) {\n throw new ProxyRequestError(403, 'Upstream origin is not allowed')\n }\n\n const upstream = await fetch(upstreamUrl)\n const contentType = upstream.headers.get('content-type') ?? 'application/octet-stream'\n const body = Buffer.from(await upstream.arrayBuffer())\n send(res, upstream.status, body, contentType)\n}\n\nexport { generateArtifactHtml }\n\nexport function getTopupArtifactUrl(): string | null {\n return artifactServerState?.url ?? null\n}\n\nexport function getTopupUrl(): string | null {\n return getTopupArtifactUrl() ?? getCopiedTopupUrl()\n}\n\nexport async function stopTopupServer(): Promise<void> {\n if (!artifactServerState) {\n return\n }\n\n const { server } = artifactServerState\n artifactServerState = null\n await new Promise<void>((resolve) => server.close(() => resolve()))\n}\n\nexport async function startTopupServer(account: PaymentWalletAccount | string): Promise<string> {\n const walletAddress = toWalletAddress(account)\n\n if (artifactServerState && artifactServerState.address.toLowerCase() === walletAddress.toLowerCase()) {\n return artifactServerState.url\n }\n\n const assetServerUrl = await startCopiedTopupServer(walletAddress)\n\n if (artifactServerState) {\n await stopTopupServer()\n }\n\n const server = createServer((req, res) => {\n const reqUrl = req.url ?? '/'\n let proxyTarget: NormalizedProxyTarget\n try {\n proxyTarget = normalizeProxyTarget(reqUrl)\n } catch (err) {\n if (err instanceof ProxyRequestError) {\n send(res, err.status, JSON.stringify({ error: err.message }) + '\\n', 'application/json; charset=utf-8')\n return\n }\n send(res, 400, JSON.stringify({ error: 'Invalid request target' }) + '\\n', 'application/json; charset=utf-8')\n return\n }\n const { pathname } = proxyTarget\n\n if (pathname === '/' || pathname === '/index.html') {\n const artifactUrl = artifactServerState?.url ?? assetServerUrl\n send(res, 200, generateArtifactHtml(walletAddress, artifactUrl), 'text/html; charset=utf-8')\n return\n }\n\n if (pathname.startsWith('/assets/') || pathname.startsWith('/api/')) {\n void proxyToCopiedServer(proxyTarget, res, assetServerUrl).catch((err) => {\n if (err instanceof ProxyRequestError) {\n send(res, err.status, JSON.stringify({ error: err.message }) + '\\n', 'application/json; charset=utf-8')\n return\n }\n send(res, 502, JSON.stringify({ error: (err as Error).message }) + '\\n', 'application/json; charset=utf-8')\n })\n return\n }\n\n send(res, 404, JSON.stringify({ error: 'Not found' }) + '\\n', 'application/json; charset=utf-8')\n })\n\n const url = await new Promise<string>((resolve, reject) => {\n server.once('error', reject)\n server.listen(0, '127.0.0.1', () => {\n const addressInfo = server.address()\n if (!addressInfo || typeof addressInfo === 'string') {\n reject(new Error('Failed to start topup artifact server'))\n return\n }\n resolve(`http://localhost:${addressInfo.port}`)\n })\n })\n\n artifactServerState = {\n address: walletAddress,\n assetServerUrl,\n server,\n url,\n }\n\n return url\n}\n"],"mappings":"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"}
@@ -197,6 +197,9 @@ const USDC_ABI = [{
197
197
  type: "uint256"
198
198
  }]
199
199
  }];
200
+ function walletAddress(wallet) {
201
+ return typeof wallet === "string" ? wallet : wallet.address;
202
+ }
200
203
  async function getBalanceUsdc(wallet) {
201
204
  const envRpcUrl = process.env.BASE_RPC_URL;
202
205
  const rpcUrls = [...envRpcUrl ? [envRpcUrl] : [], ...PUBLIC_BASE_RPC_URLS.filter((url) => url !== envRpcUrl)];
@@ -208,7 +211,7 @@ async function getBalanceUsdc(wallet) {
208
211
  address: USDC_ADDRESS$1,
209
212
  abi: USDC_ABI,
210
213
  functionName: "balanceOf",
211
- args: [wallet.address]
214
+ args: [walletAddress(wallet)]
212
215
  }), 6);
213
216
  } catch {}
214
217
  return "unknown";
@@ -220,7 +223,7 @@ async function getBalanceEth(wallet) {
220
223
  return (0, viem.formatEther)(await (0, viem.createPublicClient)({
221
224
  chain: viem_chains.base,
222
225
  transport: (0, viem.http)(rpcUrl)
223
- }).getBalance({ address: wallet.address }));
226
+ }).getBalance({ address: walletAddress(wallet) }));
224
227
  } catch {}
225
228
  return "unknown";
226
229
  }
@@ -241,10 +244,22 @@ const logoPng = loadAsset("logo.png");
241
244
  const bgPatternPng = loadAsset("bg-pattern.png");
242
245
  let server = null;
243
246
  let serverPort = null;
247
+ function assertWalletAddress(wallet) {
248
+ const walletAddress = typeof wallet === "string" ? wallet : wallet.address;
249
+ if (!(0, viem.isAddress)(walletAddress)) throw new Error("Wallet address must be a valid 0x-prefixed 20-byte EVM address");
250
+ return walletAddress;
251
+ }
252
+ function escapeHtml(value) {
253
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
254
+ }
255
+ function jsonForScript(value) {
256
+ return JSON.stringify(value).replaceAll("<", "\\u003c").replaceAll(">", "\\u003e").replaceAll("&", "\\u0026");
257
+ }
244
258
  function getTopupUrl$1() {
245
259
  return serverPort ? `http://localhost:${serverPort}` : null;
246
260
  }
247
261
  async function startTopupServer$1(wallet) {
262
+ const walletAddress = assertWalletAddress(wallet);
248
263
  if (server && serverPort) return `http://localhost:${serverPort}`;
249
264
  return new Promise((resolve, reject) => {
250
265
  server = (0, node_http.createServer)((req, res) => {
@@ -253,11 +268,11 @@ async function startTopupServer$1(wallet) {
253
268
  "Content-Type": "application/json",
254
269
  "Access-Control-Allow-Origin": "*"
255
270
  });
256
- res.end(JSON.stringify({ address: wallet.address }));
271
+ res.end(JSON.stringify({ address: walletAddress }));
257
272
  return;
258
273
  }
259
274
  if (req.url === "/api/balance") {
260
- Promise.all([getBalanceUsdc(wallet), getBalanceEth(wallet)]).then(([balanceUsdc, balanceEth]) => {
275
+ Promise.all([getBalanceUsdc(walletAddress), getBalanceEth(walletAddress)]).then(([balanceUsdc, balanceEth]) => {
261
276
  res.writeHead(200, {
262
277
  "Content-Type": "application/json",
263
278
  "Access-Control-Allow-Origin": "*"
@@ -297,7 +312,7 @@ async function startTopupServer$1(wallet) {
297
312
  return;
298
313
  }
299
314
  res.writeHead(200, { "Content-Type": "text/html" });
300
- res.end(generatePage(wallet.address));
315
+ res.end(generatePage(walletAddress));
301
316
  });
302
317
  server.listen(0, "127.0.0.1", () => {
303
318
  const addr = server.address();
@@ -311,7 +326,11 @@ async function startTopupServer$1(wallet) {
311
326
  server.on("error", reject);
312
327
  });
313
328
  }
314
- function generateArtifactHtml(walletAddress, topupUrl) {
329
+ function generateArtifactHtml(walletAddressInput, topupUrl) {
330
+ const walletAddress = assertWalletAddress(walletAddressInput);
331
+ const safeWalletAddress = escapeHtml(walletAddress);
332
+ const safeTopupUrl = escapeHtml(topupUrl);
333
+ const topupUrlJson = jsonForScript(topupUrl);
315
334
  return `<!DOCTYPE html>
316
335
  <html lang="en">
317
336
  <head>
@@ -321,7 +340,7 @@ function generateArtifactHtml(walletAddress, topupUrl) {
321
340
  * { margin: 0; padding: 0; box-sizing: border-box; }
322
341
  body {
323
342
  font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
324
- background: rgba(10, 12, 17, 1) url('${topupUrl}/assets/bg-pattern.png') top left / contain no-repeat;
343
+ background: rgba(10, 12, 17, 1) url('${safeTopupUrl}/assets/bg-pattern.png') top left / contain no-repeat;
325
344
  color: rgba(255, 255, 255, 0.9);
326
345
  display: flex;
327
346
  justify-content: center;
@@ -397,11 +416,11 @@ function generateArtifactHtml(walletAddress, topupUrl) {
397
416
  </head>
398
417
  <body>
399
418
  <div class="card">
400
- <div class="logo"><img src="${topupUrl}/assets/logo.png" alt="Chain Insights"></div>
419
+ <div class="logo"><img src="${safeTopupUrl}/assets/logo.png" alt="Chain Insights"></div>
401
420
  <p class="subtitle">Fund your wallet with USDC on Base</p>
402
421
  <div class="qr">${generateQrSvg(walletAddress, { cellSize: 8 })}</div>
403
422
  <div class="address-wrap">
404
- <div class="address" id="addr" onclick="selectAndCopy()">${walletAddress}</div>
423
+ <div class="address" id="addr" onclick="selectAndCopy()">${safeWalletAddress}</div>
405
424
  <div class="address-hint" id="addrHint">Click to copy address</div>
406
425
  </div>
407
426
  <div class="badge"><span class="dot"></span>Base Network &middot; USDC</div>
@@ -460,8 +479,9 @@ function generateArtifactHtml(walletAddress, topupUrl) {
460
479
 
461
480
  // Live balance polling
462
481
  var lastBal = null;
482
+ var TOPUP_URL = ${topupUrlJson};
463
483
  function fetchBal() {
464
- fetch('${topupUrl}/api/balance')
484
+ fetch(TOPUP_URL + '/api/balance')
465
485
  .then(function(r) { return r.json(); })
466
486
  .then(function(d) {
467
487
  var el = document.getElementById('bal');
@@ -514,7 +534,8 @@ function selectAndCopy() {
514
534
  </body>
515
535
  </html>`;
516
536
  }
517
- function generatePage(walletAddress) {
537
+ function generatePage(walletAddressInput) {
538
+ const walletAddress = assertWalletAddress(walletAddressInput);
518
539
  return `<!DOCTYPE html>
519
540
  <html lang="en">
520
541
  <head>
@@ -741,7 +762,7 @@ function generatePage(walletAddress) {
741
762
  <div class="qr-container" id="qr"></div>
742
763
 
743
764
  <div class="address-box">
744
- <code id="address">${walletAddress}</code>
765
+ <code id="address">${escapeHtml(walletAddress)}</code>
745
766
  <button class="copy-btn" onclick="copyAddress()" id="copyBtn" title="Copy address">&#x2398;</button>
746
767
  </div>
747
768
 
@@ -767,7 +788,7 @@ function generatePage(walletAddress) {
767
788
  </div>
768
789
 
769
790
  <script>
770
- const WALLET = '${walletAddress}';
791
+ const WALLET = ${jsonForScript(walletAddress)};
771
792
  const USDC = '${USDC_ADDRESS}';
772
793
  const CHAIN_ID = '${BASE_CHAIN_ID}';
773
794
 
@@ -833,21 +854,23 @@ var topup_server_exports = /* @__PURE__ */ require_chunk.__exportAll({
833
854
  generateArtifactHtml: () => generateArtifactHtml,
834
855
  getTopupArtifactUrl: () => getTopupArtifactUrl,
835
856
  getTopupUrl: () => getTopupUrl,
836
- startTopupServer: () => startTopupServer
857
+ startTopupServer: () => startTopupServer,
858
+ stopTopupServer: () => stopTopupServer
837
859
  });
838
- const FALLBACK_PRIVATE_KEY = `0x${"0".repeat(63)}1`;
839
860
  let artifactServerState = null;
840
- function toWalletData(account) {
841
- if (typeof account === "string") return {
842
- address: account,
843
- privateKey: FALLBACK_PRIVATE_KEY,
844
- createdAt: (/* @__PURE__ */ new Date(0)).toISOString()
845
- };
846
- return {
847
- address: account.address,
848
- privateKey: account.privateKey,
849
- createdAt: (/* @__PURE__ */ new Date(0)).toISOString()
850
- };
861
+ const REQUEST_TARGET_BASE = "http://localhost";
862
+ var ProxyRequestError = class extends Error {
863
+ status;
864
+ constructor(status, message) {
865
+ super(message);
866
+ this.name = "ProxyRequestError";
867
+ this.status = status;
868
+ }
869
+ };
870
+ function toWalletAddress(account) {
871
+ const address = typeof account === "string" ? account : account.address;
872
+ if (!(0, viem.isAddress)(address)) throw new Error("Wallet address must be a valid 0x-prefixed 20-byte EVM address");
873
+ return address;
851
874
  }
852
875
  function send(res, status, body, contentType) {
853
876
  res.writeHead(status, {
@@ -857,8 +880,26 @@ function send(res, status, body, contentType) {
857
880
  });
858
881
  res.end(body);
859
882
  }
860
- async function proxyToCopiedServer(reqUrl, res, assetServerUrl) {
861
- const upstreamUrl = new URL(reqUrl, assetServerUrl);
883
+ function normalizeProxyTarget(reqUrl) {
884
+ if (!reqUrl.startsWith("/") || reqUrl.startsWith("//")) throw new ProxyRequestError(400, "Proxy request target must be an origin-form path");
885
+ const parsed = new URL(reqUrl, REQUEST_TARGET_BASE);
886
+ if (parsed.origin !== REQUEST_TARGET_BASE) throw new ProxyRequestError(400, "Absolute and protocol-relative proxy targets are not allowed");
887
+ let decodedPathname;
888
+ try {
889
+ decodedPathname = decodeURIComponent(parsed.pathname);
890
+ } catch {
891
+ throw new ProxyRequestError(400, "Proxy request target contains invalid encoding");
892
+ }
893
+ if (decodedPathname.startsWith("//") || /^\/[a-z][a-z0-9+.-]*:\/\//i.test(decodedPathname)) throw new ProxyRequestError(400, "Encoded host override targets are not allowed");
894
+ return {
895
+ pathAndSearch: `${parsed.pathname}${parsed.search}`,
896
+ pathname: parsed.pathname
897
+ };
898
+ }
899
+ async function proxyToCopiedServer(proxyTarget, res, assetServerUrl) {
900
+ const allowedOrigin = new URL(assetServerUrl).origin;
901
+ const upstreamUrl = new URL(proxyTarget.pathAndSearch, assetServerUrl);
902
+ if (upstreamUrl.origin !== allowedOrigin) throw new ProxyRequestError(403, "Upstream origin is not allowed");
862
903
  const upstream = await fetch(upstreamUrl);
863
904
  const contentType = upstream.headers.get("content-type") ?? "application/octet-stream";
864
905
  const body = Buffer.from(await upstream.arrayBuffer());
@@ -870,24 +911,41 @@ function getTopupArtifactUrl() {
870
911
  function getTopupUrl() {
871
912
  return getTopupArtifactUrl() ?? getTopupUrl$1();
872
913
  }
914
+ async function stopTopupServer() {
915
+ if (!artifactServerState) return;
916
+ const { server } = artifactServerState;
917
+ artifactServerState = null;
918
+ await new Promise((resolve) => server.close(() => resolve()));
919
+ }
873
920
  async function startTopupServer(account) {
874
- const wallet = toWalletData(account);
875
- if (artifactServerState && artifactServerState.address.toLowerCase() === wallet.address.toLowerCase()) return artifactServerState.url;
876
- const assetServerUrl = await startTopupServer$1(wallet);
877
- if (artifactServerState) {
878
- await new Promise((resolve) => artifactServerState?.server.close(() => resolve()));
879
- artifactServerState = null;
880
- }
921
+ const walletAddress = toWalletAddress(account);
922
+ if (artifactServerState && artifactServerState.address.toLowerCase() === walletAddress.toLowerCase()) return artifactServerState.url;
923
+ const assetServerUrl = await startTopupServer$1(walletAddress);
924
+ if (artifactServerState) await stopTopupServer();
881
925
  const server = (0, node_http.createServer)((req, res) => {
882
926
  const reqUrl = req.url ?? "/";
883
- const pathname = new URL(reqUrl, "http://localhost").pathname;
927
+ let proxyTarget;
928
+ try {
929
+ proxyTarget = normalizeProxyTarget(reqUrl);
930
+ } catch (err) {
931
+ if (err instanceof ProxyRequestError) {
932
+ send(res, err.status, JSON.stringify({ error: err.message }) + "\n", "application/json; charset=utf-8");
933
+ return;
934
+ }
935
+ send(res, 400, JSON.stringify({ error: "Invalid request target" }) + "\n", "application/json; charset=utf-8");
936
+ return;
937
+ }
938
+ const { pathname } = proxyTarget;
884
939
  if (pathname === "/" || pathname === "/index.html") {
885
- const artifactUrl = artifactServerState?.url ?? assetServerUrl;
886
- send(res, 200, generateArtifactHtml(wallet.address, artifactUrl), "text/html; charset=utf-8");
940
+ send(res, 200, generateArtifactHtml(walletAddress, artifactServerState?.url ?? assetServerUrl), "text/html; charset=utf-8");
887
941
  return;
888
942
  }
889
943
  if (pathname.startsWith("/assets/") || pathname.startsWith("/api/")) {
890
- proxyToCopiedServer(reqUrl, res, assetServerUrl).catch((err) => {
944
+ proxyToCopiedServer(proxyTarget, res, assetServerUrl).catch((err) => {
945
+ if (err instanceof ProxyRequestError) {
946
+ send(res, err.status, JSON.stringify({ error: err.message }) + "\n", "application/json; charset=utf-8");
947
+ return;
948
+ }
891
949
  send(res, 502, JSON.stringify({ error: err.message }) + "\n", "application/json; charset=utf-8");
892
950
  });
893
951
  return;
@@ -906,7 +964,7 @@ async function startTopupServer(account) {
906
964
  });
907
965
  });
908
966
  artifactServerState = {
909
- address: wallet.address,
967
+ address: walletAddress,
910
968
  assetServerUrl,
911
969
  server,
912
970
  url
@@ -59,8 +59,12 @@ Primary GraphRAG MCP config:
59
59
  ```bash
60
60
  chain-insights config get graphMcpEndpoint
61
61
  chain-insights config set graphMcpEndpoint https://staging-mcp.chain-insights.ai/mcp
62
+ export CHAIN_INSIGHTS_GRAPH_MCP_ENDPOINT=https://staging-mcp.chain-insights.ai/mcp
62
63
  ```
63
64
 
65
+ The runtime default is local loopback: `http://127.0.0.1:8012/mcp`. Hosted
66
+ endpoints are operator configuration, not hardcoded package defaults.
67
+
64
68
  Supported config keys:
65
69
 
66
70
  | Key | Purpose |
@@ -17,6 +17,7 @@ node bin/cli.js --help
17
17
  Current AML tools live in the Chain Insights layer:
18
18
 
19
19
  - `address_risk`
20
+ - `stake_insights`
20
21
  - `track_funds`
21
22
  - `scam_topology`
22
23
 
package/docs/debugging.md CHANGED
@@ -6,25 +6,21 @@ here.
6
6
 
7
7
  ## Local GraphRAG MCP Debug
8
8
 
9
- Start GraphRAG MCP with debug bypass from the RBMK ML repo:
9
+ Start your local GraphRAG MCP development endpoint with debug bearer auth
10
+ enabled. The exact startup command depends on your GraphRAG MCP checkout or
11
+ deployment.
10
12
 
11
13
  ```bash
12
- cd /home/aphex5/work/rbmk/repos/ml
13
- test -f .env || cp .env.example .env
14
- set -a
15
- . ./.env
16
- set +a
17
- docker compose -f compose/shared.yml build graphrag-mcp-go
18
- MCP_DEBUG_BYPASS_ENABLED=true \
19
- MCP_DEBUG_BYPASS_TOKEN=chain-insights-dev-debug \
20
- docker compose -f compose/shared.yml up -d graphrag-mcp-go
14
+ export GRAPHRAG_MCP_ENDPOINT=http://localhost:8012/mcp
15
+ export GRAPHRAG_DEBUG_TOKEN=chain-insights-dev-debug
21
16
  ```
22
17
 
23
18
  Point Chain Insights at the local endpoint:
24
19
 
25
20
  ```bash
26
- cd /home/aphex5/work/chain-insights
27
- node bin/cli.js debug on --token chain-insights-dev-debug --endpoint http://localhost:8012/mcp
21
+ node bin/cli.js debug on \
22
+ --token "${GRAPHRAG_DEBUG_TOKEN}" \
23
+ --endpoint "${GRAPHRAG_MCP_ENDPOINT}"
28
24
  node bin/cli.js mcp tools --refresh
29
25
  ```
30
26
 
@@ -34,10 +30,10 @@ Inspect the GraphRAG MCP endpoint directly:
34
30
 
35
31
  ```bash
36
32
  npx @modelcontextprotocol/inspector \
37
- --cli http://localhost:8012/mcp \
33
+ --cli "${GRAPHRAG_MCP_ENDPOINT:-http://localhost:8012/mcp}" \
38
34
  --transport http \
39
35
  --method tools/list \
40
- --header "X-MCP-Debug-Token: chain-insights-dev-debug"
36
+ --header "X-MCP-Debug-Token: ${GRAPHRAG_DEBUG_TOKEN:-chain-insights-dev-debug}"
41
37
  ```
42
38
 
43
39
  Inspect the Chain Insights proxy:
@@ -12,8 +12,9 @@ The GraphRAG MCP public graph surface is intentionally small:
12
12
  | `graph_query` | Run one read-only GQL/Cypher query through the universal graph endpoint |
13
13
  | `graph_query_batch` | Run related read-only graph-language queries as one MCP call |
14
14
 
15
- Chain Insights AML tools such as `address_risk`, `track_funds`, and
16
- `scam_topology` are recipes built over `graph_query_batch`. They are not
15
+ Chain Insights AML tools such as `address_risk`, `stake_insights`,
16
+ `track_funds`, and `scam_topology` are recipes built over
17
+ `graph_query_batch`. They are not
17
18
  assumed to exist on the GraphRAG MCP endpoint.
18
19
 
19
20
  ## Query Rules
@@ -111,6 +112,63 @@ cia mcp track-funds \
111
112
  --case 1
112
113
  ```
113
114
 
115
+ ## Stake Insights
116
+
117
+ `stake_insights` explains Bittensor staking behavior around one address,
118
+ coldkey, or hotkey. It keeps stake semantics separate from generic money-flow
119
+ semantics and reads direct `STAKES_IN` relationships from the graph endpoint.
120
+
121
+ Required input:
122
+
123
+ - `network`
124
+ - exactly one of `address`, `coldkey`, or `hotkey`
125
+
126
+ Optional input:
127
+
128
+ - `netuid`
129
+ - `start_timestamp_ms`
130
+ - `end_timestamp_ms`
131
+ - `start_block`
132
+ - `end_block`
133
+ - `depth`
134
+ - `include_attachments`
135
+
136
+ The first release returns direct coldkey-hotkey-netuid stake relationships. The
137
+ current GraphRAG stake parity surface supports timestamp windows; block windows
138
+ are accepted by the tool contract but fail explicitly until the backend exposes
139
+ block-range fields.
140
+
141
+ Result facts include:
142
+
143
+ - `stake_totals`: total staked, total unstaked, moved-in/out amounts, net
144
+ staked, first activity, and last activity.
145
+ - `active_relationships`: coldkey-hotkey-netuid relationships with amount,
146
+ event counts, first/last activity, transaction ids when present, topology
147
+ graph, and source backend.
148
+ - `stake_movements`: aggregate `stake_added`, `stake_removed`,
149
+ `stake_moved_in`, and `stake_moved_out` movement rows.
150
+ - `top_counterparties`: counterparties ranked by stake amount.
151
+ - `query_evidence`: `graph_query_batch` query ids, topology graph, row counts,
152
+ source backends, and partial errors.
153
+
154
+ CLI examples:
155
+
156
+ ```bash
157
+ cia mcp stake-insights \
158
+ --network bittensor \
159
+ --coldkey 5... \
160
+ --netuid 19
161
+ ```
162
+
163
+ ```bash
164
+ cia mcp call stake_insights \
165
+ network=bittensor \
166
+ address=5... \
167
+ netuid=19 \
168
+ start_timestamp_ms=1769126300000 \
169
+ end_timestamp_ms=1769126600000
170
+ ```
171
+
114
172
  ## Scam Topology
115
173
 
116
174
  `scam_topology` expands the topology around a known victim incident so the
package/docs/mcp-proxy.md CHANGED
@@ -20,6 +20,47 @@ Use this MCP server configuration:
20
20
 
21
21
  The proxy reads the same local Chain Insights config as the CLI.
22
22
 
23
+ ## GraphRAG MCP Endpoint Configuration
24
+
25
+ The endpoint lives in Chain Insights config, not in the MCP client registration.
26
+ The npm package default is the local development endpoint
27
+ `http://127.0.0.1:8012/mcp`; hosted endpoints must be set explicitly.
28
+
29
+ Set local development:
30
+
31
+ ```bash
32
+ chain-insights config set graphMcpEndpoint http://127.0.0.1:8012/mcp
33
+ ```
34
+
35
+ Set hosted staging for approved testers:
36
+
37
+ ```bash
38
+ chain-insights config set graphMcpEndpoint https://staging-mcp.chain-insights.ai/mcp
39
+ ```
40
+
41
+ Use a one-shot environment override:
42
+
43
+ ```bash
44
+ export CHAIN_INSIGHTS_GRAPH_MCP_ENDPOINT=https://staging-mcp.chain-insights.ai/mcp
45
+ ```
46
+
47
+ Configuration precedence:
48
+
49
+ 1. `CHAIN_INSIGHTS_GRAPH_MCP_ENDPOINT`
50
+ 2. `GRAPH_MCP_ENDPOINT` legacy alias
51
+ 3. saved `graphMcpEndpoint`
52
+ 4. local default `http://127.0.0.1:8012/mcp`
53
+
54
+ Validation rules:
55
+
56
+ - local `http://` is accepted only for localhost and loopback addresses
57
+ - remote endpoints must use `https://`
58
+ - endpoint URLs with credentials, query strings, or fragments are rejected
59
+
60
+ Keep hosted endpoint values in operator config or environment variables. Do not
61
+ bake hosted endpoint URLs into MCP client JSON, source code, or workspace
62
+ templates.
63
+
23
64
  ## Behavior
24
65
 
25
66
  The proxy:
@@ -51,6 +92,9 @@ The proxy:
51
92
 
52
93
  Remote graph tools are discovered from the configured GraphRAG MCP endpoint. The
53
94
  expected primitive graph tools are `graph_query` and `graph_query_batch`.
95
+ Chain Insights adds high-level local graph recipes such as `address_risk`,
96
+ `stake_insights`, `track_funds`, and `scam_topology` when the remote endpoint
97
+ only exposes primitives.
54
98
 
55
99
  ## Auth Modes
56
100
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "chain-insights",
3
- "version": "0.2.18",
4
- "description": "AML investigation CLI and MCP proxy for blockchain risk, fund-flow tracing, case reports, and x402-paid GraphRAG MCP access",
3
+ "version": "0.2.21",
4
+ "description": "AML investigation CLI and MCP proxy for blockchain risk, fund-flow tracing, case reports, and GraphRAG MCP access",
5
5
  "homepage": "https://chain-insights.ai",
6
6
  "repository": {
7
7
  "type": "git",
@@ -22,6 +22,8 @@ names in debugging or contributor docs when the detail is necessary.
22
22
 
23
23
  - `address_risk`: screen one address for risk, behavior, neighborhood context,
24
24
  and exchange exposure.
25
+ - `stake_insights`: explain Bittensor coldkey-hotkey-netuid staking behavior,
26
+ net stake movement, counterparties, activity range, and source backend.
25
27
  - `track_funds`: trace victim/source funds through intermediaries to exchange
26
28
  deposit candidates.
27
29
  - `scam_topology`: expand known victim incident topology into reviewable scam
@@ -89,8 +91,8 @@ Before claiming the developer experience is good, run from a clean directory
89
91
  outside this repository:
90
92
 
91
93
  ```bash
92
- mkdir -p /home/aphex5/work/chain-insights-dx-dogfood
93
- cd /home/aphex5/work/chain-insights-dx-dogfood
94
+ mkdir -p /tmp/chain-insights-dx-dogfood
95
+ cd /tmp/chain-insights-dx-dogfood
94
96
  cia --version
95
97
  cia init .
96
98
  cia mcp networks