soulprint-network 0.3.2 → 0.3.3
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.
|
@@ -21,12 +21,35 @@
|
|
|
21
21
|
* SOULPRINT_REGISTRY_ADDR=0x...
|
|
22
22
|
* SOULPRINT_LEDGER_ADDR=0x...
|
|
23
23
|
*/
|
|
24
|
+
export declare const ProposalState: {
|
|
25
|
+
readonly ACTIVE: 0;
|
|
26
|
+
readonly APPROVED: 1;
|
|
27
|
+
readonly EXECUTED: 2;
|
|
28
|
+
readonly REJECTED: 3;
|
|
29
|
+
readonly EXPIRED: 4;
|
|
30
|
+
};
|
|
31
|
+
export type ProposalStateType = typeof ProposalState[keyof typeof ProposalState];
|
|
32
|
+
export interface GovernanceProposal {
|
|
33
|
+
id: number;
|
|
34
|
+
newHash: string;
|
|
35
|
+
rationale: string;
|
|
36
|
+
proposerDid: string;
|
|
37
|
+
proposerAddr: string;
|
|
38
|
+
createdAt: number;
|
|
39
|
+
approvedAt: number;
|
|
40
|
+
executedAt: number;
|
|
41
|
+
votesFor: number;
|
|
42
|
+
votesAgainst: number;
|
|
43
|
+
state: ProposalStateType;
|
|
44
|
+
stateName: string;
|
|
45
|
+
}
|
|
24
46
|
export interface BlockchainConfig {
|
|
25
47
|
rpcUrl: string;
|
|
26
48
|
privateKey: string;
|
|
27
49
|
registryAddr: string;
|
|
28
50
|
ledgerAddr: string;
|
|
29
51
|
validatorRegAddr?: string;
|
|
52
|
+
governanceAddr?: string;
|
|
30
53
|
protocolHash: string;
|
|
31
54
|
}
|
|
32
55
|
export interface OnChainReputation {
|
|
@@ -44,6 +67,7 @@ export declare class SoulprintBlockchainClient {
|
|
|
44
67
|
private registry;
|
|
45
68
|
private ledger;
|
|
46
69
|
private validatorReg;
|
|
70
|
+
private governance;
|
|
47
71
|
private connected;
|
|
48
72
|
constructor(config: BlockchainConfig);
|
|
49
73
|
/**
|
|
@@ -115,4 +139,56 @@ export declare class SoulprintBlockchainClient {
|
|
|
115
139
|
did: string;
|
|
116
140
|
compatible: boolean;
|
|
117
141
|
}>>;
|
|
142
|
+
/**
|
|
143
|
+
* Retorna el hash actualmente aprobado por governance.
|
|
144
|
+
* Los nodos deben comparar esto con su PROTOCOL_HASH al arrancar.
|
|
145
|
+
*/
|
|
146
|
+
getCurrentApprovedHash(): Promise<string | null>;
|
|
147
|
+
/**
|
|
148
|
+
* Verifica si el hash actual del nodo está aprobado por governance.
|
|
149
|
+
*/
|
|
150
|
+
isHashApproved(hash: string): Promise<boolean>;
|
|
151
|
+
/**
|
|
152
|
+
* Propone un upgrade del PROTOCOL_HASH.
|
|
153
|
+
* Solo validadores con identidad verificada pueden proponer.
|
|
154
|
+
*
|
|
155
|
+
* @returns txHash + proposalId, o null si falla
|
|
156
|
+
*/
|
|
157
|
+
proposeUpgrade(params: {
|
|
158
|
+
did: string;
|
|
159
|
+
newHash: string;
|
|
160
|
+
rationale: string;
|
|
161
|
+
}): Promise<{
|
|
162
|
+
txHash: string;
|
|
163
|
+
proposalId: number;
|
|
164
|
+
} | null>;
|
|
165
|
+
/**
|
|
166
|
+
* Vota en una propuesta de governance.
|
|
167
|
+
* @param approve true = a favor | false = en contra / veto
|
|
168
|
+
*/
|
|
169
|
+
voteOnProposal(params: {
|
|
170
|
+
proposalId: number;
|
|
171
|
+
did: string;
|
|
172
|
+
approve: boolean;
|
|
173
|
+
}): Promise<string | null>;
|
|
174
|
+
/**
|
|
175
|
+
* Ejecuta una propuesta aprobada una vez que el timelock expiró.
|
|
176
|
+
*/
|
|
177
|
+
executeProposal(proposalId: number): Promise<string | null>;
|
|
178
|
+
/**
|
|
179
|
+
* Retorna estado completo de una propuesta.
|
|
180
|
+
*/
|
|
181
|
+
getProposal(proposalId: number): Promise<GovernanceProposal | null>;
|
|
182
|
+
/**
|
|
183
|
+
* Lista todas las propuestas activas o en timelock.
|
|
184
|
+
*/
|
|
185
|
+
getActiveProposals(): Promise<GovernanceProposal[]>;
|
|
186
|
+
/**
|
|
187
|
+
* Historial de todos los hashes aprobados (auditoría).
|
|
188
|
+
*/
|
|
189
|
+
getHashHistory(): Promise<string[]>;
|
|
190
|
+
/**
|
|
191
|
+
* Segundos restantes del timelock de una propuesta aprobada.
|
|
192
|
+
*/
|
|
193
|
+
getTimelockRemaining(proposalId: number): Promise<number>;
|
|
118
194
|
}
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
* SOULPRINT_LEDGER_ADDR=0x...
|
|
23
23
|
*/
|
|
24
24
|
import { existsSync, readFileSync } from "node:fs";
|
|
25
|
-
import { join } from "node:path";
|
|
25
|
+
import { join, dirname } from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
// ESM-compatible __dirname
|
|
28
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
26
29
|
// ── ABI mínimos para interactuar con los contratos ────────────────────────────
|
|
27
30
|
// Solo las funciones que usa el nodo validador
|
|
28
31
|
const REGISTRY_ABI = [
|
|
@@ -45,7 +48,43 @@ const VALIDATOR_REG_ABI = [
|
|
|
45
48
|
"function heartbeat(string did, uint32 totalVerified) external",
|
|
46
49
|
"function getActiveNodes() view returns (tuple(string url, string did, bytes32 protocolHash, uint64 registeredAt, uint64 lastSeen, uint32 totalVerified, bool active, bool compatible)[])",
|
|
47
50
|
"function PROTOCOL_HASH() view returns (bytes32)",
|
|
51
|
+
"function compatibleNodes() view returns (uint256)",
|
|
48
52
|
];
|
|
53
|
+
const GOVERNANCE_ABI = [
|
|
54
|
+
// State
|
|
55
|
+
"function currentApprovedHash() view returns (bytes32)",
|
|
56
|
+
"function totalProposals() view returns (uint256)",
|
|
57
|
+
"function APPROVAL_THRESHOLD_BPS() view returns (uint256)",
|
|
58
|
+
"function VETO_THRESHOLD_BPS() view returns (uint256)",
|
|
59
|
+
"function TIMELOCK_DELAY() view returns (uint64)",
|
|
60
|
+
"function MINIMUM_QUORUM() view returns (uint32)",
|
|
61
|
+
// Actions
|
|
62
|
+
"function proposeUpgrade(string did, bytes32 newHash, string rationale) external returns (uint256)",
|
|
63
|
+
"function voteOnProposal(uint256 proposalId, string did, bool approve) external",
|
|
64
|
+
"function executeProposal(uint256 proposalId) external",
|
|
65
|
+
// Views
|
|
66
|
+
"function getProposal(uint256 id) view returns (tuple(uint256 id, bytes32 newHash, string rationale, string proposerDid, address proposerAddr, uint64 createdAt, uint64 approvedAt, uint64 executedAt, uint32 votesFor, uint32 votesAgainst, uint8 state))",
|
|
67
|
+
"function getActiveProposals() view returns (tuple(uint256 id, bytes32 newHash, string rationale, string proposerDid, address proposerAddr, uint64 createdAt, uint64 approvedAt, uint64 executedAt, uint32 votesFor, uint32 votesAgainst, uint8 state)[])",
|
|
68
|
+
"function getHashHistory() view returns (bytes32[])",
|
|
69
|
+
"function isCurrentHashValid(bytes32 hash) view returns (bool)",
|
|
70
|
+
"function getApprovalPercentage(uint256 id) view returns (uint256 forPct, uint256 againstPct, uint32 activeNodes)",
|
|
71
|
+
"function timelockRemaining(uint256 id) view returns (uint64)",
|
|
72
|
+
// Events
|
|
73
|
+
"event ProposalCreated(uint256 indexed id, bytes32 newHash, string proposerDid, string rationale, uint64 expiresAt)",
|
|
74
|
+
"event VoteCast(uint256 indexed proposalId, string voterDid, bool approve, uint32 totalFor, uint32 totalAgainst)",
|
|
75
|
+
"event ProposalApproved(uint256 indexed id, bytes32 newHash, uint64 executeAfter)",
|
|
76
|
+
"event ProposalExecuted(uint256 indexed id, bytes32 oldHash, bytes32 newHash, uint64 timestamp)",
|
|
77
|
+
"event ProposalRejected(uint256 indexed id, string reason)",
|
|
78
|
+
"event EmergencyVeto(uint256 indexed id, uint32 vetoes, uint32 totalVoters)",
|
|
79
|
+
];
|
|
80
|
+
// ── ProposalState enum (mirror de Solidity) ───────────────────────────────────
|
|
81
|
+
export const ProposalState = {
|
|
82
|
+
ACTIVE: 0,
|
|
83
|
+
APPROVED: 1,
|
|
84
|
+
EXECUTED: 2,
|
|
85
|
+
REJECTED: 3,
|
|
86
|
+
EXPIRED: 4,
|
|
87
|
+
};
|
|
49
88
|
// ── Load config from env + deployments ───────────────────────────────────────
|
|
50
89
|
export function loadBlockchainConfig() {
|
|
51
90
|
const rpcUrl = process.env.SOULPRINT_RPC_URL;
|
|
@@ -54,7 +93,7 @@ export function loadBlockchainConfig() {
|
|
|
54
93
|
if (!rpcUrl || !privateKey)
|
|
55
94
|
return null;
|
|
56
95
|
// Buscar direcciones en deployments/
|
|
57
|
-
const deploymentsDir = join(
|
|
96
|
+
const deploymentsDir = join(__dir, "..", "..", "..", "blockchain", "deployments");
|
|
58
97
|
const deployFile = join(deploymentsDir, `${network}.json`);
|
|
59
98
|
if (!existsSync(deployFile)) {
|
|
60
99
|
console.warn(`[blockchain] No deployment found for ${network}. Run: npx hardhat run scripts/deploy.ts --network ${network}`);
|
|
@@ -67,6 +106,7 @@ export function loadBlockchainConfig() {
|
|
|
67
106
|
registryAddr: deployment.contracts.SoulprintRegistry,
|
|
68
107
|
ledgerAddr: deployment.contracts.AttestationLedger,
|
|
69
108
|
validatorRegAddr: deployment.contracts.ValidatorRegistry,
|
|
109
|
+
governanceAddr: deployment.contracts.GovernanceModule,
|
|
70
110
|
protocolHash: deployment.protocolHash,
|
|
71
111
|
};
|
|
72
112
|
}
|
|
@@ -78,6 +118,7 @@ export class SoulprintBlockchainClient {
|
|
|
78
118
|
registry = null;
|
|
79
119
|
ledger = null;
|
|
80
120
|
validatorReg = null;
|
|
121
|
+
governance = null;
|
|
81
122
|
connected = false;
|
|
82
123
|
constructor(config) {
|
|
83
124
|
this.config = config;
|
|
@@ -100,6 +141,12 @@ export class SoulprintBlockchainClient {
|
|
|
100
141
|
if (this.config.validatorRegAddr) {
|
|
101
142
|
this.validatorReg = new ethers.Contract(this.config.validatorRegAddr, VALIDATOR_REG_ABI, this.signer);
|
|
102
143
|
}
|
|
144
|
+
if (this.config.governanceAddr) {
|
|
145
|
+
this.governance = new ethers.Contract(this.config.governanceAddr, GOVERNANCE_ABI, this.signer);
|
|
146
|
+
const govHash = await this.governance.currentApprovedHash();
|
|
147
|
+
console.log(`[blockchain] Governance: ${this.config.governanceAddr}`);
|
|
148
|
+
console.log(`[blockchain] Current approved hash: ${govHash.slice(0, 10)}...`);
|
|
149
|
+
}
|
|
103
150
|
// Verificar que el contrato tiene el mismo PROTOCOL_HASH
|
|
104
151
|
const onChainHash = await this.registry.PROTOCOL_HASH();
|
|
105
152
|
if (onChainHash.toLowerCase() !== this.config.protocolHash.toLowerCase()) {
|
|
@@ -277,4 +324,177 @@ export class SoulprintBlockchainClient {
|
|
|
277
324
|
return [];
|
|
278
325
|
}
|
|
279
326
|
}
|
|
327
|
+
// ── Governance ─────────────────────────────────────────────────────────────
|
|
328
|
+
/**
|
|
329
|
+
* Retorna el hash actualmente aprobado por governance.
|
|
330
|
+
* Los nodos deben comparar esto con su PROTOCOL_HASH al arrancar.
|
|
331
|
+
*/
|
|
332
|
+
async getCurrentApprovedHash() {
|
|
333
|
+
if (!this.connected || !this.governance)
|
|
334
|
+
return null;
|
|
335
|
+
try {
|
|
336
|
+
return await this.governance.currentApprovedHash();
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Verifica si el hash actual del nodo está aprobado por governance.
|
|
344
|
+
*/
|
|
345
|
+
async isHashApproved(hash) {
|
|
346
|
+
if (!this.connected || !this.governance)
|
|
347
|
+
return true; // fallback: asumir OK
|
|
348
|
+
try {
|
|
349
|
+
return await this.governance.isCurrentHashValid(hash);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Propone un upgrade del PROTOCOL_HASH.
|
|
357
|
+
* Solo validadores con identidad verificada pueden proponer.
|
|
358
|
+
*
|
|
359
|
+
* @returns txHash + proposalId, o null si falla
|
|
360
|
+
*/
|
|
361
|
+
async proposeUpgrade(params) {
|
|
362
|
+
if (!this.connected || !this.governance)
|
|
363
|
+
return null;
|
|
364
|
+
try {
|
|
365
|
+
const tx = await this.governance.proposeUpgrade(params.did, params.newHash, params.rationale);
|
|
366
|
+
const receipt = await tx.wait();
|
|
367
|
+
// Extraer proposalId del evento ProposalCreated
|
|
368
|
+
const log = receipt.logs?.find((l) => l.fragment?.name === "ProposalCreated");
|
|
369
|
+
const proposalId = log ? Number(log.args[0]) : -1;
|
|
370
|
+
console.log(`[governance] ✅ Proposal #${proposalId} created | tx: ${receipt.hash}`);
|
|
371
|
+
return { txHash: receipt.hash, proposalId };
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
console.error(`[governance] proposeUpgrade failed: ${err.message?.slice(0, 100)}`);
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Vota en una propuesta de governance.
|
|
380
|
+
* @param approve true = a favor | false = en contra / veto
|
|
381
|
+
*/
|
|
382
|
+
async voteOnProposal(params) {
|
|
383
|
+
if (!this.connected || !this.governance)
|
|
384
|
+
return null;
|
|
385
|
+
try {
|
|
386
|
+
const tx = await this.governance.voteOnProposal(params.proposalId, params.did, params.approve);
|
|
387
|
+
const receipt = await tx.wait();
|
|
388
|
+
const action = params.approve ? "✅ FOR" : "🚫 AGAINST";
|
|
389
|
+
console.log(`[governance] ${action} Proposal #${params.proposalId} | tx: ${receipt.hash}`);
|
|
390
|
+
return receipt.hash;
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
console.error(`[governance] voteOnProposal failed: ${err.message?.slice(0, 100)}`);
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Ejecuta una propuesta aprobada una vez que el timelock expiró.
|
|
399
|
+
*/
|
|
400
|
+
async executeProposal(proposalId) {
|
|
401
|
+
if (!this.connected || !this.governance)
|
|
402
|
+
return null;
|
|
403
|
+
try {
|
|
404
|
+
const tx = await this.governance.executeProposal(proposalId);
|
|
405
|
+
const receipt = await tx.wait();
|
|
406
|
+
console.log(`[governance] ✅ Proposal #${proposalId} EXECUTED | tx: ${receipt.hash}`);
|
|
407
|
+
return receipt.hash;
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
console.error(`[governance] executeProposal failed: ${err.message?.slice(0, 100)}`);
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Retorna estado completo de una propuesta.
|
|
416
|
+
*/
|
|
417
|
+
async getProposal(proposalId) {
|
|
418
|
+
if (!this.connected || !this.governance)
|
|
419
|
+
return null;
|
|
420
|
+
try {
|
|
421
|
+
// Verificar que la propuesta existe (Solidity retorna zero-struct si no existe)
|
|
422
|
+
const total = Number(await this.governance.totalProposals());
|
|
423
|
+
if (proposalId >= total)
|
|
424
|
+
return null;
|
|
425
|
+
const STATE_NAMES = ["ACTIVE", "APPROVED", "EXECUTED", "REJECTED", "EXPIRED"];
|
|
426
|
+
const p = await this.governance.getProposal(proposalId);
|
|
427
|
+
return {
|
|
428
|
+
id: Number(p.id),
|
|
429
|
+
newHash: p.newHash,
|
|
430
|
+
rationale: p.rationale,
|
|
431
|
+
proposerDid: p.proposerDid,
|
|
432
|
+
proposerAddr: p.proposerAddr,
|
|
433
|
+
createdAt: Number(p.createdAt),
|
|
434
|
+
approvedAt: Number(p.approvedAt),
|
|
435
|
+
executedAt: Number(p.executedAt),
|
|
436
|
+
votesFor: Number(p.votesFor),
|
|
437
|
+
votesAgainst: Number(p.votesAgainst),
|
|
438
|
+
state: Number(p.state),
|
|
439
|
+
stateName: STATE_NAMES[Number(p.state)] ?? "UNKNOWN",
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Lista todas las propuestas activas o en timelock.
|
|
448
|
+
*/
|
|
449
|
+
async getActiveProposals() {
|
|
450
|
+
if (!this.connected || !this.governance)
|
|
451
|
+
return [];
|
|
452
|
+
try {
|
|
453
|
+
const STATE_NAMES = ["ACTIVE", "APPROVED", "EXECUTED", "REJECTED", "EXPIRED"];
|
|
454
|
+
const proposals = await this.governance.getActiveProposals();
|
|
455
|
+
return proposals.map((p) => ({
|
|
456
|
+
id: Number(p.id),
|
|
457
|
+
newHash: p.newHash,
|
|
458
|
+
rationale: p.rationale,
|
|
459
|
+
proposerDid: p.proposerDid,
|
|
460
|
+
proposerAddr: p.proposerAddr,
|
|
461
|
+
createdAt: Number(p.createdAt),
|
|
462
|
+
approvedAt: Number(p.approvedAt),
|
|
463
|
+
executedAt: Number(p.executedAt),
|
|
464
|
+
votesFor: Number(p.votesFor),
|
|
465
|
+
votesAgainst: Number(p.votesAgainst),
|
|
466
|
+
state: Number(p.state),
|
|
467
|
+
stateName: STATE_NAMES[Number(p.state)] ?? "UNKNOWN",
|
|
468
|
+
}));
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
return [];
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Historial de todos los hashes aprobados (auditoría).
|
|
476
|
+
*/
|
|
477
|
+
async getHashHistory() {
|
|
478
|
+
if (!this.connected || !this.governance)
|
|
479
|
+
return [];
|
|
480
|
+
try {
|
|
481
|
+
return await this.governance.getHashHistory();
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
return [];
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Segundos restantes del timelock de una propuesta aprobada.
|
|
489
|
+
*/
|
|
490
|
+
async getTimelockRemaining(proposalId) {
|
|
491
|
+
if (!this.connected || !this.governance)
|
|
492
|
+
return 0;
|
|
493
|
+
try {
|
|
494
|
+
return Number(await this.governance.timelockRemaining(proposalId));
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return 0;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
280
500
|
}
|
package/dist/validator.js
CHANGED
|
@@ -9,6 +9,7 @@ import { encryptGossip, decryptGossip } from "./crypto/gossip-cipher.js";
|
|
|
9
9
|
import { selectGossipPeers, routingStats } from "./crypto/peer-router.js";
|
|
10
10
|
import { NullifierConsensus, AttestationConsensus, StateSyncManager } from "./consensus/index.js";
|
|
11
11
|
import { BlockchainAnchor } from "./blockchain/blockchain-anchor.js";
|
|
12
|
+
import { SoulprintBlockchainClient, loadBlockchainConfig, } from "./blockchain/blockchain-client.js";
|
|
12
13
|
import { publishAttestationP2P, onAttestationReceived, getP2PStats, } from "./p2p.js";
|
|
13
14
|
// ── Config ────────────────────────────────────────────────────────────────────
|
|
14
15
|
const PORT = parseInt(process.env.SOULPRINT_PORT ?? String(PROTOCOL.DEFAULT_HTTP_PORT));
|
|
@@ -668,8 +669,15 @@ export function startValidatorNode(port = PORT) {
|
|
|
668
669
|
const anchor = new BlockchainAnchor({
|
|
669
670
|
storePath: join(NODE_DIR, "blockchain-queue"),
|
|
670
671
|
});
|
|
672
|
+
// Cliente blockchain directo (para governance)
|
|
673
|
+
const bcConfig = loadBlockchainConfig();
|
|
674
|
+
const client = bcConfig
|
|
675
|
+
? new SoulprintBlockchainClient(bcConfig)
|
|
676
|
+
: null;
|
|
671
677
|
// Conectar en background (no bloquea el arranque del nodo)
|
|
672
678
|
anchor.connect().catch(() => { });
|
|
679
|
+
if (client)
|
|
680
|
+
client.connect().catch(() => { });
|
|
673
681
|
// Escuchar eventos del consenso y anclar async
|
|
674
682
|
nullifierConsensus.on("committed", (entry) => {
|
|
675
683
|
anchor.anchorNullifier({
|
|
@@ -796,6 +804,89 @@ export function startValidatorNode(port = PORT) {
|
|
|
796
804
|
return json(res, 400, { error: "Invalid consensus message" });
|
|
797
805
|
}
|
|
798
806
|
}
|
|
807
|
+
// ── Governance endpoints ──────────────────────────────────────────────────
|
|
808
|
+
// GET /governance — estado del hash aprobado + propuestas activas
|
|
809
|
+
if (cleanUrl === "/governance" && req.method === "GET") {
|
|
810
|
+
const [currentHash, active, history] = await Promise.all([
|
|
811
|
+
client?.getCurrentApprovedHash() ?? null,
|
|
812
|
+
client?.getActiveProposals() ?? [],
|
|
813
|
+
client?.getHashHistory() ?? [],
|
|
814
|
+
]);
|
|
815
|
+
return json(res, 200, {
|
|
816
|
+
currentApprovedHash: currentHash ?? PROTOCOL_HASH,
|
|
817
|
+
blockchainConnected: !!client?.isConnected,
|
|
818
|
+
activeProposals: active.length,
|
|
819
|
+
hashHistory: history,
|
|
820
|
+
nodeCompatible: !currentHash || currentHash.toLowerCase() === ("0x" + PROTOCOL_HASH).toLowerCase(),
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
// GET /governance/proposals — lista de propuestas activas
|
|
824
|
+
if (cleanUrl === "/governance/proposals" && req.method === "GET") {
|
|
825
|
+
if (!client?.isConnected)
|
|
826
|
+
return json(res, 503, { error: "Blockchain not connected" });
|
|
827
|
+
const proposals = await client.getActiveProposals();
|
|
828
|
+
return json(res, 200, { proposals, total: proposals.length });
|
|
829
|
+
}
|
|
830
|
+
// GET /governance/proposal/:id — detalle de una propuesta
|
|
831
|
+
if (cleanUrl.match(/^\/governance\/proposal\/\d+$/) && req.method === "GET") {
|
|
832
|
+
if (!client?.isConnected)
|
|
833
|
+
return json(res, 503, { error: "Blockchain not connected" });
|
|
834
|
+
const proposalId = parseInt(cleanUrl.split("/").pop());
|
|
835
|
+
const proposal = await client.getProposal(proposalId);
|
|
836
|
+
if (!proposal)
|
|
837
|
+
return json(res, 404, { error: "Proposal not found" });
|
|
838
|
+
const remaining = await client.getTimelockRemaining(proposalId);
|
|
839
|
+
return json(res, 200, { ...proposal, timelockRemainingSeconds: remaining });
|
|
840
|
+
}
|
|
841
|
+
// POST /governance/propose — proponer upgrade del PROTOCOL_HASH
|
|
842
|
+
// Body: { did, newHash, rationale }
|
|
843
|
+
if (cleanUrl === "/governance/propose" && req.method === "POST") {
|
|
844
|
+
if (!client?.isConnected)
|
|
845
|
+
return json(res, 503, { error: "Blockchain not connected" });
|
|
846
|
+
const body = await readBody(req);
|
|
847
|
+
if (!body?.did || !body?.newHash || !body?.rationale) {
|
|
848
|
+
return json(res, 400, { error: "Required: did, newHash, rationale" });
|
|
849
|
+
}
|
|
850
|
+
const result = await client.proposeUpgrade({
|
|
851
|
+
did: body.did,
|
|
852
|
+
newHash: body.newHash,
|
|
853
|
+
rationale: body.rationale,
|
|
854
|
+
});
|
|
855
|
+
if (!result)
|
|
856
|
+
return json(res, 500, { error: "Proposal failed — check validator logs" });
|
|
857
|
+
return json(res, 201, result);
|
|
858
|
+
}
|
|
859
|
+
// POST /governance/vote — votar en una propuesta
|
|
860
|
+
// Body: { proposalId, did, approve }
|
|
861
|
+
if (cleanUrl === "/governance/vote" && req.method === "POST") {
|
|
862
|
+
if (!client?.isConnected)
|
|
863
|
+
return json(res, 503, { error: "Blockchain not connected" });
|
|
864
|
+
const body = await readBody(req);
|
|
865
|
+
if (body?.proposalId === undefined || !body?.did || body?.approve === undefined) {
|
|
866
|
+
return json(res, 400, { error: "Required: proposalId, did, approve" });
|
|
867
|
+
}
|
|
868
|
+
const txHash = await client.voteOnProposal({
|
|
869
|
+
proposalId: Number(body.proposalId),
|
|
870
|
+
did: body.did,
|
|
871
|
+
approve: Boolean(body.approve),
|
|
872
|
+
});
|
|
873
|
+
if (!txHash)
|
|
874
|
+
return json(res, 500, { error: "Vote failed — check validator logs" });
|
|
875
|
+
return json(res, 200, { txHash, proposalId: body.proposalId, approve: body.approve });
|
|
876
|
+
}
|
|
877
|
+
// POST /governance/execute — ejecutar propuesta post-timelock
|
|
878
|
+
// Body: { proposalId }
|
|
879
|
+
if (cleanUrl === "/governance/execute" && req.method === "POST") {
|
|
880
|
+
if (!client?.isConnected)
|
|
881
|
+
return json(res, 503, { error: "Blockchain not connected" });
|
|
882
|
+
const body = await readBody(req);
|
|
883
|
+
if (body?.proposalId === undefined)
|
|
884
|
+
return json(res, 400, { error: "Required: proposalId" });
|
|
885
|
+
const txHash = await client.executeProposal(Number(body.proposalId));
|
|
886
|
+
if (!txHash)
|
|
887
|
+
return json(res, 500, { error: "Execute failed — timelock not expired or proposal not approved" });
|
|
888
|
+
return json(res, 200, { txHash, proposalId: body.proposalId, executed: true });
|
|
889
|
+
}
|
|
799
890
|
json(res, 404, { error: "Not found" });
|
|
800
891
|
});
|
|
801
892
|
server.listen(port, () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "soulprint-network",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"@libp2p/mdns": "11.0.47",
|
|
44
44
|
"@libp2p/ping": "2.0.37",
|
|
45
45
|
"@libp2p/tcp": "10.1.19",
|
|
46
|
+
"ethers": "^6.16.0",
|
|
46
47
|
"libp2p": "2.10.0",
|
|
47
48
|
"nodemailer": "^8.0.1",
|
|
48
49
|
"otpauth": "^9.5.0",
|