From 7b97b42663e1fd16a9bb5dfe05a218389ee6d8d2 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:03:36 -0500 Subject: [PATCH 1/6] fix: validatePaymentSetup considers floor pricing --- src/add/add.ts | 2 +- src/common/upload-flow.ts | 19 +++++-- src/core/payments/constants.ts | 53 +++++++++++++++++++ src/core/payments/floor-pricing.ts | 82 ++++++++++++++++++++++++++++++ src/core/payments/index.ts | 44 ++++++++-------- 5 files changed, 172 insertions(+), 28 deletions(-) create mode 100644 src/core/payments/constants.ts create mode 100644 src/core/payments/floor-pricing.ts diff --git a/src/add/add.ts b/src/add/add.ts index 28a703e3..cbf79b01 100644 --- a/src/add/add.ts +++ b/src/add/add.ts @@ -124,7 +124,7 @@ export async function runAdd(options: AddOptions): Promise { // Check payment setup (may configure permissions if needed) // Actual CAR size will be checked later spinner.start('Checking payment setup...') - await validatePaymentSetup(synapse, 0, spinner) + await validatePaymentSetup(synapse, 0, spinner, { suppressSuggestions: true }) // Create CAR from file or directory const packingMsg = isDirectory diff --git a/src/common/upload-flow.ts b/src/common/upload-flow.ts index 63d40c0c..bb04c3ef 100644 --- a/src/common/upload-flow.ts +++ b/src/common/upload-flow.ts @@ -104,11 +104,18 @@ export async function performAutoFunding(synapse: Synapse, fileSize: number, spi * Validate payment setup and capacity for upload * * @param synapse - Initialized Synapse instance - * @param fileSize - Size of file to upload in bytes + * @param fileSize - Size of file to upload in bytes (use 0 for minimum setup check) * @param spinner - Optional spinner for progress + * @param options - Optional configuration + * @param options.suppressSuggestions - If true, don't display suggestion warnings * @returns true if validation passes, exits process if not */ -export async function validatePaymentSetup(synapse: Synapse, fileSize: number, spinner?: Spinner): Promise { +export async function validatePaymentSetup( + synapse: Synapse, + fileSize: number, + spinner?: Spinner, + options?: { suppressSuggestions?: boolean } +): Promise { const readiness = await checkUploadReadiness({ synapse, fileSize, @@ -186,7 +193,7 @@ export async function validatePaymentSetup(synapse: Synapse, fileSize: number, s } // Show warning if suggestions exist (even if upload is possible) - if (suggestions.length > 0 && capacity?.canUpload) { + if (suggestions.length > 0 && capacity?.canUpload && !options?.suppressSuggestions) { spinner?.stop(`${pc.yellow('⚠')} Payment capacity check passed with warnings`) log.line('') log.line(pc.bold('Suggestions:')) @@ -194,8 +201,12 @@ export async function validatePaymentSetup(synapse: Synapse, fileSize: number, s log.indent(`• ${suggestion}`) }) log.flush() + } else if (fileSize === 0) { + // Different message based on whether this is minimum setup (fileSize=0) or actual capacity check + // Note: 0.06 USDFC is the floor price, but with 10% buffer, ~0.066 USDFC is actually required + spinner?.stop(`${pc.green('✓')} Minimum payment setup verified (~0.066 USDFC required)`) } else { - spinner?.stop(`${pc.green('✓')} Payment capacity verified`) + spinner?.stop(`${pc.green('✓')} Payment capacity verified for ${formatFileSize(fileSize)}`) } } diff --git a/src/core/payments/constants.ts b/src/core/payments/constants.ts new file mode 100644 index 00000000..6f044cc1 --- /dev/null +++ b/src/core/payments/constants.ts @@ -0,0 +1,53 @@ +/** + * Payment-related constants for Filecoin Onchain Cloud + * + * This module contains all constants used in payment operations including + * decimals, lockup periods, buffer configurations, and pricing minimums. + */ + +import { ethers } from 'ethers' + +/** + * USDFC token decimals (ERC20 standard) + */ +export const USDFC_DECIMALS = 18 + +/** + * Minimum FIL balance required for gas fees + */ +export const MIN_FIL_FOR_GAS = ethers.parseEther('0.1') + +/** + * Default lockup period required by WarmStorage (in days) + */ +export const DEFAULT_LOCKUP_DAYS = 30 + +/** + * Floor price per piece for WarmStorage (minimum cost regardless of size) + * This is 0.06 USDFC per 30 days per piece + */ +export const FLOOR_PRICE_PER_30_DAYS = ethers.parseUnits('0.06', USDFC_DECIMALS) + +/** + * Number of days the floor price covers + */ +export const FLOOR_PRICE_DAYS = 30 + +/** + * Maximum allowances for trusted WarmStorage service + * Using MaxUint256 which MetaMask displays as "Unlimited" + */ +export const MAX_RATE_ALLOWANCE = ethers.MaxUint256 +export const MAX_LOCKUP_ALLOWANCE = ethers.MaxUint256 + +/** + * Standard buffer configuration (10%) used across deposit/lockup calculations + */ +export const BUFFER_NUMERATOR = 11n +export const BUFFER_DENOMINATOR = 10n + +/** + * Maximum precision scale used when converting small TiB (as a float) to integer(BigInt) math + */ +export const STORAGE_SCALE_MAX = 10_000_000 +export const STORAGE_SCALE_MAX_BI = BigInt(STORAGE_SCALE_MAX) diff --git a/src/core/payments/floor-pricing.ts b/src/core/payments/floor-pricing.ts new file mode 100644 index 00000000..f698a295 --- /dev/null +++ b/src/core/payments/floor-pricing.ts @@ -0,0 +1,82 @@ +/** + * Floor pricing calculations for WarmStorage + * + * This module handles the minimum rate per piece (floor price) logic. + * The floor price ensures that small files meet the minimum cost requirement + * of 0.06 USDFC per 30 days, regardless of their actual size. + * + * Implementation follows the pattern from synapse-sdk PR #375: + * 1. Calculate base cost from piece size + * 2. Calculate floor cost (minimum per piece) + * 3. Return max(base cost, floor cost) + */ + +import { TIME_CONSTANTS } from '@filoz/synapse-sdk' +import { DEFAULT_LOCKUP_DAYS, FLOOR_PRICE_DAYS, FLOOR_PRICE_PER_30_DAYS } from './constants.js' +import type { StorageAllowances } from './types.js' + +/** + * Calculate floor-adjusted allowances for a piece + * + * This function applies the floor pricing (minimum rate per piece) to ensure + * that small files meet the minimum cost requirement. + * + * Example usage: + * ```typescript + * const storageInfo = await synapse.storage.getStorageInfo() + * const pricing = storageInfo.pricing.noCDN.perTiBPerEpoch + * + * // For a small file (1 KB) + * const allowances = calculateFloorAdjustedAllowances(1024, pricing) + * // Will return floor price allowances (0.06 USDFC per 30 days) + * + * // For a large file (10 GiB) + * const allowances = calculateFloorAdjustedAllowances(10 * 1024 * 1024 * 1024, pricing) + * // Will return calculated allowances based on size (floor doesn't apply) + * ``` + * + * @param baseAllowances - Base allowances calculated from piece size + * @returns Floor-adjusted allowances for the piece + */ +export function applyFloorPricing(baseAllowances: StorageAllowances): StorageAllowances { + // Calculate floor rate per epoch + // floor price is per 30 days, so we divide by (30 days * epochs per day) + const epochsInFloorPeriod = BigInt(FLOOR_PRICE_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY + const floorRateAllowance = FLOOR_PRICE_PER_30_DAYS / epochsInFloorPeriod + + // Calculate floor lockup (floor rate * lockup period) + const epochsInLockupDays = BigInt(DEFAULT_LOCKUP_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY + const floorLockupAllowance = floorRateAllowance * epochsInLockupDays + + // Apply floor pricing: use max of base and floor + const rateAllowance = + baseAllowances.rateAllowance > floorRateAllowance ? baseAllowances.rateAllowance : floorRateAllowance + + const lockupAllowance = + baseAllowances.lockupAllowance > floorLockupAllowance ? baseAllowances.lockupAllowance : floorLockupAllowance + + return { + rateAllowance, + lockupAllowance, + storageCapacityTiB: baseAllowances.storageCapacityTiB, + } +} + +/** + * Get the floor pricing allowances (minimum cost regardless of size) + * + * @returns Floor price allowances + */ +export function getFloorAllowances(): StorageAllowances { + const epochsInFloorPeriod = BigInt(FLOOR_PRICE_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY + const rateAllowance = FLOOR_PRICE_PER_30_DAYS / epochsInFloorPeriod + + const epochsInLockupDays = BigInt(DEFAULT_LOCKUP_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY + const lockupAllowance = rateAllowance * epochsInLockupDays + + return { + rateAllowance, + lockupAllowance, + storageCapacityTiB: 0, // Floor price is not size-based + } +} diff --git a/src/core/payments/index.ts b/src/core/payments/index.ts index 4250aa13..111b1b08 100644 --- a/src/core/payments/index.ts +++ b/src/core/payments/index.ts @@ -18,25 +18,27 @@ import { SIZE_CONSTANTS, type Synapse, TIME_CONSTANTS, TOKENS } from '@filoz/synapse-sdk' import { ethers } from 'ethers' import { isSessionKeyMode } from '../synapse/index.js' +import { + BUFFER_DENOMINATOR, + BUFFER_NUMERATOR, + DEFAULT_LOCKUP_DAYS, + MAX_LOCKUP_ALLOWANCE, + MAX_RATE_ALLOWANCE, + MIN_FIL_FOR_GAS, + STORAGE_SCALE_MAX, + STORAGE_SCALE_MAX_BI, + USDFC_DECIMALS, +} from './constants.js' +import { applyFloorPricing } from './floor-pricing.js' import type { PaymentStatus, ServiceApprovalStatus, StorageAllowances, StorageRunwaySummary } from './types.js' -// 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 = 30 // WarmStorage requires 30 days lockup +// Re-export commonly used constants +export { DEFAULT_LOCKUP_DAYS, STORAGE_SCALE_MAX, USDFC_DECIMALS } from './constants.js' +export * from './floor-pricing.js' export * from './top-up.js' export * from './types.js' -// 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 @@ -47,12 +49,6 @@ 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. @@ -708,8 +704,9 @@ export function computeAdjustmentForExactDaysWithPiece( const currentRateUsed = status.currentAllowances.rateUsed ?? 0n const currentLockupUsed = status.currentAllowances.lockupUsed ?? 0n - // Calculate required allowances for the new file - const newPieceAllowances = calculateRequiredAllowances(pieceSizeBytes, pricePerTiBPerEpoch) + // 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 @@ -924,8 +921,9 @@ export function calculatePieceUploadRequirements( insufficientDeposit: bigint canUpload: boolean } { - // Calculate requirements - const required = calculateRequiredAllowances(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 From 9d4be50537dcca0563c672ed47133e239632fd31 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:29:17 -0500 Subject: [PATCH 2/6] test: add floorPricing tests --- src/core/payments/index.ts | 4 +- src/test/unit/floor-pricing.test.ts | 187 ++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 src/test/unit/floor-pricing.test.ts diff --git a/src/core/payments/index.ts b/src/core/payments/index.ts index 111b1b08..8c7961f1 100644 --- a/src/core/payments/index.ts +++ b/src/core/payments/index.ts @@ -32,8 +32,8 @@ import { import { applyFloorPricing } from './floor-pricing.js' import type { PaymentStatus, ServiceApprovalStatus, StorageAllowances, StorageRunwaySummary } from './types.js' -// Re-export commonly used constants -export { DEFAULT_LOCKUP_DAYS, STORAGE_SCALE_MAX, USDFC_DECIMALS } from './constants.js' +// Re-export all constants +export * from './constants.js' export * from './floor-pricing.js' export * from './top-up.js' diff --git a/src/test/unit/floor-pricing.test.ts b/src/test/unit/floor-pricing.test.ts new file mode 100644 index 00000000..197293cf --- /dev/null +++ b/src/test/unit/floor-pricing.test.ts @@ -0,0 +1,187 @@ +import { SIZE_CONSTANTS, TIME_CONSTANTS } from '@filoz/synapse-sdk' +import { ethers } from 'ethers' +import { describe, expect, it } from 'vitest' +import { + applyFloorPricing, + BUFFER_DENOMINATOR, + BUFFER_NUMERATOR, + calculatePieceUploadRequirements, + calculateRequiredAllowances, + computeAdjustmentForExactDaysWithPiece, + DEFAULT_LOCKUP_DAYS, + FLOOR_PRICE_DAYS, + FLOOR_PRICE_PER_30_DAYS, + getFloorAllowances, + type PaymentStatus, + type ServiceApprovalStatus, +} from '../../core/payments/index.js' + +function makeStatus(params: { filecoinPayBalance: bigint; lockupUsed?: bigint; rateUsed?: bigint }): PaymentStatus { + const currentAllowances: ServiceApprovalStatus = { + rateAllowance: 0n, + lockupAllowance: 0n, + lockupUsed: params.lockupUsed ?? 0n, + rateUsed: params.rateUsed ?? 0n, + maxLockupPeriod: BigInt(DEFAULT_LOCKUP_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY, + } + + return { + network: 'calibration', + address: '0x0000000000000000000000000000000000000000', + filBalance: 0n, + walletUsdfcBalance: 0n, + filecoinPayBalance: params.filecoinPayBalance, + currentAllowances, + } +} + +function getBufferedFloorDeposit(): bigint { + const floor = getFloorAllowances() + return (floor.lockupAllowance * BUFFER_NUMERATOR) / BUFFER_DENOMINATOR +} + +describe('Floor Pricing Constants', () => { + it('floor price is 0.06 USDFC', () => { + const expected = ethers.parseUnits('0.06', 18) + expect(FLOOR_PRICE_PER_30_DAYS).toBe(expected) + }) + + it('floor price covers 30 days', () => { + expect(FLOOR_PRICE_DAYS).toBe(30) + }) +}) + +describe('getFloorAllowances', () => { + it('returns floor rate and lockup allowances', () => { + const floor = getFloorAllowances() + + // Floor rate per epoch = 0.06 USDFC / (30 days * epochs per day) + const epochsInFloorPeriod = BigInt(FLOOR_PRICE_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY + const expectedRateAllowance = FLOOR_PRICE_PER_30_DAYS / epochsInFloorPeriod + + expect(floor.rateAllowance).toBe(expectedRateAllowance) + + // Floor lockup = floor rate * lockup period + const epochsInLockup = BigInt(DEFAULT_LOCKUP_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY + const expectedLockupAllowance = expectedRateAllowance * epochsInLockup + + expect(floor.lockupAllowance).toBe(expectedLockupAllowance) + expect(floor.storageCapacityTiB).toBe(0) // Floor is not size-based + }) +}) + +describe('applyFloorPricing', () => { + const mockPricing = 100_000_000_000_000n // Some arbitrary price per TiB per epoch + + const smallCases = [ + { label: 'tiny file (1 byte)', size: 1 }, + { label: 'small file (1 KB)', size: 1024 }, + { label: 'medium file (1 MB)', size: 1024 * 1024 }, + ] + + for (const { label, size } of smallCases) { + it(`applies floor pricing to ${label}`, () => { + const baseAllowances = calculateRequiredAllowances(size, mockPricing) + const floorAdjusted = applyFloorPricing(baseAllowances) + const floor = getFloorAllowances() + + expect(floorAdjusted.rateAllowance).toBe(floor.rateAllowance) + expect(floorAdjusted.lockupAllowance).toBe(floor.lockupAllowance) + }) + } + + it('does not apply floor for large file when base cost exceeds floor', () => { + // Use a very high price to ensure base cost exceeds floor + const highPrice = 1_000_000_000_000_000_000n // 1 USDFC per TiB per epoch + const largeFile = Number(SIZE_CONSTANTS.GiB) * 100 // 100 GiB + const baseAllowances = calculateRequiredAllowances(largeFile, highPrice) + const floorAdjusted = applyFloorPricing(baseAllowances) + const floor = getFloorAllowances() + + // Base cost should exceed floor, so base is returned + expect(floorAdjusted.rateAllowance).toBeGreaterThan(floor.rateAllowance) + expect(floorAdjusted.lockupAllowance).toBeGreaterThan(floor.lockupAllowance) + expect(floorAdjusted.rateAllowance).toBe(baseAllowances.rateAllowance) + }) +}) + +describe('calculatePieceUploadRequirements - Floor Pricing Integration', () => { + const mockPricing = 100_000_000_000_000n + + const floorSizes = [ + { label: '0-byte file', size: 0 }, + { label: '1 KB file', size: 1024 }, + ] + + for (const { label, size } of floorSizes) { + it(`enforces floor price for ${label}`, () => { + const status = makeStatus({ filecoinPayBalance: 0n }) + const requirements = calculatePieceUploadRequirements(status, size, mockPricing) + const floor = getFloorAllowances() + + expect(requirements.required.rateAllowance).toBe(floor.rateAllowance) + expect(requirements.required.lockupAllowance).toBe(floor.lockupAllowance) + }) + } + + it('requires deposit with 10% buffer applied', () => { + const status = makeStatus({ filecoinPayBalance: 0n }) + const requirements = calculatePieceUploadRequirements(status, 0, mockPricing) + const bufferedFloor = getBufferedFloorDeposit() + expect(requirements.totalDepositNeeded).toBe(bufferedFloor) + + // User needs to deposit the buffered amount + expect(requirements.insufficientDeposit).toBe(bufferedFloor) + expect(requirements.canUpload).toBe(false) + }) + + it('allows upload when deposit meets buffered floor price', () => { + const bufferedFloor = getBufferedFloorDeposit() + const status = makeStatus({ filecoinPayBalance: bufferedFloor }) + + const requirements = calculatePieceUploadRequirements(status, 0, mockPricing) + + expect(requirements.canUpload).toBe(true) + expect(requirements.insufficientDeposit).toBe(0n) + }) + + it('blocks upload when deposit is below buffered floor price', () => { + const bufferedFloor = getBufferedFloorDeposit() + const slightlyLess = bufferedFloor - 1n + const status = makeStatus({ filecoinPayBalance: slightlyLess }) + + const requirements = calculatePieceUploadRequirements(status, 0, mockPricing) + + expect(requirements.canUpload).toBe(false) + expect(requirements.insufficientDeposit).toBe(1n) + }) +}) + +describe('computeAdjustmentForExactDaysWithPiece - Floor Pricing Integration', () => { + const mockPricing = 100_000_000_000_000n + + it('applies floor pricing for small file in auto-fund calculation', () => { + const status = makeStatus({ filecoinPayBalance: 0n, lockupUsed: 0n, rateUsed: 0n }) + const adjustment = computeAdjustmentForExactDaysWithPiece(status, 30, 1024, mockPricing) + const floor = getFloorAllowances() + + // Should use floor-adjusted allowances + expect(adjustment.newRateUsed).toBe(floor.rateAllowance) + expect(adjustment.newLockupUsed).toBe(floor.lockupAllowance) + }) + + it('calculates correct deposit delta with floor pricing', () => { + const status = makeStatus({ filecoinPayBalance: 0n, lockupUsed: 0n, rateUsed: 0n }) + const adjustment = computeAdjustmentForExactDaysWithPiece(status, 30, 0, mockPricing) + const floor = getFloorAllowances() + + // Target deposit = buffered lockup + runway + const bufferedLockup = getBufferedFloorDeposit() + const perDay = floor.rateAllowance * TIME_CONSTANTS.EPOCHS_PER_DAY + const safety = perDay > 0n ? perDay / 24n : 1n + const runwayCost = 30n * perDay + safety + + expect(adjustment.delta).toBeGreaterThan(0n) + expect(adjustment.targetDeposit).toBe(bufferedLockup + runwayCost) + }) +}) From 63170726857b08881351d2a288e89aa7fc34954b Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:37:41 -0500 Subject: [PATCH 3/6] Update src/test/unit/floor-pricing.test.ts --- src/test/unit/floor-pricing.test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/test/unit/floor-pricing.test.ts b/src/test/unit/floor-pricing.test.ts index 197293cf..ad6c84c2 100644 --- a/src/test/unit/floor-pricing.test.ts +++ b/src/test/unit/floor-pricing.test.ts @@ -40,16 +40,6 @@ function getBufferedFloorDeposit(): bigint { return (floor.lockupAllowance * BUFFER_NUMERATOR) / BUFFER_DENOMINATOR } -describe('Floor Pricing Constants', () => { - it('floor price is 0.06 USDFC', () => { - const expected = ethers.parseUnits('0.06', 18) - expect(FLOOR_PRICE_PER_30_DAYS).toBe(expected) - }) - - it('floor price covers 30 days', () => { - expect(FLOOR_PRICE_DAYS).toBe(30) - }) -}) describe('getFloorAllowances', () => { it('returns floor rate and lockup allowances', () => { From c8103fe93afffd3bb1415eb23de18d0c32969ba9 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:37:47 -0500 Subject: [PATCH 4/6] Update src/test/unit/floor-pricing.test.ts --- src/test/unit/floor-pricing.test.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/test/unit/floor-pricing.test.ts b/src/test/unit/floor-pricing.test.ts index ad6c84c2..ccd53778 100644 --- a/src/test/unit/floor-pricing.test.ts +++ b/src/test/unit/floor-pricing.test.ts @@ -41,24 +41,6 @@ function getBufferedFloorDeposit(): bigint { } -describe('getFloorAllowances', () => { - it('returns floor rate and lockup allowances', () => { - const floor = getFloorAllowances() - - // Floor rate per epoch = 0.06 USDFC / (30 days * epochs per day) - const epochsInFloorPeriod = BigInt(FLOOR_PRICE_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY - const expectedRateAllowance = FLOOR_PRICE_PER_30_DAYS / epochsInFloorPeriod - - expect(floor.rateAllowance).toBe(expectedRateAllowance) - - // Floor lockup = floor rate * lockup period - const epochsInLockup = BigInt(DEFAULT_LOCKUP_DAYS) * TIME_CONSTANTS.EPOCHS_PER_DAY - const expectedLockupAllowance = expectedRateAllowance * epochsInLockup - - expect(floor.lockupAllowance).toBe(expectedLockupAllowance) - expect(floor.storageCapacityTiB).toBe(0) // Floor is not size-based - }) -}) describe('applyFloorPricing', () => { const mockPricing = 100_000_000_000_000n // Some arbitrary price per TiB per epoch From 1a0b11fca48882143b3ad58ec1ff8d9c5ee267f5 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:38:14 -0500 Subject: [PATCH 5/6] chore: fix lint --- src/test/unit/floor-pricing.test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/test/unit/floor-pricing.test.ts b/src/test/unit/floor-pricing.test.ts index ccd53778..f316eb9e 100644 --- a/src/test/unit/floor-pricing.test.ts +++ b/src/test/unit/floor-pricing.test.ts @@ -1,5 +1,4 @@ import { SIZE_CONSTANTS, TIME_CONSTANTS } from '@filoz/synapse-sdk' -import { ethers } from 'ethers' import { describe, expect, it } from 'vitest' import { applyFloorPricing, @@ -9,8 +8,6 @@ import { calculateRequiredAllowances, computeAdjustmentForExactDaysWithPiece, DEFAULT_LOCKUP_DAYS, - FLOOR_PRICE_DAYS, - FLOOR_PRICE_PER_30_DAYS, getFloorAllowances, type PaymentStatus, type ServiceApprovalStatus, @@ -40,8 +37,6 @@ function getBufferedFloorDeposit(): bigint { return (floor.lockupAllowance * BUFFER_NUMERATOR) / BUFFER_DENOMINATOR } - - describe('applyFloorPricing', () => { const mockPricing = 100_000_000_000_000n // Some arbitrary price per TiB per epoch From 5169f7b69dddbbb5659d99c5e197cac74bd57a47 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:23:38 -0500 Subject: [PATCH 6/6] chore: cleanup add output --- src/common/upload-flow.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/common/upload-flow.ts b/src/common/upload-flow.ts index bb04c3ef..d056a015 100644 --- a/src/common/upload-flow.ts +++ b/src/common/upload-flow.ts @@ -195,7 +195,6 @@ export async function validatePaymentSetup( // Show warning if suggestions exist (even if upload is possible) if (suggestions.length > 0 && capacity?.canUpload && !options?.suppressSuggestions) { spinner?.stop(`${pc.yellow('⚠')} Payment capacity check passed with warnings`) - log.line('') log.line(pc.bold('Suggestions:')) suggestions.forEach((suggestion) => { log.indent(`• ${suggestion}`) @@ -215,14 +214,14 @@ export async function validatePaymentSetup( */ function displayPaymentIssues(capacityCheck: PaymentCapacityCheck, fileSize: number, spinner?: Spinner): void { spinner?.stop(`${pc.red('✗')} Insufficient deposit for this file`) - log.line('') log.line(pc.bold('File Requirements:')) - log.indent(`File size: ${formatFileSize(fileSize)} (${capacityCheck.storageTiB.toFixed(4)} TiB)`) + if (fileSize === 0) { + log.indent(`File size: ${formatFileSize(fileSize)} (${capacityCheck.storageTiB.toFixed(4)} TiB)`) + } log.indent(`Storage cost: ${formatUSDFC(capacityCheck.required.rateAllowance)} USDFC/epoch`) log.indent( - `Required deposit: ${formatUSDFC(capacityCheck.required.lockupAllowance + capacityCheck.required.lockupAllowance / 10n)} USDFC` + `Required deposit: ${formatUSDFC(capacityCheck.required.lockupAllowance + capacityCheck.required.lockupAllowance / 10n)} USDFC ${pc.gray(`(includes ${DEFAULT_LOCKUP_DAYS}-day safety reserve)`)}` ) - log.indent(pc.gray(`(includes ${DEFAULT_LOCKUP_DAYS}-day safety reserve)`)) log.line('') log.line(pc.bold('Suggested actions:'))