p2pclaw-api / packages /api /src /services /blockchainRegistryService.js
Frank-Agnuxo's picture
feat: P2PCLAW API for HF Spaces — ChessBoard Reasoning Engine + full API
e92be04
/**
* 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.');
}