UNPKG

filecoin-pin

Version:

Bridge IPFS content to Filecoin Onchain Cloud using familiar tools

786 lines 31.5 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, 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 { 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) { return (amount * BUFFER_NUMERATOR) / BUFFER_DENOMINATOR; } // Helper to remove the buffer (inverse of withBuffer) function withoutBuffer(amount) { 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) { 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) { 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) { 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) { 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) { 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 function validatePaymentRequirements(hasSufficientGas, walletUsdfcBalance, isCalibnet) { if (!hasSufficientGas) { const result = { 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, amount) { const needsAllowanceUpdate = (await checkAllowances(synapse)).needsUpdate; const amountMoreThanCurrentAllowance = (await synapse.payments.allowance(synapse.getPaymentsAddress(), TOKENS.USDFC)) < amount; let tx; 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, amount) { 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, rateAllowance, lockupAllowance) { 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) { 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, }; } /** * 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) { 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) { // 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, pricePerTiBPerEpoch) { // 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, pricePerTiBPerEpoch) { 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, pricePerTiBPerEpoch) { 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, days) { 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, days) { 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, targetDeposit) { 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, days, pieceSizeBytes, pricePerTiBPerEpoch) { 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, pricePerTiBPerEpoch) { 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, pricePerTiBPerEpoch) { const paddedSizeBytes = padSizeToPDPLeaves(pieceSizeBytes); const storageTiB = paddedSizeBytes / Number(SIZE_CONSTANTS.TiB); return calculateStorageAllowances(storageTiB, pricePerTiBPerEpoch); } export function calculateStorageRunway(status) { 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, }; } /** * 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, pieceSizeBytes, pricePerTiBPerEpoch) { // 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, pieceSizeBytes) { // 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 = { 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; } //# sourceMappingURL=index.js.map