UNPKG

filecoin-pin

Version:

Bridge IPFS content to Filecoin Onchain Cloud using familiar tools

328 lines 15.4 kB
/** * Payment status display command * * Shows current payment configuration and balances for Filecoin Onchain Cloud. * This provides a quick overview of the user's payment setup without making changes. */ import { SIZE_CONSTANTS } from '@filoz/synapse-core/utils'; import { TIME_CONSTANTS } from '@filoz/synapse-sdk'; import { ethers } from 'ethers'; import pc from 'picocolors'; import { TELEMETRY_CLI_APP_NAME } from '../common/constants.js'; import { calculateActualStorage, listDataSets } from '../core/data-set/index.js'; import { calculateDepositCapacity, calculateStorageRunway, checkFILBalance, checkUSDFCBalance, FLOOR_PRICE_DAYS, FLOOR_PRICE_PER_30_DAYS, getPaymentStatus, } from '../core/payments/index.js'; import { cleanupSynapseService, initializeSynapse } from '../core/synapse/index.js'; import { formatFIL, formatUSDFC } from '../core/utils/format.js'; import { formatRunwaySummary } from '../core/utils/index.js'; import { getCLILogger, parseCLIAuth } from '../utils/cli-auth.js'; import { cancel, createSpinner, formatFileSize, intro, outro } from '../utils/cli-helpers.js'; import { log } from '../utils/cli-logger.js'; import { displayDepositWarning } from './setup.js'; const STORAGE_DISPLAY_PRECISION_DIGITS = 6; const STORAGE_DISPLAY_PRECISION = 10n ** BigInt(STORAGE_DISPLAY_PRECISION_DIGITS); const { TiB } = SIZE_CONSTANTS; /** * Convert a payment rate (USDFC per epoch) to storage bytes using the provider's pricing. * * This calculates: "How much storage does this payment rate cover?" * * Formula: storageBytes = (rate / pricePerTiBPerEpoch) * TiB * * NOTE: This calculation assumes a linear relationship between payment rate and * storage size, which breaks down when floor pricing is applied to small files. * The result represents "storage equivalent" at the given rate, not actual bytes stored. * Use calculateActualStorage() from core/data-set for accurate byte counts. * * @param ratePerEpoch - Payment rate in USDFC per epoch * @param pricePerTiBPerEpoch - Provider's price for 1 TiB per epoch in USDFC * @returns Storage bytes that the rate covers, or null if invalid inputs */ function convertRateToStorageBytes(ratePerEpoch, pricePerTiBPerEpoch) { if (ratePerEpoch <= 0n || pricePerTiBPerEpoch <= 0n) { return null; } // storageTiBScaled preserves fractional precision using STORAGE_DISPLAY_PRECISION scaling const storageTiBScaled = (ratePerEpoch * STORAGE_DISPLAY_PRECISION) / pricePerTiBPerEpoch; if (storageTiBScaled <= 0n) { return null; } // Convert scaled TiB to bytes: TiB * 1024^4 bytes, then unscale return (storageTiBScaled * TiB) / STORAGE_DISPLAY_PRECISION; } /** * Display current payment status * * @param options - Options from command line */ export async function showPaymentStatus(options) { intro(pc.bold('Filecoin Onchain Cloud Payment Status')); const spinner = createSpinner(); spinner.start('Fetching current configuration...'); try { // Parse and validate authentication const authConfig = parseCLIAuth(options); const logger = getCLILogger(); const synapse = await initializeSynapse({ ...authConfig, telemetry: { sentrySetTags: { appName: TELEMETRY_CLI_APP_NAME } } }, logger); const network = synapse.getNetwork(); const client = synapse.getClient(); const address = await client.getAddress(); // Check balances and status const filStatus = await checkFILBalance(synapse); // Early exit if account has no funds if (filStatus.balance === 0n) { spinner.stop('━━━ Current Status ━━━'); log.line(`Address: ${address}`); log.line(`Network: ${network}`); log.line(''); log.line(`${pc.red('✗')} Account has no FIL balance`); log.line(''); log.line(`Get test FIL from: ${filStatus.isCalibnet ? 'https://faucet.calibnet.chainsafe-fil.io/' : 'Purchase FIL from an exchange'}`); log.flush(); cancel('Account not funded'); throw new Error('Account has no FIL balance'); } const walletUsdfcBalance = await checkUSDFCBalance(synapse); // Check if we have USDFC tokens before continuing if (walletUsdfcBalance === 0n) { spinner.stop('━━━ Current Status ━━━'); log.line(`Address: ${address}`); log.line(`Network: ${network}`); log.line(''); log.line(`${pc.red('✗')} No USDFC tokens found`); log.line(''); const helpMessage = filStatus.isCalibnet ? 'Get test USDFC from: https://docs.secured.finance/usdfc-stablecoin/getting-started/getting-test-usdfc-on-testnet' : 'Mint USDFC with FIL: https://docs.secured.finance/usdfc-stablecoin/getting-started/minting-usdfc-step-by-step'; log.line(` ${pc.cyan(helpMessage)}`); log.flush(); cancel('USDFC required to use Filecoin Onchain Cloud'); throw new Error('No USDFC tokens found'); } const status = await getPaymentStatus(synapse); // Get storage pricing for capacity calculation and spend summaries const storageInfo = await synapse.storage.getStorageInfo(); const pricePerTiBPerEpoch = storageInfo.pricing.noCDN.perTiBPerEpoch; let paymentRailsData = null; if (options.includeRails === true) { paymentRailsData = await fetchPaymentRailsData(synapse); } spinner.stop(`${pc.green('✓')} Configuration loaded`); // Display all status information log.line('━━━ Current Status ━━━'); // Show wallet balances log.line(pc.bold('Wallet')); log.indent(`Owner address: ${address}`); log.indent(`Network: ${network}`); log.indent(`FIL: ${formatFIL(filStatus.balance, filStatus.isCalibnet)}`); log.indent(`USDFC: ${formatUSDFC(walletUsdfcBalance)} USDFC`); log.line(''); // Show deposit and capacity const lockupUsed = status.currentAllowances.lockupUsed ?? 0n; const rateUsed = status.currentAllowances.rateUsed ?? 0n; const availableDeposit = status.filecoinPayBalance > lockupUsed ? status.filecoinPayBalance - lockupUsed : 0n; const capacity = calculateDepositCapacity(status.filecoinPayBalance, pricePerTiBPerEpoch); const runway = calculateStorageRunway(status); const runwayDisplay = formatRunwaySummary(runway); const dailyCost = runway.perDay; const monthlyCost = dailyCost * TIME_CONSTANTS.DAYS_PER_MONTH; log.line(pc.bold('Filecoin Pay')); log.indent(`Balance: ${formatUSDFC(status.filecoinPayBalance)} USDFC`); log.indent(`Locked: ${formatUSDFC(lockupUsed)} USDFC (30-day reserve)`); log.indent(`Available: ${formatUSDFC(availableDeposit)} USDFC`); if (rateUsed > 0n) { log.indent(`Epoch cost: ${formatUSDFC(rateUsed)} USDFC`); log.indent(`Daily cost: ${formatUSDFC(dailyCost)} USDFC`); log.indent(`Monthly cost: ${formatUSDFC(monthlyCost)} USDFC`); } else { log.indent(`Epoch cost: ${pc.gray('0 USDFC')}`); log.indent(`Daily cost: ${pc.gray('0 USDFC')}`); log.indent(`Monthly cost: ${pc.gray('0 USDFC')}`); } if (paymentRailsData != null) { displayPaymentRailsSummary(paymentRailsData, 1); } log.line(''); // Show storage usage details log.line(pc.bold('WarmStorage Usage')); let actualStorageResult = null; try { spinner.start('Fetching data sets...'); // Get all active data sets for this address const dataSets = await listDataSets(synapse, { withProviderDetails: false, address, filter: (ds) => ds.isLive, // Only count active/live data sets logger, }); spinner.stop(`${pc.green('✓')} Data sets fetched`); spinner.start('Calculating actual storage from data sets...'); actualStorageResult = await calculateActualStorage(synapse, dataSets, { logger, onProgress: (progress) => { if (progress.type === 'actual-storage:progress') { spinner.message(`Calculating actual storage from data sets (${progress.data.dataSetsProcessed}/${progress.data.dataSetCount})`); } }, }); if (actualStorageResult.timedOut) { spinner.stop(`${pc.yellow('⚠')} Calculation timed out`); } else if (actualStorageResult.warnings.length > 0) { spinner.stop(`${pc.yellow('⚠')} Actual storage calculated with ${actualStorageResult.warnings.length} warning(s)`); } else { spinner.stop(`${pc.green('✓')} Actual storage calculated`); } if (actualStorageResult.warnings.length > 0) { for (const warning of actualStorageResult.warnings) { log.indent(pc.yellow(`⚠ ${warning.message}`)); } } if (actualStorageResult.totalBytes > 0n) { const formattedSize = formatFileSize(actualStorageResult.totalBytes); log.indent(`Stored: ${formattedSize}`); } else { log.indent(pc.gray('Stored: 0 B')); } } catch (error) { spinner.stop(`${pc.yellow('⚠')} Could not calculate actual storage`); log.indent(pc.gray(` Error: ${error instanceof Error ? error.message : String(error)}`)); } if (runway.state === 'active') { log.indent(`Runway: ~${runwayDisplay}`); } else { log.indent(pc.gray(`Runway: ${runwayDisplay}`)); } const capacityTibPerMonth = ethers.parseUnits(capacity.tibPerMonth.toString(), 18); const capacityBytes = (capacityTibPerMonth * TiB) / 10n ** 18n; const capacityLine = `Funding could cover ~${formatFileSize(capacityBytes)} for one month`; log.indent(capacityLine); log.flush(); const billedBytes = convertRateToStorageBytes(rateUsed, pricePerTiBPerEpoch); if (billedBytes != null) { // Calculate what storage the floor price represents at current pricing const epochsInFloorPeriod = BigInt(FLOOR_PRICE_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY; const floorRatePerEpoch = FLOOR_PRICE_PER_30_DAYS / epochsInFloorPeriod; const floorEquivalentBytes = convertRateToStorageBytes(floorRatePerEpoch, pricePerTiBPerEpoch); const floorEquivalentFormatted = floorEquivalentBytes ? formatFileSize(floorEquivalentBytes) : '~24.6 GiB'; const sectionContent = [ pc.gray('Filecoin Onchain Cloud uses floor pricing for DataSets.'), pc.gray(`Each DataSet is billed a minimum of ${formatUSDFC(FLOOR_PRICE_PER_30_DAYS, 2)} USDFC per 30 days.`), pc.gray(`This is equivalent to ~${floorEquivalentFormatted} per month.`), `Billed capacity: ~${formatFileSize(billedBytes)}`, ]; if (actualStorageResult != null && billedBytes > actualStorageResult.totalBytes) { const additionalStorage = billedBytes - actualStorageResult.totalBytes; sectionContent.push(`Storage remaining: ~${formatFileSize(additionalStorage)}`); } log.indent(pc.bold('Storage usage details:')); for (const content of sectionContent) { log.indent(content, 2); } } // Show deposit warning if needed displayDepositWarning(status.filecoinPayBalance, status.currentAllowances.lockupUsed); log.flush(); // Show success outro outro('Status check complete'); } catch (error) { spinner.stop(`${pc.red('✗')} Status check failed`); log.line(''); log.line(`${pc.red('Error:')} ${error instanceof Error ? error.message : String(error)}`); log.flush(); cancel('Status check failed'); process.exitCode = 1; } finally { await cleanupSynapseService(); } } /** * Fetch payment rails data without displaying anything */ async function fetchPaymentRailsData(synapse) { try { // Get rails as payer const payerRails = await synapse.payments.getRailsAsPayer(); if (payerRails.length === 0) { return { activeRails: 0, terminatedRails: 0, totalActiveRate: 0n, totalPendingSettlements: 0n, railsNeedingSettlement: 0, }; } // Analyze rails for summary let totalPendingSettlements = 0n; let totalActiveRate = 0n; let activeRails = 0; let terminatedRails = 0; let railsNeedingSettlement = 0; for (const rail of payerRails) { try { const railDetails = await synapse.payments.getRail(rail.railId); const settlementPreview = await synapse.payments.getSettlementAmounts(rail.railId); if (rail.isTerminated) { terminatedRails++; } else { activeRails++; totalActiveRate += railDetails.paymentRate; } // Check for pending settlements if (settlementPreview.totalSettledAmount > 0n) { totalPendingSettlements += settlementPreview.totalSettledAmount; railsNeedingSettlement++; } } catch (error) { log.warn(`Could not analyze rail ${rail.railId}: ${error instanceof Error ? error.message : String(error)}`); } } return { activeRails, terminatedRails, totalActiveRate, totalPendingSettlements, railsNeedingSettlement, }; } catch { return { activeRails: 0, terminatedRails: 0, totalActiveRate: 0n, totalPendingSettlements: 0n, railsNeedingSettlement: 0, error: 'Unable to fetch rail information', }; } } /** * Display payment rails summary */ function displayPaymentRailsSummary(data, indentLevel = 1) { log.indent(pc.bold('Payment Rails'), indentLevel); if (data.error) { log.indent(pc.gray(data.error), indentLevel + 1); return; } if (data.activeRails === 0 && data.terminatedRails === 0) { log.indent(pc.gray('No active payment rails'), indentLevel + 1); return; } log.indent(`${data.activeRails} active, ${data.terminatedRails} terminated`, indentLevel + 1); if (data.totalPendingSettlements > 0n) { log.indent(`Pending settlement: ${formatUSDFC(data.totalPendingSettlements)} USDFC`, indentLevel + 1); } if (data.railsNeedingSettlement > 0) { log.indent(`${data.railsNeedingSettlement} rail(s) need settlement`, indentLevel + 1); } } //# sourceMappingURL=status.js.map