recon-crypto-mcp 0.1.0

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 (204) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/dist/abis/aave-pool.d.ts +174 -0
  4. package/dist/abis/aave-pool.js +101 -0
  5. package/dist/abis/aave-pool.js.map +1 -0
  6. package/dist/abis/aave-ui-pool-data-provider.d.ts +232 -0
  7. package/dist/abis/aave-ui-pool-data-provider.js +107 -0
  8. package/dist/abis/aave-ui-pool-data-provider.js.map +1 -0
  9. package/dist/abis/access-control.d.ts +94 -0
  10. package/dist/abis/access-control.js +51 -0
  11. package/dist/abis/access-control.js.map +1 -0
  12. package/dist/abis/compound-comet.d.ts +120 -0
  13. package/dist/abis/compound-comet.js +84 -0
  14. package/dist/abis/compound-comet.js.map +1 -0
  15. package/dist/abis/eigenlayer-delegation-manager.d.ts +23 -0
  16. package/dist/abis/eigenlayer-delegation-manager.js +18 -0
  17. package/dist/abis/eigenlayer-delegation-manager.js.map +1 -0
  18. package/dist/abis/eigenlayer-strategy-manager.d.ts +53 -0
  19. package/dist/abis/eigenlayer-strategy-manager.js +47 -0
  20. package/dist/abis/eigenlayer-strategy-manager.js.map +1 -0
  21. package/dist/abis/erc20.d.ts +78 -0
  22. package/dist/abis/erc20.js +10 -0
  23. package/dist/abis/erc20.js.map +1 -0
  24. package/dist/abis/lido.d.ts +80 -0
  25. package/dist/abis/lido.js +60 -0
  26. package/dist/abis/lido.js.map +1 -0
  27. package/dist/abis/morpho-blue.d.ts +321 -0
  28. package/dist/abis/morpho-blue.js +193 -0
  29. package/dist/abis/morpho-blue.js.map +1 -0
  30. package/dist/abis/uniswap-pool.d.ts +70 -0
  31. package/dist/abis/uniswap-pool.js +53 -0
  32. package/dist/abis/uniswap-pool.js.map +1 -0
  33. package/dist/abis/uniswap-position-manager.d.ts +71 -0
  34. package/dist/abis/uniswap-position-manager.js +41 -0
  35. package/dist/abis/uniswap-position-manager.js.map +1 -0
  36. package/dist/config/cache.d.ts +12 -0
  37. package/dist/config/cache.js +12 -0
  38. package/dist/config/cache.js.map +1 -0
  39. package/dist/config/chains.d.ts +24 -0
  40. package/dist/config/chains.js +158 -0
  41. package/dist/config/chains.js.map +1 -0
  42. package/dist/config/contracts.d.ts +107 -0
  43. package/dist/config/contracts.js +123 -0
  44. package/dist/config/contracts.js.map +1 -0
  45. package/dist/config/user-config.d.ts +15 -0
  46. package/dist/config/user-config.js +93 -0
  47. package/dist/config/user-config.js.map +1 -0
  48. package/dist/data/apis/etherscan.d.ts +30 -0
  49. package/dist/data/apis/etherscan.js +109 -0
  50. package/dist/data/apis/etherscan.js.map +1 -0
  51. package/dist/data/cache.d.ts +18 -0
  52. package/dist/data/cache.js +47 -0
  53. package/dist/data/cache.js.map +1 -0
  54. package/dist/data/format.d.ts +6 -0
  55. package/dist/data/format.js +44 -0
  56. package/dist/data/format.js.map +1 -0
  57. package/dist/data/prices.d.ts +12 -0
  58. package/dist/data/prices.js +81 -0
  59. package/dist/data/prices.js.map +1 -0
  60. package/dist/data/rpc.d.ts +17 -0
  61. package/dist/data/rpc.js +68 -0
  62. package/dist/data/rpc.js.map +1 -0
  63. package/dist/index.d.ts +2 -0
  64. package/dist/index.js +344 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/modules/balances/index.d.ts +20 -0
  67. package/dist/modules/balances/index.js +49 -0
  68. package/dist/modules/balances/index.js.map +1 -0
  69. package/dist/modules/balances/schemas.d.ts +32 -0
  70. package/dist/modules/balances/schemas.js +21 -0
  71. package/dist/modules/balances/schemas.js.map +1 -0
  72. package/dist/modules/bitcoin/index.d.ts +26 -0
  73. package/dist/modules/bitcoin/index.js +128 -0
  74. package/dist/modules/bitcoin/index.js.map +1 -0
  75. package/dist/modules/bitcoin/schemas.d.ts +49 -0
  76. package/dist/modules/bitcoin/schemas.js +51 -0
  77. package/dist/modules/bitcoin/schemas.js.map +1 -0
  78. package/dist/modules/bitcoin/send.d.ts +52 -0
  79. package/dist/modules/bitcoin/send.js +158 -0
  80. package/dist/modules/bitcoin/send.js.map +1 -0
  81. package/dist/modules/bitcoin/utxo.d.ts +99 -0
  82. package/dist/modules/bitcoin/utxo.js +116 -0
  83. package/dist/modules/bitcoin/utxo.js.map +1 -0
  84. package/dist/modules/compound/actions.d.ts +8 -0
  85. package/dist/modules/compound/actions.js +154 -0
  86. package/dist/modules/compound/actions.js.map +1 -0
  87. package/dist/modules/compound/index.d.ts +26 -0
  88. package/dist/modules/compound/index.js +141 -0
  89. package/dist/modules/compound/index.js.map +1 -0
  90. package/dist/modules/compound/schemas.d.ts +95 -0
  91. package/dist/modules/compound/schemas.js +37 -0
  92. package/dist/modules/compound/schemas.js.map +1 -0
  93. package/dist/modules/execution/index.d.ts +51 -0
  94. package/dist/modules/execution/index.js +271 -0
  95. package/dist/modules/execution/index.js.map +1 -0
  96. package/dist/modules/execution/schemas.d.ts +183 -0
  97. package/dist/modules/execution/schemas.js +88 -0
  98. package/dist/modules/execution/schemas.js.map +1 -0
  99. package/dist/modules/feedback/index.d.ts +28 -0
  100. package/dist/modules/feedback/index.js +161 -0
  101. package/dist/modules/feedback/index.js.map +1 -0
  102. package/dist/modules/feedback/rate-limit.d.ts +15 -0
  103. package/dist/modules/feedback/rate-limit.js +110 -0
  104. package/dist/modules/feedback/rate-limit.js.map +1 -0
  105. package/dist/modules/feedback/schemas.d.ts +41 -0
  106. package/dist/modules/feedback/schemas.js +40 -0
  107. package/dist/modules/feedback/schemas.js.map +1 -0
  108. package/dist/modules/morpho/actions.d.ts +8 -0
  109. package/dist/modules/morpho/actions.js +265 -0
  110. package/dist/modules/morpho/actions.js.map +1 -0
  111. package/dist/modules/morpho/index.d.ts +26 -0
  112. package/dist/modules/morpho/index.js +94 -0
  113. package/dist/modules/morpho/index.js.map +1 -0
  114. package/dist/modules/morpho/schemas.d.ts +130 -0
  115. package/dist/modules/morpho/schemas.js +34 -0
  116. package/dist/modules/morpho/schemas.js.map +1 -0
  117. package/dist/modules/portfolio/index.d.ts +3 -0
  118. package/dist/modules/portfolio/index.js +197 -0
  119. package/dist/modules/portfolio/index.js.map +1 -0
  120. package/dist/modules/portfolio/schemas.d.ts +20 -0
  121. package/dist/modules/portfolio/schemas.js +15 -0
  122. package/dist/modules/portfolio/schemas.js.map +1 -0
  123. package/dist/modules/positions/aave.d.ts +22 -0
  124. package/dist/modules/positions/aave.js +197 -0
  125. package/dist/modules/positions/aave.js.map +1 -0
  126. package/dist/modules/positions/actions.d.ts +18 -0
  127. package/dist/modules/positions/actions.js +205 -0
  128. package/dist/modules/positions/actions.js.map +1 -0
  129. package/dist/modules/positions/index.d.ts +37 -0
  130. package/dist/modules/positions/index.js +59 -0
  131. package/dist/modules/positions/index.js.map +1 -0
  132. package/dist/modules/positions/schemas.d.ts +55 -0
  133. package/dist/modules/positions/schemas.js +25 -0
  134. package/dist/modules/positions/schemas.js.map +1 -0
  135. package/dist/modules/positions/uniswap.d.ts +4 -0
  136. package/dist/modules/positions/uniswap.js +181 -0
  137. package/dist/modules/positions/uniswap.js.map +1 -0
  138. package/dist/modules/prices/index.d.ts +19 -0
  139. package/dist/modules/prices/index.js +22 -0
  140. package/dist/modules/prices/index.js.map +1 -0
  141. package/dist/modules/security/index.d.ts +26 -0
  142. package/dist/modules/security/index.js +13 -0
  143. package/dist/modules/security/index.js.map +1 -0
  144. package/dist/modules/security/permissions.d.ts +8 -0
  145. package/dist/modules/security/permissions.js +96 -0
  146. package/dist/modules/security/permissions.js.map +1 -0
  147. package/dist/modules/security/risk-score.d.ts +18 -0
  148. package/dist/modules/security/risk-score.js +116 -0
  149. package/dist/modules/security/risk-score.js.map +1 -0
  150. package/dist/modules/security/schemas.d.ts +31 -0
  151. package/dist/modules/security/schemas.js +16 -0
  152. package/dist/modules/security/schemas.js.map +1 -0
  153. package/dist/modules/security/verification.d.ts +4 -0
  154. package/dist/modules/security/verification.js +82 -0
  155. package/dist/modules/security/verification.js.map +1 -0
  156. package/dist/modules/shared/approval.d.ts +71 -0
  157. package/dist/modules/shared/approval.js +130 -0
  158. package/dist/modules/shared/approval.js.map +1 -0
  159. package/dist/modules/staking/actions.d.ts +22 -0
  160. package/dist/modules/staking/actions.js +100 -0
  161. package/dist/modules/staking/actions.js.map +1 -0
  162. package/dist/modules/staking/eigenlayer.d.ts +14 -0
  163. package/dist/modules/staking/eigenlayer.js +105 -0
  164. package/dist/modules/staking/eigenlayer.js.map +1 -0
  165. package/dist/modules/staking/index.d.ts +24 -0
  166. package/dist/modules/staking/index.js +59 -0
  167. package/dist/modules/staking/index.js.map +1 -0
  168. package/dist/modules/staking/lido.d.ts +14 -0
  169. package/dist/modules/staking/lido.js +113 -0
  170. package/dist/modules/staking/lido.js.map +1 -0
  171. package/dist/modules/staking/schemas.d.ts +34 -0
  172. package/dist/modules/staking/schemas.js +20 -0
  173. package/dist/modules/staking/schemas.js.map +1 -0
  174. package/dist/modules/swap/index.d.ts +47 -0
  175. package/dist/modules/swap/index.js +376 -0
  176. package/dist/modules/swap/index.js.map +1 -0
  177. package/dist/modules/swap/lifi.d.ts +17 -0
  178. package/dist/modules/swap/lifi.js +44 -0
  179. package/dist/modules/swap/lifi.js.map +1 -0
  180. package/dist/modules/swap/oneinch.d.ts +26 -0
  181. package/dist/modules/swap/oneinch.js +33 -0
  182. package/dist/modules/swap/oneinch.js.map +1 -0
  183. package/dist/modules/swap/schemas.d.ts +65 -0
  184. package/dist/modules/swap/schemas.js +46 -0
  185. package/dist/modules/swap/schemas.js.map +1 -0
  186. package/dist/setup.d.ts +2 -0
  187. package/dist/setup.js +257 -0
  188. package/dist/setup.js.map +1 -0
  189. package/dist/signing/pre-sign-check.d.ts +8 -0
  190. package/dist/signing/pre-sign-check.js +231 -0
  191. package/dist/signing/pre-sign-check.js.map +1 -0
  192. package/dist/signing/session.d.ts +28 -0
  193. package/dist/signing/session.js +26 -0
  194. package/dist/signing/session.js.map +1 -0
  195. package/dist/signing/tx-store.d.ts +29 -0
  196. package/dist/signing/tx-store.js +85 -0
  197. package/dist/signing/tx-store.js.map +1 -0
  198. package/dist/signing/walletconnect.d.ts +33 -0
  199. package/dist/signing/walletconnect.js +226 -0
  200. package/dist/signing/walletconnect.js.map +1 -0
  201. package/dist/types/index.d.ts +222 -0
  202. package/dist/types/index.js +18 -0
  203. package/dist/types/index.js.map +1 -0
  204. package/package.json +134 -0
