UNPKG

filecoin-pin

Version:

Bridge IPFS content to Filecoin Onchain Cloud using familiar tools

329 lines 14.4 kB
/** * Common upload flow shared between import and add commands * * This module provides reusable functions for the Synapse upload workflow * including payment validation, storage context creation, and result display. */ import pc from 'picocolors'; import { DEFAULT_LOCKUP_DAYS } from '../core/payments/index.js'; import { cleanupSynapseService } from '../core/synapse/index.js'; import { checkUploadReadiness, executeUpload, getDownloadURL, getServiceURL, } from '../core/upload/index.js'; import { formatUSDFC } from '../core/utils/format.js'; import { autoFund } from '../payments/fund.js'; import { cancel, formatFileSize } from '../utils/cli-helpers.js'; import { log } from '../utils/cli-logger.js'; import { createSpinnerFlow } from '../utils/multi-operation-spinner.js'; /** * Perform auto-funding if requested * Automatically ensures a minimum of 30 days of runway based on current usage + new file requirements * * @param synapse - Initialized Synapse instance * @param fileSize - Size of file being uploaded (in bytes) * @param spinner - Optional spinner for progress */ export async function performAutoFunding(synapse, fileSize, spinner) { spinner?.start('Checking funding requirements for upload...'); try { const fundOptions = { synapse, fileSize, }; if (spinner !== undefined) { fundOptions.spinner = spinner; } const result = await autoFund(fundOptions); spinner?.stop(`${pc.green('✓')} Funding requirements met`); if (result.adjusted) { log.line(''); log.line(pc.bold('Auto-funding completed:')); log.indent(`Deposited ${formatUSDFC(result.delta)} USDFC`); log.indent(`Total deposited: ${formatUSDFC(result.newDepositedAmount)} USDFC`); log.indent(`Runway: ~${result.newRunwayDays} day(s)${result.newRunwayHours > 0 ? ` ${result.newRunwayHours} hour(s)` : ''}`); if (result.transactionHash) { log.indent(pc.gray(`Transaction: ${result.transactionHash}`)); } log.line(''); log.flush(); } } catch (error) { spinner?.stop(`${pc.red('✗')} Auto-funding failed`); log.line(''); log.line(`${pc.red('Error:')} ${error instanceof Error ? error.message : String(error)}`); log.flush(); await cleanupSynapseService(); cancel('Operation cancelled - auto-funding failed'); process.exit(1); } } /** * Validate payment setup and capacity for upload * * @param synapse - Initialized Synapse instance * @param fileSize - Size of file to upload in bytes (use 0 for minimum setup check) * @param spinner - Optional spinner for progress * @param options - Optional configuration * @param options.suppressSuggestions - If true, don't display suggestion warnings * @returns true if validation passes, exits process if not */ export async function validatePaymentSetup(synapse, fileSize, spinner, options) { const readiness = await checkUploadReadiness({ synapse, fileSize, onProgress: (event) => { if (!spinner) return; switch (event.type) { case 'checking-balances': { spinner.message('Checking payment setup requirements...'); return; } case 'checking-allowances': { spinner.message('Checking WarmStorage permissions...'); return; } case 'configuring-allowances': { spinner.message('Configuring WarmStorage permissions (one-time setup)...'); return; } case 'validating-capacity': { spinner.message('Validating payment capacity...'); return; } case 'allowances-configured': { // No spinner change; we log once readiness completes. return; } } }, }); const { validation, allowances, capacity, suggestions } = readiness; if (!validation.isValid) { spinner?.stop(`${pc.red('✗')} Payment setup incomplete`); log.line(''); log.line(`${pc.red('✗')} ${validation.errorMessage}`); if (validation.helpMessage) { log.line(''); log.line(` ${pc.cyan(validation.helpMessage)}`); } log.line(''); log.line(`${pc.yellow('⚠')} Your payment setup is not complete. Please run:`); log.indent(pc.cyan('filecoin-pin payments setup')); log.line(''); log.line('For more information, run:'); log.indent(pc.cyan('filecoin-pin payments status')); log.flush(); await cleanupSynapseService(); cancel('Operation cancelled - payment setup required'); process.exit(1); } if (allowances.updated) { spinner?.stop(`${pc.green('✓')} WarmStorage permissions configured`); if (allowances.transactionHash) { log.indent(pc.gray(`Transaction: ${allowances.transactionHash}`)); log.flush(); } spinner?.start('Validating payment capacity...'); } else { spinner?.message('Validating payment capacity...'); } if (!capacity?.canUpload) { if (capacity) { displayPaymentIssues(capacity, fileSize, spinner); } await cleanupSynapseService(); cancel('Operation cancelled - insufficient payment capacity'); process.exit(1); } // Show warning if suggestions exist (even if upload is possible) if (suggestions.length > 0 && capacity?.canUpload && !options?.suppressSuggestions) { spinner?.stop(`${pc.yellow('⚠')} Payment capacity check passed with warnings`); log.line(pc.bold('Suggestions:')); suggestions.forEach((suggestion) => { log.indent(`• ${suggestion}`); }); log.flush(); } else if (fileSize === 0) { // Different message based on whether this is minimum setup (fileSize=0) or actual capacity check // Note: 0.06 USDFC is the floor price, but with 10% buffer, ~0.066 USDFC is actually required spinner?.stop(`${pc.green('✓')} Minimum payment setup verified (~0.066 USDFC required)`); } else { spinner?.stop(`${pc.green('✓')} Payment capacity verified for ${formatFileSize(fileSize)}`); } } /** * Display payment capacity issues and suggestions */ function displayPaymentIssues(capacityCheck, fileSize, spinner) { spinner?.stop(`${pc.red('✗')} Insufficient deposit for this file`); log.line(pc.bold('File Requirements:')); if (fileSize === 0) { log.indent(`File size: ${formatFileSize(fileSize)} (${capacityCheck.storageTiB.toFixed(4)} TiB)`); } log.indent(`Storage cost: ${formatUSDFC(capacityCheck.required.rateAllowance)} USDFC/epoch`); log.indent(`Required deposit: ${formatUSDFC(capacityCheck.required.lockupAllowance + capacityCheck.required.lockupAllowance / 10n)} USDFC ${pc.gray(`(includes ${DEFAULT_LOCKUP_DAYS}-day safety reserve)`)}`); log.line(''); log.line(pc.bold('Suggested actions:')); capacityCheck.suggestions.forEach((suggestion) => { log.indent(`• ${suggestion}`); }); log.line(''); // Calculate suggested deposit const suggestedDeposit = capacityCheck.issues.insufficientDeposit ? formatUSDFC(capacityCheck.issues.insufficientDeposit) : '0'; log.line(`${pc.yellow('⚠')} To fix this, run:`); log.indent(pc.cyan(`filecoin-pin payments setup --deposit ${suggestedDeposit} --auto`)); log.flush(); } /** * Upload CAR data to Synapse with progress tracking * * @param synapseService - Initialized Synapse service with storage context * @param carData - CAR file data as Uint8Array * @param rootCid - Root CID of the content * @param options - Upload flow options * @returns Upload result with transaction hash */ export async function performUpload(synapseService, carData, rootCid, options) { const { contextType, logger, spinner, pieceMetadata } = options; // Create spinner flow manager for tracking all operations const flow = createSpinnerFlow(spinner); // Start with upload operation flow.addOperation('upload', 'Uploading to Filecoin...'); let transactionHash; let pieceCid; function getIpniAdvertisementMsg(attemptCount) { return `Checking for IPNI provider records (check #${attemptCount})`; } const uploadResult = await executeUpload(synapseService, carData, rootCid, { logger, contextId: `${contextType}-${Date.now()}`, ...(pieceMetadata && { pieceMetadata }), onProgress(event) { switch (event.type) { case 'onUploadComplete': { pieceCid = event.data.pieceCid; flow.completeOperation('upload', 'Upload complete', { type: 'success', details: (() => { const serviceURL = getServiceURL(synapseService.providerInfo); if (serviceURL != null && serviceURL !== '') { return { title: 'Download IPFS CAR from SP', content: [pc.gray(`${serviceURL.replace(/\/$/, '')}/ipfs/${rootCid}`)], }; } return; })(), }); // Start adding piece to dataset operation flow.addOperation('add-to-dataset', 'Adding piece to DataSet...'); break; } case 'onPieceAdded': { if (event.data.txHash) { transactionHash = event.data.txHash; } const network = synapseService.synapse.getNetwork(); const explorerUrls = [pc.gray(`Piece: https://pdp.vxb.ai/${network}/piece/${pieceCid}`)]; if (transactionHash) { const filfoxBase = network === 'mainnet' ? 'https://filfox.info' : `https://${network}.filfox.info`; explorerUrls.push(pc.gray(`Transaction: ${filfoxBase}/en/message/${transactionHash}`)); } flow.completeOperation('add-to-dataset', 'Piece added to DataSet (unconfirmed on-chain)', { type: 'success', details: { title: 'Explorer URLs', content: explorerUrls, }, }); // Start chain confirmation operation flow.addOperation('chain', 'Confirming piece added to DataSet on-chain'); break; } case 'onPieceConfirmed': { flow.completeOperation('chain', 'Piece added to DataSet (confirmed on-chain)', { type: 'success', }); break; } case 'ipniProviderResults.retryUpdate': { const attemptCount = event.data.retryCount === 0 ? 1 : event.data.retryCount + 1; flow.addOperation('ipni', getIpniAdvertisementMsg(attemptCount)); break; } case 'ipniProviderResults.complete': { // complete event is only emitted when result === true (success) flow.completeOperation('ipni', 'IPNI provider records found. IPFS retrieval possible.', { type: 'success', details: { title: 'IPFS Retrieval URLs', content: [ pc.gray(`ipfs://${rootCid}`), pc.gray(`https://inbrowser.link/ipfs/${rootCid}`), pc.gray(`https://dweb.link/ipfs/${rootCid}`), ], }, }); break; } case 'ipniProviderResults.failed': { flow.completeOperation('ipni', 'IPNI provider records not found.', { type: 'warning', details: { title: 'IPFS retrieval is not possible yet.', content: [pc.gray(`IPNI provider records for this SP does not exist for the provided root CID`)], }, }); break; } default: { break; } } }, }); return { ...uploadResult, network: synapseService.synapse.getNetwork(), transactionHash: uploadResult.transactionHash, }; } /** * Display results for import or add command * * @param result - Result data to display * @param operation - Operation name ('Import' or 'Add') */ export function displayUploadResults(result, operation, network) { log.line(`Network: ${pc.bold(network)}`); log.line(''); log.line(pc.bold(`${operation} Details`)); log.indent(`File: ${result.filePath}`); log.indent(`Size: ${formatFileSize(result.fileSize)}`); log.indent(`Root CID: ${result.rootCid}`); log.line(''); log.line(pc.bold('Filecoin Storage')); log.indent(`Piece CID: ${result.pieceCid}`); log.indent(`Piece ID: ${result.pieceId?.toString() || 'N/A'}`); log.indent(`Data Set ID: ${result.dataSetId}`); log.line(''); log.line(pc.bold('Storage Provider')); log.indent(`Provider ID: ${result.providerInfo.id}`); log.indent(`Name: ${result.providerInfo.name}`); const downloadURL = getDownloadURL(result.providerInfo, result.pieceCid); if (downloadURL) { log.indent(`Direct Download URL: ${downloadURL}`); } if (result.transactionHash) { log.line(''); log.line(pc.bold('Transaction')); log.indent(`Hash: ${result.transactionHash}`); } log.flush(); } //# sourceMappingURL=upload-flow.js.map