filecoin-pin
Version:
Bridge IPFS content to Filecoin Onchain Cloud using familiar tools
342 lines (314 loc) • 11.4 kB
text/typescript
/**
* Payment setup utilities and display functions
*
* This module provides UI utilities and display functions for payment setup,
* building on the shared payment operations exposed from `src/core/payments`.
*/
import { TIME_CONSTANTS } from '@filoz/synapse-sdk'
import { ethers } from 'ethers'
import pc from 'picocolors'
import {
calculateActualCapacity,
DEFAULT_LOCKUP_DAYS,
getStorageScale,
USDFC_DECIMALS,
} from '../core/payments/index.js'
import { formatFIL, formatUSDFC } from '../core/utils/format.js'
import { log } from '../utils/cli-logger.js'
/**
* Parse storage allowance string
*
* Parses different storage allowance formats:
* - "1TiB/month" or "500GiB/month" - Human-friendly storage units
* - "0.0000565" - Direct USDFC per epoch (returns null, needs price lookup)
*
* @param input - Storage allowance string
* @returns Parsed TiB per month or null if it's a direct USDFC amount
*/
export function parseStorageAllowance(input: string): number | null {
// Check if input is a storage unit (e.g., "1TiB/month", "500GiB/month")
const storageMatch = input.match(/^(\d+(?:\.\d+)?)\s*(TiB|GiB|MiB)\/month$/i)
if (storageMatch?.[1] && storageMatch[2]) {
const amount = parseFloat(storageMatch[1])
const unit = storageMatch[2].toUpperCase()
// Convert to TiB
let tibPerMonth: number
switch (unit) {
case 'TIB':
tibPerMonth = amount
break
case 'GIB':
tibPerMonth = amount / 1024
break
case 'MIB':
tibPerMonth = amount / (1024 * 1024)
break
default:
throw new Error(`Unknown storage unit: ${unit}`)
}
return tibPerMonth
}
// Validate that it's a valid number for USDFC per epoch
try {
ethers.parseUnits(input, USDFC_DECIMALS)
return null // Valid USDFC amount, but need pricing to convert to TiB
} catch {
throw new Error(
`Invalid storage allowance format: ${input}. Use "1TiB/month", "500GiB/month", or a decimal number for USDFC per epoch (e.g., "0.0000565")`
)
}
}
/**
* Display the payment status summary
*
* Shows three sections: Wallet, Filecoin Pay Deposit, and WarmStorage Service Permissions
*
* @param network - Network name
* @param filBalance - FIL balance in wei
* @param isCalibnet - Whether this is calibnet testnet
* @param walletUsdfcBalance - USDFC balance in wei
* @param filecoinPayBalance - Amount deposited in Filecoin Pay
* @param rateAllowance - Maximum rate per epoch
* @param lockupAllowance - Maximum lockup amount
* @param pricePerTiBPerEpoch - Current storage price
*/
export function displayPaymentSummary(
network: string,
filBalance: bigint,
isCalibnet: boolean,
walletUsdfcBalance: bigint,
filecoinPayBalance: bigint,
rateAllowance: bigint,
lockupAllowance: bigint,
pricePerTiBPerEpoch: bigint
): void {
// Start the summary section (Setup Complete is shown by spinner in auto mode)
log.line(`Network: ${pc.bold(network)}`)
log.line('')
// Section 1: Wallet
log.line(pc.bold('Wallet'))
log.indent(formatFIL(filBalance, isCalibnet))
log.indent(`${formatUSDFC(walletUsdfcBalance)} USDFC`)
log.line('')
// Section 2: Filecoin Pay deposit
log.line(pc.bold('Filecoin Pay Deposit'))
log.indent(`${formatUSDFC(filecoinPayBalance)} USDFC`)
log.indent(pc.gray('(spendable on any service)'))
// Section 3: WarmStorage service permissions
log.line('')
if (rateAllowance > 0n) {
const monthlyRate = rateAllowance * TIME_CONSTANTS.EPOCHS_PER_MONTH
displayServicePermissions(
'Your WarmStorage Service Limits',
monthlyRate,
lockupAllowance,
filecoinPayBalance,
pricePerTiBPerEpoch,
false
)
} else {
log.line(pc.bold('Your WarmStorage Service Limits'))
log.indent(pc.gray('No limits set'))
}
log.flush() // Flush everything at the end
}
/**
* Display account and balance information
*
* @param address - Wallet address
* @param network - Network name (mainnet/calibration)
* @param filBalance - FIL balance in wei
* @param isCalibnet - Whether on calibration testnet
* @param hasSufficientGas - Whether wallet has enough FIL for gas
* @param walletUsdfcBalance - USDFC balance in wei
* @param filecoinPayBalance - Amount deposited to Filecoin Pay
*/
export function displayAccountInfo(
address: string,
network: string,
filBalance: bigint,
isCalibnet: boolean,
_hasSufficientGas: boolean,
walletUsdfcBalance: bigint,
filecoinPayBalance: bigint
): void {
log.line(pc.bold('Account:'))
log.indent(pc.gray(`Wallet: ${address}`))
log.indent(pc.gray(`Network: ${network}`))
log.line(pc.bold('Balances:'))
log.indent(pc.gray(`FIL: ${formatFIL(filBalance, isCalibnet)}`))
log.indent(pc.gray(`USDFC wallet: ${formatUSDFC(walletUsdfcBalance)} USDFC`))
log.indent(pc.gray(`USDFC deposited: ${formatUSDFC(filecoinPayBalance)} USDFC`))
log.flush()
}
/**
* Display deposit warning if balance is too low for active storage
*
* Warns when the available deposit (total deposit minus locked amount)
* is insufficient to maintain active storage operations.
*
* @param filecoinPayBalance - Current deposit balance
* @param lockupUsed - Amount currently locked for active storage
*/
export function displayDepositWarning(filecoinPayBalance: bigint, lockupUsed: bigint): void {
if (lockupUsed > 0n) {
// Calculate available deposit after accounting for locked funds
const availableDeposit = filecoinPayBalance - lockupUsed
// Warn if available deposit is too low (less than 10% of lockup as safety margin)
const safetyMargin = lockupUsed / 10n // 10% safety margin
if (availableDeposit < safetyMargin) {
const needed = lockupUsed + safetyMargin - filecoinPayBalance
log.newline()
log.message(pc.yellow(`⚠ Warning: Low deposit balance`))
log.indent(pc.yellow(`Your deposit: ${formatUSDFC(filecoinPayBalance)} USDFC`))
log.indent(pc.yellow(`Amount locked: ${formatUSDFC(lockupUsed)} USDFC`))
log.indent(pc.yellow(`Available: ${formatUSDFC(availableDeposit > 0n ? availableDeposit : 0n)} USDFC`))
log.indent(pc.yellow(`Deposit at least ${formatUSDFC(needed)} more USDFC to maintain safety margin`))
log.indent(pc.gray(`Without sufficient deposit, storage may be terminated`))
}
}
}
/**
* Format storage capacity with smart unit selection
*
* @param gib - Capacity in GiB
* @returns Formatted string with appropriate unit
*/
function formatStorageCapacity(gib: number): string {
if (gib >= 1024) {
const tib = gib / 1024
// Use 1 decimal place if under 100 TiB
if (tib < 100) {
return `${tib.toFixed(1)} TiB/month`
}
return `${Math.round(tib).toLocaleString()} TiB/month`
}
// Use GiB between 1 GiB and 1 TiB
if (gib >= 0.9) {
if (gib < 100) {
return `${gib.toFixed(1)} GiB/month`
}
return `${Math.round(gib).toLocaleString()} GiB/month`
}
// For sub‑1 GiB, show MiB to avoid "0.0 GiB"
const mib = gib * 1024
if (mib < 10) {
return `${mib.toFixed(1)} MiB/month`
}
return `${Math.round(mib).toLocaleString()} MiB/month`
}
/**
* Helper to calculate storage capacity in GiB from allowances
*/
function calculateStorageCapacity(rateAllowance: bigint, lockupAllowance: bigint, pricePerTiBPerEpoch: bigint): number {
const tibFromRate = calculateActualCapacity(rateAllowance, pricePerTiBPerEpoch)
const defaultLockupEpochs = BigInt(DEFAULT_LOCKUP_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY
const maxRateFromLockup = lockupAllowance / defaultLockupEpochs
const tibFromLockup = calculateActualCapacity(maxRateFromLockup, pricePerTiBPerEpoch)
const tibPerMonth = Math.min(tibFromRate, tibFromLockup * 3)
const gibPerMonth = tibPerMonth * 1024
return gibPerMonth
}
/**
* Storage capacity information with deposit limitations
*/
export interface StorageCapacityInfo {
actualGiB: number
potentialGiB: number
isDepositLimited: boolean
additionalDepositNeeded: bigint
}
/**
* Calculate actual capacity with deposit limitations
*/
function calculateActualCapacityWithDeposit(
filecoinPayBalance: bigint,
rateAllowance: bigint,
lockupAllowance: bigint,
pricePerTiBPerEpoch: bigint
): StorageCapacityInfo {
const potentialGiB = calculateStorageCapacity(rateAllowance, lockupAllowance, pricePerTiBPerEpoch)
const requiredLockup = lockupAllowance
const monthlyPayment = rateAllowance * TIME_CONSTANTS.EPOCHS_PER_MONTH
const requiredDeposit = requiredLockup + monthlyPayment
let actualGiB: number
let isDepositLimited = false
let additionalDepositNeeded = 0n
if (filecoinPayBalance >= requiredDeposit) {
actualGiB = potentialGiB
} else {
isDepositLimited = true
additionalDepositNeeded = requiredDeposit - filecoinPayBalance
const scale = getStorageScale(potentialGiB)
const scaleFactor = Number((filecoinPayBalance * BigInt(scale)) / requiredDeposit) / scale
actualGiB = potentialGiB * scaleFactor
}
return {
actualGiB,
potentialGiB,
isDepositLimited,
additionalDepositNeeded,
}
}
/**
* Display capacity information based on deposit and limits
*
* @param capacity - Calculated capacity information
*/
export function displayCapacity(capacity: StorageCapacityInfo): void {
if (capacity.isDepositLimited) {
log.indent(`→ Current capacity: ~${formatStorageCapacity(capacity.actualGiB)} ${pc.yellow('(deposit-limited)')}`)
log.indent(
`→ Potential: ~${formatStorageCapacity(capacity.potentialGiB)} (deposit ${formatUSDFC(capacity.additionalDepositNeeded)} more)`
)
} else {
log.indent(`→ Estimated capacity: ~${formatStorageCapacity(capacity.actualGiB)}`)
log.indent(pc.gray(' (excludes data set creation fee and optional CDN add-on rates)'))
}
}
/**
* Display current pricing information
*
* @param pricePerGiBPerMonth - Price per GiB per month
* @param pricePerTiBPerMonth - Price per TiB per month
*/
export function displayPricing(pricePerGiBPerMonth: bigint, pricePerTiBPerMonth: bigint): void {
log.line(pc.bold('Current Pricing:'))
log.indent(`1 GiB/month: ${formatUSDFC(pricePerGiBPerMonth)} USDFC`)
log.indent(`1 TiB/month: ${formatUSDFC(pricePerTiBPerMonth)} USDFC`)
log.indent(pc.gray('(for each upload, WarmStorage service will reserve 30 days of costs as security)'))
log.flush()
}
/**
* Display WarmStorage service permissions with capacity information
*
* @param title - Section title to display
* @param monthlyRate - Rate allowance in USDFC per month
* @param lockupAmount - Lockup allowance amount
* @param filecoinPayBalance - Total deposited amount
* @param pricePerTiBPerEpoch - Current pricing per TiB per epoch
*/
export function displayServicePermissions(
title: string,
monthlyRate: bigint,
lockupAmount: bigint,
filecoinPayBalance: bigint,
pricePerTiBPerEpoch: bigint,
shouldFlush: boolean = true
): void {
// Calculate capacity
const ratePerEpoch = monthlyRate / TIME_CONSTANTS.EPOCHS_PER_MONTH
const capacity = calculateActualCapacityWithDeposit(
filecoinPayBalance,
ratePerEpoch,
lockupAmount,
pricePerTiBPerEpoch
)
log.line(pc.bold(title))
log.indent(`Max payment: ${formatUSDFC(monthlyRate)} USDFC/month`)
log.indent(`Max reserve: ${formatUSDFC(lockupAmount)} USDFC (10-day lockup)`)
displayCapacity(capacity)
if (shouldFlush) {
log.flush()
}
}