mpp32-mcp-server 1.4.1 → 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 +101 -0
- package/README.md +6 -0
- package/SECURITY.md +77 -0
- package/dist/index.js +173 -9
- package/dist/x402-signers.js +12 -14
- package/package.json +17 -14
- package/dist/pivx-provider.d.ts +0 -42
- package/dist/pivx-provider.js +0 -232
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,107 @@ 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
|
+
|
|
74
|
+
## [1.5.0] - 2026-06-01
|
|
75
|
+
|
|
76
|
+
### Added
|
|
77
|
+
|
|
78
|
+
* **`try_solana_token_intelligence_free` MCP tool — zero-friction preview.**
|
|
79
|
+
Hits the backend `/api/intelligence/demo` endpoint. Returns the same alpha
|
|
80
|
+
score, rug risk, whale activity, smart money signals, pump probability, and
|
|
81
|
+
market data payload as the paid endpoint, but requires no `MPP32_AGENT_KEY`
|
|
82
|
+
and no Solana private key. Rate-limited to 10 calls/minute per IP. Intended
|
|
83
|
+
as the first call new users (and Claude itself) make when evaluating MPP32 —
|
|
84
|
+
see the data quality, THEN configure paid access. This closes the
|
|
85
|
+
conversion-funnel gap where new agents previously hit a 402 wall on their
|
|
86
|
+
very first call before ever seeing the oracle's output.
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
|
|
90
|
+
* **`get_solana_token_intelligence` description updated** to point new users
|
|
91
|
+
with no keys configured at `try_solana_token_intelligence_free` first.
|
|
92
|
+
* **`get_mpp32_diagnostics` output updated** to mention the free demo when
|
|
93
|
+
the user is not yet ready to pay end-to-end.
|
|
94
|
+
|
|
95
|
+
### Backend (server-side compatibility, no client action required)
|
|
96
|
+
|
|
97
|
+
* MPP32 backend now defaults to PayAI (`https://facilitator.payai.network`)
|
|
98
|
+
as the x402 facilitator, with Coinbase CDP as a documented fallback. PayAI
|
|
99
|
+
is the only public facilitator that supports Solana **mainnet**
|
|
100
|
+
(`solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`); the previous default
|
|
101
|
+
(`x402.org/facilitator`) is testnet/devnet-only and was silently failing
|
|
102
|
+
every mainnet settlement. The backend now refuses to boot in production
|
|
103
|
+
if no configured facilitator supports the configured network — making this
|
|
104
|
+
exact regression class impossible. No MCP client change required: the
|
|
105
|
+
signer's network detection (`x402-signers.ts`) and protocol handling are
|
|
106
|
+
unchanged.
|
|
107
|
+
|
|
7
108
|
## [1.4.0] - 2026-05-21
|
|
8
109
|
|
|
9
110
|
### Added
|
package/README.md
CHANGED
|
@@ -22,6 +22,12 @@ One install. Pay any x402 endpoint on Solana from your agent. Browse a federated
|
|
|
22
22
|
|
|
23
23
|
The MCP server ships signers for both Solana and Base out of the box. When a paid call returns a 402, the server picks the network that matches the key you configured and settles the payment in one round trip.
|
|
24
24
|
|
|
25
|
+
## Try it free first (no keys, no setup)
|
|
26
|
+
|
|
27
|
+
The MCP server exposes a **`try_solana_token_intelligence_free`** tool that returns the full Intelligence Oracle payload (alpha score, rug risk, whale activity, smart money signals, 24h pump probability, market data) for any Solana token with **zero configuration** — no `MPP32_AGENT_KEY`, no Solana key, no payment. Rate-limited to 10 calls/minute per IP. Use it to evaluate the oracle, then switch to `get_solana_token_intelligence` for unlimited attributed usage at $0.008/query (M32 holders save up to 40%).
|
|
28
|
+
|
|
29
|
+
Just install the server and ask your agent: _"Use mpp32 to get a free intelligence preview for SOL."_
|
|
30
|
+
|
|
25
31
|
## Why this beats running your own integrations
|
|
26
32
|
|
|
27
33
|
Most agent stacks stop at "the model can call a function." That works until the function costs money. The moment your agent needs premium data, a paid model, a trading signal, or a token analytics call, you are back to building accounts, storing API keys, watching budgets, and writing custom 402 handlers for every provider.
|
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.
|
|
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,8 +251,9 @@ 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
|
-
`**Ready to pay end-to-end:** ${readyToPay ? "YES — try `get_solana_token_intelligence` with token=\"M32\" to confirm." : "NO — see the missing items above."}`,
|
|
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
|
``,
|
|
257
258
|
`**If a variable shows NOT SET but you set it in claude_desktop_config.json:**`,
|
|
258
259
|
`1. Confirm the file path Claude Desktop actually reads:`,
|
|
@@ -450,7 +451,7 @@ server.tool("call_mpp32_endpoint", "Call any HTTP-callable service in the MPP32
|
|
|
450
451
|
return await callViaLegacyProxy(slug, method, parsedBody, query, path);
|
|
451
452
|
});
|
|
452
453
|
// ── Tool 3: get_solana_token_intelligence ───────────────────────────────────
|
|
453
|
-
server.tool("get_solana_token_intelligence", "Get real-time Solana token intelligence from the MPP32 Intelligence Oracle. Returns alpha score (0-100), rug risk assessment, whale activity, smart money signals, 24h pump probability, projected ROI ranges, and aggregated DexScreener/Jupiter/CoinGecko market data. Costs $0.008 per query, paid automatically via x402 (USDC on Solana) or Tempo (pathUSD on Eth L2). M32 token holders receive up to 40% discount once their wallet is signature-verified. Set MPP32_AGENT_KEY in config to attribute calls to your dashboard.", {
|
|
454
|
+
server.tool("get_solana_token_intelligence", "Get real-time Solana token intelligence from the MPP32 Intelligence Oracle. Returns alpha score (0-100), rug risk assessment, whale activity, smart money signals, 24h pump probability, projected ROI ranges, and aggregated DexScreener/Jupiter/CoinGecko market data. Costs $0.008 per query, paid automatically via x402 (USDC on Solana) or Tempo (pathUSD on Eth L2). M32 token holders receive up to 40% discount once their wallet is signature-verified. Set MPP32_AGENT_KEY in config to attribute calls to your dashboard. **New users with no keys configured: call `try_solana_token_intelligence_free` first** — it returns the same payload, rate-limited to 10/min, no payment or key required.", {
|
|
454
455
|
token: z
|
|
455
456
|
.string()
|
|
456
457
|
.describe("Solana token mint address or ticker symbol (e.g. SOL, BONK, JUP, M32, or full base58 address)."),
|
|
@@ -466,6 +467,63 @@ server.tool("get_solana_token_intelligence", "Get real-time Solana token intelli
|
|
|
466
467
|
// Legacy path — direct call to /api/intelligence with manual 402 handling.
|
|
467
468
|
return await legacyIntelligenceCall(token, walletAddress);
|
|
468
469
|
});
|
|
470
|
+
// ── Tool 3b: Free Intelligence Demo ────────────────────────────────────────
|
|
471
|
+
// No payment, no key. Hits /api/intelligence/demo, which is rate-limited per
|
|
472
|
+
// IP. Intended as the first call new users (and Claude itself) make when
|
|
473
|
+
// trying MPP32 — they see real alpha scores and signals BEFORE encountering
|
|
474
|
+
// any payment wall. This is the conversion funnel fix: agents today bounce
|
|
475
|
+
// off the 402, so we let them taste the product first.
|
|
476
|
+
server.tool("try_solana_token_intelligence_free", "FREE preview of the MPP32 Intelligence Oracle. No payment, no agent key, no Solana private key required. Returns the SAME payload as the paid endpoint (alpha score, rug risk, whale activity, smart money signals, pump probability, market data) for any Solana token. Rate-limited to 10 calls/minute per IP. Use this to evaluate the data quality. Once you're convinced, set MPP32_AGENT_KEY + a payment key and switch to `get_solana_token_intelligence` for unlimited, attributed usage and M32-holder discounts.", {
|
|
477
|
+
token: z
|
|
478
|
+
.string()
|
|
479
|
+
.describe("Solana token mint address or ticker symbol (e.g. SOL, BONK, JUP, M32, or full base58 address)."),
|
|
480
|
+
}, async ({ token }) => {
|
|
481
|
+
try {
|
|
482
|
+
const res = await fetchWithTimeout(`${API_URL}/api/intelligence/demo`, {
|
|
483
|
+
method: "POST",
|
|
484
|
+
headers: { "Content-Type": "application/json" },
|
|
485
|
+
body: JSON.stringify({ token }),
|
|
486
|
+
});
|
|
487
|
+
const text = await res.text();
|
|
488
|
+
let formatted;
|
|
489
|
+
try {
|
|
490
|
+
formatted = JSON.stringify(JSON.parse(text), null, 2);
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
formatted = text;
|
|
494
|
+
}
|
|
495
|
+
if (res.status === 429) {
|
|
496
|
+
return {
|
|
497
|
+
content: [{
|
|
498
|
+
type: "text",
|
|
499
|
+
text: `Demo rate limit reached (10 calls/minute per IP). Wait a minute and retry, or set up paid access for unlimited queries:\n\n1. Get an agent key: ${API_URL}/agent-console\n2. Add MPP32_AGENT_KEY + MPP32_SOLANA_PRIVATE_KEY to your MCP config\n3. Call \`get_solana_token_intelligence\` instead — $0.008/query, M32 holders save up to 40%.`,
|
|
500
|
+
}],
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
if (!res.ok) {
|
|
504
|
+
return {
|
|
505
|
+
content: [{
|
|
506
|
+
type: "text",
|
|
507
|
+
text: `Demo returned HTTP ${res.status}:\n\n\`\`\`json\n${formatted}\n\`\`\``,
|
|
508
|
+
}],
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
content: [{
|
|
513
|
+
type: "text",
|
|
514
|
+
text: `**MPP32 Intelligence Oracle (FREE DEMO)** — \`${token}\`\n\n${formatted}\n\n---\n_Demo result. Same payload as the paid endpoint. Rate-limited to 10/min/IP. For unlimited usage and dashboard attribution, set MPP32_AGENT_KEY (get one at ${API_URL}/agent-console) and use \`get_solana_token_intelligence\`._`,
|
|
515
|
+
}],
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
catch (err) {
|
|
519
|
+
return {
|
|
520
|
+
content: [{
|
|
521
|
+
type: "text",
|
|
522
|
+
text: `Network error reaching ${API_URL}: ${err instanceof Error ? err.message : String(err)}`,
|
|
523
|
+
}],
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
});
|
|
469
527
|
// ── Tool 4: M32-gated Whale Tracker ───────────────────────────────────────
|
|
470
528
|
server.tool("get_m32_whale_tracker", "M32-gated whale analysis for any Solana token. Returns top 20 holders, concentration risk, holder distribution, and buy/sell pressure. Requires the caller to hold 1,000,000+ M32 tokens (balance verified on-chain via X-Wallet-Address header). Free for qualifying holders — no payment required. Returns 403 if the wallet holds insufficient M32.", {
|
|
471
529
|
token: z
|
|
@@ -574,11 +632,25 @@ server.tool("scan_portfolio_m32", "M32-gated full wallet portfolio scan. Discove
|
|
|
574
632
|
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
575
633
|
}
|
|
576
634
|
});
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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.", {
|
|
582
654
|
filter: z
|
|
583
655
|
.enum(["all", "passing", "failing"])
|
|
584
656
|
.default("all")
|
|
@@ -655,7 +727,7 @@ server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance inte
|
|
|
655
727
|
return {
|
|
656
728
|
content: [{
|
|
657
729
|
type: "text",
|
|
658
|
-
text: `Failed to fetch PIVX governance data: ${err instanceof Error ? err.message : String(err)}. The tool
|
|
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.`,
|
|
659
731
|
}],
|
|
660
732
|
};
|
|
661
733
|
}
|
|
@@ -1464,6 +1536,95 @@ async function completeX402Payment(paymentRequiredHeader, keys) {
|
|
|
1464
1536
|
protocolUsed: result.protocolUsed,
|
|
1465
1537
|
};
|
|
1466
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
|
+
}
|
|
1467
1628
|
// ── Start ───────────────────────────────────────────────────────────────────
|
|
1468
1629
|
async function main() {
|
|
1469
1630
|
const transport = new StdioServerTransport();
|
|
@@ -1476,6 +1637,9 @@ async function main() {
|
|
|
1476
1637
|
.filter(Boolean)
|
|
1477
1638
|
.join(", ") || "no keys (catalog-only legacy mode)";
|
|
1478
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();
|
|
1479
1643
|
// Per-variable status so a user staring at this log can immediately see
|
|
1480
1644
|
// whether their env vars made it through. Values are fingerprinted.
|
|
1481
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)`;
|
package/dist/x402-signers.js
CHANGED
|
@@ -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,
|
|
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("
|
|
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
|
-
|
|
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.
|
|
96
|
+
return deps.base58.decode(raw);
|
|
98
97
|
}
|
|
99
98
|
async function buildSolanaSigner(rawKey, deps) {
|
|
100
|
-
|
|
99
|
+
const bytes = decodeSolanaSecret(rawKey, deps);
|
|
101
100
|
if (bytes.length === 32) {
|
|
102
|
-
// 32-byte seed — kit
|
|
103
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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.
|
|
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": ">=
|
|
73
|
+
"node": ">=20.10.0"
|
|
74
|
+
},
|
|
75
|
+
"publishConfig": {
|
|
76
|
+
"access": "public"
|
|
72
77
|
},
|
|
73
78
|
"dependencies": {
|
|
74
|
-
"@modelcontextprotocol/sdk": "
|
|
75
|
-
"@
|
|
76
|
-
"@solana-program/
|
|
77
|
-
"@solana/
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
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": "
|
|
94
|
-
"typescript": "
|
|
96
|
+
"@types/node": "22.19.18",
|
|
97
|
+
"typescript": "5.9.3"
|
|
95
98
|
}
|
|
96
99
|
}
|
package/dist/pivx-provider.d.ts
DELETED
|
@@ -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>;
|
package/dist/pivx-provider.js
DELETED
|
@@ -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
|
-
}
|