UNPKG

filecoin-pin

Version:

Bridge IPFS content to Filecoin Onchain Cloud using familiar tools

826 lines (744 loc) 26.6 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' // Constants export const USDFC_DECIMALS = 18 const MIN_FIL_FOR_GAS = ethers.parseEther('0.1') // Minimum FIL padding for gas const DEFAULT_LOCKUP_DAYS = 10 // WarmStorage requires 10 days lockup // Maximum allowances for trusted WarmStorage service // Using MaxUint256 which MetaMask displays as "Unlimited" const MAX_RATE_ALLOWANCE = ethers.MaxUint256 const MAX_LOCKUP_ALLOWANCE = ethers.MaxUint256 // Standard buffer configuration (10%) used across deposit/lockup calculations const BUFFER_NUMERATOR = 11n const BUFFER_DENOMINATOR = 10n // 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 } /** * Maximum precision scale used when converting small TiB (as a float) to integer(BigInt) math */ export const STORAGE_SCALE_MAX = 10_000_000 const STORAGE_SCALE_MAX_BI = BigInt(STORAGE_SCALE_MAX) /** * 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)) } /** * Service approval status from the Payments contract */ export interface ServiceApprovalStatus { rateAllowance: bigint lockupAllowance: bigint lockupUsed: bigint maxLockupPeriod?: bigint rateUsed?: bigint } /** * Complete payment status including balances and approvals */ export interface PaymentStatus { network: string address: string filBalance: bigint usdfcBalance: bigint depositedAmount: bigint currentAllowances: ServiceApprovalStatus } /** * Storage allowance calculations */ export interface StorageAllowances { rateAllowance: bigint lockupAllowance: bigint storageCapacityTiB: number } /** * 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.getSigner() 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 usdfcBalance = await checkUSDFCBalance(synapse) * * if (usdfcBalance === 0n) { * console.log('No USDFC tokens found') * } else { * const formatted = ethers.formatUnits(usdfcBalance, USDFC_DECIMALS) * console.log(`USDFC Balance: ${formatted}`) * } * ``` * * @param synapse - Initialized Synapse instance * @returns USDFC balance in wei (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 depositedAmount = await synapse.payments.balance(TOKENS.USDFC) return depositedAmount } /** * 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.usdfcBalance, 18)}`) * console.log(`Deposited: ${ethers.formatUnits(status.depositedAmount, 18)}`) * ``` * * @param synapse - Initialized Synapse instance * @returns Complete payment status */ export async function getPaymentStatus(synapse: Synapse): Promise<PaymentStatus> { const signer = synapse.getSigner() const network = synapse.getNetwork() const warmStorageAddress = synapse.getWarmStorageAddress() // Run all async operations in parallel for efficiency const [address, filStatus, usdfcBalance, depositedAmount, currentAllowances] = await Promise.all([ signer.getAddress(), checkFILBalance(synapse), checkUSDFCBalance(synapse), getDepositedBalance(synapse), synapse.payments.serviceApproval(warmStorageAddress, TOKENS.USDFC), ]) return { network, address, filBalance: filStatus.balance, usdfcBalance, depositedAmount, currentAllowances, } } /** * Deposit USDFC into the Payments contract * * This demonstrates the two-step process required for depositing ERC20 tokens: * 1. Approve the Payments contract to spend USDFC (standard ERC20 approval) * 2. Call deposit to move funds into the Payments contract * * Example usage: * ```typescript * const amountToDeposit = ethers.parseUnits('100', 18) // 100 USDFC * const { approvalTx, 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<{ approvalTx?: string depositTx: string }> { const paymentsAddress = synapse.getPaymentsAddress() // Step 1: Check current allowance const currentAllowance = await synapse.payments.allowance(paymentsAddress, TOKENS.USDFC) let approvalTx: string | undefined // Step 2: Approve if needed (skip if already approved) if (currentAllowance < amount) { const approveTx = await synapse.payments.approve(paymentsAddress, amount, TOKENS.USDFC) await approveTx.wait() approvalTx = approveTx.hash } // Step 3: Make the deposit const depositTransaction = await synapse.payments.deposit(amount, TOKENS.USDFC) await depositTransaction.wait() const result: { approvalTx?: string; depositTx: string } = { depositTx: depositTransaction.hash, } if (approvalTx) { result.approvalTx = approvalTx } return result } /** * 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 10 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) const needsUpdate = currentAllowances.rateAllowance < MAX_RATE_ALLOWANCE || currentAllowances.lockupAllowance < MAX_LOCKUP_ALLOWANCE return { needsUpdate, currentAllowances, } } /** * 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<{ transactionHash: string currentAllowances: ServiceApprovalStatus }> { 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 * * 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 }> { const checkResult = await checkAllowances(synapse) if (checkResult.needsUpdate) { 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 (10 days worth) const epochsIn10Days = BigInt(DEFAULT_LOCKUP_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY const lockupAllowance = rateAllowance * epochsIn10Days 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 { if (pricePerTiBPerEpoch === 0n) return 0 // 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 10-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 { if (pricePerTiBPerEpoch === 0n) return 0 // Calculate how much this covers for 10 days const epochsIn10Days = BigInt(DEFAULT_LOCKUP_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY const ratePerEpoch = usdfcAmount / epochsIn10Days return calculateActualCapacity(ratePerEpoch, pricePerTiBPerEpoch) } /** * Compute the additional deposit required to fund current usage for a duration. * * The WarmStorage service maintains ~10 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.depositedAmount > lockupUsed ? status.depositedAmount - lockupUsed : 0n, rateUsed, perDay: rateUsed * TIME_CONSTANTS.EPOCHS_PER_DAY, lockupUsed, } } if (rateUsed === 0n) { return { topUp: 0n, available: status.depositedAmount > lockupUsed ? status.depositedAmount - lockupUsed : 0n, rateUsed, perDay: 0n, lockupUsed, } } const epochsNeeded = BigInt(Math.ceil(days)) * TIME_CONSTANTS.EPOCHS_PER_DAY const spendNeeded = rateUsed * epochsNeeded const available = status.depositedAmount > lockupUsed ? status.depositedAmount - 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.depositedAmount > lockupUsed ? status.depositedAmount - 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.depositedAmount return { delta, clampedTarget, lockupUsed } } /** * 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 10-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 (10 days) + at least some buffer const epochsIn10Days = 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) / (epochsIn10Days * 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 * epochsIn10Days const totalRequired = withBuffer(requiredLockup) return { tibPerMonth, gibPerMonth, monthlyPayment, requiredLockup, totalRequired, isDepositSufficient: depositAmount >= totalRequired, } } /** * Calculate required allowances from CAR file size * * Simple wrapper that converts file size to storage allowances. * * @param carSizeBytes - Size of the CAR file in bytes * @param pricePerTiBPerEpoch - Current pricing from storage service * @returns Required allowances for the file */ export function calculateRequiredAllowances(carSizeBytes: number, pricePerTiBPerEpoch: bigint): StorageAllowances { const storageTiB = carSizeBytes / Number(SIZE_CONSTANTS.TiB) return calculateStorageAllowances(storageTiB, pricePerTiBPerEpoch) } /** * 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[] } /** * Validate payment capacity for a specific CAR file * * This function checks if the deposit is sufficient for the file upload. It * does not account for allowances since WarmStorage is assumed to be given * full trust with max allowances. * * 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 carSizeBytes - Size of the CAR file in bytes * @returns Capacity check result */ export async function validatePaymentCapacity(synapse: Synapse, carSizeBytes: number): Promise<PaymentCapacityCheck> { // First ensure allowances are at max 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 = carSizeBytes / Number(SIZE_CONSTANTS.TiB) // Calculate requirements const required = calculateRequiredAllowances(carSizeBytes, pricePerTiBPerEpoch) const totalDepositNeeded = withBuffer(required.lockupAllowance) const result: PaymentCapacityCheck = { canUpload: true, storageTiB, required, issues: {}, suggestions: [], } // Only check deposit if (status.depositedAmount < totalDepositNeeded) { result.canUpload = false result.issues.insufficientDeposit = totalDepositNeeded - status.depositedAmount const depositNeeded = ethers.formatUnits(totalDepositNeeded - status.depositedAmount, 18) result.suggestions.push(`Deposit at least ${depositNeeded} USDFC`) } // Add warning if approaching deposit limit const totalLockupAfter = status.currentAllowances.lockupUsed + required.lockupAllowance if (totalLockupAfter > withoutBuffer(status.depositedAmount) && result.canUpload) { const additionalDeposit = ethers.formatUnits(withBuffer(totalLockupAfter) - status.depositedAmount, 18) result.suggestions.push(`Consider depositing ${additionalDeposit} more USDFC for safety margin`) } return result }