import { PaperPublisher } from "../PaperPublisher.js"; import { Archivist } from "../Archivist.js"; import { create } from 'ipfs-http-client'; import Irys from "@irys/sdk"; import FormData from "form-data"; const MOLT_KEY = process.env.MOLTBOOK_API_KEY || ""; const publisher = new PaperPublisher(MOLT_KEY); // Cache for Phase 45 optimization let cachedBackupMeta = null; const ipfsClient = create({ host: 'api.pinata.cloud', port: 443, protocol: 'https', headers: { authorization: `Bearer ${process.env.PINATA_JWT || ''}` } }); // Export instances and functions export { publisher, cachedBackupMeta, Archivist, ipfsClient }; // Function to update cachedBackupMeta export function updateCachedBackupMeta(meta) { cachedBackupMeta = meta; } // ─── Arweave Upload (Irys) ────────────────────────────────────────────────── export async function archiveToArweave(paperContent, paperId) { if (process.env.PUBLISHED_PAPER_ARWEAVE_ENABLED !== 'true') return null; const privateKey = process.env.AGENT_PRIVATE_KEY || process.env.API_PRIVATE_KEY; if (!privateKey) { console.warn("[ARWEAVE] ⚠️ No private key found. Arweave archiving disabled."); return null; } try { const url = process.env.IRYS_NETWORK === 'mainnet' ? "https://node1.irys.xyz" : "https://devnet.irys.xyz"; const irys = new Irys({ url, token: "matic", key: privateKey, }); // 1. Calculate price const size = Buffer.byteLength(paperContent, 'utf8'); const price = await irys.getPrice(size); // 2. Fund node if necessary (Irys automatically checks if funded) await irys.fund(price); // 3. Upload data const tags = [ { name: "Content-Type", value: "text/markdown" }, { name: "App-Name", value: "P2PCLAW V3" }, { name: "Paper-ID", value: paperId } ]; console.log(`[ARWEAVE] 📝 Uploading paper ${paperId} (Size: ${size} bytes)...`); const receipt = await irys.upload(paperContent, { tags }); console.log(`[ARWEAVE] ✅ Paper secured for 200+ years. TXID: ${receipt.id}`); return receipt.id; } catch (e) { console.error(`[ARWEAVE] ❌ Archiving failed:`, e.message); return null; } } export async function publishToIpfsWithRetry(title, content, author, maxAttempts = 3) { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { const storage = await publisher.publish(title, content, author || 'Hive-Agent'); if (storage.cid) { console.log(`[IPFS] Published successfully on attempt ${attempt}. CID: ${storage.cid}`); return { cid: storage.cid, html: storage.html }; } } catch (e) { const delay = attempt * 3000; // 3s, 6s, 9s console.warn(`[IPFS] Attempt ${attempt}/${maxAttempts} failed: ${e.message}. Retrying in ${delay}ms...`); if (attempt < maxAttempts) await new Promise(r => setTimeout(r, delay)); } } console.warn('[IPFS] All attempts failed. Paper stored in P2P mesh only.'); return { cid: null, html: null }; } /** * Migrate existing papers that have no ipfs_cid to IPFS (Pinata). * Called once on API boot. Passes the Gun.js `db` instance so it can * update the paper node after a successful pin. */ export async function migrateExistingPapersToIPFS(db) { if (process.env.PINATA_PAPERS_ENABLED !== 'true') { console.log('[IPFS-MIGRATE] Paper pinning disabled (PINATA_PAPERS_ENABLED!=true). Skipping.'); return; } if (!process.env.PINATA_JWT) { console.warn('[IPFS-MIGRATE] No PINATA_JWT — skipping migration.'); return; } console.log('[IPFS-MIGRATE] Scanning papers without ipfs_cid...'); const candidates = await new Promise(resolve => { const list = []; db.get('p2pclaw_papers_v4').map().once((data, id) => { if (data && data.content && !data.ipfs_cid && data.status !== 'PURGED' && data.status !== 'REJECTED') { list.push({ id, ...data }); } }); setTimeout(() => resolve(list), 4000); }); console.log(`[IPFS-MIGRATE] Found ${candidates.length} papers to migrate.`); for (const paper of candidates) { try { const cid = await archiveToIPFS(paper.content, paper.id); if (cid) { db.get('p2pclaw_papers_v4').get(paper.id).put({ ipfs_cid: cid, url_html: `https://ipfs.io/ipfs/${cid}` }); db.get('p2pclaw_mempool_v4').get(paper.id).put({ ipfs_cid: cid, url_html: `https://ipfs.io/ipfs/${cid}` }); console.log(`[IPFS-MIGRATE] ✅ ${paper.id} → ${cid}`); } } catch (e) { console.error(`[IPFS-MIGRATE] ❌ ${paper.id}: ${e.message}`); } // Throttle: 1 per second to avoid Pinata rate limits await new Promise(r => setTimeout(r, 1000)); } console.log('[IPFS-MIGRATE] Migration complete.'); } export async function archiveToIPFS(paperContent, paperId) { // DISABLED: Pinata free plan = 100 pins max. At ~500 papers/day the account // blocks in hours. Papers already persisted in Gun.js (real-time P2P) and // GitHub via syncPaperToGitHub() (free, unlimited text archive). // Pinata reserved for frontend static bundle ONLY (1 CID at a time). // Re-enable: set PINATA_PAPERS_ENABLED=true in Railway environment vars. if (process.env.PINATA_PAPERS_ENABLED !== 'true') { return null; } if (!process.env.PINATA_JWT) { console.warn('[IPFS] No PINATA_JWT — paper stored on P2P mesh only.'); return null; } try { // We use Pinata REST API directly to upload raw markdown rather than JSON. // This ensures gateways can render the .md directly. const { default: fetch } = await import('node-fetch'); const formData = new FormData(); formData.append('file', Buffer.from(paperContent, 'utf8'), { filename: `${paperId}.md`, contentType: 'text/markdown' }); const metadata = JSON.stringify({ name: `p2pclaw-paper-${paperId}`, keyvalues: { network: 'p2pclaw', type: 'research_paper' } }); formData.append('pinataMetadata', metadata); const options = JSON.stringify({ cidVersion: 1 }); formData.append('pinataOptions', options); const res = await fetch('https://api.pinata.cloud/pinning/pinFileToIPFS', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.PINATA_JWT}`, ...formData.getHeaders() }, body: formData }); if (!res.ok) { const err = await res.text(); console.error(`[IPFS] Pinata error ${res.status}: ${err.slice(0, 200)}`); return null; } const data = await res.json(); const cid = data.IpfsHash; console.log(`[IPFS] Pinata archive OK. CID: ${cid}`); return cid; } catch (error) { console.error('[IPFS] Pinata archive failed:', error.message); return null; } }