mpp32-mcp-server 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,73 @@ All notable changes to `mpp32-mcp-server` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.7.0] - 2026-06-07
8
+
9
+ ### Changed
10
+
11
+ * **Supply-chain cleanup.** Dropped three direct dependencies that were
12
+ driving Socket.dev alerts on the published package:
13
+ * `tweetnacl` (unmaintained since 2020): the 32-byte-seed → 64-byte
14
+ keypair derivation now uses
15
+ `@solana/kit`'s `createKeyPairSignerFromPrivateKeyBytes` directly
16
+ via WebCrypto Ed25519.
17
+ * `cheerio` (and its 21 transitives, including the deprecated
18
+ `whatwg-encoding@3.1.1`): `get_pivx_dao_intelligence` now calls the
19
+ MPP32 backend's `/api/governance` endpoint instead of scraping
20
+ `pivx.org` client-side. Same payload; the backend has served it since
21
+ 1.4.0.
22
+ * `bs58` + `base-x`: replaced with the already-in-tree `@scure/base`
23
+ `base58` codec.
24
+ Net effect: published install drops from 181 to ~155 transitive
25
+ packages, zero deprecated, zero unmaintained-since-2020.
26
+ * **All direct dependencies pinned to exact versions** (no `^` ranges).
27
+ * **`engines.node` raised to `>=20.10.0`** (Node 18 reached end-of-life
28
+ in April 2025; WebCrypto Ed25519 is native on 20.10+).
29
+
30
+ ### Added
31
+
32
+ * **npm provenance.** Releases now ship with an [npm provenance
33
+ attestation](https://docs.npmjs.com/generating-provenance-statements)
34
+ linking the tarball to the exact GitHub Actions run that built it.
35
+ Verify with `npm audit signatures`.
36
+ * **`SECURITY.md` included in the published tarball**, describing
37
+ runtime behavior, the env vars the server reads, and the egress
38
+ allow-list.
39
+ * **`docs/EGRESS.md`** (repo) enumerates every outbound host the MCP
40
+ server reaches and why.
41
+ * **`socket.yml`** (repo) documents the small set of behavior alerts we
42
+ consciously accept (`envVars`, `networkAccess`, `hasBin`) with links
43
+ to where each behavior is implemented.
44
+
45
+ ### Removed
46
+
47
+ * `pivx-provider.ts` (the cheerio-based client-side scraper). Replaced
48
+ by a 25-line wrapper around `${MPP32_API_URL}/api/governance`.
49
+
50
+ ### Security
51
+
52
+ * No code-level vulnerability fixed; this release exists to eliminate
53
+ supply-chain-risk surface area on the published package.
54
+
55
+ ## [1.6.0] - 2026-06-02
56
+
57
+ ### Added
58
+
59
+ * **Auto Sign In With Solana (SIWS) at startup.** When both `MPP32_AGENT_KEY`
60
+ and `MPP32_SOLANA_PRIVATE_KEY` are configured, the MCP server proves wallet
61
+ ownership to the MPP32 backend on the first run and activates M32 holder
62
+ pricing on every subsequent paid query for the rest of the process lifetime.
63
+ The signed message is the canonical SIWS structure (single use nonce, five
64
+ minute expiry, domain bound to mpp32.org). Holders pay $0.0048 per
65
+ Intelligence Oracle query at the 1M tier and $0.0064 at the 250K tier with
66
+ zero extra setup beyond the Solana key already used for x402 payments.
67
+
68
+ ### Changed
69
+
70
+ * **`get_mpp32_diagnostics` output adds an "M32 holder pricing" capability
71
+ row** showing the verified wallet, the tier, and the active discount once
72
+ SIWS has run.
73
+
7
74
  ## [1.5.0] - 2026-06-01
8
75
 
9
76
  ### Added
package/SECURITY.md ADDED
@@ -0,0 +1,77 @@
1
+ # Security Policy — `mpp32-mcp-server`
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Report security issues privately to **`security@mpp32.org`**.
6
+
7
+ Include:
8
+
9
+ - A description of the issue, including the affected version of
10
+ `mpp32-mcp-server`.
11
+ - Steps to reproduce, or a minimal proof of concept.
12
+ - The Node.js version, OS, and MCP host (Claude Desktop, Cursor,
13
+ Windsurf, …) you tested against.
14
+ - Your name or handle if you'd like credit in the fix release.
15
+
16
+ We aim to acknowledge reports within **3 business days** and provide a
17
+ remediation timeline within **10 business days**. Please do not file public
18
+ GitHub issues for security vulnerabilities until a fix has shipped.
19
+
20
+ ## Supported versions
21
+
22
+ | Version | Supported |
23
+ |:---------|:-----------|
24
+ | `1.7.x` | ✅ current |
25
+ | `1.6.x` | High-severity only |
26
+ | `< 1.6` | ❌ |
27
+
28
+ ## What this package does at runtime
29
+
30
+ Understanding the package's behavior is the first step in any audit. The
31
+ MCP server is a stdio process. It:
32
+
33
+ 1. **Reads three environment variables** for authentication and signing:
34
+ `MPP32_AGENT_KEY`, `MPP32_PRIVATE_KEY` (EVM), and
35
+ `MPP32_SOLANA_PRIVATE_KEY`. Values are never logged, transmitted as
36
+ plaintext to third parties, or written to disk. See
37
+ `src/index.ts` for the exact validation rules.
38
+ 2. **Makes outbound HTTPS requests** to a small, fixed set of hosts
39
+ needed to discover services and settle x402 payments. The complete
40
+ egress allow-list is documented in [`docs/EGRESS.md`](../docs/EGRESS.md).
41
+ 3. **Signs Solana and EVM payment payloads locally** using
42
+ `@solana/kit` (WebCrypto Ed25519) and `viem` (secp256k1) inside the
43
+ user's Node process. Private keys never leave the machine.
44
+
45
+ There is no install script, no native code, no filesystem write, no
46
+ shell execution, and no dynamic `require`/`eval` in this package.
47
+
48
+ ## Provenance
49
+
50
+ Starting with `1.7.0`, npm releases are published from a GitHub Actions
51
+ workflow using OIDC and include an [npm provenance attestation][prov]
52
+ linking the published tarball to the exact commit and workflow run that
53
+ produced it. Verify with:
54
+
55
+ ```sh
56
+ npm audit signatures
57
+ ```
58
+
59
+ [prov]: https://docs.npmjs.com/generating-provenance-statements
60
+
61
+ ## Hardening already in place (backend)
62
+
63
+ The MCP server depends on a backend at `mpp32.org` for the federated
64
+ catalog and `/api/agent/execute`. Backend-side hardening is documented
65
+ in the [root SECURITY.md](../SECURITY.md):
66
+
67
+ - Production refuses to boot when `MPP_SECRET_KEY` is missing or matches
68
+ a committed default.
69
+ - All outbound URLs from user submissions and agent execute calls run
70
+ through an SSRF guard.
71
+ - Agent session API keys are hashed at rest with SHA-256.
72
+
73
+ ## Disclosure
74
+
75
+ We follow coordinated disclosure. Reporters who act in good faith and
76
+ follow this policy will not be subject to legal action for their
77
+ research.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
5
  import { signX402Payment } from "./x402-signers.js";
6
- const SERVER_VERSION = "1.5.0";
6
+ const SERVER_VERSION = "1.7.0";
7
7
  // ── Env loading: trim and sanitize aggressively ─────────────────────────────
8
8
  // Copy-paste from Claude Desktop / Cursor / Windsurf JSON config UIs frequently
9
9
  // adds trailing \n, \r, NBSP, BOM, or wraps the value in literal quotes. Any
@@ -251,6 +251,7 @@ server.tool("get_mpp32_diagnostics", "Report what the mpp32-mcp-server detected
251
251
  `- Federated service execution: ${AGENT_KEY ? "yes" : "no — set MPP32_AGENT_KEY"}`,
252
252
  `- x402 (USDC on Solana) payment: ${SOLANA_PRIVATE_KEY ? "yes" : "no — set MPP32_SOLANA_PRIVATE_KEY"}`,
253
253
  `- x402 (USDC on Base/EVM) payment: ${PRIVATE_KEY ? "yes" : "no — set MPP32_PRIVATE_KEY"}`,
254
+ `- M32 holder pricing (SIWS verified): ${siwsVerifiedAddress ? `yes — ${siwsTier} tier, ${siwsDiscountPercent}% off every paid query` : (AGENT_KEY && SOLANA_PRIVATE_KEY ? "pending — auto verification runs once at startup" : "no — set MPP32_AGENT_KEY + MPP32_SOLANA_PRIVATE_KEY")}`,
254
255
  ``,
255
256
  `**Ready to pay end-to-end:** ${readyToPay ? "YES — try `get_solana_token_intelligence` with token=\"M32\" to confirm." : "NO — see the missing items above. Meanwhile, you can still call `try_solana_token_intelligence_free` (10/min/IP, no keys required) to evaluate the oracle."}`,
256
257
  ``,
@@ -631,11 +632,25 @@ server.tool("scan_portfolio_m32", "M32-gated full wallet portfolio scan. Discove
631
632
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
632
633
  }
633
634
  });