@@ -0,0 +1,93 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, lstatSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ const CONFIG_DIR = join(homedir(), ".recon-crypto-mcp");
5
+ const CONFIG_PATH = join(CONFIG_DIR, "config.json");
6
+ /** Read the user config file; returns null if it doesn't exist. Throws on malformed JSON. */
7
+ export function readUserConfig() {
8
+ if (!existsSync(CONFIG_PATH))
9
+ return null;
10
+ const raw = readFileSync(CONFIG_PATH, "utf8");
11
+ try {
12
+ return JSON.parse(raw);
13
+ }
14
+ catch (err) {
15
+ throw new Error(`~/.recon-crypto-mcp/config.json is malformed: ${err.message}. Delete it or re-run \`recon-crypto-mcp-setup\`.`);
16
+ }
17
+ }
18
+ export function writeUserConfig(config) {
19
+ if (!existsSync(CONFIG_DIR)) {
20
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
21
+ }
22
+ // Refuse to follow symlinks or hardlinks when writing the config. A local
23
+ // attacker with write access to ~/.recon-crypto-mcp (or with a race-window before
24
+ // first-run setup creates the dir) could pre-place config.json as a symlink
25
+ // to another file (~/.ssh/authorized_keys, ~/.bashrc, etc.) so the next
26
+ // writeFileSync clobbers it. lstatSync on the path (not following the link)
27
+ // catches this: if the entry exists but isn't a regular file, bail loudly.
28
+ if (existsSync(CONFIG_PATH)) {
29
+ const st = lstatSync(CONFIG_PATH);
30
+ if (!st.isFile() || st.isSymbolicLink() || st.nlink > 1) {
31
+ throw new Error(`Refusing to write ${CONFIG_PATH}: path is a symlink, hardlink, or non-regular file. ` +
32
+ `Inspect the file manually and remove it before re-running setup.`);
33
+ }
34
+ }
35
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
36
+ }
37
+ /**
38
+ * Hook invoked whenever `patchUserConfig` is about to change the rpc section
39
+ * of the user config. rpc.ts registers a handler that drops cached viem
40
+ * clients + the verified-chain-id memo so subsequent calls re-resolve URLs
41
+ * and re-verify. Avoids a direct import of rpc.ts here (which would cycle).
42
+ */
43
+ let rpcChangeHook = null;
44
+ export function onRpcConfigChange(hook) {
45
+ rpcChangeHook = hook;
46
+ }
47
+ function rpcPatchChangesRpc(base, patch) {
48
+ if (!patch.rpc)
49
+ return false;
50
+ // Any key in patch.rpc that differs from base.rpc counts. JSON.stringify
51
+ // is fine here — both sides are small, schema-controlled objects.
52
+ const baseRpc = JSON.stringify(base.rpc);
53
+ const mergedRpc = JSON.stringify({ ...base.rpc, ...patch.rpc });
54
+ return baseRpc !== mergedRpc;
55
+ }
56
+ /** Merge a partial update into the existing config (or create a fresh one). */
57
+ export function patchUserConfig(patch) {
58
+ const existing = readUserConfig();
59
+ const base = existing ?? { rpc: { provider: "custom" } };
60
+ const merged = {
61
+ ...base,
62
+ ...patch,
63
+ rpc: { ...base.rpc, ...(patch.rpc ?? {}) },
64
+ walletConnect: { ...base.walletConnect, ...(patch.walletConnect ?? {}) },
65
+ };
66
+ writeUserConfig(merged);
67
+ // If rpc changed, invalidate cached clients + chain-id verification so the
68
+ // next live call re-resolves the URL and re-runs verifyChainId against the
69
+ // new endpoint. Without this, a config rewrite pointing at a hostile RPC
70
+ // would still be bypassed by the in-memory verifiedChains Set.
71
+ if (rpcChangeHook && rpcPatchChangesRpc(base, patch))
72
+ rpcChangeHook();
73
+ return merged;
74
+ }
75
+ export function getConfigPath() {
76
+ return CONFIG_PATH;
77
+ }
78
+ export function getConfigDir() {
79
+ return CONFIG_DIR;
80
+ }
81
+ /** Pull the Etherscan API key from env (highest priority) or user config. */
82
+ export function resolveEtherscanApiKey(userConfig) {
83
+ return process.env.ETHERSCAN_API_KEY || userConfig?.etherscanApiKey;
84
+ }
85
+ /** Pull the 1inch Developer Portal API key from env or user config; undefined if none set. */
86
+ export function resolveOneInchApiKey(userConfig) {
87
+ return process.env.ONEINCH_API_KEY || userConfig?.oneInchApiKey;
88
+ }
89
+ /** Pull the WalletConnect project ID from env or user config; undefined if none set. */
90
+ export function resolveWalletConnectProjectId(userConfig) {
91
+ return process.env.WALLETCONNECT_PROJECT_ID || userConfig?.walletConnect?.projectId;
92
+ }
93
+ //# sourceMappingURL=user-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-config.js","sourceRoot":"","sources":["../../src/config/user-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import type { SupportedChain } from "../../types/index.js";
2
+ export interface ContractInfo {
3
+ address: `0x${string}`;
4
+ chain: SupportedChain;
5
+ isVerified: boolean;
6
+ isProxy: boolean;
7
+ implementation?: `0x${string}`;
8
+ /**
9
+ * Etherscan-reported contract name. This is attacker-controllable at deploy
10
+ * time — a malicious contract can set ContractName = "Aave V3 Pool" or bury
11
+ * prompt-injection payloads in it. We sanitize to a short, safe subset
12
+ * (alphanumerics / dots / underscores / dashes, ≤64 chars) and callers
13
+ * should NOT display this field without that sanitization.
14
+ */
15
+ contractName?: string;
16
+ compilerVersion?: string;
17
+ abi?: unknown[];
18
+ }
19
+ /**
20
+ * Sanitize a free-form name from Etherscan for agent/user display. Strips
21
+ * anything that could carry a newline or steer the model (markdown fences,
22
+ * angle brackets, braces, quotes) and caps length. We never want the raw
23
+ * string hitting the agent transcript — it's attacker-controlled.
24
+ */
25
+ export declare function sanitizeContractName(raw: string | undefined): string | undefined;
26
+ /**
27
+ * Fetch contract verification info from Etherscan / Arbiscan.
28
+ * Cached for 24 hours — verification state rarely changes.
29
+ */
30
+ export declare function getContractInfo(address: `0x${string}`, chain: SupportedChain): Promise<ContractInfo>;
@@ -0,0 +1,109 @@
1
+ import { cache } from "../cache.js";
2
+ import { CACHE_TTL } from "../../config/cache.js";
3
+ import { resolveEtherscanApiKey, readUserConfig } from "../../config/user-config.js";
4
+ const API_BASE = {
5
+ ethereum: "https://api.etherscan.io/api",
6
+ arbitrum: "https://api.arbiscan.io/api",
7
+ polygon: "https://api.polygonscan.com/api",
8
+ };
9
+ /**
10
+ * Sanitize a free-form name from Etherscan for agent/user display. Strips
11
+ * anything that could carry a newline or steer the model (markdown fences,
12
+ * angle brackets, braces, quotes) and caps length. We never want the raw
13
+ * string hitting the agent transcript — it's attacker-controlled.
14
+ */
15
+ export function sanitizeContractName(raw) {
16
+ if (!raw)
17
+ return undefined;
18
+ const cleaned = raw.replace(/[^A-Za-z0-9._\-]/g, "").slice(0, 64);
19
+ return cleaned.length > 0 ? cleaned : undefined;
20
+ }
21
+ /**
22
+ * Fetch contract verification info from Etherscan / Arbiscan.
23
+ * Cached for 24 hours — verification state rarely changes.
24
+ */
25
+ export async function getContractInfo(address, chain) {
26
+ const key = `etherscan:${chain}:${address.toLowerCase()}`;
27
+ const hit = cache.get(key);
28
+ if (hit)
29
+ return hit;
30
+ const apiKey = resolveEtherscanApiKey(readUserConfig());
31
+ const params = new URLSearchParams({
32
+ module: "contract",
33
+ action: "getsourcecode",
34
+ address,
35
+ });
36
+ if (apiKey)
37
+ params.set("apikey", apiKey);
38
+ const url = `${API_BASE[chain]}?${params.toString()}`;
39
+ const res = await fetch(url);
40
+ if (!res.ok) {
41
+ throw new Error(`Etherscan request failed: ${res.status} ${res.statusText}`);
42
+ }
43
+ // Bound the response before JSON-parsing. Etherscan has been well-behaved,
44
+ // but the response is cached 24h in memory — an unbounded blob is both a
45
+ // memory-pressure vector and gives a MITM with a broken TLS setup room to
46
+ // inject a huge payload. 2MB covers the largest verified contracts we've
47
+ // seen (fully-resolved imports of flattened DeFi protocols top out ~1MB)
48
+ // with a comfortable margin.
49
+ const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
50
+ const text = await res.text();
51
+ if (text.length > MAX_RESPONSE_BYTES) {
52
+ throw new Error(`Etherscan response for ${address} on ${chain} exceeds ${MAX_RESPONSE_BYTES} bytes (got ${text.length}). Refusing to parse.`);
53
+ }
54
+ const body = JSON.parse(text);
55
+ if (body.status !== "1" || !body.result?.[0]) {
56
+ // Unverified contracts still return a valid response with empty SourceCode.
57
+ const result = {
58
+ address,
59
+ chain,
60
+ isVerified: false,
61
+ isProxy: false,
62
+ };
63
+ cache.set(key, result, CACHE_TTL.SECURITY_VERIFICATION);
64
+ return result;
65
+ }
66
+ const item = body.result[0];
67
+ const isVerified = item.SourceCode !== "";
68
+ let abi;
69
+ if (isVerified && item.ABI && item.ABI !== "Contract source code not verified") {
70
+ try {
71
+ const parsed = JSON.parse(item.ABI);
72
+ // Cap ABI length. 5000 items is ~10× the largest proxy ABIs we've seen
73
+ // (LiFi Diamond is ~1000 entries); anything bigger is either a pathological
74
+ // contract or a hostile response trying to blow up memory on scan paths.
75
+ const MAX_ABI_ITEMS = 5000;
76
+ if (Array.isArray(parsed) && parsed.length <= MAX_ABI_ITEMS) {
77
+ abi = parsed;
78
+ }
79
+ else {
80
+ abi = undefined;
81
+ }
82
+ }
83
+ catch {
84
+ abi = undefined;
85
+ }
86
+ }
87
+ const info = {
88
+ address,
89
+ chain,
90
+ isVerified,
91
+ isProxy: item.Proxy === "1",
92
+ implementation: item.Implementation && /^0x[0-9a-fA-F]{40}$/.test(item.Implementation)
93
+ ? item.Implementation
94
+ : undefined,
95
+ contractName: sanitizeContractName(item.ContractName),
96
+ // Restrict compiler version to the safe pattern (e.g. v0.8.17+commit.8df45f5f)
97
+ // so an attacker can't embed control characters or URLs in this field either.
98
+ compilerVersion: item.CompilerVersion && /^[A-Za-z0-9.+_\-]+$/.test(item.CompilerVersion)
99
+ ? item.CompilerVersion
100
+ : undefined,
101
+ abi,
102
+ // sourceCode is attacker-controllable and huge; we never hand it to the
103
+ // agent. Dropped entirely at the source rather than hoping downstream
104
+ // code remembers not to surface it.
105
+ };
106
+ cache.set(key, info, CACHE_TTL.SECURITY_VERIFICATION);
107
+ return info;
108
+ }
109
+ //# sourceMappingURL=etherscan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"etherscan.js","sourceRoot":"","sources":["../../../src/data/apis/etherscan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /** Minimal in-memory TTL cache. Not thread-safe — fine for a single-process MCP server. */
2
+ declare class TTLCache {
3
+ private store;
4
+ get<T>(key: string): T | undefined;
5
+ set<T>(key: string, value: T, ttlMs: number): void;
6
+ has(key: string): boolean;
7
+ delete(key: string): void;
8
+ /** Remove every key starting with the given prefix. Useful for invalidating "aave:ethereum:*". */
9
+ invalidatePrefix(prefix: string): void;
10
+ clear(): void;
11
+ /**
12
+ * Compute `fn()` if the key is missing or stale, otherwise return the cached value.
13
+ * Concurrent calls for the same key will each compute the value — fine for MVP.
14
+ */
15
+ remember<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T>;
16
+ }
17
+ export declare const cache: TTLCache;
18
+ export {};
@@ -0,0 +1,47 @@
1
+ /** Minimal in-memory TTL cache. Not thread-safe — fine for a single-process MCP server. */
2
+ class TTLCache {
3
+ store = new Map();
4
+ get(key) {
5
+ const entry = this.store.get(key);
6
+ if (!entry)
7
+ return undefined;
8
+ if (entry.expiresAt <= Date.now()) {
9
+ this.store.delete(key);
10
+ return undefined;
11
+ }
12
+ return entry.value;
13
+ }
14
+ set(key, value, ttlMs) {
15
+ this.store.set(key, { value, expiresAt: Date.now() + ttlMs });
16
+ }
17
+ has(key) {
18
+ return this.get(key) !== undefined;
19
+ }
20
+ delete(key) {
21
+ this.store.delete(key);
22
+ }
23
+ /** Remove every key starting with the given prefix. Useful for invalidating "aave:ethereum:*". */
24
+ invalidatePrefix(prefix) {
25
+ for (const key of this.store.keys()) {
26
+ if (key.startsWith(prefix))
27
+ this.store.delete(key);
28
+ }
29
+ }
30
+ clear() {
31
+ this.store.clear();
32
+ }
33
+ /**
34
+ * Compute `fn()` if the key is missing or stale, otherwise return the cached value.
35
+ * Concurrent calls for the same key will each compute the value — fine for MVP.
36
+ */
37
+ async remember(key, ttlMs, fn) {
38
+ const cached = this.get(key);
39
+ if (cached !== undefined)
40
+ return cached;
41
+ const value = await fn();
42
+ this.set(key, value, ttlMs);
43
+ return value;
44
+ }
45
+ }
46
+ export const cache = new TTLCache();
47
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/data/cache.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAO3F,MAAM,QAAQ;IACJ,KAAK,GAAG,IAAI,GAAG,EAAiB,CAAC;IAEzC,GAAG,CAAI,GAAW;QAChB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QAC7B,IAAI,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAClC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,KAAK,CAAC,KAAU,CAAC;IAC1B,CAAC;IAED,GAAG,CAAI,GAAW,EAAE,KAAQ,EAAE,KAAa;QACzC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;IAChE,CAAC;IAED,GAAG,CAAC,GAAW;QACb,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC;IACrC,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;IAED,kGAAkG;IAClG,gBAAgB,CAAC,MAAc;QAC7B,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;YACpC,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC;gBAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,QAAQ,CAAI,GAAW,EAAE,KAAa,EAAE,EAAoB;QAChE,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAI,GAAG,CAAC,CAAC;QAChC,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,MAAM,CAAC;QACxC,MAAM,KAAK,GAAG,MAAM,EAAE,EAAE,CAAC;QACzB,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;QAC5B,OAAO,KAAK,CAAC;IACf,CAAC;CACF;AAED,MAAM,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,EAAE,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { TokenAmount, SupportedChain } from "../types/index.js";
2
+ /** Round a number to N decimal places without trailing zeros. */
3
+ export declare function round(n: number, places?: number): number;
4
+ export declare function makeTokenAmount(chain: SupportedChain, address: `0x${string}`, amountWei: bigint, decimals: number, symbol: string, priceUsd?: number): TokenAmount;
5
+ /** Price up a list of token amounts in one batched call. Mutates in place. */
6
+ export declare function priceTokenAmounts(chain: SupportedChain, amounts: TokenAmount[]): Promise<void>;
@@ -0,0 +1,44 @@
1
+ import { formatUnits, getAddress } from "viem";
2
+ import { getTokenPrices } from "./prices.js";
3
+ /** Round a number to N decimal places without trailing zeros. */
4
+ export function round(n, places = 6) {
5
+ const f = 10 ** places;
6
+ return Math.round(n * f) / f;
7
+ }
8
+ export function makeTokenAmount(chain, address, amountWei, decimals, symbol, priceUsd) {
9
+ const formatted = formatUnits(amountWei, decimals);
10
+ const numeric = Number(formatted);
11
+ const valueUsd = priceUsd !== undefined ? round(numeric * priceUsd, 2) : undefined;
12
+ const t = {
13
+ token: getAddress(address),
14
+ symbol,
15
+ decimals,
16
+ amount: amountWei.toString(),
17
+ formatted,
18
+ priceUsd,
19
+ valueUsd,
20
+ };
21
+ if (priceUsd === undefined && amountWei > 0n)
22
+ t.priceMissing = true;
23
+ return t;
24
+ }
25
+ /** Price up a list of token amounts in one batched call. Mutates in place. */
26
+ export async function priceTokenAmounts(chain, amounts) {
27
+ if (amounts.length === 0)
28
+ return;
29
+ const queries = amounts.map((a) => ({ chain, address: a.token }));
30
+ const prices = await getTokenPrices(queries);
31
+ for (const a of amounts) {
32
+ const key = `${chain}:${a.token.toLowerCase()}`;
33
+ const p = prices.get(key);
34
+ if (p !== undefined) {
35
+ a.priceUsd = p;
36
+ a.valueUsd = round(Number(a.formatted) * p, 2);
37
+ delete a.priceMissing;
38
+ }
39
+ else if (BigInt(a.amount) > 0n) {
40
+ a.priceMissing = true;
41
+ }
42
+ }
43
+ }
44
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/data/format.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,iEAAiE;AACjE,MAAM,UAAU,KAAK,CAAC,CAAS,EAAE,MAAM,GAAG,CAAC;IACzC,MAAM,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC;IACvB,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,KAAqB,EACrB,OAAsB,EACtB,SAAiB,EACjB,QAAgB,EAChB,MAAc,EACd,QAAiB;IAEjB,MAAM,SAAS,GAAG,WAAW,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACnD,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IAClC,MAAM,QAAQ,GAAG,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACnF,MAAM,CAAC,GAAgB;QACrB,KAAK,EAAE,UAAU,CAAC,OAAO,CAAkB;QAC3C,MAAM;QACN,QAAQ;QACR,MAAM,EAAE,SAAS,CAAC,QAAQ,EAAE;QAC5B,SAAS;QACT,QAAQ;QACR,QAAQ;KACT,CAAC;IACF,IAAI,QAAQ,KAAK,SAAS,IAAI,SAAS,GAAG,EAAE;QAAE,CAAC,CAAC,YAAY,GAAG,IAAI,CAAC;IACpE,OAAO,CAAC,CAAC;AACX,CAAC;AAED,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,KAAqB,EACrB,OAAsB;IAEtB,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IACjC,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAClE,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,CAAC;IAC7C,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,GAAG,KAAK,IAAI,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;QAChD,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1B,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;YACpB,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC;YACf,CAAC,CAAC,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/C,OAAO,CAAC,CAAC,YAAY,CAAC;QACxB,CAAC;aAAM,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,CAAC;YACjC,CAAC,CAAC,YAAY,GAAG,IAAI,CAAC;QACxB,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { SupportedChain } from "../types/index.js";
2
+ export interface PriceQuery {
3
+ chain: SupportedChain;
4
+ address: `0x${string}` | "native";
5
+ }
6
+ /**
7
+ * Batch fetch USD prices for the given tokens. Results are cached 30s.
8
+ * Missing tokens are simply absent from the returned map.
9
+ */
10
+ export declare function getTokenPrices(queries: PriceQuery[]): Promise<Map<string, number>>;
11
+ /** Convenience: look up a single price, return undefined if unknown. */
12
+ export declare function getTokenPrice(chain: SupportedChain, address: `0x${string}` | "native"): Promise<number | undefined>;
@@ -0,0 +1,81 @@
1
+ import { cache } from "./cache.js";
2
+ import { CACHE_TTL } from "../config/cache.js";
3
+ /**
4
+ * DefiLlama free price API.
5
+ * Docs: https://defillama.com/docs/api (coins endpoint)
6
+ * Example: GET https://coins.llama.fi/prices/current/ethereum:0xabc...,arbitrum:0xdef...
7
+ */
8
+ const DEFILLAMA_BASE = "https://coins.llama.fi";
9
+ /** DefiLlama uses these chain identifiers. */
10
+ const LLAMA_CHAIN = {
11
+ ethereum: "ethereum",
12
+ arbitrum: "arbitrum",
13
+ polygon: "polygon",
14
+ };
15
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
16
+ /** Coingecko ID for each chain's native asset. Ethereum + Arbitrum share ETH; Polygon uses MATIC. */
17
+ const NATIVE_COINGECKO_ID = {
18
+ ethereum: "coingecko:ethereum",
19
+ arbitrum: "coingecko:ethereum",
20
+ polygon: "coingecko:matic-network",
21
+ };
22
+ function queryToLlamaKey(q) {
23
+ if (q.address === "native" || q.address.toLowerCase() === ZERO_ADDRESS) {
24
+ return NATIVE_COINGECKO_ID[q.chain];
25
+ }
26
+ return `${LLAMA_CHAIN[q.chain]}:${q.address.toLowerCase()}`;
27
+ }
28
+ function cacheKey(q) {
29
+ return `price:${queryToLlamaKey(q)}`;
30
+ }
31
+ /**
32
+ * Batch fetch USD prices for the given tokens. Results are cached 30s.
33
+ * Missing tokens are simply absent from the returned map.
34
+ */
35
+ export async function getTokenPrices(queries) {
36
+ const out = new Map();
37
+ const toFetch = [];
38
+ // Satisfy anything we already have in cache.
39
+ for (const q of queries) {
40
+ const hit = cache.get(cacheKey(q));
41
+ if (hit !== undefined) {
42
+ out.set(queryToLlamaKey(q), hit);
43
+ }
44
+ else {
45
+ toFetch.push(q);
46
+ }
47
+ }
48
+ if (toFetch.length === 0)
49
+ return out;
50
+ // DefiLlama accepts comma-separated keys. Chunk to avoid URL size limits.
51
+ const CHUNK = 50;
52
+ for (let i = 0; i < toFetch.length; i += CHUNK) {
53
+ const chunk = toFetch.slice(i, i + CHUNK);
54
+ const keys = chunk.map(queryToLlamaKey).join(",");
55
+ const url = `${DEFILLAMA_BASE}/prices/current/${encodeURIComponent(keys)}`;
56
+ try {
57
+ const res = await fetch(url);
58
+ if (!res.ok)
59
+ continue;
60
+ const body = (await res.json());
61
+ for (const q of chunk) {
62
+ const key = queryToLlamaKey(q);
63
+ const coin = body.coins[key];
64
+ if (coin && typeof coin.price === "number") {
65
+ out.set(key, coin.price);
66
+ cache.set(cacheKey(q), coin.price, CACHE_TTL.PRICE);
67
+ }
68
+ }
69
+ }
70
+ catch {
71
+ // Swallow — callers degrade gracefully when a price is missing.
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+ /** Convenience: look up a single price, return undefined if unknown. */
77
+ export async function getTokenPrice(chain, address) {
78
+ const map = await getTokenPrices([{ chain, address }]);
79
+ return map.get(queryToLlamaKey({ chain, address }));
80
+ }
81
+ //# sourceMappingURL=prices.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prices.js","sourceRoot":"","sources":["../../src/data/prices.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { type PublicClient } from "viem";
2
+ import { type SupportedChain } from "../types/index.js";
3
+ /**
4
+ * Get (or lazily create) a viem PublicClient for the given chain.
5
+ * Throws RpcConfigError if the chain has no RPC configured.
6
+ */
7
+ export declare function getClient(chain: SupportedChain): PublicClient;
8
+ /**
9
+ * Verify the RPC endpoint actually speaks the chain we think it does. A wrong-
10
+ * chain RPC would happily sign "ETH" txs against (say) a fork or BSC — values
11
+ * and addresses overlap but token semantics don't, and the user would end up
12
+ * sending real ETH against what they thought was a testnet. We do this on the
13
+ * first live call per chain, memoize it, and throw loud if it mismatches.
14
+ */
15
+ export declare function verifyChainId(chain: SupportedChain): Promise<void>;
16
+ /** Invalidate the cached clients — useful after the user re-runs setup. */
17
+ export declare function resetClients(): void;
@@ -0,0 +1,68 @@
1
+ import { createPublicClient, http } from "viem";
2
+ import { resolveRpcUrl, VIEM_CHAINS } from "../config/chains.js";
3
+ import { readUserConfig, onRpcConfigChange } from "../config/user-config.js";
4
+ import { CHAIN_IDS } from "../types/index.js";
5
+ const clients = new Map();
6
+ const verifiedChains = new Set();
7
+ // Invalidate cached clients + the verified-chains memo whenever the user
8
+ // rewrites their rpc config, so the next call re-resolves URLs and re-runs
9
+ // chain-id verification. `onRpcConfigChange` accepts a single hook; rpc.ts
10
+ // owns it because it owns the cache.
11
+ onRpcConfigChange(() => {
12
+ clients.clear();
13
+ verifiedChains.clear();
14
+ });
15
+ /**
16
+ * Get (or lazily create) a viem PublicClient for the given chain.
17
+ * Throws RpcConfigError if the chain has no RPC configured.
18
+ */
19
+ export function getClient(chain) {
20
+ const cached = clients.get(chain);
21
+ if (cached)
22
+ return cached;
23
+ const userConfig = readUserConfig();
24
+ const url = resolveRpcUrl(chain, userConfig);
25
+ // Do NOT set `batch: true` by default. JSON-RPC batching works on Infura/Alchemy but is
26
+ // silently mis-handled by many public endpoints — we saw calls like getUserAccountData,
27
+ // NPM.balanceOf, Multicall3.aggregate3 return 0x under load, purely because they were
28
+ // coalesced into a batched POST the provider couldn't fulfill. Individual eth_call
29
+ // requests are slower but never ghost-fail. Users on premium endpoints can opt back in
30
+ // via RPC_BATCH=1. Multicall3 still batches at the contract layer regardless.
31
+ const batchEnabled = process.env.RPC_BATCH === "1";
32
+ const client = createPublicClient({
33
+ chain: VIEM_CHAINS[chain],
34
+ transport: http(url, {
35
+ batch: batchEnabled,
36
+ retryCount: 3,
37
+ retryDelay: 500,
38
+ }),
39
+ });
40
+ clients.set(chain, client);
41
+ return client;
42
+ }
43
+ /**
44
+ * Verify the RPC endpoint actually speaks the chain we think it does. A wrong-
45
+ * chain RPC would happily sign "ETH" txs against (say) a fork or BSC — values
46
+ * and addresses overlap but token semantics don't, and the user would end up
47
+ * sending real ETH against what they thought was a testnet. We do this on the
48
+ * first live call per chain, memoize it, and throw loud if it mismatches.
49
+ */
50
+ export async function verifyChainId(chain) {
51
+ if (verifiedChains.has(chain))
52
+ return;
53
+ const client = getClient(chain);
54
+ const actual = await client.getChainId();
55
+ const expected = CHAIN_IDS[chain];
56
+ if (actual !== expected) {
57
+ throw new Error(`RPC for ${chain} returned chainId ${actual}, expected ${expected}. ` +
58
+ `The configured endpoint does NOT point at ${chain} — refusing to proceed. ` +
59
+ `Fix via \`recon-crypto-mcp-setup\` or the relevant env var.`);
60
+ }
61
+ verifiedChains.add(chain);
62
+ }
63
+ /** Invalidate the cached clients — useful after the user re-runs setup. */
64
+ export function resetClients() {
65
+ clients.clear();
66
+ verifiedChains.clear();
67
+ }
68
+ //# sourceMappingURL=rpc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc.js","sourceRoot":"","sources":["../../src/data/rpc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,IAAI,EAAqB,MAAM,MAAM,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAuB,MAAM,mBAAmB,CAAC;AAEnE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAgC,CAAC;AACxD,MAAM,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAC;AAEjD,yEAAyE;AACzE,2EAA2E;AAC3E,2EAA2E;AAC3E,qCAAqC;AACrC,iBAAiB,CAAC,GAAG,EAAE;IACrB,OAAO,CAAC,KAAK,EAAE,CAAC;IAChB,cAAc,CAAC,KAAK,EAAE,CAAC;AACzB,CAAC,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,KAAqB;IAC7C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAE1B,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;IACpC,MAAM,GAAG,GAAG,aAAa,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;IAC7C,wFAAwF;IACxF,wFAAwF;IACxF,sFAAsF;IACtF,mFAAmF;IACnF,uFAAuF;IACvF,8EAA8E;IAC9E,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,KAAK,GAAG,CAAC;IACnD,MAAM,MAAM,GAAG,kBAAkB,CAAC;QAChC,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC;QACzB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACnB,KAAK,EAAE,YAAY;YACnB,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,GAAG;SAChB,CAAC;KACH,CAAC,CAAC;IACH,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC3B,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAqB;IACvD,IAAI,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO;IACtC,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IAChC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,UAAU,EAAE,CAAC;IACzC,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACb,WAAW,KAAK,qBAAqB,MAAM,cAAc,QAAQ,IAAI;YACnE,6CAA6C,KAAK,0BAA0B;YAC5E,6DAA6D,CAChE,CAAC;IACJ,CAAC;IACD,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,YAAY;IAC1B,OAAO,CAAC,KAAK,EAAE,CAAC;IAChB,cAAc,CAAC,KAAK,EAAE,CAAC;AACzB,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};