soulprint-network 0.3.8 → 0.4.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/dist/code-hash.json +5 -4
- package/dist/mcp-registry-client.d.ts +49 -0
- package/dist/mcp-registry-client.js +164 -0
- package/dist/p2p.d.ts +1 -0
- package/dist/p2p.js +32 -0
- package/dist/server.js +34 -0
- package/dist/validator.js +192 -1
- package/package.json +12 -10
package/dist/code-hash.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"codeHash": "
|
|
3
|
-
"codeHashHex": "
|
|
4
|
-
"computedAt": "2026-02-
|
|
5
|
-
"fileCount":
|
|
2
|
+
"codeHash": "71e41f8c443a60f95aa13ae942aa7c36a242ca69b02c43cdb973aaed92f004d5",
|
|
3
|
+
"codeHashHex": "0x71e41f8c443a60f95aa13ae942aa7c36a242ca69b02c43cdb973aaed92f004d5",
|
|
4
|
+
"computedAt": "2026-02-25T03:16:23.336Z",
|
|
5
|
+
"fileCount": 19,
|
|
6
6
|
"files": [
|
|
7
7
|
"blockchain/blockchain-anchor.ts",
|
|
8
8
|
"blockchain/blockchain-client.ts",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"crypto/gossip-cipher.ts",
|
|
19
19
|
"crypto/peer-router.ts",
|
|
20
20
|
"index.ts",
|
|
21
|
+
"mcp-registry-client.ts",
|
|
21
22
|
"p2p.ts",
|
|
22
23
|
"peer-challenge.ts",
|
|
23
24
|
"server.ts",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface MCPEntry {
|
|
2
|
+
address: string;
|
|
3
|
+
owner: string;
|
|
4
|
+
name: string;
|
|
5
|
+
url: string;
|
|
6
|
+
did: string;
|
|
7
|
+
category: string;
|
|
8
|
+
description: string;
|
|
9
|
+
registeredAt: number;
|
|
10
|
+
verifiedAt: number;
|
|
11
|
+
revokedAt: number;
|
|
12
|
+
exists: boolean;
|
|
13
|
+
verified: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function isVerifiedOnChain(mcpAddress: string): Promise<boolean>;
|
|
16
|
+
export declare function getMCPEntry(mcpAddress: string): Promise<MCPEntry | null>;
|
|
17
|
+
export declare function getAllMCPEntries(): Promise<MCPEntry[]>;
|
|
18
|
+
export declare function getVerifiedMCPEntries(): Promise<MCPEntry[]>;
|
|
19
|
+
export declare function getSuperAdmin(): Promise<string>;
|
|
20
|
+
export declare function registerMCPOnChain(params: {
|
|
21
|
+
ownerPrivateKey: string;
|
|
22
|
+
mcpAddress: string;
|
|
23
|
+
name: string;
|
|
24
|
+
url: string;
|
|
25
|
+
did?: string;
|
|
26
|
+
category?: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
}): Promise<{
|
|
29
|
+
success: boolean;
|
|
30
|
+
txHash?: string;
|
|
31
|
+
error?: string;
|
|
32
|
+
}>;
|
|
33
|
+
export declare function verifyMCPOnChain(mcpAddress: string): Promise<{
|
|
34
|
+
success: boolean;
|
|
35
|
+
txHash?: string;
|
|
36
|
+
error?: string;
|
|
37
|
+
}>;
|
|
38
|
+
export declare function revokeMCPOnChain(mcpAddress: string, reason: string): Promise<{
|
|
39
|
+
success: boolean;
|
|
40
|
+
txHash?: string;
|
|
41
|
+
error?: string;
|
|
42
|
+
}>;
|
|
43
|
+
export declare function getRegistryInfo(): Promise<{
|
|
44
|
+
contract: string;
|
|
45
|
+
network: string;
|
|
46
|
+
superAdmin: string;
|
|
47
|
+
totalMCPs: number;
|
|
48
|
+
explorer: string;
|
|
49
|
+
}>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-registry.ts — Cliente para MCPRegistry.sol en Base Sepolia
|
|
3
|
+
*
|
|
4
|
+
* Expone:
|
|
5
|
+
* - isVerifiedOnChain(address) → bool
|
|
6
|
+
* - getMCPEntry(address) → MCPEntry
|
|
7
|
+
* - getVerifiedMCPs() → lista pública
|
|
8
|
+
* - verifyMCP / revokeMCP → solo desde la wallet admin (con ADMIN_PRIVATE_KEY)
|
|
9
|
+
* - registerMCP → cualquiera puede registrar
|
|
10
|
+
*/
|
|
11
|
+
import { ethers } from "ethers";
|
|
12
|
+
// ── Config ──────────────────────────────────────────────────────────────────
|
|
13
|
+
const RPC_URL = process.env.BASE_RPC_URL || "https://sepolia.base.org";
|
|
14
|
+
const MCP_REGISTRY_ADDR = process.env.MCP_REGISTRY_ADDR || "0x59EA3c8f60ecbAe22B4c323A8dDc2b0BCd9D3C2a";
|
|
15
|
+
const ADMIN_PRIVATE_KEY = process.env.ADMIN_PRIVATE_KEY || ""; // solo admin lo tiene
|
|
16
|
+
const ABI = [
|
|
17
|
+
// Lectura pública
|
|
18
|
+
"function isVerified(address mcpAddress) view returns (bool)",
|
|
19
|
+
"function totalMCPs() view returns (uint256)",
|
|
20
|
+
"function superAdmin() view returns (address)",
|
|
21
|
+
"function getAllMCPs() view returns (address[])",
|
|
22
|
+
"function mcps(address) view returns (address owner, string name, string url, string did, string category, string description, uint64 registeredAt, uint64 verifiedAt, uint64 revokedAt, bool exists)",
|
|
23
|
+
// Escritura (cualquiera puede registrar)
|
|
24
|
+
"function registerMCP(address mcpAddress, string name, string url, string did, string category, string description)",
|
|
25
|
+
// Escritura (solo admin)
|
|
26
|
+
"function verify(address mcpAddress)",
|
|
27
|
+
"function revoke(address mcpAddress, string reason)",
|
|
28
|
+
"function updateMCP(address mcpAddress, string newUrl, string newDid, string newDescription)",
|
|
29
|
+
// Eventos
|
|
30
|
+
"event MCPRegistered(address indexed mcpAddress, string name, string url, string category)",
|
|
31
|
+
"event MCPVerified(address indexed mcpAddress, string name, address verifiedBy)",
|
|
32
|
+
"event MCPRevoked(address indexed mcpAddress, string name, string reason)",
|
|
33
|
+
];
|
|
34
|
+
function getProvider() {
|
|
35
|
+
return new ethers.JsonRpcProvider(RPC_URL);
|
|
36
|
+
}
|
|
37
|
+
function getReadContract() {
|
|
38
|
+
return new ethers.Contract(MCP_REGISTRY_ADDR, ABI, getProvider());
|
|
39
|
+
}
|
|
40
|
+
function getWriteContract() {
|
|
41
|
+
if (!ADMIN_PRIVATE_KEY)
|
|
42
|
+
throw new Error("ADMIN_PRIVATE_KEY no configurada");
|
|
43
|
+
const provider = getProvider();
|
|
44
|
+
const wallet = new ethers.Wallet(ADMIN_PRIVATE_KEY, provider);
|
|
45
|
+
return new ethers.Contract(MCP_REGISTRY_ADDR, ABI, wallet);
|
|
46
|
+
}
|
|
47
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
48
|
+
function formatEntry(addr, raw) {
|
|
49
|
+
return {
|
|
50
|
+
address: addr,
|
|
51
|
+
owner: raw.owner,
|
|
52
|
+
name: raw.name,
|
|
53
|
+
url: raw.url,
|
|
54
|
+
did: raw.did,
|
|
55
|
+
category: raw.category,
|
|
56
|
+
description: raw.description,
|
|
57
|
+
registeredAt: Number(raw.registeredAt),
|
|
58
|
+
verifiedAt: Number(raw.verifiedAt),
|
|
59
|
+
revokedAt: Number(raw.revokedAt),
|
|
60
|
+
exists: raw.exists,
|
|
61
|
+
verified: raw.verifiedAt > 0n && raw.revokedAt === 0n,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// ── Consultas públicas ───────────────────────────────────────────────────────
|
|
65
|
+
export async function isVerifiedOnChain(mcpAddress) {
|
|
66
|
+
try {
|
|
67
|
+
const c = getReadContract();
|
|
68
|
+
return await c.isVerified(mcpAddress);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function getMCPEntry(mcpAddress) {
|
|
75
|
+
try {
|
|
76
|
+
const c = getReadContract();
|
|
77
|
+
const raw = await c.mcps(mcpAddress);
|
|
78
|
+
if (!raw.exists)
|
|
79
|
+
return null;
|
|
80
|
+
return formatEntry(mcpAddress, raw);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function getAllMCPEntries() {
|
|
87
|
+
try {
|
|
88
|
+
const c = getReadContract();
|
|
89
|
+
const addrs = await c.getAllMCPs();
|
|
90
|
+
const entries = await Promise.all(addrs.map(a => getMCPEntry(a)));
|
|
91
|
+
return entries.filter(Boolean);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export async function getVerifiedMCPEntries() {
|
|
98
|
+
const all = await getAllMCPEntries();
|
|
99
|
+
return all.filter(e => e.verified);
|
|
100
|
+
}
|
|
101
|
+
export async function getSuperAdmin() {
|
|
102
|
+
try {
|
|
103
|
+
const c = getReadContract();
|
|
104
|
+
return await c.superAdmin();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// ── Escritura: registro (cualquiera) ─────────────────────────────────────────
|
|
111
|
+
export async function registerMCPOnChain(params) {
|
|
112
|
+
try {
|
|
113
|
+
const provider = getProvider();
|
|
114
|
+
const wallet = new ethers.Wallet(params.ownerPrivateKey, provider);
|
|
115
|
+
const contract = new ethers.Contract(MCP_REGISTRY_ADDR, ABI, wallet);
|
|
116
|
+
const tx = await contract.registerMCP(params.mcpAddress, params.name, params.url, params.did ?? "", params.category ?? "general", params.description ?? "");
|
|
117
|
+
const receipt = await tx.wait();
|
|
118
|
+
return { success: true, txHash: receipt.hash };
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
return { success: false, error: e.message };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ── Escritura: admin — verificar ─────────────────────────────────────────────
|
|
125
|
+
export async function verifyMCPOnChain(mcpAddress) {
|
|
126
|
+
try {
|
|
127
|
+
const c = getWriteContract();
|
|
128
|
+
const tx = await c.verify(mcpAddress);
|
|
129
|
+
const r = await tx.wait();
|
|
130
|
+
return { success: true, txHash: r.hash };
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
return { success: false, error: e.message };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ── Escritura: admin — revocar ───────────────────────────────────────────────
|
|
137
|
+
export async function revokeMCPOnChain(mcpAddress, reason) {
|
|
138
|
+
try {
|
|
139
|
+
const c = getWriteContract();
|
|
140
|
+
const tx = await c.revoke(mcpAddress, reason);
|
|
141
|
+
const r = await tx.wait();
|
|
142
|
+
return { success: true, txHash: r.hash };
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
return { success: false, error: e.message };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// ── Info del registry ────────────────────────────────────────────────────────
|
|
149
|
+
export async function getRegistryInfo() {
|
|
150
|
+
try {
|
|
151
|
+
const c = getReadContract();
|
|
152
|
+
const [admin, total] = await Promise.all([c.superAdmin(), c.totalMCPs()]);
|
|
153
|
+
return {
|
|
154
|
+
contract: MCP_REGISTRY_ADDR,
|
|
155
|
+
network: "Base Sepolia (chainId: 84532)",
|
|
156
|
+
superAdmin: admin,
|
|
157
|
+
totalMCPs: Number(total),
|
|
158
|
+
explorer: `https://sepolia.basescan.org/address/${MCP_REGISTRY_ADDR}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return { contract: MCP_REGISTRY_ADDR, network: "Base Sepolia", superAdmin: "", totalMCPs: 0, explorer: "" };
|
|
163
|
+
}
|
|
164
|
+
}
|
package/dist/p2p.d.ts
CHANGED
|
@@ -35,3 +35,4 @@ export declare function publishAttestationP2P(node: SoulprintP2PNode, att: BotAt
|
|
|
35
35
|
export declare function onAttestationReceived(node: SoulprintP2PNode, handler: (att: BotAttestation, fromPeer: string) => void): void;
|
|
36
36
|
export declare function getP2PStats(node: SoulprintP2PNode): P2PStats;
|
|
37
37
|
export declare function stopP2PNode(node: SoulprintP2PNode): Promise<void>;
|
|
38
|
+
export declare function dialP2PPeer(node: SoulprintP2PNode, maddrStr: string, timeoutMs?: number): Promise<boolean>;
|
package/dist/p2p.js
CHANGED
|
@@ -114,3 +114,35 @@ export async function stopP2PNode(node) {
|
|
|
114
114
|
}
|
|
115
115
|
catch { /* ignorar */ }
|
|
116
116
|
}
|
|
117
|
+
// ── dial a peer by multiaddr string (best-effort — HTTP gossip is the primary mechanism) ──
|
|
118
|
+
// Note: in WSL2/NAT environments mDNS multicast doesn't work.
|
|
119
|
+
// This function attempts libp2p TCP dial using the peer's advertised multiaddrs.
|
|
120
|
+
// Falls back gracefully — HTTP gossip layer (known_peers) handles message propagation.
|
|
121
|
+
export async function dialP2PPeer(node, maddrStr, timeoutMs = 5_000) {
|
|
122
|
+
try {
|
|
123
|
+
// Extract PeerId from the multiaddr string and add multiaddr to peer store
|
|
124
|
+
// so libp2p can use its own internal Multiaddr class (avoids version mismatch)
|
|
125
|
+
const peerIdStr = maddrStr.split("/p2p/")[1];
|
|
126
|
+
if (!peerIdStr)
|
|
127
|
+
return false;
|
|
128
|
+
// Use peer store to register the address, then dial by PeerId
|
|
129
|
+
// This lets libp2p use its own Multiaddr parsing internally
|
|
130
|
+
const { peerIdFromString } = await import("@libp2p/peer-id");
|
|
131
|
+
const peerId = peerIdFromString(peerIdStr);
|
|
132
|
+
// Register the address in the peer store
|
|
133
|
+
await node.peerStore.merge(peerId, {
|
|
134
|
+
multiaddrs: [maddrStr],
|
|
135
|
+
}).catch(() => {
|
|
136
|
+
// peerStore.merge signature varies by libp2p version — try patch
|
|
137
|
+
return node.peerStore.patch(peerId, { multiaddrs: [maddrStr] }).catch(() => { });
|
|
138
|
+
});
|
|
139
|
+
await Promise.race([
|
|
140
|
+
node.dial(peerId),
|
|
141
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("dial timeout")), timeoutMs)),
|
|
142
|
+
]);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createSoulprintP2PNode, MAINNET_BOOTSTRAP, stopP2PNode } from "./p2p.js
|
|
|
11
11
|
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
12
12
|
const HTTP_PORT = parseInt(process.env.SOULPRINT_PORT ?? "4888");
|
|
13
13
|
const P2P_PORT = parseInt(process.env.SOULPRINT_P2P_PORT ?? String(HTTP_PORT + 2000));
|
|
14
|
+
globalThis._startTime = Date.now();
|
|
14
15
|
// Bootstrap nodes: variables de entorno o mainnet predefinidos
|
|
15
16
|
const bootstrapEnv = (process.env.SOULPRINT_BOOTSTRAP ?? "")
|
|
16
17
|
.split(",")
|
|
@@ -41,6 +42,39 @@ catch (err) {
|
|
|
41
42
|
console.warn(`⚠️ P2P no disponible — solo HTTP gossip activo`);
|
|
42
43
|
console.warn(` Error: ${err?.message ?? String(err)}\n`);
|
|
43
44
|
}
|
|
45
|
+
// ─── HTTP Bootstrap Peers (auto-registro) ─────────────────────────────────────
|
|
46
|
+
// SOULPRINT_BOOTSTRAP_HTTP=http://node1:4888,http://node2:4888
|
|
47
|
+
// Registra peers HTTP automáticamente al arrancar (útil en WSL2 / Docker / cloud)
|
|
48
|
+
const httpBootstraps = (process.env.SOULPRINT_BOOTSTRAP_HTTP ?? "")
|
|
49
|
+
.split(",").map(s => s.trim()).filter(s => s.startsWith("http"));
|
|
50
|
+
if (httpBootstraps.length > 0) {
|
|
51
|
+
console.log(`🔗 Bootstrap HTTP: ${httpBootstraps.length} peer(s) configurados`);
|
|
52
|
+
// Esperar 2s a que el HTTP server esté listo antes de registrar
|
|
53
|
+
setTimeout(async () => {
|
|
54
|
+
const PROTOCOL_HASH = process.env.SOULPRINT_PROTOCOL_HASH
|
|
55
|
+
?? "dfe1ccca1270ec86f93308dc4b981bab1d6bd74bdcc334059f4380b407ca07ca";
|
|
56
|
+
for (const peerUrl of httpBootstraps) {
|
|
57
|
+
try {
|
|
58
|
+
const r = await fetch(`http://localhost:${HTTP_PORT}/peers/register`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: { "Content-Type": "application/json" },
|
|
61
|
+
body: JSON.stringify({ url: peerUrl, protocol_hash: PROTOCOL_HASH }),
|
|
62
|
+
signal: AbortSignal.timeout(15_000),
|
|
63
|
+
});
|
|
64
|
+
const d = await r.json();
|
|
65
|
+
if (d.ok) {
|
|
66
|
+
console.log(` ✅ Bootstrap peer registrado: ${peerUrl} (total peers: ${d.peers})`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.warn(` ⚠️ Bootstrap peer rechazado: ${peerUrl} — ${d.error ?? d.reason ?? "?"}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
console.warn(` ❌ No se pudo conectar a bootstrap peer: ${peerUrl} — ${e.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}, 2_000);
|
|
77
|
+
}
|
|
44
78
|
// ─── Graceful shutdown ────────────────────────────────────────────────────────
|
|
45
79
|
async function shutdown(signal) {
|
|
46
80
|
console.log(`\n${signal} recibido — cerrando...`);
|
package/dist/validator.js
CHANGED
|
@@ -12,7 +12,8 @@ import { NullifierConsensus, AttestationConsensus, StateSyncManager } from "./co
|
|
|
12
12
|
import { BlockchainAnchor } from "./blockchain/blockchain-anchor.js";
|
|
13
13
|
import { SoulprintBlockchainClient, loadBlockchainConfig, } from "./blockchain/blockchain-client.js";
|
|
14
14
|
import { getCodeIntegrity, logCodeIntegrity, computeRuntimeHash } from "./code-integrity.js";
|
|
15
|
-
import {
|
|
15
|
+
import { getMCPEntry, getVerifiedMCPEntries, getAllMCPEntries, getRegistryInfo, verifyMCPOnChain, revokeMCPOnChain, registerMCPOnChain, } from "./mcp-registry-client.js";
|
|
16
|
+
import { publishAttestationP2P, onAttestationReceived, getP2PStats, dialP2PPeer, } from "./p2p.js";
|
|
16
17
|
// ── Config ────────────────────────────────────────────────────────────────────
|
|
17
18
|
const PORT = parseInt(process.env.SOULPRINT_PORT ?? String(PROTOCOL.DEFAULT_HTTP_PORT));
|
|
18
19
|
const NODE_DIR = join(homedir(), ".soulprint", "node");
|
|
@@ -565,6 +566,32 @@ async function handlePeerRegister(req, res) {
|
|
|
565
566
|
}
|
|
566
567
|
peers.push(url);
|
|
567
568
|
savePeers();
|
|
569
|
+
// ── Auto-dial libp2p layer ──────────────────────────────────────────────────
|
|
570
|
+
// WSL2 / NAT: mDNS no funciona → al registrar un peer HTTP, intentamos
|
|
571
|
+
// conectar también vía libp2p usando sus multiaddrs del /info endpoint.
|
|
572
|
+
if (p2pNode) {
|
|
573
|
+
setImmediate(async () => {
|
|
574
|
+
try {
|
|
575
|
+
const infoRes = await fetch(`${url}/info`, { signal: AbortSignal.timeout(3_000) });
|
|
576
|
+
if (infoRes.ok) {
|
|
577
|
+
const info = await infoRes.json();
|
|
578
|
+
const addrs = info?.p2p?.multiaddrs ?? [];
|
|
579
|
+
let dialed = false;
|
|
580
|
+
for (const ma of addrs) {
|
|
581
|
+
const ok = await dialP2PPeer(p2pNode, ma);
|
|
582
|
+
if (ok) {
|
|
583
|
+
console.log(`[peer] 🔗 P2P dial OK: ${ma}`);
|
|
584
|
+
dialed = true;
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (!dialed)
|
|
589
|
+
console.log(`[peer] ℹ️ P2P dial failed for ${url} (mDNS fallback)`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
catch { /* non-critical — HTTP gossip is the fallback */ }
|
|
593
|
+
});
|
|
594
|
+
}
|
|
568
595
|
json(res, 200, { ok: true, peers: peers.length, protocol_hash: PROTOCOL_HASH });
|
|
569
596
|
}
|
|
570
597
|
// ── GET /peers ─────────────────────────────────────────────────────────────────
|
|
@@ -897,6 +924,32 @@ export function startValidatorNode(port = PORT) {
|
|
|
897
924
|
return handleInfo(res, nodeKeypair);
|
|
898
925
|
if (cleanUrl === "/protocol" && req.method === "GET")
|
|
899
926
|
return handleProtocol(res);
|
|
927
|
+
// GET /network/stats — stats públicas para la landing page
|
|
928
|
+
if (cleanUrl === "/network/stats" && req.method === "GET") {
|
|
929
|
+
const p2pStats = p2pNode ? getP2PStats(p2pNode) : null;
|
|
930
|
+
const httpPeers = peers.length;
|
|
931
|
+
const libp2pPeers = p2pStats?.peers ?? 0;
|
|
932
|
+
return json(res, 200, {
|
|
933
|
+
node_did: nodeKeypair.did.slice(0, 20) + "...",
|
|
934
|
+
version: VERSION,
|
|
935
|
+
protocol_hash: PROTOCOL_HASH.slice(0, 16) + "...",
|
|
936
|
+
// identidades y reputación
|
|
937
|
+
verified_identities: Object.keys(nullifiers).length,
|
|
938
|
+
reputation_profiles: Object.keys(repStore).length,
|
|
939
|
+
// peers — HTTP gossip (funciona en todos los entornos)
|
|
940
|
+
known_peers: httpPeers,
|
|
941
|
+
// peers — libp2p P2P (requiere multicast/bootstrap; 0 en WSL2)
|
|
942
|
+
p2p_peers: libp2pPeers,
|
|
943
|
+
p2p_pubsub_peers: p2pStats?.pubsubPeers ?? 0,
|
|
944
|
+
p2p_enabled: !!p2pNode,
|
|
945
|
+
// total = max de ambas capas (HTTP gossip es el piso mínimo garantizado)
|
|
946
|
+
total_peers: Math.max(httpPeers, libp2pPeers),
|
|
947
|
+
// estado general
|
|
948
|
+
uptime_ms: Date.now() - (globalThis._startTime ?? Date.now()),
|
|
949
|
+
timestamp: Date.now(),
|
|
950
|
+
mcps_verified: null,
|
|
951
|
+
});
|
|
952
|
+
}
|
|
900
953
|
if (cleanUrl === "/verify" && req.method === "POST")
|
|
901
954
|
return handleVerify(req, res, nodeKeypair, ip);
|
|
902
955
|
if (cleanUrl === "/token/renew" && req.method === "POST")
|
|
@@ -1074,6 +1127,137 @@ export function startValidatorNode(port = PORT) {
|
|
|
1074
1127
|
return json(res, 500, { error: "Execute failed - timelock not expired or proposal not approved" });
|
|
1075
1128
|
return json(res, 200, { txHash, proposalId: body.proposalId, executed: true });
|
|
1076
1129
|
}
|
|
1130
|
+
// ── MCPRegistry — consulta pública ────────────────────────────────────────
|
|
1131
|
+
// GET /mcps/verified — lista todos los MCPs verificados on-chain
|
|
1132
|
+
if (cleanUrl === "/mcps/verified" && req.method === "GET") {
|
|
1133
|
+
const [verified, info] = await Promise.all([getVerifiedMCPEntries(), getRegistryInfo()]);
|
|
1134
|
+
return json(res, 200, {
|
|
1135
|
+
total: verified.length,
|
|
1136
|
+
registry: info,
|
|
1137
|
+
mcps: verified.map(e => ({
|
|
1138
|
+
address: e.address,
|
|
1139
|
+
name: e.name,
|
|
1140
|
+
url: e.url,
|
|
1141
|
+
category: e.category,
|
|
1142
|
+
description: e.description,
|
|
1143
|
+
verified_at: new Date(e.verifiedAt * 1000).toISOString(),
|
|
1144
|
+
badge: "✅ VERIFIED",
|
|
1145
|
+
})),
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
// GET /mcps/all — lista todos los MCPs (verificados + pendientes)
|
|
1149
|
+
if (cleanUrl === "/mcps/all" && req.method === "GET") {
|
|
1150
|
+
const [all, info] = await Promise.all([getAllMCPEntries(), getRegistryInfo()]);
|
|
1151
|
+
return json(res, 200, {
|
|
1152
|
+
total: all.length,
|
|
1153
|
+
registry: info,
|
|
1154
|
+
mcps: all.map(e => ({
|
|
1155
|
+
address: e.address,
|
|
1156
|
+
name: e.name,
|
|
1157
|
+
url: e.url,
|
|
1158
|
+
category: e.category,
|
|
1159
|
+
verified: e.verified,
|
|
1160
|
+
registered_at: new Date(e.registeredAt * 1000).toISOString(),
|
|
1161
|
+
verified_at: e.verifiedAt > 0 ? new Date(e.verifiedAt * 1000).toISOString() : null,
|
|
1162
|
+
revoked_at: e.revokedAt > 0 ? new Date(e.revokedAt * 1000).toISOString() : null,
|
|
1163
|
+
})),
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
// GET /mcps/status/:address — estado de un MCP específico
|
|
1167
|
+
if (cleanUrl.match(/^\/mcps\/status\/0x[0-9a-fA-F]{40}$/) && req.method === "GET") {
|
|
1168
|
+
const addr = cleanUrl.split("/").pop();
|
|
1169
|
+
const entry = await getMCPEntry(addr);
|
|
1170
|
+
if (!entry)
|
|
1171
|
+
return json(res, 404, { address: addr, registered: false, verified: false });
|
|
1172
|
+
return json(res, 200, {
|
|
1173
|
+
address: addr,
|
|
1174
|
+
name: entry.name,
|
|
1175
|
+
url: entry.url,
|
|
1176
|
+
category: entry.category,
|
|
1177
|
+
registered: true,
|
|
1178
|
+
verified: entry.verified,
|
|
1179
|
+
registered_at: new Date(entry.registeredAt * 1000).toISOString(),
|
|
1180
|
+
verified_at: entry.verifiedAt > 0 ? new Date(entry.verifiedAt * 1000).toISOString() : null,
|
|
1181
|
+
revoked_at: entry.revokedAt > 0 ? new Date(entry.revokedAt * 1000).toISOString() : null,
|
|
1182
|
+
badge: entry.verified ? "✅ VERIFIED by Soulprint" : "⏳ Registered — pending verification",
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
// ── MCPRegistry — admin (requiere ADMIN_TOKEN o ADMIN_PRIVATE_KEY) ──────
|
|
1186
|
+
// Verificar admin token (Bearer header o ADMIN_TOKEN env)
|
|
1187
|
+
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || "";
|
|
1188
|
+
function isAdmin() {
|
|
1189
|
+
if (!ADMIN_TOKEN)
|
|
1190
|
+
return !!process.env.ADMIN_PRIVATE_KEY; // fallback: solo key
|
|
1191
|
+
const auth = (req.headers["authorization"] ?? "");
|
|
1192
|
+
return auth === `Bearer ${ADMIN_TOKEN}`;
|
|
1193
|
+
}
|
|
1194
|
+
// POST /admin/mcp/verify — verificar un MCP
|
|
1195
|
+
if (cleanUrl === "/admin/mcp/verify" && req.method === "POST") {
|
|
1196
|
+
if (!isAdmin())
|
|
1197
|
+
return json(res, 401, { error: "Unauthorized — Bearer ADMIN_TOKEN required" });
|
|
1198
|
+
if (!process.env.ADMIN_PRIVATE_KEY)
|
|
1199
|
+
return json(res, 503, { error: "ADMIN_PRIVATE_KEY not configured" });
|
|
1200
|
+
const body = await readBody(req);
|
|
1201
|
+
if (!body?.address)
|
|
1202
|
+
return json(res, 400, { error: "Required: address" });
|
|
1203
|
+
const result = await verifyMCPOnChain(body.address);
|
|
1204
|
+
if (!result.success)
|
|
1205
|
+
return json(res, 500, { error: result.error });
|
|
1206
|
+
return json(res, 200, {
|
|
1207
|
+
address: body.address,
|
|
1208
|
+
verified: true,
|
|
1209
|
+
txHash: result.txHash,
|
|
1210
|
+
explorer: `https://sepolia.basescan.org/tx/${result.txHash}`,
|
|
1211
|
+
message: `✅ MCP ${body.address} verified on-chain by Soulprint`,
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
// POST /admin/mcp/revoke — revocar un MCP
|
|
1215
|
+
if (cleanUrl === "/admin/mcp/revoke" && req.method === "POST") {
|
|
1216
|
+
if (!isAdmin())
|
|
1217
|
+
return json(res, 401, { error: "Unauthorized — Bearer ADMIN_TOKEN required" });
|
|
1218
|
+
if (!process.env.ADMIN_PRIVATE_KEY)
|
|
1219
|
+
return json(res, 503, { error: "ADMIN_PRIVATE_KEY not configured" });
|
|
1220
|
+
const body = await readBody(req);
|
|
1221
|
+
if (!body?.address || !body?.reason)
|
|
1222
|
+
return json(res, 400, { error: "Required: address, reason" });
|
|
1223
|
+
const result = await revokeMCPOnChain(body.address, body.reason);
|
|
1224
|
+
if (!result.success)
|
|
1225
|
+
return json(res, 500, { error: result.error });
|
|
1226
|
+
return json(res, 200, {
|
|
1227
|
+
address: body.address,
|
|
1228
|
+
revoked: true,
|
|
1229
|
+
reason: body.reason,
|
|
1230
|
+
txHash: result.txHash,
|
|
1231
|
+
explorer: `https://sepolia.basescan.org/tx/${result.txHash}`,
|
|
1232
|
+
message: `🚫 MCP ${body.address} revoked. Reason: "${body.reason}"`,
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
// POST /admin/mcp/register — registrar MCP (permissionless, cualquiera)
|
|
1236
|
+
if (cleanUrl === "/admin/mcp/register" && req.method === "POST") {
|
|
1237
|
+
const body = await readBody(req);
|
|
1238
|
+
if (!body?.ownerKey || !body?.address || !body?.name || !body?.url) {
|
|
1239
|
+
return json(res, 400, { error: "Required: ownerKey, address, name, url" });
|
|
1240
|
+
}
|
|
1241
|
+
const result = await registerMCPOnChain({
|
|
1242
|
+
ownerPrivateKey: body.ownerKey,
|
|
1243
|
+
mcpAddress: body.address,
|
|
1244
|
+
name: body.name,
|
|
1245
|
+
url: body.url,
|
|
1246
|
+
did: body.did ?? "",
|
|
1247
|
+
category: body.category ?? "general",
|
|
1248
|
+
description: body.description ?? "",
|
|
1249
|
+
});
|
|
1250
|
+
if (!result.success)
|
|
1251
|
+
return json(res, 500, { error: result.error });
|
|
1252
|
+
return json(res, 201, {
|
|
1253
|
+
address: body.address,
|
|
1254
|
+
name: body.name,
|
|
1255
|
+
txHash: result.txHash,
|
|
1256
|
+
explorer: `https://sepolia.basescan.org/tx/${result.txHash}`,
|
|
1257
|
+
message: `✅ MCP registered. Contact Soulprint admin to verify.`,
|
|
1258
|
+
next_step: "POST /admin/mcp/verify with Bearer ADMIN_TOKEN",
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1077
1261
|
json(res, 404, { error: "Not found" });
|
|
1078
1262
|
});
|
|
1079
1263
|
server.listen(port, () => {
|
|
@@ -1108,6 +1292,13 @@ export function startValidatorNode(port = PORT) {
|
|
|
1108
1292
|
console.log(` GET /consensus/state-info handshake para state-sync`);
|
|
1109
1293
|
console.log(` GET /consensus/state bulk state sync paginado`);
|
|
1110
1294
|
console.log(` POST /consensus/message recibir msg PROPOSE/VOTE/COMMIT/ATTEST`);
|
|
1295
|
+
console.log(`\n MCPRegistry (on-chain, Base Sepolia):`);
|
|
1296
|
+
console.log(` GET /mcps/verified MCPs verificados por Soulprint`);
|
|
1297
|
+
console.log(` GET /mcps/all todos los MCPs registrados`);
|
|
1298
|
+
console.log(` GET /mcps/status/:address estado de un MCP`);
|
|
1299
|
+
console.log(` POST /admin/mcp/register registrar MCP (permissionless)`);
|
|
1300
|
+
console.log(` POST /admin/mcp/verify 🔐 verificar (Bearer ADMIN_TOKEN)`);
|
|
1301
|
+
console.log(` POST /admin/mcp/revoke 🔐 revocar (Bearer ADMIN_TOKEN)`);
|
|
1111
1302
|
console.log(`\n`);
|
|
1112
1303
|
});
|
|
1113
1304
|
return server;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "soulprint-network",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Soulprint validator node — HTTP server that verifies ZK proofs, co-signs SPTs, anti-Sybil registry",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -11,11 +11,6 @@
|
|
|
11
11
|
"dist",
|
|
12
12
|
"README.md"
|
|
13
13
|
],
|
|
14
|
-
"scripts": {
|
|
15
|
-
"build": "tsc && node scripts/compute-code-hash.mjs",
|
|
16
|
-
"start": "node dist/server.js",
|
|
17
|
-
"build:hash": "node scripts/compute-code-hash.mjs"
|
|
18
|
-
},
|
|
19
14
|
"publishConfig": {
|
|
20
15
|
"access": "public"
|
|
21
16
|
},
|
|
@@ -42,16 +37,18 @@
|
|
|
42
37
|
"@libp2p/identify": "3.0.39",
|
|
43
38
|
"@libp2p/kad-dht": "16.1.3",
|
|
44
39
|
"@libp2p/mdns": "11.0.47",
|
|
40
|
+
"@libp2p/peer-id": "^6.0.4",
|
|
45
41
|
"@libp2p/ping": "2.0.37",
|
|
46
42
|
"@libp2p/tcp": "10.1.19",
|
|
43
|
+
"@multiformats/multiaddr": "^13.0.1",
|
|
47
44
|
"ethers": "^6.16.0",
|
|
48
45
|
"libp2p": "2.10.0",
|
|
49
46
|
"nodemailer": "^8.0.1",
|
|
50
47
|
"otpauth": "^9.5.0",
|
|
51
48
|
"otplib": "^13.3.0",
|
|
52
|
-
"
|
|
53
|
-
"soulprint-
|
|
54
|
-
"
|
|
49
|
+
"uint8arrays": "5.1.0",
|
|
50
|
+
"soulprint-core": "0.1.11",
|
|
51
|
+
"soulprint-zkp": "0.1.5"
|
|
55
52
|
},
|
|
56
53
|
"devDependencies": {
|
|
57
54
|
"@types/node": "^20.0.0",
|
|
@@ -67,5 +64,10 @@
|
|
|
67
64
|
"import": "./dist/index.js",
|
|
68
65
|
"types": "./dist/index.d.ts"
|
|
69
66
|
}
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"build": "tsc && node scripts/compute-code-hash.mjs",
|
|
70
|
+
"start": "node dist/server.js",
|
|
71
|
+
"build:hash": "node scripts/compute-code-hash.mjs"
|
|
70
72
|
}
|
|
71
|
-
}
|
|
73
|
+
}
|