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