diff --git a/README.md b/README.md index 801f528..716d689 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,10 @@ 11. [Pipe Commands](#11-pipe-commands) 12. [Transaction Simulation](#12-transaction-simulation) 13. [RNS Operations](#13-rns-operations) + 14. [Gas Estimator](#14-gas-estimator) + 15. [Developer Health Metrics](#15-developer-health-metrics) - [Contributing](#contributing) +- [Dependency Notes](#dependency-notes) ## Installation @@ -1090,8 +1093,7 @@ Output example: > - Both checksummed and non-checksummed addresses are supported > - The command will show appropriate error messages if the name or address cannot be resolved -<<<<<<< HEAD -### 12. Gas Estimator +### 14. Gas Estimator The `gas` command allows you to estimate gas costs for transactions and contract interactions on the Rootstock blockchain. It provides detailed analysis including gas price, estimated gas limits, and optimization tips. @@ -1147,8 +1149,124 @@ The command provides: - Cost in RBTC and Wei - Recommended gas limits (with buffers) - Optimization tips (if applicable) -======= ->>>>>>> main + +--- + +### 15. Developer Health Metrics + +The `devmetrics` command generates a combined developer health report by aggregating GitHub repository activity and Rootstock on-chain contract metrics into a single, structured output. + +#### Usage + +```bash +rsk-cli devmetrics --repo --contract
[options] +``` + +#### Options + +| Flag | Description | Default | +|---|---|---| +| `-r, --repo ` | GitHub repository in `owner/repo` format. Repeatable for batch mode. | Required | +| `-c, --contract
` | Rootstock contract address (`0x...`). Repeatable for batch mode. | Required | +| `-f, --format ` | Output format: `table`, `json`, or `markdown` | `table` | +| `--ci` | CI/CD mode — forces JSON output, suppresses all progress messages | `false` | +| `--github-token ` | GitHub personal access token (overrides `GITHUB_TOKEN` env var) | — | +| `--network ` | Rootstock network: `mainnet` or `testnet` | `mainnet` | +| `--rpc-url ` | Custom Rootstock RPC URL — must use `http://` or `https://` (overrides `--network`) | — | + +#### Examples + +**Single repo + contract (table output):** +```bash +rsk-cli devmetrics \ + --repo rsksmart/rsk-cli \ + --contract 0x9158c22b1799a2527ce8b95f9f1ff5e133fba27d \ + --network mainnet +``` + +**Multiple repos mapped to one contract:** +```bash +rsk-cli devmetrics \ + --repo rsksmart/rsk-cli \ + --repo rsksmart/rif-wallet \ + --contract 0x9158c22b1799a2527ce8b95f9f1ff5e133fba27d +``` + +**Markdown report:** +```bash +rsk-cli devmetrics \ + --repo rsksmart/rsk-cli \ + --contract 0x9158c22b1799a2527ce8b95f9f1ff5e133fba27d \ + --format markdown > report.md +``` + +**CI/CD pipeline (JSON to stdout):** +```bash +rsk-cli devmetrics \ + --repo rsksmart/rsk-cli \ + --contract 0x9158c22b1799a2527ce8b95f9f1ff5e133fba27d \ + --ci +``` + +**With authenticated GitHub access (5,000 req/hour):** +```bash +# Via environment variable (recommended) +GITHUB_TOKEN=ghp_xxx rsk-cli devmetrics --repo rsksmart/rsk-cli --contract 0x... + +# Via flag +rsk-cli devmetrics --repo rsksmart/rsk-cli --contract 0x... --github-token ghp_xxx +``` + +**Testnet with custom RPC:** +```bash +rsk-cli devmetrics \ + --repo rsksmart/rsk-cli \ + --contract 0x... \ + --rpc-url https://public-node.testnet.rsk.co +``` + +#### GitHub Metrics Reported + +| Metric | Description | +|---|---| +| ⭐ Stars | Total repository stars | +| 📝 Last Commit | Date of most recent commit | +| 🐛 Open Issues | Open issue count (includes PRs — GitHub API limitation) | +| 🔀 Open PRs | Open pull request count (accurate with token, sampled without) | +| 👥 Contributors | Contributor count (first-page estimate, up to 100 with token) | + +#### Rootstock Metrics Reported + +| Metric | Description | +|---|---| +| 📦 Deployment Block | Block number when the contract was deployed (bounded search) | +| 📊 Total Transactions | Estimated transaction count directed at the contract | +| ⏰ Last Transaction | Timestamp of the most recent detected transaction | +| ⛽ Gas Usage | Average / min / max gas across sampled transactions | + +> **Note**: Rootstock metrics use bounded block-window sampling for performance. Transaction counts and gas figures are estimates over the search window, not exact lifetime totals. + +#### Authentication Notes + +Without a GitHub token the API rate limit is **60 requests/hour**. With a valid `GITHUB_TOKEN` it is **5,000 requests/hour**. The command shows remaining quota in table mode. + +#### Security Note + +The `--rpc-url` flag validates that the provided URL uses `http://` or `https://` before it is used. Other protocols (`file://`, `ftp://`, internal IPs) are rejected at startup. + +--- + +## Dependency Notes + +### `@ethereum-attestation-service/eas-sdk` pinned to `2.7.0` + +The EAS SDK dependency is intentionally pinned to **`2.7.0`** (exact, not a range) rather than the latest `2.9.0`. + +**Reason:** EAS SDK `2.9.0` uses extensionless internal ESM imports (e.g. `export * from './eas'`) that are incompatible with Node.js ≥ 18's strict ESM module resolver. Under this project's runtime (Node 22, `"type": "module"`), the missing `.js` extension causes a fatal `ERR_MODULE_NOT_FOUND` at startup — crashing the entire CLI before any command runs, including commands completely unrelated to attestation. + +`2.7.0` ships with fully-qualified `.js` extensions in its ESM build and is fully compatible. The pin will be re-evaluated when the upstream package ships a corrected ESM build. + +--- ## Contributing diff --git a/bin/index.ts b/bin/index.ts index 7d3aefc..f2df4d0 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -29,6 +29,7 @@ import { validateAndFormatAddressRSK } from "../src/utils/index.js"; import { rnsUpdateCommand } from "../src/commands/rnsUpdate.js"; import { rnsTransferCommand } from "../src/commands/rnsTransfer.js"; import { rnsRegisterCommand } from "../src/commands/rnsRegister.js"; +import { devmetricsCommand } from "../src/commands/devmetrics.js"; interface CommandOptions { testnet?: boolean; @@ -712,4 +713,42 @@ program } }); +program + .command("devmetrics") + .description( + "Generate developer health reports combining GitHub activity and Rootstock on-chain metrics" + ) + .option( + "-r, --repo ", + "GitHub repository in owner/repo format (repeatable)", + (val: string, prev: string[]) => [...prev, val], + [] as string[] + ) + .option( + "-c, --contract
", + "Rootstock contract address (repeatable)", + (val: string, prev: string[]) => [...prev, val], + [] as string[] + ) + .option("-f, --format ", "Output format: table, json, or markdown", "table") + .option("--ci", "CI/CD mode — forces JSON output") + .option("--github-token ", "GitHub personal access token") + .option("--network ", "Rootstock network: mainnet or testnet", "mainnet") + .option("--rpc-url ", "Rootstock RPC URL (overrides --network)") + .action(async (options: any) => { + try { + await devmetricsCommand({ + repos: options.repo ?? [], + contracts: options.contract ?? [], + format: options.format, + ci: !!options.ci, + githubToken: options.githubToken, + network: options.network, + rpcUrl: options.rpcUrl, + }); + } catch (error: any) { + logError(false, `Error during devmetrics: ${error.message || error}`); + } + }); + program.parse(process.argv); diff --git a/package-lock.json b/package-lock.json index 0f0b6a9..4e60730 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "1.4.0", "license": "MIT", "dependencies": { - "@ethereum-attestation-service/eas-sdk": "^2.7.0", + "@ethereum-attestation-service/eas-sdk": "2.7.0", + "@octokit/rest": "^22.0.1", "@openzeppelin/contracts": "^5.0.2", "@rsksmart/rns-resolver.js": "^1.1.0", "@rsksmart/rns-sdk": "^1.0.0-beta.9", @@ -36,8 +37,10 @@ "@types/bun": "latest", "@types/figlet": "^1.5.8", "@types/uuid": "^10.0.0", + "@vitest/coverage-v8": "^4.1.4", "solc": "0.8.28", - "typescript": "^5.0.0" + "typescript": "^5.0.0", + "vitest": "^4.1.4" }, "peerDependencies": { "typescript": "^5.0.0" @@ -49,6 +52,66 @@ "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", "license": "MIT" }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -56,34 +119,476 @@ "license": "MIT", "optional": true, "engines": { - "node": ">=0.1.90" + "node": ">=0.1.90" + } + }, + "node_modules/@ensdomains/address-encoder": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@ensdomains/address-encoder/-/address-encoder-0.2.22.tgz", + "integrity": "sha512-4it+XDvf0U0vLFECLi4oXFBhelz1I/Mo9AWVgS3zry3tMPUFBzM+a27ndNCPZk+glaQiY+R2GOIa3tYOypK29g==", + "license": "BSD-3-Clause", + "dependencies": { + "bech32": "^2.0.0", + "blakejs": "^1.1.0", + "bn.js": "^4.11.8", + "bs58": "^4.0.1", + "crypto-addr-codec": "^0.1.8", + "js-crc": "^0.2.0", + "js-sha256": "^0.9.0", + "js-sha512": "^0.8.0", + "nano-base32": "^1.0.1", + "ripemd160": "^2.0.2", + "sha3": "^2.1.3" + } + }, + "node_modules/@ensdomains/eth-ens-namehash": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@ensdomains/eth-ens-namehash/-/eth-ens-namehash-2.0.15.tgz", + "integrity": "sha512-JRDFP6+Hczb1E0/HhIg0PONgBYasfGfDheujmfxaZaAv/NAH4jE6Kf48WbqfRZdxt4IZI3jl3Ri7sZ1nP09lgw==", + "license": "ISC" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@ensdomains/address-encoder": { - "version": "0.2.22", - "resolved": "https://registry.npmjs.org/@ensdomains/address-encoder/-/address-encoder-0.2.22.tgz", - "integrity": "sha512-4it+XDvf0U0vLFECLi4oXFBhelz1I/Mo9AWVgS3zry3tMPUFBzM+a27ndNCPZk+glaQiY+R2GOIa3tYOypK29g==", - "license": "BSD-3-Clause", - "dependencies": { - "bech32": "^2.0.0", - "blakejs": "^1.1.0", - "bn.js": "^4.11.8", - "bs58": "^4.0.1", - "crypto-addr-codec": "^0.1.8", - "js-crc": "^0.2.0", - "js-sha256": "^0.9.0", - "js-sha512": "^0.8.0", - "nano-base32": "^1.0.1", - "ripemd160": "^2.0.2", - "sha3": "^2.1.3" + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@ensdomains/eth-ens-namehash": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/@ensdomains/eth-ens-namehash/-/eth-ens-namehash-2.0.15.tgz", - "integrity": "sha512-JRDFP6+Hczb1E0/HhIg0PONgBYasfGfDheujmfxaZaAv/NAH4jE6Kf48WbqfRZdxt4IZI3jl3Ri7sZ1nP09lgw==", - "license": "ISC" - }, "node_modules/@ethereum-attestation-service/eas-contracts": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@ethereum-attestation-service/eas-contracts/-/eas-contracts-1.7.1.tgz", @@ -94,9 +599,9 @@ } }, "node_modules/@ethereum-attestation-service/eas-sdk": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@ethereum-attestation-service/eas-sdk/-/eas-sdk-2.9.0.tgz", - "integrity": "sha512-jEtBlhfm0HFkl64jAa4rxOXjEQkblTHqSmLFhttPf9y+ALEOk4qgJzV9knnJ7Yh+jFs1jxbTrVeUGap03Fwy9g==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@ethereum-attestation-service/eas-sdk/-/eas-sdk-2.7.0.tgz", + "integrity": "sha512-JpbUty2ab+FK5AZdoNhH/yCAUfruC2J6sxIhOaF923qip4ibs3M8XwFRr8R67xWEzcML7WKn8rFz0icJiJnxUA==", "license": "MIT", "dependencies": { "@ethereum-attestation-service/eas-contracts": "1.7.1", @@ -1487,6 +1992,34 @@ } } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@metamask/abi-utils": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@metamask/abi-utils/-/abi-utils-2.0.4.tgz", @@ -1872,25 +2405,531 @@ "integrity": "sha512-Fdjli4DCcFHb4Zgsz0uEJXZ2K7VEO+w5KVv7HmT7WO10iODdU9csC2az4jrhEsRtiR9Gfd74FlG0NYlw1BMdyA==", "license": "MIT", "optional": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/@openzeppelin/contracts": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.4.0.tgz", - "integrity": "sha512-eCYgWnLg6WO+X52I16TZt8uEjbtdkgLC0SUX/xnAksjjrQI4Xfn4iBRoI5j55dmlOhDv1Y7BoR3cU7e3WWhC6A==", - "license": "MIT" - }, - "node_modules/@openzeppelin/merkle-tree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@openzeppelin/merkle-tree/-/merkle-tree-1.0.8.tgz", - "integrity": "sha512-E2c9/Y3vjZXwVvPZKqCKUn7upnvam1P1ZhowJyZVQSkzZm5WhumtaRr+wkUXrZVfkIc7Gfrl7xzabElqDL09ow==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", + "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", + "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "json-with-bigint": "^3.5.3", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz", + "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==", + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@openzeppelin/contracts": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.4.0.tgz", + "integrity": "sha512-eCYgWnLg6WO+X52I16TZt8uEjbtdkgLC0SUX/xnAksjjrQI4Xfn4iBRoI5j55dmlOhDv1Y7BoR3cU7e3WWhC6A==", + "license": "MIT" + }, + "node_modules/@openzeppelin/merkle-tree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@openzeppelin/merkle-tree/-/merkle-tree-1.0.8.tgz", + "integrity": "sha512-E2c9/Y3vjZXwVvPZKqCKUn7upnvam1P1ZhowJyZVQSkzZm5WhumtaRr+wkUXrZVfkIc7Gfrl7xzabElqDL09ow==", + "license": "MIT", + "dependencies": { + "@metamask/abi-utils": "^2.0.4", + "ethereum-cryptography": "^3.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@metamask/abi-utils": "^2.0.4", - "ethereum-cryptography": "^3.0.0" - } + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@rsksmart/rns-resolver.js": { "version": "1.1.0", @@ -2058,6 +3097,13 @@ "node": ">=6" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/bn.js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.2.0.tgz", @@ -2077,6 +3123,17 @@ "bun-types": "1.3.9" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -2086,6 +3143,20 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/figlet": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@types/figlet/-/figlet-1.7.0.tgz", @@ -2164,6 +3235,151 @@ "integrity": "sha512-FZJgC5Bxuqg7Rhsm/bx6gAruHHhDQ55r+s0JhDh8CQ16fD7NsJJ+p8YMMQDhSQoIrSmjpqqYWA96oQVMNkjRyA==", "license": "MIT" }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.4.tgz", + "integrity": "sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.4", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.4", + "vitest": "4.1.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", + "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", + "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", + "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", + "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.4", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", + "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.4", + "@vitest/utils": "4.1.4", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", + "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", + "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.4", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/abitype": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", @@ -2301,6 +3517,28 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2357,6 +3595,12 @@ "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", "license": "MIT" }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "license": "Apache-2.0" + }, "node_modules/big-integer": { "version": "1.6.36", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.36.tgz", @@ -2634,6 +3878,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -2818,6 +4072,13 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", @@ -3038,6 +4299,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -3050,6 +4318,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3071,6 +4381,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/eth-ens-namehash": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/eth-ens-namehash/-/eth-ens-namehash-2.0.8.tgz", @@ -3240,6 +4560,32 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/figlet": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.10.0.tgz", @@ -3971,6 +5317,13 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -4261,6 +5614,58 @@ "ws": "*" } }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/js-base64": { "version": "3.7.8", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", @@ -4291,6 +5696,13 @@ "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==", "license": "MIT" }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -4303,6 +5715,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-with-bigint": { + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz", + "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==", + "license": "MIT" + }, "node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", @@ -4398,16 +5816,54 @@ "dependencies": { "has-flag": "^4.0.0" }, - "engines": { - "node": ">=8" + "engines": { + "node": ">=8" + } + }, + "node_modules/lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lru_map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", - "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==", - "license": "MIT" - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4660,6 +6116,25 @@ "integrity": "sha512-sxEtoTqAPdjWVGv71Q17koMFGsOMSiHsIFEvzOM7cNp8BXB4AnEwmDabm5dorusJf/v1z7QxaZYxUorU9RKaAw==", "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/node-addon-api": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", @@ -4692,6 +6167,17 @@ "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", "license": "MIT" }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4961,6 +6447,13 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pbkdf2": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", @@ -4978,6 +6471,13 @@ "node": ">= 0.10" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", @@ -5008,6 +6508,35 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -5183,6 +6712,51 @@ "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", "license": "MIT" }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, "node_modules/run-async": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", @@ -5339,6 +6913,13 @@ "buffer": "6.0.3" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -5402,6 +6983,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -5412,6 +7003,13 @@ "source-map": "^0.6.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/stacktrace-parser": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", @@ -5442,6 +7040,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -5529,6 +7134,82 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -5632,6 +7313,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5658,6 +7340,12 @@ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -5761,6 +7449,216 @@ } } }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", + "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.4", + "@vitest/mocker": "4.1.4", + "@vitest/pretty-format": "4.1.4", + "@vitest/runner": "4.1.4", + "@vitest/snapshot": "4.1.4", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.4", + "@vitest/browser-preview": "4.1.4", + "@vitest/browser-webdriverio": "4.1.4", + "@vitest/coverage-istanbul": "4.1.4", + "@vitest/coverage-v8": "4.1.4", + "@vitest/ui": "4.1.4", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/which-typed-array": { "version": "1.1.20", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", @@ -5782,6 +7680,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", @@ -5828,6 +7743,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index cf113e6..39a8e3d 100644 --- a/package.json +++ b/package.json @@ -13,14 +13,17 @@ "scripts": { "build": "tsc", "dev": "tsc -w", - "wallet": "pnpm run build && node dist/bin/index.js wallet", - "balance": "pnpm run build && node dist/bin/index.js balance", - "transfer": "pnpm run build && node dist/bin/index.js transfer --testnet --address 0xa5f45f5bddefC810C48aCC1D5CdA5e5a4c6BC59E --value 0.000001", - "transferToken": "pnpm run build && node dist/bin/index.js transfer --testnet --token 0x32Cd6c5831531F96f57d1faf4DDdf0222c4Af8AB --address 0x8A0d290b2EE35eFde47810CA8fF057e109e4190B --value 0.000001", - "tx-status": "pnpm run build && node dist/bin/index.js tx --testnet --txid 0x876a0a9b167889350c41930a4204e5d9acf5704a7f201447a337094189af961c4", - "batch-transfer": "pnpm run build && node dist/bin/index.js batch-transfer", - "history": "pnpm run build && node dist/bin/index.js history", - "config": "pnpm run build && node dist/bin/index.js config" + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "wallet": "npm run build && node dist/bin/index.js wallet", + "balance": "npm run build && node dist/bin/index.js balance", + "transfer": "npm run build && node dist/bin/index.js transfer --testnet --address 0xa5f45f5bddefC810C48aCC1D5CdA5e5a4c6BC59E --value 0.000001", + "transferToken": "npm run build && node dist/bin/index.js transfer --testnet --token 0x32Cd6c5831531F96f57d1faf4DDdf0222c4Af8AB --address 0x8A0d290b2EE35eFde47810CA8fF057e109e4190B --value 0.000001", + "tx-status": "npm run build && node dist/bin/index.js tx --testnet --txid 0x876a0a9b167889350c41930a4204e5d9acf5704a7f201447a337094189af961c4", + "batch-transfer": "npm run build && node dist/bin/index.js batch-transfer", + "history": "npm run build && node dist/bin/index.js history", + "config": "npm run build && node dist/bin/index.js config" }, "keywords": [ "rootstock", @@ -39,14 +42,17 @@ "@types/bun": "latest", "@types/figlet": "^1.5.8", "@types/uuid": "^10.0.0", + "@vitest/coverage-v8": "^4.1.4", "solc": "0.8.28", - "typescript": "^5.0.0" + "typescript": "^5.0.0", + "vitest": "^4.1.4" }, "peerDependencies": { "typescript": "^5.0.0" }, "dependencies": { - "@ethereum-attestation-service/eas-sdk": "^2.7.0", + "@ethereum-attestation-service/eas-sdk": "2.7.0", + "@octokit/rest": "^22.0.1", "@openzeppelin/contracts": "^5.0.2", "@rsksmart/rns-resolver.js": "^1.1.0", "@rsksmart/rns-sdk": "^1.0.0-beta.9", @@ -57,8 +63,8 @@ "cli-table3": "^0.6.5", "commander": "^13.1.0", "dotenv": "^16.3.1", - "figlet": "^1.7.0", "ethers": "^5.8.0", + "figlet": "^1.7.0", "fs-extra": "^11.2.0", "inquirer": "^12.1.0", "ora": "^8.0.1", diff --git a/src/__tests__/commands/devmetrics.test.ts b/src/__tests__/commands/devmetrics.test.ts new file mode 100644 index 0000000..23e8407 --- /dev/null +++ b/src/__tests__/commands/devmetrics.test.ts @@ -0,0 +1,310 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { devmetricsCommand } from "../../commands/devmetrics.js"; + +// ─── Module mocks ───────────────────────────────────────────────────────────── + +const mockGetMetricsGitHub = vi.fn(); +const mockGetMetricsRootstock = vi.fn(); +const mockIsAuthenticated = vi.fn().mockReturnValue(true); +const mockGetRateLimitStatus = vi.fn().mockResolvedValue({ + remaining: 4999, + limit: 5000, + resetAt: new Date(), +}); +const mockGetNetwork = vi.fn().mockReturnValue("mainnet"); +const mockGetRpcUrl = vi.fn().mockReturnValue("https://public-node.rsk.co"); + +vi.mock("../../services/github.service.js", () => ({ + GitHubService: vi.fn().mockImplementation(function () { + return { + getMetrics: mockGetMetricsGitHub, + isAuthenticated: mockIsAuthenticated, + getRateLimitStatus: mockGetRateLimitStatus, + }; + }), +})); + +vi.mock("../../services/rootstock.service.js", () => ({ + RootstockService: vi.fn().mockImplementation(function () { + return { + getMetrics: mockGetMetricsRootstock, + getNetwork: mockGetNetwork, + getRpcUrl: mockGetRpcUrl, + }; + }), +})); + +const mockFormatReport = vi.fn().mockReturnValue("[formatted report]"); + +vi.mock("../../formatters/devmetrics/index.js", () => ({ + formatReport: (...args: any[]) => mockFormatReport(...args), +})); + +// ─── Fixtures ───────────────────────────────────────────────────────────────── + +const GITHUB_METRICS = { + stars: 42, + lastCommitDate: "2026-04-06T10:00:00Z", + openIssuesCount: 5, + pullRequestsCount: 2, + contributorCount: 10, + repository: "rsksmart/rsk-cli", +}; + +const ROOTSTOCK_METRICS = { + contractAddress: "0x9158c22b1799a2527ce8b95f9f1ff5e133fba27d", + deploymentBlock: 5000000, + totalTransactionCount: 123, + lastTransactionTimestamp: "2026-04-10T08:00:00.000Z", + gasUsagePatterns: { average: 21000, min: 21000, max: 50000 }, +}; + +const VALID_REPO = "rsksmart/rsk-cli"; +const VALID_CONTRACT = "0x9158c22b1799a2527ce8b95f9f1ff5e133fba27d"; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function spyExit() { + return vi.spyOn(process, "exit").mockImplementation((_code?: any) => { + throw new Error(`process.exit(${_code ?? ""})`); + }); +} + +// ─── Tests ──────────────────────────────────────────────────────────────────── + +describe("devmetricsCommand (integration)", () => { + let exitSpy: ReturnType; + let consoleSpy: ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + mockGetMetricsGitHub.mockResolvedValue(GITHUB_METRICS); + mockGetMetricsRootstock.mockResolvedValue(ROOTSTOCK_METRICS); + mockIsAuthenticated.mockReturnValue(true); + mockGetRateLimitStatus.mockResolvedValue({ + remaining: 4999, + limit: 5000, + resetAt: new Date(), + }); + exitSpy = spyExit(); + consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + }); + + afterEach(() => { + exitSpy.mockRestore(); + consoleSpy.mockRestore(); + }); + + // ── Happy path ─────────────────────────────────────────────────────────────── + + it("calls GitHubService.getMetrics with owner and repo name", async () => { + await devmetricsCommand({ + repos: [VALID_REPO], + contracts: [VALID_CONTRACT], + }); + expect(mockGetMetricsGitHub).toHaveBeenCalledWith("rsksmart", "rsk-cli"); + }); + + it("calls RootstockService.getMetrics with the contract address", async () => { + await devmetricsCommand({ + repos: [VALID_REPO], + contracts: [VALID_CONTRACT], + }); + expect(mockGetMetricsRootstock).toHaveBeenCalledWith(VALID_CONTRACT); + }); + + it("calls formatReport with the assembled reports array and correct format", async () => { + await devmetricsCommand({ + repos: [VALID_REPO], + contracts: [VALID_CONTRACT], + format: "json", + }); + expect(mockFormatReport).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + repository: VALID_REPO, + contractAddress: VALID_CONTRACT, + github: expect.objectContaining({ stars: 42 }), + rootstock: expect.objectContaining({ deploymentBlock: 5000000 }), + }), + ]), + "json" + ); + }); + + it("forces JSON format when --ci flag is set", async () => { + await devmetricsCommand({ + repos: [VALID_REPO], + contracts: [VALID_CONTRACT], + ci: true, + format: "table", + }); + expect(mockFormatReport).toHaveBeenCalledWith( + expect.any(Array), + "json" + ); + }); + + it("fan-out: applies one contract to multiple repos", async () => { + await devmetricsCommand({ + repos: [VALID_REPO, "rsksmart/rif-wallet"], + contracts: [VALID_CONTRACT], + }); + expect(mockGetMetricsGitHub).toHaveBeenCalledTimes(2); + expect(mockGetMetricsRootstock).toHaveBeenCalledTimes(2); + expect(mockFormatReport).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ repository: VALID_REPO }), + expect.objectContaining({ repository: "rsksmart/rif-wallet" }), + ]), + "table" + ); + }); + + it("fan-out: applies one repo to multiple contracts", async () => { + const second = "0xabcdef1234567890abcdef1234567890abcdef12"; + await devmetricsCommand({ + repos: [VALID_REPO], + contracts: [VALID_CONTRACT, second], + }); + expect(mockGetMetricsRootstock).toHaveBeenCalledWith(VALID_CONTRACT); + expect(mockGetMetricsRootstock).toHaveBeenCalledWith(second); + }); + + it("passes githubToken to GitHubService constructor", async () => { + const { GitHubService } = await import("../../services/github.service.js"); + await devmetricsCommand({ + repos: [VALID_REPO], + contracts: [VALID_CONTRACT], + githubToken: "ghp_secret", + }); + expect(GitHubService).toHaveBeenCalledWith("ghp_secret"); + }); + + // ── Validation / exit paths ────────────────────────────────────────────────── + + it("exits 1 when no repos are supplied", async () => { + await expect( + devmetricsCommand({ repos: [], contracts: [VALID_CONTRACT] }) + ).rejects.toThrow("process.exit(1)"); + }); + + it("exits 1 when no contracts are supplied", async () => { + await expect( + devmetricsCommand({ repos: [VALID_REPO], contracts: [] }) + ).rejects.toThrow("process.exit(1)"); + }); + + it("exits 1 when a repo has an invalid format", async () => { + await expect( + devmetricsCommand({ + repos: ["invalid-repo-without-slash"], + contracts: [VALID_CONTRACT], + }) + ).rejects.toThrow("process.exit(1)"); + }); + + it("exits 1 when a contract address is invalid", async () => { + await expect( + devmetricsCommand({ + repos: [VALID_REPO], + contracts: ["not-an-address"], + }) + ).rejects.toThrow("process.exit(1)"); + }); + + it("exits 1 when --network has an unsupported value", async () => { + await expect( + devmetricsCommand({ + repos: [VALID_REPO], + contracts: [VALID_CONTRACT], + network: "polygon" as any, + }) + ).rejects.toThrow("process.exit(1)"); + }); + + it("exits 1 when --format has an unsupported value", async () => { + await expect( + devmetricsCommand({ + repos: [VALID_REPO], + contracts: [VALID_CONTRACT], + format: "xml" as any, + }) + ).rejects.toThrow("process.exit(1)"); + }); + + it("exits 1 when --rpc-url uses a non-http protocol", async () => { + await expect( + devmetricsCommand({ + repos: [VALID_REPO], + contracts: [VALID_CONTRACT], + rpcUrl: "file:///etc/passwd", + }) + ).rejects.toThrow("process.exit(1)"); + }); + + it("exits 1 when repo/contract counts are mismatched (multiple each)", async () => { + await expect( + devmetricsCommand({ + repos: [VALID_REPO, "rsksmart/rif-wallet"], + contracts: [VALID_CONTRACT, "0xabcdef1234567890abcdef1234567890abcdef12", "0xaaaa00000000000000000000000000000000aaaa"], + }) + ).rejects.toThrow("process.exit(1)"); + }); + + // ── Error handling per pair ─────────────────────────────────────────────────── + + it("exits 1 when all pairs fail, without calling formatReport", async () => { + mockGetMetricsGitHub.mockRejectedValue(new Error("GitHub API down")); + const errSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + await expect( + devmetricsCommand({ + repos: [VALID_REPO], + contracts: [VALID_CONTRACT], + }) + ).rejects.toThrow("process.exit(1)"); + expect(mockFormatReport).not.toHaveBeenCalled(); + errSpy.mockRestore(); + }); + + it("still outputs a partial report when only some pairs fail", async () => { + const errSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + mockGetMetricsGitHub + .mockResolvedValueOnce(GITHUB_METRICS) + .mockRejectedValueOnce(new Error("not found")); + + await expect( + devmetricsCommand({ + repos: [VALID_REPO, "rsksmart/nonexistent"], + contracts: [VALID_CONTRACT], + }) + ).rejects.toThrow("process.exit(1)"); + + // formatReport should have been called with the single successful report + expect(mockFormatReport).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ repository: VALID_REPO }), + ]), + "table" + ); + errSpy.mockRestore(); + }); + + it("outputs errors as JSON when CI mode is enabled and pairs fail", async () => { + const errSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + mockGetMetricsGitHub.mockRejectedValue(new Error("GitHub down")); + + await expect( + devmetricsCommand({ + repos: [VALID_REPO], + contracts: [VALID_CONTRACT], + ci: true, + }) + ).rejects.toThrow("process.exit(1)"); + + const [jsonArg] = errSpy.mock.calls[0] as [string]; + expect(() => JSON.parse(jsonArg)).not.toThrow(); + expect(JSON.parse(jsonArg)).toHaveProperty("errors"); + errSpy.mockRestore(); + }); +}); diff --git a/src/__tests__/formatters/json.formatter.test.ts b/src/__tests__/formatters/json.formatter.test.ts new file mode 100644 index 0000000..3728add --- /dev/null +++ b/src/__tests__/formatters/json.formatter.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from "vitest"; +import { formatAsJSON } from "../../formatters/devmetrics/json.formatter.js"; +import { DevMetricsReport } from "../../utils/types.js"; + +const REPORT: DevMetricsReport = { + repository: "rsksmart/rsk-cli", + contractAddress: "0x9158c22b1799a2527ce8b95f9f1ff5e133fba27d", + github: { + stars: 42, + lastCommitDate: "2026-04-06T10:00:00Z", + openIssuesCount: 5, + pullRequestsCount: 2, + contributorCount: 10, + repository: "rsksmart/rsk-cli", + }, + rootstock: { + contractAddress: "0x9158c22b1799a2527ce8b95f9f1ff5e133fba27d", + deploymentBlock: 5000000, + totalTransactionCount: 123, + lastTransactionTimestamp: "2026-04-10T08:00:00.000Z", + gasUsagePatterns: { average: 21000, min: 21000, max: 50000 }, + }, + timestamp: "2026-04-14T00:00:00.000Z", +}; + +describe("formatAsJSON", () => { + it("returns valid JSON", () => { + const output = formatAsJSON([REPORT]); + expect(() => JSON.parse(output)).not.toThrow(); + }); + + it("contains all top-level fields of the report", () => { + const parsed = JSON.parse(formatAsJSON([REPORT])); + expect(parsed).toHaveLength(1); + const r = parsed[0]; + expect(r.repository).toBe(REPORT.repository); + expect(r.contractAddress).toBe(REPORT.contractAddress); + expect(r.timestamp).toBe(REPORT.timestamp); + }); + + it("serialises GitHub metrics correctly", () => { + const parsed = JSON.parse(formatAsJSON([REPORT])); + expect(parsed[0].github.stars).toBe(42); + expect(parsed[0].github.contributorCount).toBe(10); + expect(parsed[0].github.lastCommitDate).toBe("2026-04-06T10:00:00Z"); + }); + + it("serialises Rootstock metrics correctly", () => { + const parsed = JSON.parse(formatAsJSON([REPORT])); + expect(parsed[0].rootstock.deploymentBlock).toBe(5000000); + expect(parsed[0].rootstock.totalTransactionCount).toBe(123); + expect(parsed[0].rootstock.gasUsagePatterns.average).toBe(21000); + }); + + it("outputs multiple reports as an array", () => { + const second: DevMetricsReport = { ...REPORT, repository: "rsksmart/rif-wallet" }; + const parsed = JSON.parse(formatAsJSON([REPORT, second])); + expect(parsed).toHaveLength(2); + expect(parsed[1].repository).toBe("rsksmart/rif-wallet"); + }); + + it("handles an empty reports array", () => { + const parsed = JSON.parse(formatAsJSON([])); + expect(parsed).toEqual([]); + }); + + it("handles a null deploymentBlock correctly", () => { + const withNull: DevMetricsReport = { + ...REPORT, + rootstock: { ...REPORT.rootstock, deploymentBlock: null }, + }; + const parsed = JSON.parse(formatAsJSON([withNull])); + expect(parsed[0].rootstock.deploymentBlock).toBeNull(); + }); +}); diff --git a/src/__tests__/formatters/markdown.formatter.test.ts b/src/__tests__/formatters/markdown.formatter.test.ts new file mode 100644 index 0000000..6c68b4b --- /dev/null +++ b/src/__tests__/formatters/markdown.formatter.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from "vitest"; +import { formatAsMarkdown } from "../../formatters/devmetrics/markdown.formatter.js"; +import { DevMetricsReport } from "../../utils/types.js"; + +const REPORT: DevMetricsReport = { + repository: "rsksmart/rsk-cli", + contractAddress: "0x9158c22b1799a2527ce8b95f9f1ff5e133fba27d", + github: { + stars: 6, + lastCommitDate: "2026-04-06T10:00:00Z", + openIssuesCount: 26, + pullRequestsCount: 1, + contributorCount: 17, + repository: "rsksmart/rsk-cli", + }, + rootstock: { + contractAddress: "0x9158c22b1799a2527ce8b95f9f1ff5e133fba27d", + deploymentBlock: null, + totalTransactionCount: 0, + lastTransactionTimestamp: null, + gasUsagePatterns: { average: 0, min: 0, max: 0 }, + }, + timestamp: "2026-04-14T00:00:00.000Z", +}; + +describe("formatAsMarkdown", () => { + it("starts with an H1 containing the repository name", () => { + const output = formatAsMarkdown([REPORT]); + expect(output).toMatch(/^# Developer Health Report: rsksmart\/rsk-cli/m); + }); + + it("includes the contract address", () => { + const output = formatAsMarkdown([REPORT]); + expect(output).toContain("0x9158c22b1799a2527ce8b95f9f1ff5e133fba27d"); + }); + + it("contains GitHub Metrics section heading", () => { + const output = formatAsMarkdown([REPORT]); + expect(output).toContain("## 📊 GitHub Metrics"); + }); + + it("contains Rootstock Metrics section heading", () => { + const output = formatAsMarkdown([REPORT]); + expect(output).toContain("## ⛓️ Rootstock Metrics"); + }); + + it("renders star count", () => { + const output = formatAsMarkdown([REPORT]); + expect(output).toContain("| ⭐ Stars | 6 |"); + }); + + it("renders open issues count", () => { + const output = formatAsMarkdown([REPORT]); + expect(output).toContain("| 🐛 Open Issues | 26 |"); + }); + + it("renders N/A for null deployment block", () => { + const output = formatAsMarkdown([REPORT]); + expect(output).toContain("| 📦 Deployment Block | N/A |"); + }); + + it("renders N/A for null last transaction", () => { + const output = formatAsMarkdown([REPORT]); + expect(output).toContain("| ⏰ Last Transaction | N/A |"); + }); + + it("renders a formatted last commit date when present", () => { + const output = formatAsMarkdown([REPORT]); + // The date cell should not be 'N/A' + const lastCommitRow = output + .split("\n") + .find((l) => l.startsWith("| 📝 Last Commit")); + expect(lastCommitRow).toBeDefined(); + expect(lastCommitRow).not.toContain("N/A"); + }); + + it("adds a separator between multiple reports", () => { + const second: DevMetricsReport = { ...REPORT, repository: "rsksmart/rif-wallet" }; + const output = formatAsMarkdown([REPORT, second]); + expect(output).toContain("---"); + expect(output).toContain("rsksmart/rif-wallet"); + }); + + it("does NOT add a separator for a single report", () => { + const output = formatAsMarkdown([REPORT]); + // '---' should not appear as a thematic break (only in HR positions) + const hrLines = output.split("\n").filter((l) => l.trim() === "---"); + expect(hrLines).toHaveLength(0); + }); + + it("contains a pipe-table header row", () => { + const output = formatAsMarkdown([REPORT]); + expect(output).toContain("| Metric | Value |"); + expect(output).toContain("|--------|-------|"); + }); +}); diff --git a/src/__tests__/formatters/table.formatter.test.ts b/src/__tests__/formatters/table.formatter.test.ts new file mode 100644 index 0000000..a2540d0 --- /dev/null +++ b/src/__tests__/formatters/table.formatter.test.ts @@ -0,0 +1,109 @@ +import { describe, it, expect } from "vitest"; +import { formatAsTable } from "../../formatters/devmetrics/table.formatter.js"; +import { DevMetricsReport } from "../../utils/types.js"; + +const REPORT: DevMetricsReport = { + repository: "rsksmart/rsk-cli", + contractAddress: "0x9158c22b1799a2527ce8b95f9f1ff5e133fba27d", + github: { + stars: 6, + lastCommitDate: "2026-04-06T10:00:00Z", + openIssuesCount: 26, + pullRequestsCount: 1, + contributorCount: 17, + repository: "rsksmart/rsk-cli", + }, + rootstock: { + contractAddress: "0x9158c22b1799a2527ce8b95f9f1ff5e133fba27d", + deploymentBlock: 5000000, + totalTransactionCount: 42, + lastTransactionTimestamp: "2026-04-10T08:00:00.000Z", + gasUsagePatterns: { average: 21000, min: 21000, max: 50000 }, + }, + timestamp: "2026-04-14T00:00:00.000Z", +}; + +describe("formatAsTable", () => { + it("returns a non-empty string", () => { + const output = formatAsTable([REPORT]); + expect(typeof output).toBe("string"); + expect(output.length).toBeGreaterThan(0); + }); + + it("contains the repository name", () => { + const output = formatAsTable([REPORT]); + expect(output).toContain("rsksmart/rsk-cli"); + }); + + it("contains the contract address", () => { + const output = formatAsTable([REPORT]); + expect(output).toContain("0x9158c22b1799a2527ce8b95f9f1ff5e133fba27d"); + }); + + it("contains GitHub Metrics column header", () => { + const output = formatAsTable([REPORT]); + expect(output).toContain("GitHub Metrics"); + }); + + it("contains Rootstock Metrics column header", () => { + const output = formatAsTable([REPORT]); + expect(output).toContain("Rootstock Metrics"); + }); + + it("renders star count", () => { + const output = formatAsTable([REPORT]); + expect(output).toContain("6"); + }); + + it("renders contributor count", () => { + const output = formatAsTable([REPORT]); + expect(output).toContain("17"); + }); + + it("renders deployment block number", () => { + const output = formatAsTable([REPORT]); + expect(output).toContain("5000000"); + }); + + it("renders total transaction count", () => { + const output = formatAsTable([REPORT]); + expect(output).toContain("42"); + }); + + it("renders N/A for null deployment block", () => { + const withNull: DevMetricsReport = { + ...REPORT, + rootstock: { ...REPORT.rootstock, deploymentBlock: null }, + }; + const output = formatAsTable([withNull]); + expect(output).toContain("N/A"); + }); + + it("renders N/A for null last transaction timestamp", () => { + const withNull: DevMetricsReport = { + ...REPORT, + rootstock: { ...REPORT.rootstock, lastTransactionTimestamp: null }, + }; + const output = formatAsTable([withNull]); + expect(output).toContain("N/A"); + }); + + it("renders N/A for null last commit date", () => { + const withNull: DevMetricsReport = { + ...REPORT, + github: { ...REPORT.github, lastCommitDate: null }, + }; + const output = formatAsTable([withNull]); + expect(output).toContain("N/A"); + }); + + it("handles multiple reports without throwing", () => { + const second: DevMetricsReport = { ...REPORT, repository: "rsksmart/rif-wallet" }; + expect(() => formatAsTable([REPORT, second])).not.toThrow(); + expect(formatAsTable([REPORT, second])).toContain("rif-wallet"); + }); + + it("handles an empty array without throwing", () => { + expect(() => formatAsTable([])).not.toThrow(); + }); +}); diff --git a/src/__tests__/services/github.service.test.ts b/src/__tests__/services/github.service.test.ts new file mode 100644 index 0000000..b726ddc --- /dev/null +++ b/src/__tests__/services/github.service.test.ts @@ -0,0 +1,226 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { GitHubService } from "../../services/github.service.js"; + +// ─── Octokit mock factory ───────────────────────────────────────────────────── +// +// We mock @octokit/rest at the module level so every `new Octokit()` call +// inside GitHubService returns a controllable fake. + +const makeOctokit = (overrides: Record = {}) => ({ + rateLimit: { + get: vi.fn().mockResolvedValue({ + data: { rate: { remaining: 59, limit: 60, reset: 9999999999 } }, + }), + }, + repos: { + get: vi.fn().mockResolvedValue({ + data: { stargazers_count: 10, open_issues_count: 5 }, + }), + listCommits: vi.fn().mockResolvedValue({ + data: [{ commit: { committer: { date: "2026-04-06T10:00:00Z" } } }], + }), + listContributors: vi.fn().mockResolvedValue({ data: new Array(7).fill({}) }), + }, + issues: { + listForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + pulls: { + list: vi.fn().mockResolvedValue({ data: [{}] }), + }, + search: { + issuesAndPullRequests: vi.fn().mockResolvedValue({ data: { total_count: 3 } }), + }, + ...overrides, +}); + +let mockOctokitInstance = makeOctokit(); + +vi.mock("@octokit/rest", () => ({ + Octokit: vi.fn().mockImplementation(function () { + return mockOctokitInstance; + }), +})); + +// ─── Tests ──────────────────────────────────────────────────────────────────── + +describe("GitHubService", () => { + beforeEach(() => { + mockOctokitInstance = makeOctokit(); + vi.clearAllMocks(); + }); + + // ── isAuthenticated ────────────────────────────────────────────────────────── + + describe("isAuthenticated()", () => { + it("returns true when a token is provided", () => { + const svc = new GitHubService("ghp_test"); + expect(svc.isAuthenticated()).toBe(true); + }); + + it("returns false when no token is provided", () => { + // Ensure env var is absent + const saved = process.env.GITHUB_TOKEN; + delete process.env.GITHUB_TOKEN; + const svc = new GitHubService(); + expect(svc.isAuthenticated()).toBe(false); + process.env.GITHUB_TOKEN = saved; + }); + }); + + // ── getRateLimitStatus ─────────────────────────────────────────────────────── + + describe("getRateLimitStatus()", () => { + it("returns remaining, limit, and resetAt on success", async () => { + const svc = new GitHubService("ghp_test"); + const status = await svc.getRateLimitStatus(); + expect(status).not.toBeNull(); + expect(status!.remaining).toBe(59); + expect(status!.limit).toBe(60); + expect(status!.resetAt).toBeInstanceOf(Date); + }); + + it("returns null when the API call throws", async () => { + mockOctokitInstance = makeOctokit({ + rateLimit: { get: vi.fn().mockRejectedValue(new Error("network")) }, + }); + const svc = new GitHubService("ghp_test"); + const status = await svc.getRateLimitStatus(); + expect(status).toBeNull(); + }); + + it("resets to unauthenticated and retries on 401", async () => { + let callCount = 0; + mockOctokitInstance = makeOctokit({ + rateLimit: { + get: vi.fn().mockImplementation(() => { + callCount++; + if (callCount === 1) { + const err: any = new Error("Unauthorized"); + err.status = 401; + return Promise.reject(err); + } + return Promise.resolve({ + data: { rate: { remaining: 30, limit: 60, reset: 9999999999 } }, + }); + }), + }, + }); + const svc = new GitHubService("ghp_bad"); + const status = await svc.getRateLimitStatus(); + expect(status).not.toBeNull(); + expect(status!.remaining).toBe(30); + expect(svc.isAuthenticated()).toBe(false); + }); + }); + + // ── getMetrics ─────────────────────────────────────────────────────────────── + + describe("getMetrics()", () => { + it("returns assembled GitHubMetrics on success", async () => { + const svc = new GitHubService("ghp_test"); + const metrics = await svc.getMetrics("rsksmart", "rsk-cli"); + expect(metrics.stars).toBe(10); + expect(metrics.openIssuesCount).toBe(5); + expect(metrics.lastCommitDate).toBe("2026-04-06T10:00:00Z"); + expect(metrics.pullRequestsCount).toBe(3); // from search API + expect(metrics.contributorCount).toBe(7); + expect(metrics.repository).toBe("rsksmart/rsk-cli"); + }); + + it("falls back to pulls list length when search API fails", async () => { + mockOctokitInstance = makeOctokit({ + search: { + issuesAndPullRequests: vi.fn().mockRejectedValue(new Error("rate")), + }, + }); + const svc = new GitHubService("ghp_test"); + const metrics = await svc.getMetrics("rsksmart", "rsk-cli"); + // pulls.list returns [{}] (length 1) + expect(metrics.pullRequestsCount).toBe(1); + }); + + it("returns 0 contributor count when listContributors fails", async () => { + mockOctokitInstance = makeOctokit({ + repos: { + ...makeOctokit().repos, + listContributors: vi.fn().mockRejectedValue(new Error("forbidden")), + }, + }); + const svc = new GitHubService("ghp_test"); + const metrics = await svc.getMetrics("rsksmart", "rsk-cli"); + expect(metrics.contributorCount).toBe(0); + }); + + it("throws on 404 with a descriptive message", async () => { + mockOctokitInstance = makeOctokit({ + repos: { + ...makeOctokit().repos, + get: vi.fn().mockRejectedValue(Object.assign(new Error("Not Found"), { status: 404 })), + }, + }); + const svc = new GitHubService("ghp_test"); + await expect(svc.getMetrics("rsksmart", "nonexistent")).rejects.toThrow( + "not found" + ); + }); + + it("resets to unauthenticated and retries fetchMetrics on 401 (no nested race)", async () => { + // First call to repos.get throws 401; after auth reset the mock returns normally + let reposGetCalls = 0; + mockOctokitInstance = makeOctokit({ + repos: { + ...makeOctokit().repos, + get: vi.fn().mockImplementation(() => { + reposGetCalls++; + if (reposGetCalls === 1) { + return Promise.reject(Object.assign(new Error("Unauthorized"), { status: 401 })); + } + return Promise.resolve({ data: { stargazers_count: 5, open_issues_count: 2 } }); + }), + }, + }); + const svc = new GitHubService("ghp_bad"); + const metrics = await svc.getMetrics("rsksmart", "rsk-cli"); + expect(svc.isAuthenticated()).toBe(false); + expect(metrics.stars).toBe(5); + }); + + it("throws a rate-limit error on 403 including reset time", async () => { + const resetTs = String(Math.floor(Date.now() / 1000) + 3600); + mockOctokitInstance = makeOctokit({ + repos: { + ...makeOctokit().repos, + get: vi.fn().mockRejectedValue( + Object.assign(new Error("Forbidden"), { + status: 403, + response: { headers: { "x-ratelimit-reset": resetTs } }, + }) + ), + }, + }); + const svc = new GitHubService(); + await expect(svc.getMetrics("rsksmart", "rsk-cli")).rejects.toThrow( + /rate limit/i + ); + }); + + it("rejects when the total timeout fires", async () => { + mockOctokitInstance = makeOctokit({ + repos: { + ...makeOctokit().repos, + // Never resolves + get: vi.fn().mockReturnValue(new Promise(() => {})), + }, + }); + const svc = new GitHubService("ghp_test"); + // Reduce TOTAL_TIMEOUT via a short race instead of waiting 60 s + const race = Promise.race([ + svc.getMetrics("rsksmart", "rsk-cli"), + new Promise((_, reject) => + setTimeout(() => reject(new Error("test timeout sentinel")), 200) + ), + ]); + await expect(race).rejects.toThrow("test timeout sentinel"); + }); + }); +}); diff --git a/src/__tests__/services/rootstock.service.test.ts b/src/__tests__/services/rootstock.service.test.ts new file mode 100644 index 0000000..b24a2de --- /dev/null +++ b/src/__tests__/services/rootstock.service.test.ts @@ -0,0 +1,183 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { RootstockService } from "../../services/rootstock.service.js"; + +// ─── ethers mock ────────────────────────────────────────────────────────────── +// +// We mock the entire ethers module so JsonRpcProvider is replaced by a +// controllable fake. Each test can override individual methods as needed. + +const makeProvider = (overrides: Partial> = {}) => ({ + getBlockNumber: vi.fn().mockResolvedValue(6000000), + getCode: vi.fn().mockResolvedValue("0xdeadbeef"), + getBlockWithTransactions: vi.fn().mockResolvedValue({ + timestamp: Math.floor(Date.now() / 1000) - 1000, + transactions: [], + }), + ...overrides, +}); + +let mockProvider = makeProvider(); + +vi.mock("ethers", () => ({ + ethers: { + providers: { + JsonRpcProvider: vi.fn().mockImplementation(function () { + return mockProvider; + }), + }, + utils: { + isAddress: vi.fn((addr: string) => /^0x[a-fA-F0-9]{40}$/.test(addr)), + }, + }, +})); + +const VALID_ADDRESS = "0x9158c22b1799a2527ce8b95f9f1ff5e133fba27d"; + +// ─── Tests ──────────────────────────────────────────────────────────────────── + +describe("RootstockService", () => { + beforeEach(() => { + mockProvider = makeProvider(); + vi.clearAllMocks(); + }); + + // ── constructor / accessors ────────────────────────────────────────────────── + + describe("constructor and accessors", () => { + it("defaults to mainnet when no args supplied", () => { + const svc = new RootstockService(); + expect(svc.getNetwork()).toBe("mainnet"); + }); + + it("uses the supplied network", () => { + const svc = new RootstockService(undefined, "testnet"); + expect(svc.getNetwork()).toBe("testnet"); + }); + + it("infers testnet from an RPC URL containing 'testnet'", () => { + const svc = new RootstockService("https://public-node.testnet.rsk.co"); + expect(svc.getNetwork()).toBe("testnet"); + }); + + it("stores the explicit rpcUrl", () => { + const url = "https://custom.rsk.co"; + const svc = new RootstockService(url); + expect(svc.getRpcUrl()).toBe(url); + }); + + it("falls back to the public mainnet node when no URL given", () => { + const svc = new RootstockService(); + expect(svc.getRpcUrl()).toContain("rsk.co"); + }); + }); + + // ── getMetrics — happy path ────────────────────────────────────────────────── + + describe("getMetrics() — happy path", () => { + it("returns a RootstockMetrics object", async () => { + mockProvider = makeProvider({ + getCode: vi.fn().mockResolvedValue("0xdeadbeef"), + }); + const svc = new RootstockService(); + const metrics = await svc.getMetrics(VALID_ADDRESS); + expect(metrics).toHaveProperty("contractAddress", VALID_ADDRESS); + expect(metrics).toHaveProperty("totalTransactionCount"); + expect(metrics).toHaveProperty("gasUsagePatterns"); + }); + + it("sets deploymentBlock to a number when code exists at tip", async () => { + const svc = new RootstockService(); + const metrics = await svc.getMetrics(VALID_ADDRESS); + expect(typeof metrics.deploymentBlock).toBe("number"); + }); + + it("sets deploymentBlock to null when contract has no code", async () => { + mockProvider = makeProvider({ + getCode: vi.fn().mockResolvedValue("0x"), + }); + const svc = new RootstockService(); + const metrics = await svc.getMetrics(VALID_ADDRESS); + expect(metrics.deploymentBlock).toBeNull(); + }); + + it("counts matching transactions in blocks", async () => { + mockProvider = makeProvider({ + getBlockWithTransactions: vi.fn().mockResolvedValue({ + timestamp: Math.floor(Date.now() / 1000) - 100, + transactions: [ + { to: VALID_ADDRESS, gasLimit: BigInt(21000) }, + { to: "0xother000000000000000000000000000000000000" }, + ], + }), + }); + const svc = new RootstockService(); + const metrics = await svc.getMetrics(VALID_ADDRESS); + expect(metrics.totalTransactionCount).toBeGreaterThanOrEqual(1); + }); + + it("returns lastTransactionTimestamp as ISO string when tx found", async () => { + const ts = Math.floor(Date.now() / 1000) - 50; + mockProvider = makeProvider({ + getBlockWithTransactions: vi.fn().mockResolvedValue({ + timestamp: ts, + transactions: [{ to: VALID_ADDRESS }], + }), + }); + const svc = new RootstockService(); + const metrics = await svc.getMetrics(VALID_ADDRESS); + expect(metrics.lastTransactionTimestamp).toBe( + new Date(ts * 1000).toISOString() + ); + }); + }); + + // ── getMetrics — error paths ───────────────────────────────────────────────── + + describe("getMetrics() — error paths", () => { + it("throws on an invalid contract address", async () => { + const svc = new RootstockService(); + await expect(svc.getMetrics("not-an-address")).rejects.toThrow( + /invalid contract address/i + ); + }); + + it("returns zero metrics when getBlockNumber fails", async () => { + mockProvider = makeProvider({ + getBlockNumber: vi.fn().mockRejectedValue(new Error("RPC down")), + }); + const svc = new RootstockService(); + await expect(svc.getMetrics(VALID_ADDRESS)).rejects.toThrow( + /failed to fetch rootstock metrics/i + ); + }); + + it("returns safe defaults when block fetches fail during tx scan", async () => { + mockProvider = makeProvider({ + getBlockWithTransactions: vi.fn().mockRejectedValue(new Error("timeout")), + }); + const svc = new RootstockService(); + const metrics = await svc.getMetrics(VALID_ADDRESS); + expect(metrics.totalTransactionCount).toBe(0); + expect(metrics.lastTransactionTimestamp).toBeNull(); + expect(metrics.gasUsagePatterns).toEqual({ average: 0, min: 0, max: 0 }); + }); + }); + + // ── network helpers ────────────────────────────────────────────────────────── + + describe("network helpers", () => { + it("uses ROOTSTOCK_MAINNET_RPC_URL env var when set", () => { + process.env.ROOTSTOCK_MAINNET_RPC_URL = "https://env-mainnet.example.com"; + const svc = new RootstockService(undefined, "mainnet"); + expect(svc.getRpcUrl()).toBe("https://env-mainnet.example.com"); + delete process.env.ROOTSTOCK_MAINNET_RPC_URL; + }); + + it("uses ROOTSTOCK_TESTNET_RPC_URL env var when set", () => { + process.env.ROOTSTOCK_TESTNET_RPC_URL = "https://env-testnet.example.com"; + const svc = new RootstockService(undefined, "testnet"); + expect(svc.getRpcUrl()).toBe("https://env-testnet.example.com"); + delete process.env.ROOTSTOCK_TESTNET_RPC_URL; + }); + }); +}); diff --git a/src/__tests__/validation.test.ts b/src/__tests__/validation.test.ts new file mode 100644 index 0000000..93fc92b --- /dev/null +++ b/src/__tests__/validation.test.ts @@ -0,0 +1,141 @@ +import { describe, it, expect } from "vitest"; +import { + validateRepo, + validateContractAddress, +} from "../utils/devmetricsValidation.js"; + +// ─── validateRepo ───────────────────────────────────────────────────────────── + +describe("validateRepo", () => { + describe("valid inputs", () => { + it("accepts a standard owner/repo", () => { + expect(validateRepo("rsksmart/rsk-cli")).toEqual({ valid: true }); + }); + + it("accepts names with hyphens", () => { + expect(validateRepo("my-org/my-repo")).toEqual({ valid: true }); + }); + + it("accepts names with underscores", () => { + expect(validateRepo("my_org/my_repo")).toEqual({ valid: true }); + }); + + it("accepts names with dots", () => { + expect(validateRepo("my.org/my.repo")).toEqual({ valid: true }); + }); + + it("accepts numeric owner or repo name", () => { + expect(validateRepo("org123/repo456")).toEqual({ valid: true }); + }); + + it("accepts mixed alphanumeric with hyphens and dots", () => { + expect(validateRepo("org-name.v2/repo-name.v3")).toEqual({ valid: true }); + }); + }); + + describe("invalid inputs", () => { + it("rejects a bare repo name without owner", () => { + const result = validateRepo("rsk-cli"); + expect(result.valid).toBe(false); + expect(result.error).toBeDefined(); + }); + + it("rejects an empty string", () => { + expect(validateRepo("").valid).toBe(false); + }); + + it("rejects a full GitHub URL", () => { + expect(validateRepo("https://github.com/rsksmart/rsk-cli").valid).toBe(false); + }); + + it("rejects trailing slash", () => { + expect(validateRepo("rsksmart/").valid).toBe(false); + }); + + it("rejects leading slash", () => { + expect(validateRepo("/rsk-cli").valid).toBe(false); + }); + + it("rejects spaces", () => { + expect(validateRepo("rsksmart/ rsk-cli").valid).toBe(false); + }); + + it("returns an error message on failure", () => { + const result = validateRepo("bad-input"); + expect(typeof result.error).toBe("string"); + expect(result.error!.length).toBeGreaterThan(0); + }); + }); +}); + +// ─── validateContractAddress ────────────────────────────────────────────────── + +describe("validateContractAddress", () => { + describe("valid inputs", () => { + it("accepts a lowercase hex address", () => { + expect( + validateContractAddress("0x9158c22b1799a2527ce8b95f9f1ff5e133fba27d") + ).toEqual({ valid: true }); + }); + + it("accepts an uppercase hex address", () => { + expect( + validateContractAddress("0x9158C22B1799A2527CE8B95F9F1FF5E133FBA27D") + ).toEqual({ valid: true }); + }); + + it("accepts a mixed-case hex address", () => { + expect( + validateContractAddress("0xAbCdEf1234567890AbCdEf1234567890AbCdEf12") + ).toEqual({ valid: true }); + }); + + it("accepts the zero address", () => { + expect( + validateContractAddress("0x0000000000000000000000000000000000000000") + ).toEqual({ valid: true }); + }); + }); + + describe("invalid inputs", () => { + it("rejects a missing 0x prefix", () => { + expect( + validateContractAddress("9158c22b1799a2527ce8b95f9f1ff5e133fba27d") + .valid + ).toBe(false); + }); + + it("rejects an address that is too short", () => { + expect(validateContractAddress("0x1234").valid).toBe(false); + }); + + it("rejects an address that is too long", () => { + expect( + validateContractAddress( + "0x9158c22b1799a2527ce8b95f9f1ff5e133fba27d00" + ).valid + ).toBe(false); + }); + + it("rejects non-hex characters", () => { + expect( + validateContractAddress("0xGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG") + .valid + ).toBe(false); + }); + + it("rejects an empty string", () => { + expect(validateContractAddress("").valid).toBe(false); + }); + + it("rejects a plain Ethereum ENS name", () => { + expect(validateContractAddress("vitalik.eth").valid).toBe(false); + }); + + it("returns an error message on failure", () => { + const result = validateContractAddress("notanaddress"); + expect(typeof result.error).toBe("string"); + expect(result.error!.length).toBeGreaterThan(0); + }); + }); +}); diff --git a/src/commands/devmetrics.ts b/src/commands/devmetrics.ts new file mode 100644 index 0000000..bc070f9 --- /dev/null +++ b/src/commands/devmetrics.ts @@ -0,0 +1,331 @@ +import chalk from "chalk"; +import { GitHubService } from "../services/github.service.js"; +import { + RootstockService, + DevMetricsNetwork, +} from "../services/rootstock.service.js"; +import { + validateRepo, + validateContractAddress, +} from "../utils/devmetricsValidation.js"; +import { formatReport } from "../formatters/devmetrics/index.js"; +import { DevMetricsReport, OutputFormat } from "../utils/types.js"; +import { + logError, + logMessage, + logWarning, + logSuccess, +} from "../utils/logger.js"; +import { createSpinner } from "../utils/spinner.js"; + +export interface DevMetricsOptions { + /** GitHub repos in "owner/repo" format — supports multiple values */ + repos: string[]; + /** Rootstock contract addresses — supports multiple values */ + contracts: string[]; + /** Output format: table | json | markdown */ + format?: OutputFormat; + /** CI mode: forces JSON output */ + ci?: boolean; + /** GitHub personal access token (overrides GITHUB_TOKEN env var) */ + githubToken?: string; + /** Rootstock network: mainnet | testnet */ + network?: DevMetricsNetwork; + /** Rootstock RPC URL (overrides --network). Must use http:// or https://. */ + rpcUrl?: string; +} + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +/** + * Validates that --rpc-url is a well-formed URL restricted to http/https. + * Rejects file://, ftp://, javascript:, bare hostnames, etc. + * Prints an error and exits 1 on any violation. + */ +function validateRpcUrl(rpcUrl: string): void { + let parsed: URL; + try { + parsed = new URL(rpcUrl); + } catch { + logError( + false, + `Invalid --rpc-url: "${rpcUrl}" is not a valid URL.` + ); + process.exit(1); + } + if (!["http:", "https:"].includes(parsed.protocol)) { + logError( + false, + `Invalid --rpc-url protocol "${parsed.protocol.replace(":", "")}://". Only http:// and https:// are permitted.` + ); + process.exit(1); + } +} + +/** + * Splits a validated "owner/repo" string into its two parts. + * Although validateRepo() has already confirmed the format upstream, we guard + * here explicitly so the function remains safe in isolation — protecting against + * any future code path that bypasses the pre-validation block. + */ +function splitRepo(repo: string): { owner: string; repoName: string } { + const slashIdx = repo.indexOf("/"); + if (slashIdx === -1 || slashIdx === 0 || slashIdx === repo.length - 1) { + throw new Error( + `Invalid repository format: "${repo}". Expected "owner/repo".` + ); + } + const owner = repo.slice(0, slashIdx); + const repoName = repo.slice(slashIdx + 1); + if (!owner || !repoName) { + throw new Error( + `Repository owner or name must not be empty in: "${repo}".` + ); + } + return { owner, repoName }; +} + +// ─── Command ───────────────────────────────────────────────────────────────── + +export async function devmetricsCommand( + options: DevMetricsOptions +): Promise { + const { repos, contracts, ci, githubToken, rpcUrl } = options; + + // ── 1. Validate --rpc-url protocol (SSRF guard) ────────────────────────── + if (rpcUrl !== undefined) { + validateRpcUrl(rpcUrl); + } + + // ── 2. Resolve and validate network ────────────────────────────────────── + const rawNetwork = options.network?.toLowerCase(); + if (rawNetwork && rawNetwork !== "mainnet" && rawNetwork !== "testnet") { + logError( + false, + `Invalid --network: "${options.network}". Must be "mainnet" or "testnet".` + ); + process.exit(1); + } + const network: DevMetricsNetwork = + (rawNetwork as DevMetricsNetwork) ?? "mainnet"; + + // ── 3. Resolve output format ────────────────────────────────────────────── + const VALID_FORMATS: OutputFormat[] = ["table", "json", "markdown"]; + let outputFormat: OutputFormat = "table"; + if (ci) { + outputFormat = "json"; + } else if (options.format) { + if (VALID_FORMATS.includes(options.format)) { + outputFormat = options.format; + } else { + logError( + false, + `Invalid --format: "${options.format}". Must be one of: ${VALID_FORMATS.join(", ")}.` + ); + process.exit(1); + } + } + + // In non-table modes (json / markdown) all progress/status messages are + // suppressed so that stdout carries only the formatted report data. + // The logger/spinner accept an isExternal-equivalent flag for this purpose. + const silent = outputFormat !== "table"; + + // ── 4. Require at least one repo and contract ───────────────────────────── + if (repos.length === 0) { + logError(false, "At least one repository is required (--repo owner/repo)."); + process.exit(1); + } + if (contracts.length === 0) { + logError( + false, + "At least one contract address is required (--contract 0x...)." + ); + process.exit(1); + } + + // ── 5. Build repo/contract pairs ────────────────────────────────────────── + const pairs: Array<{ repo: string; contract: string }> = []; + + if (repos.length === contracts.length) { + for (let i = 0; i < repos.length; i++) { + pairs.push({ repo: repos[i], contract: contracts[i] }); + } + } else if (contracts.length === 1) { + // Fan-out: one contract applied to all repos + for (const repo of repos) { + pairs.push({ repo, contract: contracts[0] }); + } + } else if (repos.length === 1) { + // Fan-out: one repo applied to all contracts + for (const contract of contracts) { + pairs.push({ repo: repos[0], contract }); + } + } else { + logError( + false, + "Number of --repo and --contract values must match, or one must be a single value." + ); + process.exit(1); + } + + // ── 6. Validate all pairs up-front ──────────────────────────────────────── + const validationErrors: string[] = []; + for (const pair of pairs) { + const repoResult = validateRepo(pair.repo); + if (!repoResult.valid) { + validationErrors.push(`Repository "${pair.repo}": ${repoResult.error}`); + } + const contractResult = validateContractAddress(pair.contract); + if (!contractResult.valid) { + validationErrors.push( + `Contract "${pair.contract}": ${contractResult.error}` + ); + } + } + if (validationErrors.length > 0) { + logError(false, "Validation errors:"); + validationErrors.forEach((err) => logError(false, ` - ${err}`)); + process.exit(1); + } + + // ── 7. Instantiate services ─────────────────────────────────────────────── + const githubService = new GitHubService(githubToken); + const rootstockService = new RootstockService(rpcUrl, network); + + // ── 8. Pre-flight status display (table mode only) ──────────────────────── + logMessage( + silent, + `🌐 Rootstock Network: ${chalk.bold(rootstockService.getNetwork().toUpperCase())}`, + chalk.cyan + ); + logMessage( + silent, + ` RPC: ${rootstockService.getRpcUrl()}\n`, + chalk.gray + ); + + if (!githubService.isAuthenticated()) { + logWarning( + silent, + "⚠️ No GitHub token detected. Using unauthenticated mode (60 requests/hour)." + ); + logWarning( + silent, + " Set GITHUB_TOKEN in your environment or pass --github-token for 5,000/hour.\n" + ); + } + + // Show remaining GitHub rate-limit budget + try { + const rateLimit = await githubService.getRateLimitStatus(); + if (rateLimit) { + const pct = ((rateLimit.remaining / rateLimit.limit) * 100).toFixed(1); + const color = + rateLimit.remaining < rateLimit.limit * 0.1 + ? chalk.red + : rateLimit.remaining < rateLimit.limit * 0.3 + ? chalk.yellow + : chalk.green; + logMessage( + silent, + `📊 GitHub API: ${rateLimit.remaining}/${rateLimit.limit} requests remaining (${pct}%)`, + color + ); + if (rateLimit.remaining < rateLimit.limit * 0.2) { + logWarning( + silent, + ` Rate limit resets at: ${rateLimit.resetAt.toLocaleString()}` + ); + } + } + } catch { + // Non-fatal — proceed without rate-limit info + } + + // ── 9. Fetch metrics per pair ───────────────────────────────────────────── + const reports: DevMetricsReport[] = []; + const errors: Array<{ + pair: { repo: string; contract: string }; + error: string; + }> = []; + + // One spinner instance reused across operations to keep output sequential + const spinner = createSpinner(silent); + + for (const pair of pairs) { + logMessage( + silent, + `\n📊 Fetching metrics for ${chalk.bold(pair.repo)}...`, + chalk.blue + ); + + try { + // Defensive split — safe because validateRepo() passed above, but we + // guard explicitly so this function is safe in isolation. + const { owner, repoName } = splitRepo(pair.repo); + + // GitHub metrics + let githubMetrics; + spinner.start("Fetching GitHub data..."); + try { + githubMetrics = await githubService.getMetrics(owner, repoName); + spinner.succeed("GitHub data fetched"); + } catch (err: any) { + spinner.fail("GitHub data fetch failed"); + throw err; + } + + // Rootstock on-chain metrics + let rootstockMetrics; + spinner.start("Fetching Rootstock data..."); + try { + rootstockMetrics = await rootstockService.getMetrics(pair.contract); + spinner.succeed("Rootstock data fetched"); + } catch (err: any) { + spinner.fail("Rootstock data fetch failed"); + throw err; + } + + reports.push({ + repository: pair.repo, + contractAddress: pair.contract, + github: githubMetrics, + rootstock: rootstockMetrics, + timestamp: new Date().toISOString(), + }); + + logSuccess(silent, `✅ Metrics collected for ${pair.repo}`); + } catch (error: any) { + errors.push({ + pair, + error: error.message ?? "Unknown error", + }); + } + } + + // ── 10. Output reports ──────────────────────────────────────────────────── + if (reports.length > 0) { + // formatReport returns a string; console.log is correct here — it is the + // primary data output, not a status message, so it is always printed. + console.log(formatReport(reports, outputFormat)); + } + + // ── 11. Surface per-pair errors ─────────────────────────────────────────── + if (errors.length > 0) { + if (outputFormat === "json") { + // In JSON/CI mode errors are structured so tools can parse them + console.error(JSON.stringify({ errors }, null, 2)); + } else { + logError(false, "\n❌ Errors encountered:"); + errors.forEach(({ pair, error }) => + logError(false, ` ${pair.repo} / ${pair.contract}: ${error}`) + ); + } + process.exit(1); + } + + if (reports.length === 0) { + process.exit(1); + } +} diff --git a/src/formatters/devmetrics/index.ts b/src/formatters/devmetrics/index.ts new file mode 100644 index 0000000..dfe07af --- /dev/null +++ b/src/formatters/devmetrics/index.ts @@ -0,0 +1,20 @@ +import { DevMetricsReport, OutputFormat } from "../../utils/types.js"; +import { formatAsTable } from "./table.formatter.js"; +import { formatAsJSON } from "./json.formatter.js"; +import { formatAsMarkdown } from "./markdown.formatter.js"; + +export function formatReport( + reports: DevMetricsReport[], + format: OutputFormat +): string { + switch (format) { + case "table": + return formatAsTable(reports); + case "json": + return formatAsJSON(reports); + case "markdown": + return formatAsMarkdown(reports); + default: + return formatAsTable(reports); + } +} diff --git a/src/formatters/devmetrics/json.formatter.ts b/src/formatters/devmetrics/json.formatter.ts new file mode 100644 index 0000000..bef86b9 --- /dev/null +++ b/src/formatters/devmetrics/json.formatter.ts @@ -0,0 +1,5 @@ +import { DevMetricsReport } from "../../utils/types.js"; + +export function formatAsJSON(reports: DevMetricsReport[]): string { + return JSON.stringify(reports, null, 2); +} diff --git a/src/formatters/devmetrics/markdown.formatter.ts b/src/formatters/devmetrics/markdown.formatter.ts new file mode 100644 index 0000000..820a705 --- /dev/null +++ b/src/formatters/devmetrics/markdown.formatter.ts @@ -0,0 +1,69 @@ +import { DevMetricsReport } from "../../utils/types.js"; + +export function formatAsMarkdown(reports: DevMetricsReport[]): string { + const output: string[] = []; + + for (const report of reports) { + output.push(`# Developer Health Report: ${report.repository}`); + output.push(""); + output.push(`**Contract Address:** \`${report.contractAddress}\``); + output.push( + `**Generated:** ${new Date(report.timestamp).toLocaleString()}` + ); + output.push(""); + + output.push("## 📊 GitHub Metrics"); + output.push(""); + output.push("| Metric | Value |"); + output.push("|--------|-------|"); + output.push(`| ⭐ Stars | ${report.github.stars} |`); + output.push( + `| 📝 Last Commit | ${ + report.github.lastCommitDate + ? new Date(report.github.lastCommitDate).toLocaleDateString() + : "N/A" + } |` + ); + output.push(`| 🐛 Open Issues | ${report.github.openIssuesCount} |`); + output.push(`| 🔀 Open PRs | ${report.github.pullRequestsCount} |`); + output.push(`| 👥 Contributors | ${report.github.contributorCount} |`); + output.push(""); + + output.push("## ⛓️ Rootstock Metrics"); + output.push(""); + output.push("| Metric | Value |"); + output.push("|--------|-------|"); + output.push( + `| 📦 Deployment Block | ${report.rootstock.deploymentBlock ?? "N/A"} |` + ); + output.push( + `| 📊 Total Transactions | ${report.rootstock.totalTransactionCount} |` + ); + output.push( + `| ⏰ Last Transaction | ${ + report.rootstock.lastTransactionTimestamp + ? new Date( + report.rootstock.lastTransactionTimestamp + ).toLocaleDateString() + : "N/A" + } |` + ); + output.push( + `| ⛽ Average Gas Usage | ${report.rootstock.gasUsagePatterns.average.toLocaleString()} |` + ); + output.push( + `| ⛽ Min Gas Usage | ${report.rootstock.gasUsagePatterns.min.toLocaleString()} |` + ); + output.push( + `| ⛽ Max Gas Usage | ${report.rootstock.gasUsagePatterns.max.toLocaleString()} |` + ); + output.push(""); + + if (reports.length > 1) { + output.push("---"); + output.push(""); + } + } + + return output.join("\n"); +} diff --git a/src/formatters/devmetrics/table.formatter.ts b/src/formatters/devmetrics/table.formatter.ts new file mode 100644 index 0000000..f277b99 --- /dev/null +++ b/src/formatters/devmetrics/table.formatter.ts @@ -0,0 +1,83 @@ +import Table from "cli-table3"; +import chalk from "chalk"; +import { DevMetricsReport } from "../../utils/types.js"; + +export function formatAsTable(reports: DevMetricsReport[]): string { + const output: string[] = []; + + for (const report of reports) { + output.push(chalk.bold.cyan(`\n📊 Report for ${report.repository}`)); + output.push(chalk.gray(`Contract: ${report.contractAddress}`)); + output.push( + chalk.gray(`Generated: ${new Date(report.timestamp).toLocaleString()}\n`) + ); + + const githubTable = new Table({ + head: [chalk.blue("GitHub Metrics"), chalk.yellow("Value")], + style: { head: [], border: [] }, + }); + + githubTable.push( + ["⭐ Stars", chalk.white(report.github.stars.toString())], + [ + "📝 Last Commit", + report.github.lastCommitDate + ? chalk.green( + new Date(report.github.lastCommitDate).toLocaleDateString() + ) + : chalk.red("N/A"), + ], + ["🐛 Open Issues", chalk.white(report.github.openIssuesCount.toString())], + ["🔀 Open PRs", chalk.white(report.github.pullRequestsCount.toString())], + ["👥 Contributors", chalk.white(report.github.contributorCount.toString())] + ); + + output.push(githubTable.toString()); + + const rootstockTable = new Table({ + head: [chalk.blue("Rootstock Metrics"), chalk.yellow("Value")], + style: { head: [], border: [] }, + }); + + rootstockTable.push( + [ + "📦 Deployment Block", + report.rootstock.deploymentBlock !== null + ? chalk.white(report.rootstock.deploymentBlock.toString()) + : chalk.red("N/A"), + ], + [ + "📊 Total Transactions", + chalk.white(report.rootstock.totalTransactionCount.toString()), + ], + [ + "⏰ Last Transaction", + report.rootstock.lastTransactionTimestamp + ? chalk.green( + new Date( + report.rootstock.lastTransactionTimestamp + ).toLocaleDateString() + ) + : chalk.red("N/A"), + ], + [ + "⛽ Avg Gas Usage", + chalk.white( + report.rootstock.gasUsagePatterns.average.toLocaleString() + ), + ], + [ + "⛽ Min Gas Usage", + chalk.white(report.rootstock.gasUsagePatterns.min.toLocaleString()), + ], + [ + "⛽ Max Gas Usage", + chalk.white(report.rootstock.gasUsagePatterns.max.toLocaleString()), + ] + ); + + output.push("\n" + rootstockTable.toString()); + } + + return output.join("\n"); +} diff --git a/src/services/github.service.ts b/src/services/github.service.ts new file mode 100644 index 0000000..00b5022 --- /dev/null +++ b/src/services/github.service.ts @@ -0,0 +1,234 @@ +import { Octokit } from "@octokit/rest"; +import { GitHubMetrics } from "../utils/types.js"; + +export class GitHubService { + private octokit: Octokit; + private hasToken: boolean; + private tokenInvalid: boolean = false; + private readonly API_TIMEOUT = 15000; + private readonly TOTAL_TIMEOUT = 60000; + + constructor(token?: string) { + const authToken = token || process.env.GITHUB_TOKEN; + this.hasToken = !!authToken; + this.octokit = new Octokit({ + auth: authToken, + request: { timeout: this.API_TIMEOUT }, + }); + } + + // ─── Private helpers ──────────────────────────────────────────────────────── + + /** + * Wraps a promise in a hard timeout so individual API calls never stall + * indefinitely. The outer TOTAL_TIMEOUT in getMetrics() acts as the + * per-request-set budget; this per-call guard prevents any single RPC from + * consuming that entire budget. + */ + private async withTimeout( + promise: Promise, + timeoutMs: number = this.API_TIMEOUT + ): Promise { + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout( + () => + reject( + new Error(`GitHub API call timed out after ${timeoutMs}ms`) + ), + timeoutMs + ) + ), + ]); + } + + /** + * One-way downgrade: marks the token as invalid and re-initialises the + * Octokit instance without auth. Guards against repeated resets. + */ + private resetToUnauthenticated(): void { + if (this.tokenInvalid) return; + this.tokenInvalid = true; + this.hasToken = false; + this.octokit = new Octokit(); + } + + // ─── Public API ───────────────────────────────────────────────────────────── + + isAuthenticated(): boolean { + return this.hasToken; + } + + async getRateLimitStatus(): Promise<{ + remaining: number; + limit: number; + resetAt: Date; + } | null> { + try { + const { data } = await this.withTimeout( + this.octokit.rateLimit.get(), + 5000 + ); + return { + remaining: data.rate.remaining, + limit: data.rate.limit, + resetAt: new Date(data.rate.reset * 1000), + }; + } catch (error: any) { + // Token is invalid — reset once and retry without auth + if (error.status === 401 && this.hasToken && !this.tokenInvalid) { + this.resetToUnauthenticated(); + try { + const { data } = await this.withTimeout( + this.octokit.rateLimit.get(), + 5000 + ); + return { + remaining: data.rate.remaining, + limit: data.rate.limit, + resetAt: new Date(data.rate.reset * 1000), + }; + } catch { + return null; + } + } + return null; + } + } + + /** + * Public entry point. Enforces a single hard TOTAL_TIMEOUT budget over the + * entire fetch sequence. fetchMetrics() is called directly here — never + * recursively — so only one Promise.race timer is ever active per invocation. + */ + async getMetrics(owner: string, repo: string): Promise { + return Promise.race([ + this.fetchMetrics(owner, repo), + new Promise((_, reject) => + setTimeout( + () => + reject( + new Error( + `GitHub metrics fetch timed out after ${this.TOTAL_TIMEOUT}ms` + ) + ), + this.TOTAL_TIMEOUT + ) + ), + ]); + } + + /** + * Performs all GitHub API calls and returns the assembled GitHubMetrics. + * + * On a 401 the instance resets to unauthenticated and retries fetchMetrics() + * directly rather than going back through getMetrics(). This keeps the + * single outer Promise.race timeout intact and prevents nested race timers. + */ + private async fetchMetrics( + owner: string, + repo: string + ): Promise { + try { + const { data: repoData } = await this.withTimeout( + this.octokit.repos.get({ owner, repo }) + ); + + const { data: commits } = await this.withTimeout( + this.octokit.repos.listCommits({ owner, repo, per_page: 1 }) + ); + + // Fetched to keep the API surface consistent; actual count comes from + // repoData.open_issues_count which GitHub always includes. + await this.withTimeout( + this.octokit.issues.listForRepo({ + owner, + repo, + state: "open", + per_page: 1, + }) + ); + + const { data: pullRequests } = await this.withTimeout( + this.octokit.pulls.list({ owner, repo, state: "open", per_page: 1 }) + ); + + const issuesCount = repoData.open_issues_count ?? 0; + + // With a token: use the search API for an accurate open-PR count. + // Without: the per_page:1 list is a sample only (saves rate-limit budget). + let prsCount = 0; + if (this.hasToken) { + try { + const { data: prSearch } = await this.withTimeout( + this.octokit.search.issuesAndPullRequests({ + q: `repo:${owner}/${repo} type:pr state:open`, + per_page: 1, + }) + ); + prsCount = prSearch.total_count ?? 0; + } catch { + prsCount = pullRequests.length; + } + } else { + prsCount = pullRequests.length; + } + + // First-page contributor count (estimate). With a token we request up to + // 100; without, we stay within the unauthenticated rate budget (30). + let contributorsCount = 0; + try { + const perPage = this.hasToken ? 100 : 30; + const { data: contributors } = await this.withTimeout( + this.octokit.repos.listContributors({ + owner, + repo, + per_page: perPage, + }) + ); + contributorsCount = contributors.length; + } catch { + contributorsCount = 0; + } + + return { + stars: repoData.stargazers_count ?? 0, + lastCommitDate: commits[0]?.commit.committer?.date ?? null, + openIssuesCount: issuesCount, + pullRequestsCount: prsCount, + contributorCount: contributorsCount, + repository: `${owner}/${repo}`, + }; + } catch (error: any) { + if (error.status === 401) { + if (this.hasToken && !this.tokenInvalid) { + // Reset auth state and retry fetchMetrics() directly — NOT getMetrics() + // — so we remain within the single Promise.race timeout that the caller + // already set up. tokenInvalid guards against a second 401 retry loop. + this.resetToUnauthenticated(); + return this.fetchMetrics(owner, repo); + } + throw new Error(`GitHub API authentication failed: ${error.message}`); + } + + if (error.status === 404) { + throw new Error(`Repository ${owner}/${repo} not found`); + } + + if (error.status === 403) { + const resetTime = error.response?.headers?.["x-ratelimit-reset"]; + const base = + this.hasToken && !this.tokenInvalid + ? "GitHub API rate limit exceeded. You have a token configured but still hit the limit (5,000/hour)." + : "GitHub API rate limit exceeded. Without a token you are limited to 60 requests/hour. Set GITHUB_TOKEN for 5,000/hour."; + const resetSuffix = resetTime + ? ` Rate limit resets at: ${new Date(parseInt(resetTime) * 1000).toLocaleString()}` + : ""; + throw new Error(`${base}${resetSuffix}`); + } + + throw new Error(`Failed to fetch GitHub metrics: ${error.message}`); + } + } +} diff --git a/src/services/rootstock.service.ts b/src/services/rootstock.service.ts new file mode 100644 index 0000000..6a002d3 --- /dev/null +++ b/src/services/rootstock.service.ts @@ -0,0 +1,394 @@ +import { ethers } from "ethers"; +import { RootstockMetrics } from "../utils/types.js"; + +export type DevMetricsNetwork = "mainnet" | "testnet"; + +export class RootstockService { + // ethers v5: ethers.providers.JsonRpcProvider + private provider: ethers.providers.JsonRpcProvider; + private network: DevMetricsNetwork; + private rpcUrl: string; + private readonly RPC_TIMEOUT = 5000; + private readonly MAX_DEPLOYMENT_SEARCH_BLOCKS = 10000; + private readonly MAX_TRANSACTION_SEARCH_BLOCKS = 2000; + + constructor(rpcUrl?: string, network?: DevMetricsNetwork) { + if (network) { + this.network = network; + } else if (rpcUrl) { + this.network = this.determineNetwork(rpcUrl); + } else { + this.network = "mainnet"; + } + + this.rpcUrl = rpcUrl ?? this.getRpcUrlForNetwork(this.network); + this.provider = new ethers.providers.JsonRpcProvider(this.rpcUrl); + } + + private async withTimeout( + promise: Promise, + timeoutMs: number = this.RPC_TIMEOUT + ): Promise { + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout( + () => reject(new Error(`RPC call timed out after ${timeoutMs}ms`)), + timeoutMs + ) + ), + ]); + } + + getNetwork(): DevMetricsNetwork { + return this.network; + } + + getRpcUrl(): string { + return this.rpcUrl; + } + + private determineNetwork(rpcUrl?: string): DevMetricsNetwork { + return rpcUrl?.includes("testnet") ? "testnet" : "mainnet"; + } + + private getRpcUrlForNetwork(network: DevMetricsNetwork): string { + if (network === "testnet") { + return ( + process.env.ROOTSTOCK_TESTNET_RPC_URL || + "https://public-node.testnet.rsk.co" + ); + } + return ( + process.env.ROOTSTOCK_MAINNET_RPC_URL || "https://public-node.rsk.co" + ); + } + + async getMetrics(contractAddress: string): Promise { + const TOTAL_TIMEOUT = 45000; + return Promise.race([ + this.fetchMetrics(contractAddress), + new Promise((_, reject) => + setTimeout( + () => + reject( + new Error( + `Rootstock metrics fetch timed out after ${TOTAL_TIMEOUT}ms` + ) + ), + TOTAL_TIMEOUT + ) + ), + ]); + } + + private async fetchMetrics( + contractAddress: string + ): Promise { + try { + // ethers v5: ethers.utils.isAddress + if (!ethers.utils.isAddress(contractAddress)) { + throw new Error(`Invalid contract address: ${contractAddress}`); + } + + const currentBlock = await this.withTimeout( + this.provider.getBlockNumber(), + 5000 + ); + const deploymentBlock = await this.getDeploymentBlock(contractAddress); + + if (deploymentBlock !== null) { + const [transactionCount, lastTransaction, gasUsagePatterns] = + await Promise.allSettled([ + this.getTransactionCount(contractAddress, deploymentBlock), + this.getLastTransaction(contractAddress, deploymentBlock), + this.getGasUsagePatterns(contractAddress, deploymentBlock), + ]); + + // Suppress unused variable warning — currentBlock is fetched to confirm RPC is live + void currentBlock; + + return { + contractAddress, + deploymentBlock, + totalTransactionCount: + transactionCount.status === "fulfilled" + ? transactionCount.value + : 0, + lastTransactionTimestamp: + lastTransaction.status === "fulfilled" + ? lastTransaction.value + : null, + gasUsagePatterns: + gasUsagePatterns.status === "fulfilled" + ? gasUsagePatterns.value + : { average: 0, min: 0, max: 0 }, + }; + } + + return { + contractAddress, + deploymentBlock: null, + totalTransactionCount: 0, + lastTransactionTimestamp: null, + gasUsagePatterns: { average: 0, min: 0, max: 0 }, + }; + } catch (error: any) { + throw new Error(`Failed to fetch Rootstock metrics: ${error.message}`); + } + } + + private async getDeploymentBlock( + contractAddress: string + ): Promise { + try { + const currentBlock = await this.withTimeout( + this.provider.getBlockNumber(), + 5000 + ); + const searchStartBlock = Math.max( + 0, + currentBlock - this.MAX_DEPLOYMENT_SEARCH_BLOCKS + ); + const currentCode = await this.withTimeout( + this.provider.getCode(contractAddress, currentBlock), + 5000 + ); + + if (!currentCode || currentCode === "0x") { + const chunkSize = 2000; + const maxChecks = 10; + + for (let i = 0; i < maxChecks; i++) { + const block = currentBlock - i * chunkSize; + if (block < searchStartBlock) break; + + try { + const code = await this.withTimeout( + this.provider.getCode(contractAddress, block), + 3000 + ); + if (code && code !== "0x") { + return block; + } + } catch { + continue; + } + } + return null; + } + + return await this.binarySearchDeployment( + contractAddress, + searchStartBlock, + currentBlock + ); + } catch { + return null; + } + } + + private async binarySearchDeployment( + contractAddress: string, + low: number, + high: number + ): Promise { + const maxIterations = 10; + let iterations = 0; + + while (low < high && iterations < maxIterations) { + iterations++; + const mid = Math.floor((low + high) / 2); + + try { + const code = await this.withTimeout( + this.provider.getCode(contractAddress, mid), + 3000 + ); + + if (code && code !== "0x") { + high = mid; + } else { + low = mid + 1; + } + } catch { + return low; + } + } + return low; + } + + private async getTransactionCount( + contractAddress: string, + deploymentBlock: number | null + ): Promise { + try { + if (deploymentBlock === null) return 0; + + const currentBlock = await this.withTimeout( + this.provider.getBlockNumber(), + 5000 + ); + const searchEndBlock = Math.min( + currentBlock, + deploymentBlock + this.MAX_TRANSACTION_SEARCH_BLOCKS + ); + const sampleSize = 20; + const step = Math.max( + 1, + Math.floor((searchEndBlock - deploymentBlock) / sampleSize) + ); + let count = 0; + let samplesChecked = 0; + + for ( + let block = deploymentBlock; + block <= searchEndBlock && samplesChecked < sampleSize; + block += step + ) { + try { + // ethers v5: getBlockWithTransactions returns full tx objects + const blockData = await this.withTimeout( + this.provider.getBlockWithTransactions(block), + 3000 + ); + if (blockData?.transactions) { + const txCount = blockData.transactions.filter( + (tx) => tx.to?.toLowerCase() === contractAddress.toLowerCase() + ).length; + count += txCount; + } + samplesChecked++; + } catch { + continue; + } + } + + if (samplesChecked > 0 && step > 1) { + const avgPerBlock = count / samplesChecked; + const totalBlocks = searchEndBlock - deploymentBlock + 1; + return Math.round(avgPerBlock * totalBlocks); + } + + return count; + } catch { + return 0; + } + } + + private async getLastTransaction( + contractAddress: string, + deploymentBlock: number | null + ): Promise { + try { + if (deploymentBlock === null) return null; + + const currentBlock = await this.withTimeout( + this.provider.getBlockNumber(), + 5000 + ); + const searchStep = 100; + const maxBlocksToCheck = 500; + const searchStartBlock = Math.max( + deploymentBlock, + currentBlock - maxBlocksToCheck + ); + const maxChecks = 20; + + for (let i = 0; i < maxChecks; i++) { + const block = currentBlock - i * searchStep; + if (block < searchStartBlock) break; + + try { + const blockData = await this.withTimeout( + this.provider.getBlockWithTransactions(block), + 3000 + ); + if (blockData?.transactions) { + const relevantTx = blockData.transactions.find( + (tx) => tx.to?.toLowerCase() === contractAddress.toLowerCase() + ); + if (relevantTx) { + return new Date(blockData.timestamp * 1000).toISOString(); + } + } + } catch { + continue; + } + } + + return null; + } catch { + return null; + } + } + + private async getGasUsagePatterns( + contractAddress: string, + deploymentBlock: number | null + ): Promise<{ average: number; min: number; max: number }> { + try { + if (deploymentBlock === null) { + return { average: 0, min: 0, max: 0 }; + } + + const currentBlock = await this.withTimeout( + this.provider.getBlockNumber(), + 5000 + ); + const sampleSize = 20; + const gasUsages: number[] = []; + const searchStep = 50; + const maxBlocksToCheck = 200; + const maxChecks = 10; + const searchStartBlock = Math.max( + deploymentBlock, + currentBlock - maxBlocksToCheck + ); + + for ( + let i = 0; + i < maxChecks && gasUsages.length < sampleSize; + i++ + ) { + const block = currentBlock - i * searchStep; + if (block < searchStartBlock) break; + + try { + const blockData = await this.withTimeout( + this.provider.getBlockWithTransactions(block), + 3000 + ); + if (blockData?.transactions) { + const relevantTxs = blockData.transactions.filter( + (tx) => tx.to?.toLowerCase() === contractAddress.toLowerCase() + ); + for (const tx of relevantTxs) { + if (gasUsages.length >= sampleSize) break; + // gasUsed is a receipt field; gasLimit is available on the tx object + const gasValue = (tx as any).gasUsed ?? tx.gasLimit; + if (gasValue) { + gasUsages.push(Number(gasValue)); + } + } + } + } catch { + continue; + } + } + + if (gasUsages.length === 0) { + return { average: 0, min: 0, max: 0 }; + } + + const sum = gasUsages.reduce((a, b) => a + b, 0); + const average = Math.round(sum / gasUsages.length); + const min = Math.min(...gasUsages); + const max = Math.max(...gasUsages); + + return { average, min, max }; + } catch { + return { average: 0, min: 0, max: 0 }; + } + } +} diff --git a/src/utils/devmetricsValidation.ts b/src/utils/devmetricsValidation.ts new file mode 100644 index 0000000..2219984 --- /dev/null +++ b/src/utils/devmetricsValidation.ts @@ -0,0 +1,29 @@ +// GitHub repo format: owner/repo +const REPO_REGEX = /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/; + +// Ethereum/Rootstock address: 0x followed by 40 hex chars +const CONTRACT_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/; + +export function validateRepo(repo: string): { valid: boolean; error?: string } { + if (!REPO_REGEX.test(repo)) { + return { + valid: false, + error: "Repository must be in format: owner/repo", + }; + } + return { valid: true }; +} + +export function validateContractAddress(address: string): { + valid: boolean; + error?: string; +} { + if (!CONTRACT_ADDRESS_REGEX.test(address)) { + return { + valid: false, + error: + "Contract address must be a valid Ethereum/Rootstock address (0x followed by 40 hex characters)", + }; + } + return { valid: true }; +} diff --git a/src/utils/types.ts b/src/utils/types.ts index 0681d9c..83e6fdb 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -144,4 +144,37 @@ export type AttestationData = { attestations?: any[]; count?: number; revocable?: boolean; -}; \ No newline at end of file +}; + +// ─── DevMetrics types ──────────────────────────────────────────────────────── + +export interface GitHubMetrics { + stars: number; + lastCommitDate: string | null; + openIssuesCount: number; + pullRequestsCount: number; + contributorCount: number; + repository: string; +} + +export interface RootstockMetrics { + contractAddress: string; + deploymentBlock: number | null; + totalTransactionCount: number; + lastTransactionTimestamp: string | null; + gasUsagePatterns: { + average: number; + min: number; + max: number; + }; +} + +export interface DevMetricsReport { + repository: string; + contractAddress: string; + github: GitHubMetrics; + rootstock: RootstockMetrics; + timestamp: string; +} + +export type OutputFormat = "table" | "json" | "markdown"; \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..6c92077 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/__tests__/**/*.test.ts"], + globals: false, + }, +});