UNPKG

filecoin-pin

Version:

Bridge IPFS content to Filecoin Onchain Cloud using familiar tools

294 lines (250 loc) 10.6 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' import type { PaymentSetupOptions } from './types.js' /** * Run interactive payment setup * * @param options - Initial options from command line */ export async function runInteractiveSetup(options: PaymentSetupOptions): Promise<void> { // 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: any = 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: string) => { 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: string) => { 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() } }