634
- // ── Tool 7: get_pivx_dao_intelligence ─────────────────────────────────────
635
- // Self-contained: scrapes pivx.org/proposals + Chainz CryptoID directly.
636
- // Does NOT depend on any backend endpoint — works for every npm user out of the box.
637
- import { fetchPivxGovernance } from "./pivx-provider.js";
638
- server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance intelligence. Returns active budget proposals with masternode voting tallies (Yes/No counts, net yes percentages), budget allocation status, network deflation metrics (unallocated treasury PIV that are never minted), and masternode network health. PIVX is a fully community-governed cryptocurrency where Masternode owners vote on budget proposals every ~30 days (43,200 blocks per superblock cycle, 432,000 PIV max monthly budget). Data scraped live from pivx.org/proposals and the PIVX blockchain via Chainz CryptoID. Cached for 5 minutes. Free — no payment or API key required.", {
635
+ async function fetchPivxGovernance() {
636
+ const res = await fetchWithTimeout(`${API_URL}/api/governance`, {
637
+ timeoutMs: 15_000,
638
+ headers: { Accept: "application/json" },
639
+ });
640
+ if (!res.ok) {
641
+ throw new Error(`MPP32 governance endpoint returned HTTP ${res.status}`);
642
+ }
643
+ const { data } = (await res.json());
644
+ return {
645
+ proposals: data.proposals,
646
+ network: data.network,
647
+ deflation: data.deflation,
648
+ timestamp: data.meta.timestamp,
649
+ source: data.meta.source,
650
+ cacheHit: data.meta.cacheHit,
651
+ };
652
+ }
653
+ server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance intelligence. Returns active budget proposals with masternode voting tallies (Yes/No counts, net yes percentages), budget allocation status, network deflation metrics (unallocated treasury PIV that are never minted), and masternode network health. PIVX is a fully community-governed cryptocurrency where Masternode owners vote on budget proposals every ~30 days (43,200 blocks per superblock cycle, 432,000 PIV max monthly budget). Data is served by the MPP32 backend, which aggregates pivx.org/proposals and the PIVX blockchain via Chainz CryptoID, and caches for 5 minutes. Free — no payment or API key required.", {
639
654
  filter: z
