Spaces:
Runtime error
Runtime error
File size: 7,439 Bytes
e92be04 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | 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;
}
}
|