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