640
655
  .enum(["all", "passing", "failing"])
641
656
  .default("all")
@@ -712,7 +727,7 @@ server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance inte
712
727
  return {
713
728
  content: [{
714
729
  type: "text",
715
- text: `Failed to fetch PIVX governance data: ${err instanceof Error ? err.message : String(err)}. The tool scrapes pivx.org/proposals directly — the site may be temporarily unreachable.`,
730
+ text: `Failed to fetch PIVX governance data: ${err instanceof Error ? err.message : String(err)}. The tool calls ${API_URL}/api/governance — the MPP32 backend or its upstream sources (pivx.org, chainz.cryptoid.info) may be temporarily unreachable.`,
716
731
  }],
717
732
  };
718
733
  }
@@ -1521,6 +1536,95 @@ async function completeX402Payment(paymentRequiredHeader, keys) {
1521
1536
  protocolUsed: result.protocolUsed,
1522
1537
  };
1523
1538
  }
1539
+ // ── Auto SIWS bootstrap ─────────────────────────────────────────────────────
1540
+ // When both MPP32_AGENT_KEY and MPP32_SOLANA_PRIVATE_KEY are configured the
1541
+ // MCP server proves wallet ownership to the MPP32 backend at startup, which
1542
+ // activates M32 holder pricing on every subsequent paid query for the rest of
1543
+ // the process lifetime. No user action required.
1544
+ let siwsVerifiedAddress = null;
1545
+ let siwsTier = null;
1546
+ let siwsDiscountPercent = 0;
1547
+ async function tryAutoSiws() {
1548
+ if (!AGENT_KEY || !SOLANA_PRIVATE_KEY)
1549
+ return;
1550
+ try {
1551
+ // Lazy import to keep startup fast when only catalog browsing is needed.
1552
+ const [kitMod, scureBase] = await Promise.all([
1553
+ import("@solana/kit"),
1554
+ import("@scure/base"),
1555
+ ]);
1556
+ const { base58 } = scureBase;
1557
+ const { createKeyPairFromBytes, createKeyPairFromPrivateKeyBytes, getAddressFromPublicKey, signBytes, } = kitMod;
1558
+ // Decode the private key. Supports JSON byte array, hex, and base58.
1559
+ let bytes;
1560
+ if (SOLANA_PRIVATE_KEY.startsWith("[")) {
1561
+ bytes = new Uint8Array(JSON.parse(SOLANA_PRIVATE_KEY));
1562
+ }
1563
+ else if (/^[0-9a-fA-F]+$/.test(SOLANA_PRIVATE_KEY) && SOLANA_PRIVATE_KEY.length % 2 === 0) {
1564
+ bytes = new Uint8Array(Buffer.from(SOLANA_PRIVATE_KEY, "hex"));
1565
+ }
1566
+ else {
1567
+ bytes = base58.decode(SOLANA_PRIVATE_KEY);
1568
+ }
1569
+ let keyPair;
1570
+ if (bytes.length === 32) {
1571
+ keyPair = await createKeyPairFromPrivateKeyBytes(bytes);
1572
+ }
1573
+ else if (bytes.length === 64) {
1574
+ keyPair = await createKeyPairFromBytes(bytes);
1575
+ }
1576
+ else {
1577
+ console.error(`[mpp32] SIWS skipped: Solana key has unexpected length ${bytes.length}`);
1578
+ return;
1579
+ }
1580
+ const walletAddress = await getAddressFromPublicKey(keyPair.publicKey);
1581
+ // Step 1: request a nonce bound to our existing agent session.
1582
+ const nonceRes = await fetchWithTimeout(`${API_URL}/api/auth/siws/nonce`, {
1583
+ method: "POST",
1584
+ headers: { "Content-Type": "application/json" },
1585
+ body: JSON.stringify({ wallet: walletAddress, agentKey: AGENT_KEY }),
1586
+ timeoutMs: 8_000,
1587
+ });
1588
+ if (!nonceRes.ok) {
1589
+ const text = await nonceRes.text().catch(() => "");
1590
+ console.error(`[mpp32] SIWS nonce request failed: HTTP ${nonceRes.status} ${text.slice(0, 200)}`);
1591
+ return;
1592
+ }
1593
+ const nonceBody = (await nonceRes.json());
1594
+ const message = nonceBody.data?.message;
1595
+ if (!message) {
1596
+ console.error("[mpp32] SIWS nonce response missing message");
1597
+ return;
1598
+ }
1599
+ // Step 2: sign the canonical message bytes via WebCrypto Ed25519.
1600
+ const signatureBytes = await signBytes(keyPair.privateKey, new TextEncoder().encode(message));
1601
+ const signature = base58.encode(signatureBytes);
1602
+ // Step 3: verify with the backend. Backend marks session walletVerified=true.
1603
+ const verifyRes = await fetchWithTimeout(`${API_URL}/api/auth/siws/verify`, {
1604
+ method: "POST",
1605
+ headers: { "Content-Type": "application/json" },
1606
+ body: JSON.stringify({ wallet: walletAddress, signature, agentKey: AGENT_KEY }),
1607
+ timeoutMs: 8_000,
1608
+ });
1609
+ if (!verifyRes.ok) {
1610
+ const text = await verifyRes.text().catch(() => "");
1611
+ console.error(`[mpp32] SIWS verify failed: HTTP ${verifyRes.status} ${text.slice(0, 200)}`);
1612
+ return;
1613
+ }
1614
+ const verifyBody = (await verifyRes.json());
1615
+ siwsVerifiedAddress = verifyBody.data?.walletAddress ?? walletAddress;
1616
+ siwsTier = verifyBody.data?.tier ?? "none";
1617
+ siwsDiscountPercent = verifyBody.data?.discountPercent ?? 0;
1618
+ const tierLabel = siwsDiscountPercent > 0
1619
+ ? `${siwsTier} tier (${siwsDiscountPercent}% off every paid query)`
1620
+ : "no holder tier (wallet holds zero M32, verification still active)";
1621
+ const shortAddr = `${siwsVerifiedAddress.slice(0, 6)}…${siwsVerifiedAddress.slice(-4)}`;
1622
+ console.error(`[mpp32] SIWS verified for ${shortAddr}: ${tierLabel}`);
1623
+ }
1624
+ catch (err) {
1625
+ console.error(`[mpp32] SIWS bootstrap error: ${err instanceof Error ? err.message : String(err)}`);
1626
+ }
1627
+ }
1524
1628
  // ── Start ───────────────────────────────────────────────────────────────────
