UNPKG

filecoin-pin

Version:

Bridge IPFS content to Filecoin Onchain Cloud using familiar tools

1,015 lines (916 loc) 33 kB
/** * Synapse SDK Payment Operations * * This module demonstrates comprehensive payment operations using the Synapse SDK, * providing patterns for interacting with the Filecoin Onchain Cloud payment * system (Filecoin Pay). * * Key concepts demonstrated: * - Native FIL balance checking for gas fees * - ERC20 token (USDFC) balance management * - Two-step deposit process (approve + deposit) * - Service approval configuration for storage operators * - Storage capacity calculations from pricing * * @module synapse/payments */ import { SIZE_CONSTANTS, type Synapse, TIME_CONSTANTS, TOKENS } from '@filoz/synapse-sdk' import { ethers } from 'ethers' import { isSessionKeyMode } from '../synapse/index.js' import { assertPriceNonZero } from '../utils/validate-pricing.js' import { BUFFER_DENOMINATOR, BUFFER_NUMERATOR, DEFAULT_LOCKUP_DAYS, MAX_LOCKUP_ALLOWANCE, MAX_RATE_ALLOWANCE, MIN_FIL_FOR_GAS, STORAGE_SCALE_MAX, STORAGE_SCALE_MAX_BI, USDFC_DECIMALS, } from './constants.js' import { applyFloorPricing } from './floor-pricing.js' import type { PaymentStatus, ServiceApprovalStatus, StorageAllowances, StorageRunwaySummary } from './types.js' import { padSizeToPDPLeaves } from './utils.js' // Re-export all constants export * from './constants.js' export * from './floor-pricing.js' export * from './top-up.js' export * from './types.js' // Helper to apply a buffer on top of a base amount function withBuffer(amount: bigint): bigint { return (amount * BUFFER_NUMERATOR) / BUFFER_DENOMINATOR } // Helper to remove the buffer (inverse of withBuffer) function withoutBuffer(amount: bigint): bigint { return (amount * BUFFER_DENOMINATOR) / BUFFER_NUMERATOR } /** * Compute adaptive integer scaling for a TiB value so that * Math.floor(storageTiB * scale) stays within Number.MAX_SAFE_INTEGER. * This allows us to handle numbers as small as 1/10_000_000 TiB and as large as Number.MAX_SAFE_INTEGER TiB (> 1 YiB) */ export function getStorageScale(storageTiB: number): number { if (storageTiB <= 0) return 1 const maxScaleBySafe = Math.floor(Number.MAX_SAFE_INTEGER / storageTiB) return Math.max(1, Math.min(STORAGE_SCALE_MAX, maxScaleBySafe)) } /** * Check FIL balance for gas fees * * Example usage: * ```typescript * const synapse = await Synapse.create({ privateKey, rpcURL }) * const filStatus = await checkFILBalance(synapse) * * if (filStatus.balance === 0n) { * console.log('Account does not exist on-chain or has no FIL') * } else if (!filStatus.hasSufficientGas) { * console.log('Insufficient FIL for gas fees') * } * ``` * * @param synapse - Initialized Synapse instance * @returns Balance information and network type */ export async function checkFILBalance(synapse: Synapse): Promise<{ balance: bigint isCalibnet: boolean hasSufficientGas: boolean }> { const network = synapse.getNetwork() const isCalibnet = network === 'calibration' try { const provider = synapse.getProvider() const signer = synapse.getClient() // owner wallet const address = await signer.getAddress() // Get native token balance const balance = await provider.getBalance(address) // Check if balance is sufficient for gas const hasSufficientGas = balance >= MIN_FIL_FOR_GAS return { balance, isCalibnet, hasSufficientGas, } } catch (_error) { // Account doesn't exist or network error return { balance: 0n, isCalibnet, hasSufficientGas: false, } } } /** * Check USDFC token balance in wallet * * Example usage: * ```typescript * const synapse = await Synapse.create({ privateKey, rpcURL }) * const walletUsdfcBalance = await checkUSDFCBalance(synapse) * * if (walletUsdfcBalance === 0n) { * console.log('No USDFC tokens found') * } else { * const formatted = ethers.formatUnits(walletUsdfcBalance, USDFC_DECIMALS) * console.log(`USDFC Balance: ${formatted}`) * } * ``` * * @param synapse - Initialized Synapse instance * @returns bigint USDFC balance in wallet (0 if account doesn't exist or has no balance) */ export async function checkUSDFCBalance(synapse: Synapse): Promise<bigint> { try { // Get wallet balance (not deposited balance) const balance = await synapse.payments.walletBalance(TOKENS.USDFC) return balance } catch (_error) { // Account doesn't exist, has no FIL for gas, or contract call failed // Treat as having 0 USDFC return 0n } } /** * Get deposited USDFC balance in Payments contract * * This is different from wallet balance - it's the amount * already deposited and available for payment rails. * * @param synapse - Initialized Synapse instance * @returns Deposited USDFC balance in its smallest unit */ export async function getDepositedBalance(synapse: Synapse): Promise<bigint> { const filecoinPayBalance = await synapse.payments.balance(TOKENS.USDFC) return filecoinPayBalance } /** * Get current payment status including all balances and approvals * * Example usage: * ```typescript * const status = await getPaymentStatus(synapse) * console.log(`Address: ${status.address}`) * console.log(`FIL Balance: ${ethers.formatEther(status.filBalance)}`) * console.log(`USDFC Balance: ${ethers.formatUnits(status.walletUsdfcBalance, 18)}`) * console.log(`Deposited: ${ethers.formatUnits(status.filecoinPayBalance, 18)}`) * ``` * * @param synapse - Initialized Synapse instance * @returns Complete payment status */ export async function getPaymentStatus(synapse: Synapse): Promise<PaymentStatus> { const client = synapse.getClient() // Use owner wallet, not session key const network = synapse.getNetwork() const warmStorageAddress = synapse.getWarmStorageAddress() // Run all async operations in parallel for efficiency const [address, filStatus, walletUsdfcBalance, filecoinPayBalance, currentAllowances] = await Promise.all([ client.getAddress(), checkFILBalance(synapse), checkUSDFCBalance(synapse), getDepositedBalance(synapse), synapse.payments.serviceApproval(warmStorageAddress, TOKENS.USDFC), ]) return { network, address, filBalance: filStatus.balance, walletUsdfcBalance, filecoinPayBalance, currentAllowances, } } export interface PaymentValidationResult { isValid: boolean errorMessage?: string helpMessage?: string } export function validatePaymentRequirements( hasSufficientGas: boolean, walletUsdfcBalance: bigint, isCalibnet: boolean ): PaymentValidationResult { if (!hasSufficientGas) { const result: PaymentValidationResult = { isValid: false, errorMessage: 'Insufficient FIL for gas fees', } if (isCalibnet) { result.helpMessage = 'Get test FIL from: https://faucet.calibnet.chainsafe-fil.io/' } return result } if (walletUsdfcBalance === 0n) { return { isValid: false, errorMessage: 'No USDFC tokens found', helpMessage: 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', } } return { isValid: true } } /** * Deposit USDFC into the Payments contract * * This demonstrates the single-step process required for depositing ERC20 tokens: * 1. If approval is insufficient, use permit to approve and deposit in one transaction * 2. If approval is sufficient, directly call deposit * * Example usage: * ```typescript * const amountToDeposit = ethers.parseUnits('100', 18) // 100 USDFC * const { depositTx } = await depositUSDFC(synapse, amountToDeposit) * console.log(`Deposit transaction: ${depositTx}`) * ``` * * @param synapse - Initialized Synapse instance * @param amount - Amount to deposit in USDFC (with decimals) * @returns Transaction hashes for approval and deposit */ export async function depositUSDFC( synapse: Synapse, amount: bigint ): Promise<{ depositTx: string }> { const needsAllowanceUpdate = (await checkAllowances(synapse)).needsUpdate const amountMoreThanCurrentAllowance = (await synapse.payments.allowance(synapse.getPaymentsAddress(), TOKENS.USDFC)) < amount let tx: ethers.TransactionResponse if (amountMoreThanCurrentAllowance || needsAllowanceUpdate) { tx = await synapse.payments.depositWithPermitAndApproveOperator( amount, synapse.getWarmStorageAddress(), MAX_RATE_ALLOWANCE, MAX_LOCKUP_ALLOWANCE, BigInt(DEFAULT_LOCKUP_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY ) } else { tx = await synapse.payments.deposit(amount, TOKENS.USDFC) } await tx.wait() return { depositTx: tx.hash } } /** * Withdraw USDFC from the Payments contract back to the wallet * * Example usage: * ```typescript * const amountToWithdraw = ethers.parseUnits('10', 18) // 10 USDFC * const txHash = await withdrawUSDFC(synapse, amountToWithdraw) * console.log(`Withdraw transaction: ${txHash}`) * ``` * * @param synapse - Initialized Synapse instance * @param amount - Amount to withdraw in USDFC (with decimals) * @returns Transaction hash for the withdrawal */ export async function withdrawUSDFC(synapse: Synapse, amount: bigint): Promise<string> { const tx = await synapse.payments.withdraw(amount, TOKENS.USDFC) await tx.wait() return tx.hash } /** * Set service approvals for WarmStorage operator * * This authorizes the WarmStorage contract to create payment rails on behalf * of the user. The approval consists of three parameters: * - Rate allowance: Maximum payment rate per epoch (30 seconds) * - Lockup allowance: Maximum funds that can be locked at once * - Max lockup period: How far in advance funds can be locked (in epochs) * * Example usage: * ```typescript * // Allow up to 10 USDFC per epoch rate, 1000 USDFC total lockup * const rate = ethers.parseUnits('10', 18) * const lockup = ethers.parseUnits('1000', 18) * const txHash = await setServiceApprovals(synapse, rate, lockup) * console.log(`Approval transaction: ${txHash}`) * ``` * * @param synapse - Initialized Synapse instance * @param rateAllowance - Maximum rate per epoch in USDFC * @param lockupAllowance - Maximum lockup amount in USDFC * @returns Transaction hash */ export async function setServiceApprovals( synapse: Synapse, rateAllowance: bigint, lockupAllowance: bigint ): Promise<string> { const warmStorageAddress = synapse.getWarmStorageAddress() // Max lockup period is always 30 days worth of epochs for WarmStorage const maxLockupPeriod = BigInt(DEFAULT_LOCKUP_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY // Set the service approval const tx = await synapse.payments.approveService( warmStorageAddress, rateAllowance, lockupAllowance, maxLockupPeriod, TOKENS.USDFC ) await tx.wait() return tx.hash } /** * Check if WarmStorage allowances are at maximum * * This function checks whether the current allowances for WarmStorage * are already set to maximum values (effectively infinite). * * @param synapse - Initialized Synapse instance * @returns Current allowances and whether they need updating */ export async function checkAllowances(synapse: Synapse): Promise<{ needsUpdate: boolean currentAllowances: ServiceApprovalStatus }> { const warmStorageAddress = synapse.getWarmStorageAddress() // Get current allowances const currentAllowances = await synapse.payments.serviceApproval(warmStorageAddress, TOKENS.USDFC) // Check if we need to update (not at max or max lockup period is not enough) const needsUpdate = currentAllowances.rateAllowance < MAX_RATE_ALLOWANCE || currentAllowances.lockupAllowance < MAX_LOCKUP_ALLOWANCE || currentAllowances.maxLockupPeriod < BigInt(DEFAULT_LOCKUP_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY return { needsUpdate, currentAllowances, } } /** * Result of setting maximum allowances for WarmStorage */ export interface SetMaxAllowancesResult { /** Transaction hash of the allowance update */ transactionHash: string /** Updated allowance status after the transaction */ currentAllowances: ServiceApprovalStatus } /** * Set WarmStorage allowances to maximum * * This function sets the allowances for WarmStorage to maximum values, * effectively treating it as a fully trusted service. * * @param synapse - Initialized Synapse instance * @returns Transaction hash and updated allowances */ export async function setMaxAllowances(synapse: Synapse): Promise<SetMaxAllowancesResult> { const warmStorageAddress = synapse.getWarmStorageAddress() // Set to maximum allowances const txHash = await setServiceApprovals(synapse, MAX_RATE_ALLOWANCE, MAX_LOCKUP_ALLOWANCE) // Return updated allowances const updatedAllowances = await synapse.payments.serviceApproval(warmStorageAddress, TOKENS.USDFC) return { transactionHash: txHash, currentAllowances: updatedAllowances, } } /** * Check and automatically set WarmStorage allowances to maximum if needed * * This function treats WarmStorage as a fully trusted service and ensures * that rate and lockup allowances are always set to maximum values. * This simplifies the user experience by removing the need to understand * and configure complex allowance settings by assuming that WarmStorage * can be fully trusted to manage payments on the user's behalf. * * The function will: * 1. Check current allowances for WarmStorage * 2. If either is not at maximum, update them to MAX_UINT256 * 3. Return information about what was done * * **Session Key Authentication**: When using session key authentication, * this function will not attempt to update allowances since payment * operations require the owner wallet to sign. The function will return * `updated: false` and current allowances, which may not be at maximum. * * Example usage: * ```typescript * // Call before any operation that requires payments * const result = await checkAndSetAllowances(synapse) * if (result.updated) { * console.log(`Allowances updated: ${result.transactionHash}`) * } * ``` * * @param synapse - Initialized Synapse instance * @returns Result indicating if allowances were updated and transaction hash if applicable */ export async function checkAndSetAllowances(synapse: Synapse): Promise<{ updated: boolean transactionHash?: string currentAllowances: ServiceApprovalStatus }> { // Skip automatic updates in session key mode const sessionKeyMode = isSessionKeyMode(synapse) const checkResult = await checkAllowances(synapse) if (checkResult.needsUpdate && !sessionKeyMode) { const setResult = await setMaxAllowances(synapse) return { updated: true, transactionHash: setResult.transactionHash, currentAllowances: setResult.currentAllowances, } } return { updated: false, currentAllowances: checkResult.currentAllowances, } } /** * Calculate storage allowances from TiB per month * * This utility converts human-friendly storage units (TiB/month) into the * epoch-based rates required by the payment system. It uses the actual * pricing from the storage service to calculate accurate allowances. * * Example usage: * ```typescript * const storageInfo = await synapse.storage.getStorageInfo() * const pricing = storageInfo.pricing.noCDN.perTiBPerEpoch * * // Calculate allowances for 10 TiB/month * const allowances = calculateStorageAllowances(10, pricing) * console.log(`Rate needed: ${ethers.formatUnits(allowances.rateAllowance, 18)} USDFC/epoch`) * ``` * * @param storageTiB - Desired storage capacity in TiB/month * @param pricePerTiBPerEpoch - Current pricing from storage service * @returns Calculated allowances for the specified capacity */ export function calculateStorageAllowances(storageTiB: number, pricePerTiBPerEpoch: bigint): StorageAllowances { // Use adaptive scaling to avoid Number overflow/precision issues for very large values // and to preserve precision for small fractional values. const scale = getStorageScale(storageTiB) const scaledStorage = Math.floor(storageTiB * scale) // Calculate rate allowance (per epoch payment) const rateAllowance = (pricePerTiBPerEpoch * BigInt(scaledStorage)) / BigInt(scale) // Calculate lockup allowance const epochsInLockupDays = BigInt(DEFAULT_LOCKUP_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY const lockupAllowance = rateAllowance * epochsInLockupDays return { rateAllowance, lockupAllowance, storageCapacityTiB: storageTiB, } } /** * Calculate actual storage capacity from current allowances * * This is the inverse of calculateStorageAllowances - it determines how much * storage capacity the current allowances support. * * @param rateAllowance - Current rate allowance in its smallest unit * @param pricePerTiBPerEpoch - Current pricing from storage service * @returns Storage capacity in TiB that can be supported */ export function calculateActualCapacity(rateAllowance: bigint, pricePerTiBPerEpoch: bigint): number { assertPriceNonZero(pricePerTiBPerEpoch) // Calculate TiB capacity from rate allowance const scaledQuotient = (rateAllowance * STORAGE_SCALE_MAX_BI) / pricePerTiBPerEpoch if (scaledQuotient > 0n) { return Number(scaledQuotient) / STORAGE_SCALE_MAX } // fallback for very small values that underflow to 0 after integer division const rateFloat = Number(ethers.formatUnits(rateAllowance, USDFC_DECIMALS)) const priceFloat = Number(ethers.formatUnits(pricePerTiBPerEpoch, USDFC_DECIMALS)) if (!Number.isFinite(rateFloat) || !Number.isFinite(priceFloat) || priceFloat === 0) { return 0 } return rateFloat / priceFloat } /** * Calculate storage capacity from USDFC amount * * Determines how much storage can be purchased with a given USDFC amount, * accounting for the 30-day lockup period. * * @param usdfcAmount - Amount of USDFC in its smallest unit * @param pricePerTiBPerEpoch - Current pricing from storage service * @returns Storage capacity in TiB/month */ export function calculateStorageFromUSDFC(usdfcAmount: bigint, pricePerTiBPerEpoch: bigint): number { assertPriceNonZero(pricePerTiBPerEpoch) // Calculate how much this covers for lockup const epochsInLockupDays = BigInt(DEFAULT_LOCKUP_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY const ratePerEpoch = usdfcAmount / epochsInLockupDays return calculateActualCapacity(ratePerEpoch, pricePerTiBPerEpoch) } /** * Compute the additional deposit required to fund current usage for a duration. * * The WarmStorage service maintains ~30 days of lockup (lockupUsed) and draws future * lockups from the available deposit (deposited - lockupUsed). To keep the current * rails alive for N days, ensure available >= N days of spend at the current rateUsed. * * @param status - Current payment status (from getPaymentStatus) * @param days - Number of days to keep the current usage funded * @returns Breakdown of required top-up and related values */ export function computeTopUpForDuration( status: PaymentStatus, days: number ): { topUp: bigint available: bigint rateUsed: bigint perDay: bigint lockupUsed: bigint } { const rateUsed = status.currentAllowances.rateUsed ?? 0n const lockupUsed = status.currentAllowances.lockupUsed ?? 0n if (days <= 0) { return { topUp: 0n, available: status.filecoinPayBalance > lockupUsed ? status.filecoinPayBalance - lockupUsed : 0n, rateUsed, perDay: rateUsed * TIME_CONSTANTS.EPOCHS_PER_DAY, lockupUsed, } } if (rateUsed === 0n) { return { topUp: 0n, available: status.filecoinPayBalance > lockupUsed ? status.filecoinPayBalance - lockupUsed : 0n, rateUsed, perDay: 0n, lockupUsed, } } const epochsNeeded = BigInt(Math.ceil(days)) * TIME_CONSTANTS.EPOCHS_PER_DAY const spendNeeded = rateUsed * epochsNeeded const available = status.filecoinPayBalance > lockupUsed ? status.filecoinPayBalance - lockupUsed : 0n const topUp = spendNeeded > available ? spendNeeded - available : 0n return { topUp, available, rateUsed, perDay: rateUsed * TIME_CONSTANTS.EPOCHS_PER_DAY, lockupUsed, } } /** * Compute the exact adjustment (deposit or withdraw) needed to set runway to `days`. * * Positive result indicates a deposit is needed; negative indicates a withdrawal is possible. */ export function computeAdjustmentForExactDays( status: PaymentStatus, days: number ): { delta: bigint // >0 deposit, <0 withdraw, 0 none targetAvailable: bigint available: bigint rateUsed: bigint perDay: bigint lockupUsed: bigint } { const rateUsed = status.currentAllowances.rateUsed ?? 0n const lockupUsed = status.currentAllowances.lockupUsed ?? 0n const available = status.filecoinPayBalance > lockupUsed ? status.filecoinPayBalance - lockupUsed : 0n const perDay = rateUsed * TIME_CONSTANTS.EPOCHS_PER_DAY if (days < 0) { throw new Error('days must be non-negative') } if (rateUsed === 0n) { return { delta: 0n, targetAvailable: 0n, available, rateUsed, perDay, lockupUsed, } } // Safety buffer to ensure runway >= requested days even if rateUsed shifts slightly. // Use a 1-hour buffer by default. const perHour = perDay / 24n const safety = perHour > 0n ? perHour : 1n const targetAvailable = BigInt(Math.floor(days)) * perDay + safety const delta = targetAvailable - available return { delta, targetAvailable, available, rateUsed, perDay, lockupUsed, } } /** * Compute the exact adjustment (deposit or withdraw) to reach a target absolute deposit. * * Clamps to not withdraw below the currently locked amount. */ export function computeAdjustmentForExactDeposit( status: PaymentStatus, targetDeposit: bigint ): { delta: bigint // >0 deposit, <0 withdraw, 0 none clampedTarget: bigint lockupUsed: bigint } { if (targetDeposit < 0n) throw new Error('target deposit cannot be negative') const lockupUsed = status.currentAllowances.lockupUsed ?? 0n const clampedTarget = targetDeposit < lockupUsed ? lockupUsed : targetDeposit const delta = clampedTarget - status.filecoinPayBalance return { delta, clampedTarget, lockupUsed } } /** * Compute adjustment needed to maintain target runway AFTER adding a new piece * * This function accounts for both: * - The new piece's lockup requirement * - The new piece's ongoing per-epoch cost (rate) * * @param status - Current payment status * @param days - Target runway in days * @param pieceSizeBytes - Size of the piece (CAR, File, etc.) file being uploaded in bytes * @param pricePerTiBPerEpoch - Current pricing from storage service * @returns Adjustment details including total delta needed */ export function computeAdjustmentForExactDaysWithPiece( status: PaymentStatus, days: number, pieceSizeBytes: number, pricePerTiBPerEpoch: bigint ): { delta: bigint // >0 deposit, <0 withdraw, 0 none targetDeposit: bigint currentDeposit: bigint newLockupUsed: bigint newRateUsed: bigint } { const currentRateUsed = status.currentAllowances.rateUsed ?? 0n const currentLockupUsed = status.currentAllowances.lockupUsed ?? 0n // Calculate required allowances for the new file with floor pricing applied const baseAllowances = calculateRequiredAllowances(pieceSizeBytes, pricePerTiBPerEpoch) const newPieceAllowances = applyFloorPricing(baseAllowances) // Calculate new totals after adding the piece const newRateUsed = currentRateUsed + newPieceAllowances.rateAllowance const newLockupUsed = currentLockupUsed + newPieceAllowances.lockupAllowance // Calculate deposit needed for target runway with new rate const perDay = newRateUsed * TIME_CONSTANTS.EPOCHS_PER_DAY if (days < 0) { throw new Error('days must be non-negative') } // If no ongoing spend (both current and new), just need the lockup if (newRateUsed === 0n) { const targetDeposit = newLockupUsed const delta = targetDeposit - status.filecoinPayBalance return { delta, targetDeposit, currentDeposit: status.filecoinPayBalance, newLockupUsed, newRateUsed, } } // Safety buffer to ensure runway >= requested days even if rateUsed shifts slightly const perHour = perDay / 24n const safety = perHour > 0n ? perHour : 1n // Target: lockup (with buffer) + (days worth of ongoing cost) const targetAvailable = BigInt(Math.floor(days)) * perDay + safety const targetDeposit = withBuffer(newLockupUsed) + targetAvailable const delta = targetDeposit - status.filecoinPayBalance return { delta, targetDeposit, currentDeposit: status.filecoinPayBalance, newLockupUsed, newRateUsed, } } /** * Calculate storage capacity from deposit amount * * This function calculates how much storage capacity a deposit can support, * treating WarmStorage as fully trusted with max allowances, i.e. not * accounting for allowance limits. If usage limits need to be accounted for * then the capacity can be capped by either deposit or allowances. * This function accounts for the 30-day lockup requirement. * * @param depositAmount - Amount deposited in USDFC * @param pricePerTiBPerEpoch - Current pricing from storage service * @returns Storage capacity information */ export function calculateDepositCapacity( depositAmount: bigint, pricePerTiBPerEpoch: bigint ): { tibPerMonth: number gibPerMonth: number monthlyPayment: bigint requiredLockup: bigint totalRequired: bigint isDepositSufficient: boolean } { if (pricePerTiBPerEpoch === 0n) { return { tibPerMonth: 0, gibPerMonth: 0, monthlyPayment: 0n, requiredLockup: 0n, totalRequired: 0n, isDepositSufficient: true, } } // With infinite allowances, deposit is the only limiting factor // Deposit needs to cover: lockup (30 days) + at least some buffer const epochsInLockupDays = BigInt(DEFAULT_LOCKUP_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY const epochsPerMonth = TIME_CONSTANTS.EPOCHS_PER_MONTH // Maximum storage we can support with this deposit // Reserve 10% for buffer beyond the lockup // Calculate max rate per epoch we can afford with deposit const maxRatePerEpoch = (depositAmount * BUFFER_DENOMINATOR) / (epochsInLockupDays * BUFFER_NUMERATOR) // Convert to storage capacity const tibPerMonth = calculateActualCapacity(maxRatePerEpoch, pricePerTiBPerEpoch) const gibPerMonth = tibPerMonth * 1024 // Calculate the actual costs for this capacity const monthlyPayment = maxRatePerEpoch * epochsPerMonth const requiredLockup = maxRatePerEpoch * epochsInLockupDays const totalRequired = withBuffer(requiredLockup) return { tibPerMonth, gibPerMonth, monthlyPayment, requiredLockup, totalRequired, isDepositSufficient: depositAmount >= totalRequired, } } /** * Calculate required allowances from piece size * * Simple wrapper that converts piece size to storage allowances. * * @param pieceSizeBytes - Size of the piece (CAR, File, etc.) file in bytes * @param pricePerTiBPerEpoch - Current pricing from storage service * @returns Required allowances for the piece */ export function calculateRequiredAllowances(pieceSizeBytes: number, pricePerTiBPerEpoch: bigint): StorageAllowances { const paddedSizeBytes = padSizeToPDPLeaves(pieceSizeBytes) const storageTiB = paddedSizeBytes / Number(SIZE_CONSTANTS.TiB) return calculateStorageAllowances(storageTiB, pricePerTiBPerEpoch) } export function calculateStorageRunway( status?: Pick<PaymentStatus, 'filecoinPayBalance' | 'currentAllowances'> | null ): StorageRunwaySummary { if (!status || !status.currentAllowances) { return { state: 'unknown', available: 0n, rateUsed: 0n, perDay: 0n, lockupUsed: 0n, days: 0, hours: 0, } } const rateUsed = status.currentAllowances.rateUsed ?? 0n const lockupUsed = status.currentAllowances.lockupUsed ?? 0n const filecoinPayBalance = status.filecoinPayBalance ?? 0n const available = filecoinPayBalance > lockupUsed ? filecoinPayBalance - lockupUsed : 0n if (rateUsed === 0n) { return { state: 'no-spend', available, rateUsed, perDay: 0n, lockupUsed, days: 0, hours: 0, } } const perDay = rateUsed * TIME_CONSTANTS.EPOCHS_PER_DAY if (perDay === 0n) { return { state: 'no-spend', available, rateUsed, perDay, lockupUsed, days: 0, hours: 0, } } const runwayDays = Number(available / perDay) const runwayHoursRemainder = Number(((available % perDay) * 24n) / perDay) return { state: 'active', available, rateUsed, perDay, lockupUsed, days: runwayDays, hours: runwayHoursRemainder, } } /** * Payment capacity validation for a specific file */ export interface PaymentCapacityCheck { canUpload: boolean storageTiB: number required: StorageAllowances issues: { insufficientDeposit?: bigint insufficientRateAllowance?: bigint insufficientLockupAllowance?: bigint } suggestions: string[] } /** * Calculate piece upload deposit requirements * * @param status - Current payment status * @param pieceSizeBytes - Size of the piece (CAR, File, etc.) file in bytes * @param pricePerTiBPerEpoch - Current pricing from storage service * @returns Piece upload deposit requirements */ export function calculatePieceUploadRequirements( status: PaymentStatus, pieceSizeBytes: number, pricePerTiBPerEpoch: bigint ): { required: StorageAllowances totalDepositNeeded: bigint insufficientDeposit: bigint canUpload: boolean } { // Calculate base requirements and apply floor pricing const baseRequired = calculateRequiredAllowances(pieceSizeBytes, pricePerTiBPerEpoch) const required = applyFloorPricing(baseRequired) const totalDepositNeeded = withBuffer(required.lockupAllowance) // Check if current deposit can cover the new file's lockup requirement const insufficientDeposit = status.filecoinPayBalance < totalDepositNeeded ? totalDepositNeeded - status.filecoinPayBalance : 0n return { required, totalDepositNeeded, insufficientDeposit, canUpload: insufficientDeposit === 0n, } } /** * Validate payment capacity for a specific piece size * * This function checks if the deposit is sufficient for the piece upload. It * does not account for allowances since WarmStorage is assumed to be given * full trust with max allowances. * * **Note**: This function will attempt to automatically set max allowances * unless using session key authentication, in which case allowances must * be configured separately by the owner wallet. * * Example usage: * ```typescript * const fileSize = 10 * 1024 * 1024 * 1024 // 10 GiB * const capacity = await validatePaymentCapacity(synapse, fileSize) * * if (!capacity.canUpload) { * console.error('Cannot upload file with current payment setup') * capacity.suggestions.forEach(s => console.log(` - ${s}`)) * } * ``` * * @param synapse - Initialized Synapse instance * @param pieceSizeBytes - Size of the piece (CAR, File, etc.) file in bytes * @returns Capacity check result */ export async function validatePaymentCapacity(synapse: Synapse, pieceSizeBytes: number): Promise<PaymentCapacityCheck> { // Ensure allowances are at max (automatically skips if in session key mode) await checkAndSetAllowances(synapse) // Get current status and pricing const [status, storageInfo] = await Promise.all([getPaymentStatus(synapse), synapse.storage.getStorageInfo()]) const pricePerTiBPerEpoch = storageInfo.pricing.noCDN.perTiBPerEpoch const storageTiB = pieceSizeBytes / Number(SIZE_CONSTANTS.TiB) // Calculate requirements const uploadRequirements = calculatePieceUploadRequirements(status, pieceSizeBytes, pricePerTiBPerEpoch) const result: PaymentCapacityCheck = { canUpload: uploadRequirements.canUpload, storageTiB, required: uploadRequirements.required, issues: {}, suggestions: [], } // Only check deposit if (uploadRequirements.insufficientDeposit > 0n) { result.canUpload = false result.issues.insufficientDeposit = uploadRequirements.insufficientDeposit const depositNeeded = ethers.formatUnits(uploadRequirements.insufficientDeposit, 18) result.suggestions.push(`Deposit at least ${depositNeeded} USDFC`) } // Add warning if approaching deposit limit const totalLockupAfter = status.currentAllowances.lockupUsed + uploadRequirements.required.lockupAllowance if (totalLockupAfter > withoutBuffer(status.filecoinPayBalance) && result.canUpload) { const additionalDeposit = ethers.formatUnits(withBuffer(totalLockupAfter) - status.filecoinPayBalance, 18) result.suggestions.push(`Consider depositing ${additionalDeposit} more USDFC for safety margin`) } return result }