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