1525
1629
  async function main() {
1526
1630
  const transport = new StdioServerTransport();
@@ -1533,6 +1637,9 @@ async function main() {
1533
1637
  .filter(Boolean)
1534
1638
  .join(", ") || "no keys (catalog-only legacy mode)";
1535
1639
  console.error(`[mpp32] MCP server v${SERVER_VERSION} on stdio. API ${API_URL}. Configured: ${features}. Timeout ${TIMEOUT_MS}ms.`);
1640
+ // Auto SIWS in the background. Does not block startup — if it fails the user
1641
+ // simply pays the standard rate instead of the holder rate.
1642
+ void tryAutoSiws();
1536
1643
  // Per-variable status so a user staring at this log can immediately see
1537
1644
  // whether their env vars made it through. Values are fingerprinted.
1538
1645
  const fp = (v) => !v ? "NOT SET" : v.length <= 12 ? `SET (${v.length}c)` : `SET (${v.slice(0, 6)}…${v.slice(-4)}, ${v.length}c)`;
@@ -18,12 +18,11 @@
18
18
  // { x402Version: 1, scheme: "exact", network, payload: <scheme payload> }
19
19
  // base64-encoded into the `X-Payment` HTTP header.
20
20
  async function loadSvmDeps() {
21
- const [kit, tokenProgram, computeBudgetProgram, bs58Mod, naclMod] = await Promise.all([
21
+ const [kit, tokenProgram, computeBudgetProgram, scureBase] = await Promise.all([
22
22
  import("@solana/kit"),
23
23
  import("@solana-program/token"),
24
24
  import("@solana-program/compute-budget"),
25
- import("bs58"),
26
- import("tweetnacl"),
25
+ import("@scure/base"),
27
26
  ]).catch((err) => {
28
27
  const msg = err instanceof Error ? err.message : String(err);
29
28
  throw new Error(`Could not load Solana signing libraries: ${msg}. ` +
@@ -33,6 +32,7 @@ async function loadSvmDeps() {
33
32
  return {
34
33
  address: kit.address,
35
34
  createKeyPairSignerFromBytes: kit.createKeyPairSignerFromBytes,
35
+ createKeyPairSignerFromPrivateKeyBytes: kit.createKeyPairSignerFromPrivateKeyBytes,
36
36
  createSolanaRpc: kit.createSolanaRpc,
37
37
  createTransactionMessage: kit.createTransactionMessage,
38
38
  setTransactionMessageFeePayer: kit.setTransactionMessageFeePayer,
@@ -46,8 +46,7 @@ async function loadSvmDeps() {
46
46
  TOKEN_PROGRAM_ADDRESS: tokenProgram.TOKEN_PROGRAM_ADDRESS,
47
47
  getSetComputeUnitLimitInstruction: computeBudgetProgram.getSetComputeUnitLimitInstruction,
48
48
  getSetComputeUnitPriceInstruction: computeBudgetProgram.getSetComputeUnitPriceInstruction,
49
- bs58: bs58Mod.default ?? bs58Mod,
50
- nacl: naclMod.default ?? naclMod,
49
+ base58: scureBase.base58,
51
50
  };
52
51
  }
53
52
  async function loadEvmDeps() {
@@ -94,20 +93,19 @@ function decodeSolanaSecret(raw, deps) {
94
93
  if (/^[0-9a-fA-F]+$/.test(raw) && raw.length % 2 === 0) {
95
94
  return new Uint8Array(Buffer.from(raw, "hex"));
96
95
  }
97
- return deps.bs58.decode(raw);
96
+ return deps.base58.decode(raw);
98
97
  }
99
98
  async function buildSolanaSigner(rawKey, deps) {
100
- let bytes = decodeSolanaSecret(rawKey, deps);
99
+ const bytes = decodeSolanaSecret(rawKey, deps);
101
100
  if (bytes.length === 32) {
102
- // 32-byte seed — kit's createKeyPairSignerFromBytes wants the 64-byte
103
- // expanded key. Derive via tweetnacl.
104
- const kp = deps.nacl.sign.keyPair.fromSeed(bytes);
105
- bytes = kp.secretKey;
101
+ // 32-byte seed — kit derives the public key via WebCrypto Ed25519.
102
+ return await deps.createKeyPairSignerFromPrivateKeyBytes(bytes);
106
103
  }
107
- else if (bytes.length !== 64) {
108
- throw new Error(`Solana private key must be a 32-byte seed or a 64-byte expanded key; got ${bytes.length} bytes.`);
104
+ if (bytes.length === 64) {
105
+ // 64-byte expanded key (seed || publicKey) kit's standard path.
106
+ return await deps.createKeyPairSignerFromBytes(bytes);
109
107
  }
110
- return await deps.createKeyPairSignerFromBytes(bytes);
108
+ throw new Error(`Solana private key must be a 32-byte seed or a 64-byte expanded key; got ${bytes.length} bytes.`);
111
109
  }
112
110
  // ── SVM signer ──────────────────────────────────────────────────────────────
113
111
  const DEFAULT_SOLANA_RPC = "https://api.mainnet-beta.solana.com";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mpp32-mcp-server",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "mcpName": "io.github.MPP32/mpp32-mcp-server",
5
5
  "description": "Payment layer for AI agents. One MCP, five protocols, thousands of paid APIs your agent can call.",
6
6
  "type": "module",
@@ -20,7 +20,8 @@
20
20
  "dist/**/*.d.ts",
21
21
  "README.md",
22
22
  "CHANGELOG.md",
23
- "LICENSE"
23
+ "LICENSE",
24
+ "SECURITY.md"
24
25
  ],
25
26
  "sideEffects": false,
26
27
  "scripts": {
@@ -67,19 +68,21 @@
67
68
  "bugs": {
68
69
  "url": "https://github.com/MPP32/MPP32/issues"
69
70
  },
71
+ "funding": "https://mpp32.org",
70
72
  "engines": {
71
- "node": ">=18.0.0"
73
+ "node": ">=20.10.0"
74
+ },
75
+ "publishConfig": {
76
+ "access": "public"
72
77
  },
73
78
  "dependencies": {
74
- "@modelcontextprotocol/sdk": "^1.12.0",
75
- "@solana-program/compute-budget": "^0.15.0",
76
- "@solana-program/token": "^0.13.0",
77
- "@solana/kit": "^6.9.0",
78
- "bs58": "^6.0.0",
79
- "cheerio": "^1.2.0",
80
- "tweetnacl": "^1.0.3",
81
- "viem": "^2.48.11",
82
- "zod": "^3.23.0"
79
+ "@modelcontextprotocol/sdk": "1.29.0",
80
+ "@scure/base": "1.2.6",
81
+ "@solana-program/compute-budget": "0.15.0",
82
+ "@solana-program/token": "0.13.0",
83
+ "@solana/kit": "6.9.0",
84
+ "viem": "2.52.2",
85
+ "zod": "3.25.76"
83
86
  },
84
87
  "peerDependencies": {
85
88
  "mppx": ">=0.4.0"
@@ -90,7 +93,7 @@
90
93
  }
91
94
  },
92
95
  "devDependencies": {
93
- "@types/node": "^22.0.0",
94
- "typescript": "^5.7.0"
96
+ "@types/node": "22.19.18",
97
+ "typescript": "5.9.3"
95
98
  }
96
99
  }
@@ -1,42 +0,0 @@
1
- export interface PivxProposal {
2
- name: string;
3
- url: string;
4
- status: "passing" | "failing";
5
- funded: boolean;
6
- netYesPercent: number;
7
- yesVotes: number;
8
- noVotes: number;
9
- monthlyPaymentPiv: number;
10
- monthlyPaymentUsd: number;
11
- totalPaymentPiv: number;
12
- installmentsRemaining: number;
13
- totalInstallments: number;
14
- budgetPercent: number;
15
- }
16
- export interface PivxNetworkStats {
17
- masternodeCount: number;
18
- passingThreshold: number;
19
- monthlyBudgetPiv: number;
20
- monthlyBudgetUsd: number;
21
- budgetAllocatedPiv: number;
22
- budgetAllocatedUsd: number;
23
- budgetAllocatedPercent: number;
24
- blockHeight: number;
25
- totalSupply: number;
26
- circulatingSupply: number;
27
- }
28
- export interface PivxGovernanceData {
29
- proposals: PivxProposal[];
30
- network: PivxNetworkStats;
31
- deflation: {
32
- unallocatedPivPerCycle: number;
33
- unallocatedPercent: number;
34
- annualUnallocatedPiv: number;
35
- proposalFeeBurnPiv: number;
36
- effectiveInflationReduction: string;
37
- };
38
- timestamp: string;
39
- source: string;
40
- cacheHit: boolean;
41
- }
42
- export declare function fetchPivxGovernance(): Promise<PivxGovernanceData>;
@@ -1,232 +0,0 @@
1
- import * as cheerio from "cheerio";
2
- // 5-minute cache
3
- const CACHE_TTL_MS = 5 * 60 * 1000;
4
- let cachedData = null;
5
- let cacheTimestamp = 0;
6
- const CHAINZ_BASE = "https://chainz.cryptoid.info/pivx/api.dws";
7
- async function chainzFetch(query) {
8
- const res = await fetch(`${CHAINZ_BASE}?q=${query}`, {
9
- signal: AbortSignal.timeout(8000),
10
- });
11
- if (!res.ok)
12
- throw new Error(`Chainz API ${query}: HTTP ${res.status}`);
13
- return (await res.text()).trim();
14
- }
15
- async function fetchNetworkStats() {
16
- const [masternodeCount, blockHeight, totalSupply, circulating] = await Promise.allSettled([
17
- chainzFetch("masternodecount"),
18
- chainzFetch("getblockcount"),
19
- chainzFetch("totalcoins"),
20
- chainzFetch("circulating"),
21
- ]);
22
- const mn = masternodeCount.status === "fulfilled"
23
- ? parseInt(masternodeCount.value, 10)
24
- : 0;
25
- const bh = blockHeight.status === "fulfilled"
26
- ? parseInt(blockHeight.value, 10)
27
- : 0;
28
- const ts = totalSupply.status === "fulfilled"
29
- ? parseFloat(totalSupply.value)
30
- : 0;
31
- const cs = circulating.status === "fulfilled"
32
- ? parseFloat(circulating.value)
33
- : 0;
34
- return {
35
- masternodeCount: isNaN(mn) ? 0 : mn,
36
- blockHeight: isNaN(bh) ? 0 : bh,
37
- totalSupply: isNaN(ts) ? 0 : ts,
38
- circulatingSupply: isNaN(cs) ? 0 : cs,
39
- passingThreshold: isNaN(mn) ? 0 : Math.ceil(mn * 0.1),
40
- };
41
- }
42
- function parseNumber(text) {
43
- const cleaned = text.replace(/[^0-9.\-]/g, "");
44
- const num = parseFloat(cleaned);
45
- return isNaN(num) ? 0 : num;
46
- }
47
- async function scrapePivxProposals() {
48
- const res = await fetch("https://pivx.org/proposals", {
49
- signal: AbortSignal.timeout(15000),
50
- headers: {
51
- "User-Agent": "MPP32-Governance-Oracle/1.0 (+https://mpp32.org)",
52
- Accept: "text/html",
53
- },
54
- });
55
- if (!res.ok)
56
- throw new Error(`pivx.org/proposals returned HTTP ${res.status}`);
57
- const html = await res.text();
58
- const $ = cheerio.load(html);
59
- const pageText = $("body").text();
60
- let monthlyBudgetPiv = 432000;
61
- let monthlyBudgetUsd = 0;
62
- let budgetAllocatedPiv = 0;
63
- let budgetAllocatedUsd = 0;
64
- let masternodeCount = 0;
65
- let passingThreshold = 0;
66
- const budgetMatch = pageText.match(/Monthly\s*Budget[:\s]*([\d,]+)\s*PIV/i);
67
- if (budgetMatch?.[1])
68
- monthlyBudgetPiv = parseNumber(budgetMatch[1]);
69
- const budgetUsdMatch = pageText.match(/Monthly\s*Budget[^$]*US?\$([\d,.]+)/i);
70
- if (budgetUsdMatch?.[1])
71
- monthlyBudgetUsd = parseNumber(budgetUsdMatch[1]);
72
- const allocatedMatch = pageText.match(/Budget\s*Allocated[:\s]*([\d,]+)\s*PIV/i);
73
- if (allocatedMatch?.[1])
74
- budgetAllocatedPiv = parseNumber(allocatedMatch[1]);
75
- const allocatedUsdMatch = pageText.match(/Budget\s*Allocated[^$]*US?\$([\d,.]+)/i);
76
- if (allocatedUsdMatch?.[1])
77
- budgetAllocatedUsd = parseNumber(allocatedUsdMatch[1]);
78
- const mnMatch = pageText.match(/([\d,]+)\s*masternodes?\s*online/i);
79
- if (mnMatch?.[1])
80
- masternodeCount = parseNumber(mnMatch[1]);
81
- const thresholdMatch = pageText.match(/Positive\s*votes\s*required[^:]*:\s*(\d+)/i);
82
- if (thresholdMatch?.[1])
83
- passingThreshold = parseInt(thresholdMatch[1], 10);
84
- const proposals = [];
85
- const seenHashes = new Set();
86
- $("table#js_table tbody tr[data-hash]").each((_i, el) => {
87
- const $row = $(el);
88
- const hash = $row.attr("data-hash") || "";
89
- if (!hash || seenHashes.has(hash))
90
- return;
91
- seenHashes.add(hash);
92
- const name = ($row.attr("data-title") || "").trim();
93
- if (!name)
94
- return;
95
- const cells = $row.find("td");
96
- if (cells.length < 5)
97
- return;
98
- const statusCell = $(cells[0]);
99
- const statusText = statusCell.text();
100
- const isPassing = /passing/i.test(statusText);
101
- const funded = /funded/i.test(statusText);
102
- let netYesPercent = 0;
103
- const netYesMatch = statusText.match(/([-\d.]+)%/);
104
- if (netYesMatch?.[1])
105
- netYesPercent = parseFloat(netYesMatch[1]);
106
- const nameCell = $(cells[1]);
107
- const link = nameCell.find("a").first();
108
- const url = link.attr("href") || "";
109
- const paymentCell = $(cells[2]);
110
- const monthlyPaymentPiv = parseNumber(paymentCell.attr("data-piv") || paymentCell.attr("data-order") || "0");
111
- const budgetPercent = parseFloat(paymentCell.attr("data-percent") || "0");
112
- let monthlyPaymentUsd = 0;
113
- const usdSpan = paymentCell.find(".curr-prefix").parent();
114
- if (usdSpan.length) {
115
- const usdText = usdSpan.text();
116
- const usdMatch = usdText.match(/US?\$\s*([\d,]+(?:\.\d+)?)/i);
117
- if (usdMatch?.[1])
118
- monthlyPaymentUsd = parseNumber(usdMatch[1]);
119
- }
120
- let installmentsRemaining = 1;
121
- const longLine = paymentCell.find(".long-line");
122
- if (longLine.length) {
123
- const installB = longLine.find("b").first();
124
- if (installB.length) {
125
- const n = parseInt(installB.text().trim(), 10);
126
- if (!isNaN(n))
127
- installmentsRemaining = n;
128
- }
129
- }
130
- let totalPaymentPiv = monthlyPaymentPiv;
131
- if (longLine.length) {
132
- const totalB = longLine.find("b").last();
133
- if (totalB.length) {
134
- const totalText = totalB.text();
135
- const totalMatch = totalText.match(/([\d,]+(?:\.\d+)?)\s*PIV/i);
136
- if (totalMatch?.[1])
137
- totalPaymentPiv = parseNumber(totalMatch[1]);
138
- }
139
- }
140
- const voteCell = $(cells[4]);
141
- const voteText = voteCell.text();
142
- let yesVotes = 0;
143
- let noVotes = 0;
144
- const voteMatch = voteText.match(/(\d+)\s*\/\s*(\d+)/);
145
- if (voteMatch?.[1] && voteMatch[2]) {
146
- yesVotes = parseInt(voteMatch[1], 10);
147
- noVotes = parseInt(voteMatch[2], 10);
148
- }
149
- proposals.push({
150
- name,
151
- url,
152
- status: isPassing ? "passing" : "failing",
153
- funded,
154
- netYesPercent,
155
- yesVotes,
156
- noVotes,
157
- monthlyPaymentPiv,
158
- monthlyPaymentUsd,
159
- totalPaymentPiv,
160
- installmentsRemaining,
161
- totalInstallments: installmentsRemaining,
162
- budgetPercent,
163
- });
164
- });
165
- return {
166
- proposals,
167
- budgetSummary: {
168
- monthlyBudgetPiv,
169
- monthlyBudgetUsd,
170
- budgetAllocatedPiv,
171
- budgetAllocatedUsd,
172
- masternodeCount,
173
- passingThreshold,
174
- },
175
- };
176
- }
177
- export async function fetchPivxGovernance() {
178
- if (cachedData && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
179
- return { ...cachedData, cacheHit: true };
180
- }
181
- const [scrapeResult, networkStats] = await Promise.allSettled([
182
- scrapePivxProposals(),
183
- fetchNetworkStats(),
184
- ]);
185
- const scraped = scrapeResult.status === "fulfilled" ? scrapeResult.value : null;
186
- const chainzStats = networkStats.status === "fulfilled" ? networkStats.value : {};
187
- if (!scraped) {
188
- throw new Error(`Failed to fetch PIVX governance data: ${scrapeResult.status === "rejected" ? scrapeResult.reason : "unknown"}`);
189
- }
190
- const budgetAllocatedPercent = scraped.budgetSummary.monthlyBudgetPiv > 0
191
- ? Math.round((scraped.budgetSummary.budgetAllocatedPiv /
192
- scraped.budgetSummary.monthlyBudgetPiv) *
193
- 10000) / 100
194
- : 0;
195
- const network = {
196
- masternodeCount: scraped.budgetSummary.masternodeCount ||
197
- chainzStats.masternodeCount ||
198
- 0,
199
- passingThreshold: scraped.budgetSummary.passingThreshold ||
200
- chainzStats.passingThreshold ||
201
- 0,
202
- monthlyBudgetPiv: scraped.budgetSummary.monthlyBudgetPiv,
203
- monthlyBudgetUsd: scraped.budgetSummary.monthlyBudgetUsd,
204
- budgetAllocatedPiv: scraped.budgetSummary.budgetAllocatedPiv,
205
- budgetAllocatedUsd: scraped.budgetSummary.budgetAllocatedUsd,
206
- budgetAllocatedPercent,
207
- blockHeight: chainzStats.blockHeight || 0,
208
- totalSupply: chainzStats.totalSupply || 0,
209
- circulatingSupply: chainzStats.circulatingSupply || 0,
210
- };
211
- const unallocatedPivPerCycle = network.monthlyBudgetPiv - network.budgetAllocatedPiv;
212
- const annualUnallocatedPiv = unallocatedPivPerCycle * 12;
213
- const data = {
214
- proposals: scraped.proposals,
215
- network,
216
- deflation: {
217
- unallocatedPivPerCycle,
218
- unallocatedPercent: 100 - budgetAllocatedPercent,
219
- annualUnallocatedPiv,
220
- proposalFeeBurnPiv: 50,
221
- effectiveInflationReduction: network.totalSupply > 0
222
- ? `${((annualUnallocatedPiv / network.totalSupply) * 100).toFixed(3)}%`
223
- : "N/A",
224
- },
225
- timestamp: new Date().toISOString(),
226
- source: "pivx.org/proposals + chainz.cryptoid.info",
227
- cacheHit: false,
228
- };
229
- cachedData = data;
230
- cacheTimestamp = Date.now();
231
- return data;
232
- }