Skip to content
Merged
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
"homepage": "https://github.com/filecoin-project/filecoin-pin#readme",
"dependencies": {
"@clack/prompts": "^0.11.0",
"@filoz/synapse-sdk": "^0.34.0",
"@filoz/synapse-sdk": "^0.35.0",
"@helia/unixfs": "^6.0.1",
"@ipld/car": "^5.4.2",
"commander": "^14.0.1",
Expand Down
2 changes: 1 addition & 1 deletion src/add/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { CLIAuthOptions } from '../utils/cli-auth.js'
export interface AddOptions extends CLIAuthOptions {
filePath: string
bare?: boolean
/** Auto-fund: automatically ensure minimum 10 days of runway */
/** Auto-fund: automatically ensure minimum 30 days of runway */
autoFund?: boolean
}

Expand Down
2 changes: 1 addition & 1 deletion src/common/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/**
* Minimum runway in days to ensure WarmStorage can cover costs. Used when `--auto-fund` is passed to import or add commands
*/
export const MIN_RUNWAY_DAYS = 10
export const MIN_RUNWAY_DAYS = 30
6 changes: 3 additions & 3 deletions src/common/upload-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { Synapse } from '@filoz/synapse-sdk'
import type { CID } from 'multiformats/cid'
import pc from 'picocolors'
import type { Logger } from 'pino'
import type { PaymentCapacityCheck } from '../core/payments/index.js'
import { DEFAULT_LOCKUP_DAYS, type PaymentCapacityCheck } from '../core/payments/index.js'
import { cleanupSynapseService, type SynapseService } from '../core/synapse/index.js'
import { checkUploadReadiness, executeUpload, getDownloadURL, type SynapseUploadResult } from '../core/upload/index.js'
import { formatUSDFC } from '../core/utils/format.js'
Expand Down Expand Up @@ -48,7 +48,7 @@ export interface UploadFlowResult extends SynapseUploadResult {

/**
* Perform auto-funding if requested
* Automatically ensures a minimum of 10 days of runway based on current usage + new file requirements
* Automatically ensures a minimum of 30 days of runway based on current usage + new file requirements
*
* @param synapse - Initialized Synapse instance
* @param fileSize - Size of file being uploaded (in bytes)
Expand Down Expand Up @@ -207,7 +207,7 @@ function displayPaymentIssues(capacityCheck: PaymentCapacityCheck, fileSize: num
log.indent(
`Required deposit: ${formatUSDFC(capacityCheck.required.lockupAllowance + capacityCheck.required.lockupAllowance / 10n)} USDFC`
)
log.indent(pc.gray('(includes 10-day safety reserve)'))
log.indent(pc.gray(`(includes ${DEFAULT_LOCKUP_DAYS}-day safety reserve)`))
log.line('')

log.line(pc.bold('Suggested actions:'))
Expand Down
36 changes: 19 additions & 17 deletions src/core/payments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { isSessionKeyMode } from '../synapse/index.js'
// Constants
export const USDFC_DECIMALS = 18
const MIN_FIL_FOR_GAS = ethers.parseEther('0.1') // Minimum FIL padding for gas
export const DEFAULT_LOCKUP_DAYS = 10 // WarmStorage requires 10 days lockup
export const DEFAULT_LOCKUP_DAYS = 30 // WarmStorage requires 30 days lockup

// Maximum allowances for trusted WarmStorage service
// Using MaxUint256 which MetaMask displays as "Unlimited"
Expand Down Expand Up @@ -381,7 +381,7 @@ export async function setServiceApprovals(
): Promise<string> {
const warmStorageAddress = synapse.getWarmStorageAddress()

// Max lockup period is always 10 days worth of epochs for WarmStorage
// 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
Expand Down Expand Up @@ -415,9 +415,11 @@ export async function checkAllowances(synapse: Synapse): Promise<{
// Get current allowances
const currentAllowances = await synapse.payments.serviceApproval(warmStorageAddress, TOKENS.USDFC)

// Check if we need to update (not at max)
// 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.rateAllowance < MAX_RATE_ALLOWANCE ||
currentAllowances.lockupAllowance < MAX_LOCKUP_ALLOWANCE ||
currentAllowances.maxLockupPeriod < BigInt(DEFAULT_LOCKUP_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY

return {
needsUpdate,
Expand Down Expand Up @@ -537,9 +539,9 @@ export function calculateStorageAllowances(storageTiB: number, pricePerTiBPerEpo
// 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
// Calculate lockup allowance
const epochsInLockupDays = BigInt(DEFAULT_LOCKUP_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY
const lockupAllowance = rateAllowance * epochsInLockupDays

return {
rateAllowance,
Expand Down Expand Up @@ -580,7 +582,7 @@ export function calculateActualCapacity(rateAllowance: bigint, pricePerTiBPerEpo
* 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.
* accounting for the 30-day lockup period.
*
* @param usdfcAmount - Amount of USDFC in its smallest unit
* @param pricePerTiBPerEpoch - Current pricing from storage service
Expand All @@ -589,17 +591,17 @@ export function calculateActualCapacity(rateAllowance: bigint, pricePerTiBPerEpo
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
// 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 ~10 days of lockup (lockupUsed) and draws future
* 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.
*
Expand Down Expand Up @@ -808,7 +810,7 @@ export function computeAdjustmentForExactDaysWithPiece(
* 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.
* This function accounts for the 30-day lockup requirement.
*
* @param depositAmount - Amount deposited in USDFC
* @param pricePerTiBPerEpoch - Current pricing from storage service
Expand Down Expand Up @@ -837,22 +839,22 @@ export function calculateDepositCapacity(
}

// 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
// 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) / (epochsIn10Days * BUFFER_NUMERATOR)
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 * epochsIn10Days
const requiredLockup = maxRatePerEpoch * epochsInLockupDays
const totalRequired = withBuffer(requiredLockup)

return {
Expand Down
3 changes: 0 additions & 3 deletions src/data-set/inspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,9 +244,6 @@ export function displayDataSetStatus(ctx: DataSetInspectionContext, dataSetId: n
if (base.pdpEndEpoch > 0) {
log.indent(pc.yellow(`PDP payments ended @ epoch ${base.pdpEndEpoch}`))
}
if (base.cdnEndEpoch > 0) {
log.indent(pc.yellow(`CDN payments ended @ epoch ${base.cdnEndEpoch}`))
}

log.line('')
log.line(pc.bold('Metadata'))
Expand Down
2 changes: 1 addition & 1 deletion src/import/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { CLIAuthOptions } from '../utils/cli-auth.js'

export interface ImportOptions extends CLIAuthOptions {
filePath: string
/** Auto-fund: automatically ensure minimum 10 days of runway */
/** Auto-fund: automatically ensure minimum 30 days of runway */
autoFund?: boolean
}

Expand Down
19 changes: 10 additions & 9 deletions src/payments/fund.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
computeAdjustmentForExactDays,
computeAdjustmentForExactDaysWithPiece,
computeAdjustmentForExactDeposit,
DEFAULT_LOCKUP_DAYS,
depositUSDFC,
getPaymentStatus,
validatePaymentRequirements,
Expand All @@ -31,8 +32,8 @@ import { cancel, createSpinner, intro, isInteractive, outro } from '../utils/cli
import { isTTY, log } from '../utils/cli-logger.js'
import type { AutoFundOptions, FundingAdjustmentResult, FundOptions } from './types.js'

// Helper: confirm/warn or bail when target implies < 10-day runway
async function ensureBelowTenDaysAllowed(opts: {
// Helper: confirm/warn or bail when target implies < lockup-days runway
async function ensureBelowThirtyDaysAllowed(opts: {
spinner: Spinner
warningLine1: string
warningLine2: string
Expand All @@ -43,7 +44,7 @@ async function ensureBelowTenDaysAllowed(opts: {
console.error(pc.red(warningLine1))
console.error(pc.red(warningLine2))
cancel('Fund adjustment aborted')
throw new Error('Unsafe target below 10-day baseline')
throw new Error(`Unsafe target below ${DEFAULT_LOCKUP_DAYS}-day baseline`)
}

log.line(pc.yellow('⚠ Warning'))
Expand All @@ -52,7 +53,7 @@ async function ensureBelowTenDaysAllowed(opts: {
log.flush()

const proceed = await confirm({
message: 'Proceed with reducing runway below 10 days?',
message: 'Proceed with reducing runway below 30 days?',
initialValue: false,
})
if (!proceed) {
Expand Down Expand Up @@ -347,14 +348,14 @@ export async function runFund(options: FundOptions): Promise<void> {
}
delta = 0n
}
} else if (runwayCheckDays != null && runwayCheckDays < 10) {
} else if (runwayCheckDays != null && runwayCheckDays < Number(TIME_CONSTANTS.DEFAULT_LOCKUP_DAYS)) {
const line1 = hasDays
? 'Requested runway below 10-day safety baseline.'
: 'Target deposit implies less than 10 days of runway at current spend.'
? 'Requested runway below 30-day safety baseline.'
: 'Target deposit implies less than 30 days of runway at current spend.'
const line2 = hasDays
? 'WarmStorage reserves 10 days of costs; a shorter runway risks termination.'
? 'WarmStorage reserves 30 days of costs; a shorter runway risks termination.'
: 'Increase target or accept risk: shorter runway may cause termination.'
await ensureBelowTenDaysAllowed({
await ensureBelowThirtyDaysAllowed({
spinner,
warningLine1: line1,
warningLine2: line2,
Expand Down
6 changes: 4 additions & 2 deletions src/payments/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
checkAllowances,
checkFILBalance,
checkUSDFCBalance,
DEFAULT_LOCKUP_DAYS,
depositUSDFC,
getPaymentStatus,
setMaxAllowances,
Expand Down Expand Up @@ -93,6 +94,7 @@ export async function runInteractiveSetup(options: PaymentSetupOptions): Promise
privateKey,
rpcURL: rpcUrl,
withIpni: true, // Always filter for IPNI-enabled providers
...(options.warmStorageAddress && { warmStorageAddress: options.warmStorageAddress }),
})
const network = synapse.getNetwork()
const client = synapse.getClient()
Expand Down Expand Up @@ -226,7 +228,7 @@ export async function runInteractiveSetup(options: PaymentSetupOptions): Promise
log.indent(`100 GiB capacity: ~${formatUSDFC((pricePerGiBPerMonth * 100n * 11n) / 10n)} USDFC`)
log.indent(`1 TiB capacity: ~${formatUSDFC((pricePerTiBPerMonth * 11n) / 10n)} USDFC`)
log.indent(`10 TiB capacity: ~${formatUSDFC((pricePerTiBPerMonth * 10n * 11n) / 10n)} USDFC`)
log.indent(pc.gray('(deposit covers 1 month + 10-day safety reserve)'))
log.indent(pc.gray(`(deposit covers 1 month + ${DEFAULT_LOCKUP_DAYS}-day safety reserve)`))
log.flush()

const amountStr = await text({
Expand Down Expand Up @@ -298,7 +300,7 @@ export async function runInteractiveSetup(options: PaymentSetupOptions): Promise
? `${(finalCapacity.gibPerMonth / 1024).toFixed(1)} TiB`
: `${finalCapacity.gibPerMonth.toFixed(1)} GiB`
log.indent(`Capacity: ~${capacityStr} for 1 month`)
log.indent(pc.gray('(includes 10-day safety reserve)'))
log.indent(pc.gray(`(includes ${DEFAULT_LOCKUP_DAYS}-day safety reserve)`))
}
log.flush()

Expand Down
2 changes: 1 addition & 1 deletion src/payments/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ export function displayPricing(pricePerGiBPerMonth: bigint, pricePerTiBPerMonth:
log.line(pc.bold('Current Pricing:'))
log.indent(`1 GiB/month: ${formatUSDFC(pricePerGiBPerMonth)} USDFC`)
log.indent(`1 TiB/month: ${formatUSDFC(pricePerTiBPerMonth)} USDFC`)
log.indent(pc.gray('(for each upload, WarmStorage service will reserve 10 days of costs as security)'))
log.indent(pc.gray('(for each upload, WarmStorage service will reserve 30 days of costs as security)'))
log.flush()
}

Expand Down
23 changes: 12 additions & 11 deletions src/test/unit/payments-setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ describe('Payment Setup Tests', () => {
getStorageInfo: vi.fn().mockResolvedValue({
pricing: {
noCDN: {
perTiBPerEpoch: ethers.parseUnits('0.0000565', 18),
perTiBPerDay: ethers.parseUnits('0.16272', 18),
perTiBPerMonth: ethers.parseUnits('4.8816', 18),
perTiBPerEpoch: ethers.parseUnits('0.00002893519', 18), // 2.5 USDFC/TiB/month
perTiBPerDay: ethers.parseUnits('0.08333333', 18),
perTiBPerMonth: ethers.parseUnits('2.5', 18),
},
},
}),
Expand Down Expand Up @@ -196,7 +196,7 @@ describe('Payment Setup Tests', () => {
'0xwarmstorage',
rateAllowance,
lockupAllowance,
28800n, // 10 days * 2880 epochs/day
86400n, // 30 days * 2880 epochs/day (bigint)
'USDFC'
)
})
Expand All @@ -210,7 +210,7 @@ describe('Payment Setup Tests', () => {
expect(allowances.storageCapacityTiB).toBe(1)
expect(allowances.rateAllowance).toBe(ethers.parseUnits('0.0000565', 18))
expect(allowances.lockupAllowance).toBe(
ethers.parseUnits('0.0000565', 18) * 2880n * 10n // rate * epochs/day * 10 days
ethers.parseUnits('0.0000565', 18) * 2880n * 30n // rate * epochs/day * 30 days
)
})

Expand All @@ -231,7 +231,7 @@ describe('Payment Setup Tests', () => {
// 1.5 TiB
expect(allowances.rateAllowance).toBe(ethers.parseUnits('0.00008475', 18))
expect(allowances.lockupAllowance).toBe(
ethers.parseUnits('0.00008475', 18) * 2880n * 10n // rate * epochs/day * 10 days
ethers.parseUnits('0.00008475', 18) * 2880n * 30n // rate * epochs/day * 30 days
)
})

Expand Down Expand Up @@ -355,8 +355,8 @@ describe('Payment Setup Tests', () => {
describe('calculateStorageFromUSDFC', () => {
it('should calculate storage capacity from USDFC amount with high precision', () => {
const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
// 10 days worth of 1GiB/month = 0.0015881472 USDFC
const usdfcAmount = ethers.parseUnits('0.0015881472', 18)
// 30 days worth of 1GiB/month = 0.0047644416 USDFC
const usdfcAmount = ethers.parseUnits('0.0047644416', 18)

const capacityTiB = calculateStorageFromUSDFC(usdfcAmount, pricePerTiBPerEpoch)

Expand Down Expand Up @@ -387,11 +387,12 @@ describe('Payment Setup Tests', () => {
expect(capacityTiB).toBe(0)
})

// simple testcase to show what the pricePerTibPerEpoch would need to be to get 1TiB/month with 1USDFC
// feel free to skip/delete this testcase if it becomes irrelevant
// Verify pricePerTibPerEpoch needed to get 1TiB/month with 1USDFC given 30-day lockup
// With 30-day lockup: 1 USDFC / (30 days * 2880 epochs/day) = 1 USDFC / 86400 epochs
// For 1 TiB capacity: pricePerTiBPerEpoch = 1 / 86400 = 0.000011574074 USDFC
it('should return capacity of 1 when pricePerTibPerEpoch is low', () => {
const usdfcAmount = ethers.parseUnits('1', 18)
const pricePerTiBPerEpoch = ethers.parseUnits('0.000034722219', 18)
const pricePerTiBPerEpoch = ethers.parseUnits('0.000011574074', 18)
const capacityTiB = calculateStorageFromUSDFC(usdfcAmount, pricePerTiBPerEpoch)
// within 10 decimal places accuracy of 1
expect(capacityTiB).toBeCloseTo(1, 10)
Expand Down
8 changes: 4 additions & 4 deletions src/test/unit/payments.compute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,14 @@ describe('computeAdjustmentForExactDays', () => {
it('returns positive delta when more deposit needed (includes 1-hour safety)', () => {
const rateUsed = 1_000_000_000_000_000_000n // 1 USDFC/epoch
const perDay = rateUsed * TIME_CONSTANTS.EPOCHS_PER_DAY
const days = 10
const available = perDay * 10n // exactly 10 days
const days = 30
const available = perDay * 30n // exactly 30 days
const status = makeStatus({ filecoinPayBalance: available, lockupUsed: 0n, rateUsed })
const res = computeAdjustmentForExactDays(status, days)
const perHour = perDay / 24n
const safety = perHour > 0n ? perHour : 1n
expect(res.delta).toBe(safety)
expect(res.targetAvailable).toBe(perDay * 10n + safety)
expect(res.targetAvailable).toBe(perDay * 30n + safety)
})

it('returns negative delta when withdrawal possible', () => {
Expand Down Expand Up @@ -163,7 +163,7 @@ describe('computeAdjustmentForExactDaysWithPiece', () => {
it('adds file requirements to existing usage', () => {
// Scenario: Existing storage, adding another file
const rateUsed = 1_000_000_000_000_000_000n // 1 USDFC/epoch
const lockupUsed = rateUsed * BigInt(10) * TIME_CONSTANTS.EPOCHS_PER_DAY // 10 days worth
const lockupUsed = rateUsed * BigInt(30) * TIME_CONSTANTS.EPOCHS_PER_DAY // 30 days worth
const filecoinPayBalance = (lockupUsed * 12n) / 10n // 20% buffer
const status = makeStatus({ filecoinPayBalance, lockupUsed, rateUsed })

Expand Down
2 changes: 1 addition & 1 deletion upload-action/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"description": "Helper runner for Filecoin Pin Upload GitHub Action",
"dependencies": {
"@actions/artifact": "^2.3.2",
"@filoz/synapse-sdk": "^0.33.0",
"@filoz/synapse-sdk": "^0.35.0",
"@octokit/rest": "^22.0.0",
"ethers": "^6.15.0",
"filecoin-pin": "../",
Expand Down
Loading