UNPKG

filecoin-pin

Version:

Bridge IPFS content to Filecoin Onchain Cloud using familiar tools

239 lines 11.7 kB
/** * Interactive payment setup flow with TTY support * * This module provides a guided, interactive setup experience for configuring * payment approvals. It uses @clack/prompts for a terminal interface * with password-style input for private keys and spinners for long operations. */ import { cancel, confirm, isCancel, password, text } from '@clack/prompts'; import { RPC_URLS, Synapse } from '@filoz/synapse-sdk'; import { ethers } from 'ethers'; import pc from 'picocolors'; import { calculateDepositCapacity, checkAndSetAllowances, checkFILBalance, checkUSDFCBalance, DEFAULT_LOCKUP_DAYS, depositUSDFC, getPaymentStatus, validatePaymentRequirements, } from '../core/payments/index.js'; import { cleanupProvider, cleanupSynapseService } from '../core/synapse/index.js'; import { getTelemetryConfig } from '../core/synapse/telemetry-config.js'; import { formatUSDFC } from '../core/utils/format.js'; import { createSpinner, intro, outro } from '../utils/cli-helpers.js'; import { isTTY, log } from '../utils/cli-logger.js'; import { displayAccountInfo, displayDepositWarning, displayPricing } from './setup.js'; /** * Run interactive payment setup * * @param options - Initial options from command line */ export async function runInteractiveSetup(options) { // Check for TTY support if (!isTTY()) { console.error(pc.red('Error: Interactive mode requires a TTY terminal.')); console.error('Use --auto flag for non-interactive setup.'); // Even though we're exiting early, ensure any background connections are cleaned up await cleanupSynapseService(); process.exit(1); } intro(pc.bold('Filecoin Onchain Cloud Payment Setup')); // Store provider reference for cleanup if it's a WebSocket provider let provider = null; try { // Get private key let privateKey = options.privateKey || process.env.PRIVATE_KEY; if (!privateKey) { const input = await password({ message: 'Enter your private key', validate: (value) => { if (!value) return 'Private key is required'; // Add 0x prefix if missing const key = value.startsWith('0x') ? value : `0x${value}`; // Validate format: 0x followed by 64 hex characters if (!/^0x[0-9a-fA-F]{64}$/.test(key)) { return 'Private key must be 64 hex characters (with or without 0x prefix)'; } try { new ethers.Wallet(key); return undefined; } catch { return 'Invalid private key format'; } }, }); if (isCancel(input)) { cancel('Setup cancelled'); process.exit(1); } // Add 0x prefix if it was missing privateKey = input.startsWith('0x') ? input : `0x${input}`; } // Initialize Synapse const s = createSpinner(); s.start('Initializing connection...'); const rpcUrl = options.rpcUrl || RPC_URLS.calibration.websocket; const synapse = await Synapse.create({ telemetry: getTelemetryConfig(), privateKey, rpcURL: rpcUrl, withIpni: true, // Always filter for IPNI-enabled providers ...(options.warmStorageAddress && { warmStorageAddress: options.warmStorageAddress }), }); const network = synapse.getNetwork(); const client = synapse.getClient(); const address = await client.getAddress(); // Store provider reference for cleanup if it's a WebSocket provider if (rpcUrl.match(/^wss?:\/\//)) { provider = synapse.getProvider(); } s.stop(`${pc.green('✓')} Connected to ${pc.bold(network)}`); // Check balances s.start('Checking balances...'); const filStatus = await checkFILBalance(synapse); const walletUsdfcBalance = await checkUSDFCBalance(synapse); s.stop(`${pc.green('✓')} Balance check complete`); // Validate payment requirements const validation = validatePaymentRequirements(filStatus.hasSufficientGas, walletUsdfcBalance, filStatus.isCalibnet); if (!validation.isValid) { log.line(`${pc.red('✗')} ${validation.errorMessage}`); if (validation.helpMessage) { log.line(''); log.line(` ${pc.cyan(validation.helpMessage)}`); } log.flush(); cancel('Please fund your wallet and try again'); process.exit(1); } // Now safe to get payment status since we know account exists const status = await getPaymentStatus(synapse); displayAccountInfo(address, network, filStatus.balance, filStatus.isCalibnet, filStatus.hasSufficientGas, walletUsdfcBalance, status.filecoinPayBalance); // Get storage pricing info once for all subsequent operations s.start('Getting current pricing...'); const storageInfo = await synapse.storage.getStorageInfo(); const pricePerTiBPerEpoch = storageInfo.pricing.noCDN.perTiBPerEpoch; const pricePerTiBPerMonth = storageInfo.pricing.noCDN.perTiBPerMonth; const pricePerGiBPerMonth = pricePerTiBPerMonth / 1024n; s.stop(`${pc.green('✓')} Pricing loaded`); // Initialize tracking variables let depositAmount = 0n; let actionsTaken = false; // Track if any changes were made // Show current deposit capacity const currentCapacity = calculateDepositCapacity(status.filecoinPayBalance, pricePerTiBPerEpoch); log.line(pc.bold('Current Storage Capacity:')); if (status.filecoinPayBalance > 0n) { const capacityStr = currentCapacity.gibPerMonth >= 1024 ? `${(currentCapacity.gibPerMonth / 1024).toFixed(1)} TiB` : `${currentCapacity.gibPerMonth.toFixed(1)} GiB`; log.indent(`Deposit: ${formatUSDFC(status.filecoinPayBalance)} USDFC`); log.indent(`Capacity: ~${capacityStr} for 1 month`); } else { log.indent(pc.gray('No deposit yet')); } log.flush(); // Show pricing to help user understand costs displayPricing(pricePerGiBPerMonth, pricePerTiBPerMonth); // Offer deposit options with contextual message const depositMessage = status.filecoinPayBalance === 0n ? 'Would you like to deposit USDFC to enable storage?' : 'Would you like to deposit additional USDFC?'; const shouldDeposit = await confirm({ message: depositMessage, initialValue: status.filecoinPayBalance === 0n, }); if (isCancel(shouldDeposit)) { cancel('Setup cancelled'); process.exit(1); } if (shouldDeposit) { // Show examples to help user decide log.line(pc.bold('Storage Examples (per month):')); log.indent(`100 GiB capacity: ~${formatUSDFC((pricePerGiBPerMonth * 100n * 11n) / 10n)} USDFC`); log.indent(`1 TiB capacity: ~${formatUSDFC((pricePerTiBPerMonth * 11n) / 10n)} USDFC`); log.indent(`10 TiB capacity: ~${formatUSDFC((pricePerTiBPerMonth * 10n * 11n) / 10n)} USDFC`); log.indent(pc.gray(`(deposit covers 1 month + ${DEFAULT_LOCKUP_DAYS}-day safety reserve)`)); log.flush(); const amountStr = await text({ message: 'How much USDFC would you like to deposit?', placeholder: '10.0', initialValue: status.filecoinPayBalance === 0n ? '10.0' : '5.0', validate: (value) => { try { const amount = ethers.parseUnits(value, 18); if (amount <= 0n) return 'Amount must be greater than 0'; if (amount > walletUsdfcBalance) return `Insufficient balance (have ${formatUSDFC(walletUsdfcBalance)} USDFC)`; return undefined; } catch { return 'Invalid amount'; } }, }); if (isCancel(amountStr)) { cancel('Setup cancelled'); process.exit(1); } depositAmount = ethers.parseUnits(amountStr, 18); s.start('Depositing USDFC...'); const { depositTx } = await depositUSDFC(synapse, depositAmount); s.stop(`${pc.green('✓')} Deposit complete`); log.indent(pc.gray(`Deposit tx: ${depositTx}`)); actionsTaken = true; // Show new capacity after deposit const newCapacity = calculateDepositCapacity(status.filecoinPayBalance + depositAmount, pricePerTiBPerEpoch); const newCapacityStr = newCapacity.gibPerMonth >= 1024 ? `${(newCapacity.gibPerMonth / 1024).toFixed(1)} TiB` : `${newCapacity.gibPerMonth.toFixed(1)} GiB`; log.line(''); log.line(pc.bold('New Storage Capacity:')); log.indent(`Total deposit: ${formatUSDFC(status.filecoinPayBalance + depositAmount)} USDFC`); log.indent(`Capacity: ~${newCapacityStr} for 1 month`); log.flush(); } else { const { updated, transactionHash } = await checkAndSetAllowances(synapse); if (updated) { log.indent(`${pc.green('✓')} Updated payment allowances, tx: ${transactionHash}`); } else { log.indent(`${pc.green('✓')} Deposit already sufficient (${formatUSDFC(status.filecoinPayBalance)} USDFC)`); } } // Final summary s.start('Fetching final status...'); const finalStatus = await getPaymentStatus(synapse); s.stop('━━━ Setup Complete ━━━'); const finalCapacity = calculateDepositCapacity(finalStatus.filecoinPayBalance, pricePerTiBPerEpoch); log.line(`Network: ${pc.bold(network)}`); log.line(''); log.line(pc.bold('Wallet')); log.indent(`${formatUSDFC(walletUsdfcBalance)} USDFC available`); log.line(''); log.line(pc.bold('Storage Deposit')); log.indent(`${formatUSDFC(finalStatus.filecoinPayBalance)} USDFC deposited`); if (finalCapacity.gibPerMonth > 0) { const capacityStr = finalCapacity.gibPerMonth >= 1024 ? `${(finalCapacity.gibPerMonth / 1024).toFixed(1)} TiB` : `${finalCapacity.gibPerMonth.toFixed(1)} GiB`; log.indent(`Capacity: ~${capacityStr} for 1 month`); log.indent(pc.gray(`(includes ${DEFAULT_LOCKUP_DAYS}-day safety reserve)`)); } log.flush(); // Show deposit warning if needed displayDepositWarning(finalStatus.filecoinPayBalance, finalStatus.currentAllowances.lockupUsed); // Show appropriate outro message based on whether actions were taken if (actionsTaken) { outro('Payment setup completed successfully'); } else { outro('No changes made to payment setup'); } } catch (error) { console.error(`\n${pc.red('Error:')}`, error instanceof Error ? error.message : error); process.exitCode = 1; } finally { await cleanupProvider(provider); process.exit(); } } //# sourceMappingURL=interactive.js.map