filecoin-pin
Version:
Bridge IPFS content to Filecoin Onchain Cloud using familiar tools
329 lines • 14.4 kB
JavaScript
/**
* 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