Spaces:
Runtime error
Runtime error
| /** | |
| * P2PCLAW Blockchain Registry Service | |
| * ===================================== | |
| * Anchors research paper hashes to multiple EVM blockchains for | |
| * permanent, immutable, trustless proof-of-existence. | |
| * | |
| * Storage method: 0-value transactions with JSON metadata in the data field. | |
| * No smart contract needed — the transaction hash IS the proof. | |
| * | |
| * Supported chains (in priority order): | |
| * 1. Polygon PoS mainnet — MATIC_RPC_URL + AGENT_PRIVATE_KEY (cheapest, ~$0.001) | |
| * 2. Ethereum Sepolia — ETH_SEPOLIA_RPC + AGENT_PRIVATE_KEY (free testnet) | |
| * 3. Base L2 mainnet — BASE_RPC_URL + AGENT_PRIVATE_KEY (Ethereum L2, ~$0.001) | |
| * | |
| * Environment variables: | |
| * AGENT_PRIVATE_KEY or API_PRIVATE_KEY — EVM wallet private key (same for all chains) | |
| * MATIC_RPC_URL — Polygon RPC (default: https://polygon-rpc.com/) | |
| * ETH_SEPOLIA_RPC — Sepolia RPC (default: https://rpc.sepolia.org) | |
| * BASE_RPC_URL — Base L2 RPC (default: https://mainnet.base.org) | |
| */ | |
| import { ethers } from 'ethers'; | |
| import crypto from 'crypto'; | |
| const PRIVATE_KEY = process.env.AGENT_PRIVATE_KEY || process.env.API_PRIVATE_KEY; | |
| const CHAINS = [ | |
| { | |
| id: 'polygon', | |
| name: 'Polygon PoS', | |
| rpc: process.env.MATIC_RPC_URL || 'https://polygon-rpc.com/', | |
| enabled: !!PRIVATE_KEY, | |
| }, | |
| { | |
| id: 'sepolia', | |
| name: 'Ethereum Sepolia', | |
| rpc: process.env.ETH_SEPOLIA_RPC || 'https://rpc.sepolia.org', | |
| enabled: !!PRIVATE_KEY, | |
| }, | |
| { | |
| id: 'base', | |
| name: 'Base L2', | |
| rpc: process.env.BASE_RPC_URL || 'https://mainnet.base.org', | |
| enabled: !!PRIVATE_KEY, | |
| }, | |
| ]; | |
| // Wallet cache per chain | |
| const _wallets = {}; | |
| async function getWallet(chain) { | |
| if (_wallets[chain.id]) return _wallets[chain.id]; | |
| if (!PRIVATE_KEY) return null; | |
| try { | |
| const provider = new ethers.providers.JsonRpcProvider(chain.rpc); | |
| const wallet = new ethers.Wallet(PRIVATE_KEY, provider); | |
| _wallets[chain.id] = wallet; | |
| return wallet; | |
| } catch (e) { | |
| console.error(`[BLOCKCHAIN] ❌ ${chain.name} wallet init failed: ${e.message}`); | |
| return null; | |
| } | |
| } | |
| /** | |
| * Compute a SHA-256 content hash for a paper (deterministic, chain-agnostic). | |
| */ | |
| function contentHash(title, content, paperId) { | |
| return crypto.createHash('sha256') | |
| .update(`${paperId}:${title}:${content}`) | |
| .digest('hex'); | |
| } | |
| /** | |
| * Anchor a paper to a single chain. Returns tx hash or null on failure. | |
| */ | |
| async function anchorToChain(chain, paperId, title, content, ipfsCid, authorId) { | |
| const wallet = await getWallet(chain); | |
| if (!wallet) return null; | |
| const hash = contentHash(title, content, paperId); | |
| const metadata = { | |
| v: 2, | |
| network: 'P2PCLAW', | |
| paper_id: paperId, | |
| title: title.slice(0, 120), | |
| sha256: hash, | |
| ipfs: ipfsCid || null, | |
| author: authorId, | |
| ts: Date.now(), | |
| }; | |
| const hexData = ethers.utils.hexlify( | |
| ethers.utils.toUtf8Bytes(JSON.stringify(metadata)) | |
| ); | |
| try { | |
| const tx = await wallet.sendTransaction({ | |
| to: wallet.address, | |
| value: 0, | |
| data: hexData, | |
| }); | |
| console.log(`[BLOCKCHAIN] ✅ ${chain.name} — paper ${paperId} → tx ${tx.hash}`); | |
| return tx.hash; | |
| } catch (e) { | |
| // Log warning but do not throw — blockchain failure must never block paper publishing | |
| console.warn(`[BLOCKCHAIN] ⚠️ ${chain.name} tx failed for ${paperId}: ${e.message}`); | |
| return null; | |
| } | |
| } | |
| /** | |
| * Register a paper on all configured chains (fire-and-forget, non-blocking). | |
| * | |
| * @param {string} paperId — Internal paper ID | |
| * @param {string} title — Paper title | |
| * @param {string} content — Full paper content (used for hash) | |
| * @param {string} ipfsCid — IPFS CID (optional) | |
| * @param {string} authorId — Agent ID of the author | |
| * @returns {Object} { polygon, sepolia, base, sha256 } — tx hashes per chain (null if skipped) | |
| */ | |
| export async function registerPaperOnChain(paperId, title, content, ipfsCid, authorId) { | |
| if (!PRIVATE_KEY) { | |
| console.log('[BLOCKCHAIN] ℹ️ No wallet key set (AGENT_PRIVATE_KEY). Blockchain anchoring disabled.'); | |
| return null; | |
| } | |
| const hash = contentHash(title, content || '', paperId); | |
| console.log(`[BLOCKCHAIN] 📝 Anchoring paper ${paperId} (sha256=${hash.slice(0, 16)}…)`); | |
| // Run all chains in parallel; failures are isolated | |
| const results = await Promise.allSettled( | |
| CHAINS.filter(c => c.enabled).map(chain => | |
| anchorToChain(chain, paperId, title, content, ipfsCid, authorId) | |
| ) | |
| ); | |
| const txMap = {}; | |
| CHAINS.filter(c => c.enabled).forEach((chain, i) => { | |
| txMap[chain.id] = results[i].status === 'fulfilled' ? results[i].value : null; | |
| }); | |
| const successCount = Object.values(txMap).filter(Boolean).length; | |
| console.log(`[BLOCKCHAIN] ${successCount}/${CHAINS.filter(c=>c.enabled).length} chains anchored. sha256=${hash}`); | |
| return { ...txMap, sha256: hash }; | |
| } | |
| /** | |
| * Backwards-compatible alias (old signature: title, arweaveTxId, leanProofHash, authorId) | |
| * Used by consensusService.js's existing call site. | |
| */ | |
| export async function registerPaperOnChainLegacy(title, arweaveTxId, leanProofHash, authorId) { | |
| // Old call site doesn't have paperId/content — stub with available data | |
| const paperId = `legacy-${Date.now()}`; | |
| return registerPaperOnChain(paperId, title, leanProofHash || '', arweaveTxId, authorId); | |
| } | |
| // Init log | |
| if (PRIVATE_KEY) { | |
| console.log('[BLOCKCHAIN] 🔗 Wallet key found — multi-chain anchoring enabled (Polygon + Sepolia + Base)'); | |
| // Log wallet address once | |
| const provider = new ethers.providers.JsonRpcProvider(CHAINS[0].rpc); | |
| const wallet = new ethers.Wallet(PRIVATE_KEY, provider); | |
| console.log(`[BLOCKCHAIN] 🔑 Wallet address: ${wallet.address}`); | |
| _wallets['polygon'] = wallet; | |
| } else { | |
| console.log('[BLOCKCHAIN] ℹ️ Set AGENT_PRIVATE_KEY on Railway to enable multi-chain paper anchoring.'); | |
| } | |