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.
- package/README.md +54 -12
- package/bin/cli.js +2 -3
- package/bin/install.cjs +0 -1
- package/dist/{app-DxlQE_P5.cjs → app-BxojXjtB.cjs} +1 -1
- package/dist/{app-DdWQF_zb.mjs → app-CRd39JJ8.mjs} +2 -2
- package/dist/{app-DdWQF_zb.mjs.map → app-CRd39JJ8.mjs.map} +1 -1
- package/dist/{artifact-server-4DiMvwhC.mjs → artifact-server-CP6LXQ9d.mjs} +2 -2
- package/dist/{artifact-server-4DiMvwhC.mjs.map → artifact-server-CP6LXQ9d.mjs.map} +1 -1
- package/dist/{artifact-server-B-3ho4bk.cjs → artifact-server-XbN16DwU.cjs} +1 -1
- package/dist/cli.cjs +66 -25
- package/dist/cli.mjs +66 -25
- package/dist/cli.mjs.map +1 -1
- package/dist/{config-BhYbhLDI.cjs → config-BwVx19Og.cjs} +48 -15
- package/dist/config-Drgc2HuF.mjs +77 -0
- package/dist/config-Drgc2HuF.mjs.map +1 -0
- package/dist/frontmatter-D0ccQnUM.mjs.map +1 -1
- package/dist/index.cjs +4 -4
- package/dist/index.d.cts +3 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4 -4
- package/dist/{init-CZbZegIW.mjs → init-4tn7jfhN.mjs} +3 -2
- package/dist/init-4tn7jfhN.mjs.map +1 -0
- package/dist/{init-BvpZtFiT.cjs → init-TCQY5RDJ.cjs} +2 -1
- package/dist/mcp-endpoint-BaV8h_lq.cjs +60 -0
- package/dist/mcp-endpoint-DHs1cRFH.mjs +39 -0
- package/dist/mcp-endpoint-DHs1cRFH.mjs.map +1 -0
- package/dist/mcp-proxy.cjs +108 -9
- package/dist/mcp-proxy.d.cts.map +1 -1
- package/dist/mcp-proxy.d.mts.map +1 -1
- package/dist/mcp-proxy.mjs +108 -9
- package/dist/mcp-proxy.mjs.map +1 -1
- package/dist/{public-tools-D6Q5MTcO.mjs → public-tools-B13J0MJZ.mjs} +465 -70
- package/dist/public-tools-B13J0MJZ.mjs.map +1 -0
- package/dist/{public-tools-V7ON7goq.cjs → public-tools-BC1fi0DV.cjs} +464 -68
- package/dist/resolver-D7VBb0uB.mjs.map +1 -1
- package/dist/{runner-BatyCxv7.mjs → runner-DIs04IhN.mjs} +2 -2
- package/dist/{runner-BatyCxv7.mjs.map → runner-DIs04IhN.mjs.map} +1 -1
- package/dist/{runner-CCA7SJ7X.cjs → runner-ZYowxCVl.cjs} +1 -1
- package/dist/schema-BFEWhzg7.mjs +60 -0
- package/dist/schema-BFEWhzg7.mjs.map +1 -0
- package/dist/{schema-DN-KLkYN.cjs → schema-Vl9yuOFO.cjs} +31 -8
- package/dist/{server-BDlbmGbL.mjs → server-BXLX2j_A.mjs} +2 -2
- package/dist/{server-BDlbmGbL.mjs.map → server-BXLX2j_A.mjs.map} +1 -1
- package/dist/{server-C3y1gQmZ.cjs → server-BqVdWath.cjs} +1 -1
- package/dist/{topup-server-6MH7q73X.mjs → topup-server-BJgVw6Jt.mjs} +100 -42
- package/dist/topup-server-BJgVw6Jt.mjs.map +1 -0
- package/dist/{topup-server-DjUjhNjv.cjs → topup-server-yAaXYkJP.cjs} +98 -40
- package/docs/architecture.md +4 -0
- package/docs/contributing.md +1 -0
- package/docs/debugging.md +10 -14
- package/docs/graph-tools.md +60 -2
- package/docs/mcp-proxy.md +44 -0
- package/package.json +2 -2
- package/skills/chain-insights-developer-experience/SKILL.md +4 -2
- package/skills/chain-insights-investigation/SKILL.md +1 -1
- package/skills/test-chain-insights-graphrag-mcp/SKILL.md +4 -5
- package/skills/test-chain-insights-graphrag-mcp/scripts/run-uat.sh +5 -24
- package/dist/config-9KYXaAv-.mjs +0 -44
- package/dist/config-9KYXaAv-.mjs.map +0 -1
- package/dist/init-CZbZegIW.mjs.map +0 -1
- package/dist/public-tools-D6Q5MTcO.mjs.map +0 -1
- package/dist/schema-BbQVXp36.mjs +0 -37
- package/dist/schema-BbQVXp36.mjs.map +0 -1
- 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(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\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,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\";\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 · 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\">⎘</button>\n </div>\n\n <div class=\"network-badge\">\n <span class=\"dot\"></span>\n Base Network · 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 = '✓';\n setTimeout(function() { btn.classList.remove('copied'); btn.innerHTML = '⎘'; }, 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
|
|
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
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
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:
|
|
271
|
+
res.end(JSON.stringify({ address: walletAddress }));
|
|
257
272
|
return;
|
|
258
273
|
}
|
|
259
274
|
if (req.url === "/api/balance") {
|
|
260
|
-
Promise.all([getBalanceUsdc(
|
|
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(
|
|
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(
|
|
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('${
|
|
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="${
|
|
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()">${
|
|
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 · 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('
|
|
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(
|
|
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">⎘</button>
|
|
746
767
|
</div>
|
|
747
768
|
|
|
@@ -767,7 +788,7 @@ function generatePage(walletAddress) {
|
|
|
767
788
|
</div>
|
|
768
789
|
|
|
769
790
|
<script>
|
|
770
|
-
const WALLET =
|
|
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
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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
|
-
|
|
861
|
-
|
|
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
|
|
875
|
-
if (artifactServerState && artifactServerState.address.toLowerCase() ===
|
|
876
|
-
const assetServerUrl = await startTopupServer$1(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
967
|
+
address: walletAddress,
|
|
910
968
|
assetServerUrl,
|
|
911
969
|
server,
|
|
912
970
|
url
|
package/docs/architecture.md
CHANGED
|
@@ -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 |
|
package/docs/contributing.md
CHANGED
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
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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:
|
package/docs/graph-tools.md
CHANGED
|
@@ -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`, `
|
|
16
|
-
`scam_topology` are recipes built over
|
|
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.
|
|
4
|
-
"description": "AML investigation CLI and MCP proxy for blockchain risk, fund-flow tracing, case reports, and
|
|
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 /
|
|
93
|
-
cd /
|
|
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
|