From 9a90cd803a1ab379f82d06d37694965204a59c91 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:51:04 -0400 Subject: [PATCH 01/16] feat(costs): add per-operation price list --- packages/synapse-core/src/utils/constants.ts | 20 --- .../calculate-additional-lockup-required.ts | 70 +++++---- .../warm-storage/calculate-deposit-needed.ts | 38 +++-- .../warm-storage/calculate-effective-rate.ts | 52 +++---- .../warm-storage/calculate-operation-fees.ts | 38 +++++ .../src/warm-storage/get-upload-costs.ts | 64 ++++++-- .../synapse-core/src/warm-storage/index.ts | 2 + .../src/warm-storage/price-list.ts | 88 +++++++++++ ...lculate-additional-lockup-required.test.ts | 139 ++++++++++++------ .../test/calculate-deposit-needed.test.ts | 53 +++++-- .../test/calculate-effective-rate.test.ts | 49 +++--- .../synapse-core/test/get-price-list.test.ts | 105 +++++++++++++ .../test/get-upload-costs.test.ts | 72 ++++++--- 13 files changed, 587 insertions(+), 203 deletions(-) create mode 100644 packages/synapse-core/src/warm-storage/calculate-operation-fees.ts create mode 100644 packages/synapse-core/src/warm-storage/price-list.ts create mode 100644 packages/synapse-core/test/get-price-list.test.ts diff --git a/packages/synapse-core/src/utils/constants.ts b/packages/synapse-core/src/utils/constants.ts index 997aeefdb..404c49156 100644 --- a/packages/synapse-core/src/utils/constants.ts +++ b/packages/synapse-core/src/utils/constants.ts @@ -120,26 +120,6 @@ export const DEFAULT_BUFFER_EPOCHS = 5n */ export const DEFAULT_RUNWAY_EPOCHS = 0n -/** - * CDN fixed lockup amounts charged at dataset creation time. - * These are one-time lockups for CDN egress and cache miss egress rails. - */ -export const CDN_FIXED_LOCKUP = { - /** CDN egress rail fixed lockup: 0.7 USDFC */ - cdn: 700_000_000_000_000_000n, - /** Cache miss egress rail fixed lockup: 0.3 USDFC */ - cacheMiss: 300_000_000_000_000_000n, - /** Total: 1.0 USDFC */ - total: 1_000_000_000_000_000_000n, -} as const - -/** - * USDFC sybil fee charged on new dataset creation. - * Extracted from client funds into the payments auction pool to prevent state-growth spam. - * Matches PDPVerifier.USDFC_SYBIL_FEE (immutable, only changes with contract upgrade). - */ -export const USDFC_SYBIL_FEE = 100_000_000_000_000_000n // 0.1 USDFC - export const RETRY_CONSTANTS = { /** The interval in milliseconds between polls. 4 seconds is the default interval between polls. */ POLL_INTERVAL: 4000, diff --git a/packages/synapse-core/src/warm-storage/calculate-additional-lockup-required.ts b/packages/synapse-core/src/warm-storage/calculate-additional-lockup-required.ts index d6d325dff..a7168fd85 100644 --- a/packages/synapse-core/src/warm-storage/calculate-additional-lockup-required.ts +++ b/packages/synapse-core/src/warm-storage/calculate-additional-lockup-required.ts @@ -1,5 +1,6 @@ -import { CDN_FIXED_LOCKUP, LOCKUP_PERIOD, TIME_CONSTANTS, USDFC_SYBIL_FEE } from '../utils/constants.ts' +import { TIME_CONSTANTS } from '../utils/constants.ts' import { calculateEffectiveRate } from './calculate-effective-rate.ts' +import type { getPriceList } from './price-list.ts' export namespace calculateAdditionalLockupRequired { export type ParamsType = { @@ -7,13 +8,11 @@ export namespace calculateAdditionalLockupRequired { dataSize: bigint /** Current total data size in the existing dataset, in bytes. 0n for new datasets. */ currentDataSetSize: bigint - /** Price per TiB per month from getServicePrice(). */ - pricePerTiBPerMonth: bigint - /** Minimum monthly charge from getServicePrice(). */ - minimumPricePerMonth: bigint + /** Canonical warm storage price list. */ + priceList: getPriceList.OutputType /** Epochs per month. Defaults to EPOCHS_PER_MONTH (86400). */ epochsPerMonth?: bigint - /** Lockup period in epochs. Defaults to LOCKUP_PERIOD (30 days). */ + /** Lockup period in epochs. Defaults to priceList.lockups.defaultLockupPeriod. */ lockupEpochs?: bigint /** Whether a new dataset is being created (vs adding to existing). */ isNewDataSet: boolean @@ -25,12 +24,14 @@ export namespace calculateAdditionalLockupRequired { /** Per-epoch rate increase from this upload. */ rateDeltaPerEpoch: bigint /** Lockup increase from the rate change = rateDeltaPerEpoch * lockupEpochs. */ - rateLockupDelta: bigint - /** Fixed CDN lockup (only for new CDN datasets), 0 otherwise. */ - cdnFixedLockup: bigint - /** USDFC sybil fee (only for new datasets), 0 otherwise. */ - sybilFee: bigint - /** rateLockupDelta + cdnFixedLockup + sybilFee */ + streamingLockup: bigint + /** Lifecycle lockup target for new datasets. */ + lifecycleLockup: bigint + /** CDN lockup for new CDN datasets. */ + cdnLockup: bigint + /** Cache-miss lockup for new CDN datasets. */ + cacheMissLockup: bigint + /** streamingLockup + lifecycleLockup + cdnLockup + cacheMissLockup */ total: bigint } } @@ -38,8 +39,8 @@ export namespace calculateAdditionalLockupRequired { /** * Compute how much additional lockup this upload requires. * - * Handles floor-to-floor transitions correctly: when both the current dataset size - * and the new total size are below the floor threshold, the rate delta is 0. + * Existing datasets pay only the incremental rate lockup. New datasets also + * include lifecycle and optional CDN/cache-miss lockups. * * @param params - {@link calculateAdditionalLockupRequired.ParamsType} * @returns {@link calculateAdditionalLockupRequired.OutputType} @@ -50,15 +51,21 @@ export function calculateAdditionalLockupRequired( const { dataSize, currentDataSetSize, - pricePerTiBPerMonth, - minimumPricePerMonth, + priceList, epochsPerMonth = TIME_CONSTANTS.EPOCHS_PER_MONTH, - lockupEpochs = LOCKUP_PERIOD, + lockupEpochs, isNewDataSet, withCDN, } = params - const rateParams = { pricePerTiBPerMonth, minimumPricePerMonth, epochsPerMonth } + // The price list defines the default PDP rail lockup period. + const effectiveLockupEpochs = lockupEpochs ?? priceList.lockups.defaultLockupPeriod + + const rateParams = { + storagePerTibPerMonth: priceList.rates.storagePerTibPerMonth, + provingServicePerMonth: priceList.rates.datasetFeePerMonth, + epochsPerMonth, + } let rateDeltaPerEpoch: bigint @@ -73,7 +80,8 @@ export function calculateAdditionalLockupRequired( sizeInBytes: currentDataSetSize, }) rateDeltaPerEpoch = newRate.ratePerEpoch - currentRate.ratePerEpoch - // Floor-to-floor: if both sizes are below floor, delta is 0 + // Defensive only: additive storage rate is monotonic in size, so a positive + // size delta never yields a negative rate delta in the current model. if (rateDeltaPerEpoch < 0n) rateDeltaPerEpoch = 0n } else { // New dataset or unknown current size: full rate for new data @@ -84,19 +92,21 @@ export function calculateAdditionalLockupRequired( rateDeltaPerEpoch = newRate.ratePerEpoch } - const rateLockupDelta = rateDeltaPerEpoch * lockupEpochs - - // CDN fixed lockup only applies to new CDN datasets - const cdnFixedLockup = isNewDataSet && withCDN ? CDN_FIXED_LOCKUP.total : 0n - - // Sybil fee applies to all new dataset creations - const sybilFee = isNewDataSet ? USDFC_SYBIL_FEE : 0n + const streamingLockup = rateDeltaPerEpoch * effectiveLockupEpochs + // The lifecycle reserve is seeded once per new dataset (one PDP rail each), so + // it is added per new dataset and summed across contexts by callers. CDN and + // cache-miss lockups are flat fixed amounts on the CDN rail; the lockup periods + // in the price list are rail settle windows, not rate multipliers. + const lifecycleLockup = isNewDataSet ? priceList.lockups.lifecycleReserveTarget : 0n + const cdnLockup = isNewDataSet && withCDN ? priceList.lockups.cdnLockupAmount : 0n + const cacheMissLockup = isNewDataSet && withCDN ? priceList.lockups.cacheMissLockupAmount : 0n return { rateDeltaPerEpoch, - rateLockupDelta, - cdnFixedLockup, - sybilFee, - total: rateLockupDelta + cdnFixedLockup + sybilFee, + streamingLockup, + lifecycleLockup, + cdnLockup, + cacheMissLockup, + total: streamingLockup + lifecycleLockup + cdnLockup + cacheMissLockup, } } diff --git a/packages/synapse-core/src/warm-storage/calculate-deposit-needed.ts b/packages/synapse-core/src/warm-storage/calculate-deposit-needed.ts index 083bef85e..3ebf6d3b2 100644 --- a/packages/synapse-core/src/warm-storage/calculate-deposit-needed.ts +++ b/packages/synapse-core/src/warm-storage/calculate-deposit-needed.ts @@ -1,5 +1,7 @@ import { DEFAULT_BUFFER_EPOCHS, DEFAULT_RUNWAY_EPOCHS } from '../utils/constants.ts' import { calculateAdditionalLockupRequired } from './calculate-additional-lockup-required.ts' +import { calculateOperationFees } from './calculate-operation-fees.ts' +import type { getPriceList } from './price-list.ts' export namespace calculateRunwayAmount { export type ParamsType = { @@ -73,14 +75,15 @@ export namespace calculateDepositNeeded { // Upload parameters (passed to calculateAdditionalLockupRequired) dataSize: bigint currentDataSetSize: bigint - pricePerTiBPerMonth: bigint - minimumPricePerMonth: bigint + priceList: getPriceList.OutputType /** Epochs per month. Defaults to EPOCHS_PER_MONTH (86400). */ epochsPerMonth?: bigint - /** Lockup period in epochs. Defaults to LOCKUP_PERIOD (30 days). */ + /** Lockup period in epochs. Defaults to priceList.lockups.defaultLockupPeriod. */ lockupEpochs?: bigint isNewDataSet: boolean withCDN: boolean + pieceCount?: bigint + addPiecesOperationCount?: bigint // Runway parameters currentLockupRate: bigint @@ -96,25 +99,42 @@ export namespace calculateDepositNeeded { /** Safety margin in epochs for tx execution delay. Defaults to DEFAULT_BUFFER_EPOCHS (5). */ bufferEpochs?: bigint } + + export type OutputType = { + /** Total deposit needed in token base units (0n if already sufficient). */ + depositNeeded: bigint + /** Lockup breakdown the deposit was computed from. */ + lockup: calculateAdditionalLockupRequired.OutputType + /** Operation fee breakdown the deposit was computed from. */ + fees: calculateOperationFees.OutputType + } } /** * Orchestrate lockup + runway + debt + buffer to compute total deposit needed. * + * Returns the deposit together with the lockup and fee breakdowns it was + * computed from, so callers can reuse them without recomputing. + * * @param params - {@link calculateDepositNeeded.ParamsType} - * @returns The total deposit needed in token base units (0n if already sufficient) + * @returns {@link calculateDepositNeeded.OutputType} */ -export function calculateDepositNeeded(params: calculateDepositNeeded.ParamsType): bigint { +export function calculateDepositNeeded(params: calculateDepositNeeded.ParamsType): calculateDepositNeeded.OutputType { const lockup = calculateAdditionalLockupRequired({ dataSize: params.dataSize, currentDataSetSize: params.currentDataSetSize, - pricePerTiBPerMonth: params.pricePerTiBPerMonth, - minimumPricePerMonth: params.minimumPricePerMonth, + priceList: params.priceList, epochsPerMonth: params.epochsPerMonth, lockupEpochs: params.lockupEpochs, isNewDataSet: params.isNewDataSet, withCDN: params.withCDN, }) + const fees = calculateOperationFees({ + priceList: params.priceList, + isNewDataSet: params.isNewDataSet, + pieceCount: params.pieceCount, + addPiecesOperationCount: params.addPiecesOperationCount, + }) const netRateAfterUpload = params.currentLockupRate + lockup.rateDeltaPerEpoch const extraRunwayEpochs = params.extraRunwayEpochs ?? DEFAULT_RUNWAY_EPOCHS @@ -125,7 +145,7 @@ export function calculateDepositNeeded(params: calculateDepositNeeded.ParamsType extraRunwayEpochs, }) - const rawDepositNeeded = lockup.total + runway + params.debt - params.availableFunds + const rawDepositNeeded = lockup.total + fees.total + runway + params.debt - params.availableFunds // Skip buffer when no existing rails are draining and this is a new dataset. // The deposit lands before any rail is created, so nothing consumes funds @@ -143,5 +163,5 @@ export function calculateDepositNeeded(params: calculateDepositNeeded.ParamsType }) const clamped = rawDepositNeeded > 0n ? rawDepositNeeded : 0n - return clamped + buffer + return { depositNeeded: clamped + buffer, lockup, fees } } diff --git a/packages/synapse-core/src/warm-storage/calculate-effective-rate.ts b/packages/synapse-core/src/warm-storage/calculate-effective-rate.ts index 6e14ae3aa..6032f7093 100644 --- a/packages/synapse-core/src/warm-storage/calculate-effective-rate.ts +++ b/packages/synapse-core/src/warm-storage/calculate-effective-rate.ts @@ -4,23 +4,23 @@ export namespace calculateEffectiveRate { export type ParamsType = { /** Total data size in the dataset (existing + new), in bytes. */ sizeInBytes: bigint - /** Price per TiB per month from getServicePrice(). */ - pricePerTiBPerMonth: bigint - /** Minimum monthly charge from getServicePrice(). */ - minimumPricePerMonth: bigint - /** Epochs per month from getServicePrice() (always 86400). */ + /** Storage price per TiB per month. */ + storagePerTibPerMonth: bigint + /** Monthly proving service rate for non-empty datasets. */ + provingServicePerMonth: bigint + /** Epochs per month. */ epochsPerMonth: bigint } export type OutputType = { /** - * Rate per epoch — matches the on-chain PDP rail rate. - * - * The contract computes this as a single division: - * `(totalBytes * pricePerTiBPerMonth) / (TiB * EPOCHS_PER_MONTH)` + * Rate per epoch — matches the contract's additive per-epoch rate + * (`calculateStorageSizeBasedRatePerEpoch`): the size-based storage rate plus + * the per-epoch dataset fee, each truncated independently then summed: + * `(totalBytes * storagePerTibPerMonth) / (TiB * EPOCHS_PER_MONTH) + provingServicePerMonth / EPOCHS_PER_MONTH` * * Because truncation depends on totalBytes, this value is only valid for - * the exact size it was computed for — you cannot scale it linearly to + * the exact size it was computed for; you cannot scale it linearly to * estimate costs for different sizes. * * Use for: lockup calculations, on-chain comparisons. @@ -29,7 +29,7 @@ export namespace calculateEffectiveRate { /** * Rate per month — preserves precision before epoch division. * - * Computed as `(totalBytes * pricePerTiBPerMonth) / TiB` (one fewer + * Computed as `(totalBytes * storagePerTibPerMonth) / TiB` (one fewer * division than ratePerEpoch), so it retains more precision and scales * linearly with size, making it suitable for display and cost estimation. * @@ -44,40 +44,34 @@ export namespace calculateEffectiveRate { } /** - * Mirror the contract's `_calculateStorageRate` with floor pricing. + * Calculate the expected FWSS recurring rate for a dataset size. * * Returns two rates for different use cases: * - `ratePerEpoch` — matches the on-chain rail rate (use for lockup math) * - `ratePerMonth` — higher precision, linearly scalable (use for display) * - * The contract multiplies `totalBytes * pricePerTiBPerMonth` before dividing - * by `TiB * EPOCHS_PER_MONTH` in a single step, so `ratePerEpoch` depends on - * the total size and cannot be scaled to estimate other sizes. `ratePerMonth` - * avoids the epoch division, preserving that scalability. - * - * On-chain reference: - * - `_calculateStorageRate`: {@link https://github.com/FilOzone/filecoin-services/blob/053885eba807ed40a0e834c080606f4286ab4ef2/service_contracts/src/FilecoinWarmStorageService.sol#L1388-L1397} - * - `calculateStorageSizeBasedRatePerEpoch`: {@link https://github.com/FilOzone/filecoin-services/blob/053885eba807ed40a0e834c080606f4286ab4ef2/service_contracts/src/FilecoinWarmStorageService.sol#L1349-L1370} + * Empty datasets have no recurring rate. Non-empty datasets pay the + * size-based storage rate plus the proving service rate. * * @param params - {@link calculateEffectiveRate.ParamsType} * @returns {@link calculateEffectiveRate.OutputType} */ export function calculateEffectiveRate(params: calculateEffectiveRate.ParamsType): calculateEffectiveRate.OutputType { - const { sizeInBytes, pricePerTiBPerMonth, minimumPricePerMonth, epochsPerMonth } = params + const { sizeInBytes, storagePerTibPerMonth, provingServicePerMonth, epochsPerMonth } = params + + if (sizeInBytes === 0n) { + return { ratePerEpoch: 0n, ratePerMonth: 0n } + } // One division (by TiB only) — preserves precision, linearly scalable with size - const naturalPerMonth = (pricePerTiBPerMonth * sizeInBytes) / SIZE_CONSTANTS.TiB + const storagePerMonth = (storagePerTibPerMonth * sizeInBytes) / SIZE_CONSTANTS.TiB // Two-factor division (by TiB * epochs) — matches contract's single-step division, // truncation is size-dependent so this value is only valid for this exact sizeInBytes - const naturalPerEpoch = (pricePerTiBPerMonth * sizeInBytes) / (SIZE_CONSTANTS.TiB * epochsPerMonth) - - // Floor rate per epoch - const minimumPerEpoch = minimumPricePerMonth / epochsPerMonth + const storagePerEpoch = (storagePerTibPerMonth * sizeInBytes) / (SIZE_CONSTANTS.TiB * epochsPerMonth) - // Apply floor pricing - const ratePerMonth = naturalPerMonth > minimumPricePerMonth ? naturalPerMonth : minimumPricePerMonth - const ratePerEpoch = naturalPerEpoch > minimumPerEpoch ? naturalPerEpoch : minimumPerEpoch + const ratePerMonth = storagePerMonth + provingServicePerMonth + const ratePerEpoch = storagePerEpoch + provingServicePerMonth / epochsPerMonth return { ratePerEpoch, ratePerMonth } } diff --git a/packages/synapse-core/src/warm-storage/calculate-operation-fees.ts b/packages/synapse-core/src/warm-storage/calculate-operation-fees.ts new file mode 100644 index 000000000..067a39455 --- /dev/null +++ b/packages/synapse-core/src/warm-storage/calculate-operation-fees.ts @@ -0,0 +1,38 @@ +import type { getPriceList } from './price-list.ts' + +export namespace calculateOperationFees { + export type ParamsType = { + priceList: getPriceList.OutputType + isNewDataSet: boolean + pieceCount?: bigint + addPiecesOperationCount?: bigint + } + + export type OutputType = { + createDataSetFee: bigint + addPiecesFee: bigint + total: bigint + } +} + +/** + * Compute the one-time fees an upload incurs. + * + * Scope is intentionally limited to upload-time fees: create-data-set (new + * datasets only) and add-pieces. Schedule-removals, terminate, and delete are + * post-upload lifecycle operations and are not part of an upload cost preview. + */ +export function calculateOperationFees(params: calculateOperationFees.ParamsType): calculateOperationFees.OutputType { + const pieceCount = params.pieceCount ?? 1n + const addPiecesOperationCount = params.addPiecesOperationCount ?? 1n + const createDataSetFee = params.isNewDataSet ? params.priceList.fees.createDataSetFee : 0n + const addPiecesFee = + params.priceList.fees.addPiecesBaseFee * addPiecesOperationCount + + params.priceList.fees.addPiecesPerPieceFee * pieceCount + + return { + createDataSetFee, + addPiecesFee, + total: createDataSetFee + addPiecesFee, + } +} diff --git a/packages/synapse-core/src/warm-storage/get-upload-costs.ts b/packages/synapse-core/src/warm-storage/get-upload-costs.ts index fcc0fc95d..fd715fb90 100644 --- a/packages/synapse-core/src/warm-storage/get-upload-costs.ts +++ b/packages/synapse-core/src/warm-storage/get-upload-costs.ts @@ -4,10 +4,11 @@ import { calculateAccountDebt } from '../pay/account-debt.ts' import { accounts } from '../pay/accounts.ts' import { isFwssMaxApproved } from '../pay/is-fwss-max-approved.ts' import { resolveAccountState } from '../pay/resolve-account-state.ts' -import { DEFAULT_BUFFER_EPOCHS, DEFAULT_RUNWAY_EPOCHS, LOCKUP_PERIOD } from '../utils/constants.ts' +import { DEFAULT_BUFFER_EPOCHS, DEFAULT_RUNWAY_EPOCHS, TIME_CONSTANTS } from '../utils/constants.ts' import { calculateDepositNeeded } from './calculate-deposit-needed.ts' import { calculateEffectiveRate } from './calculate-effective-rate.ts' -import { getServicePrice } from './get-service-price.ts' +import type { calculateOperationFees } from './calculate-operation-fees.ts' +import { getPriceList } from './price-list.ts' export namespace getUploadCosts { export type OptionsType = { @@ -23,6 +24,10 @@ export namespace getUploadCosts { /** Size of new data to upload, in bytes. */ dataSize: bigint + /** Number of pieces added by this operation. Default: 1 */ + pieceCount?: bigint + /** Number of addPieces operations. Default: 1 */ + addPiecesOperationCount?: bigint /** Extra runway in epochs beyond the required lockup. */ extraRunwayEpochs?: bigint @@ -32,12 +37,27 @@ export namespace getUploadCosts { export type OutputType = { /** Effective rate for the dataset after adding dataSize bytes. */ + rates: { + /** Rate per epoch — matches on-chain PDP rail rate. */ + perEpoch: bigint + /** Rate per month — full precision for display. */ + perMonth: bigint + } + /** @deprecated Use rates. */ rate: { /** Rate per epoch — matches on-chain PDP rail rate. */ perEpoch: bigint /** Rate per month — full precision for display. */ perMonth: bigint } + fees: calculateOperationFees.OutputType + lockups: { + lifecycleLockup: bigint + streamingLockup: bigint + cdnLockup: bigint + cacheMissLockup: bigint + total: bigint + } /** Total USDFC to deposit. 0n if sufficient funds available. */ depositNeeded: bigint /** Whether FWSS needs to be approved (or re-approved with maxUint256). */ @@ -64,13 +84,15 @@ export async function getUploadCosts( const isNewDataSet = options.isNewDataSet ?? true const withCDN = options.withCDN ?? false const currentDataSetSize = options.currentDataSetSize ?? 0n + const pieceCount = options.pieceCount ?? 1n + const addPiecesOperationCount = options.addPiecesOperationCount ?? 1n const extraRunwayEpochs = options.extraRunwayEpochs ?? DEFAULT_RUNWAY_EPOCHS const bufferEpochs = options.bufferEpochs ?? DEFAULT_BUFFER_EPOCHS // Fetch all needed data in parallel - const [accountInfo, pricing, approved, currentEpoch] = await Promise.all([ + const [accountInfo, priceList, approved, currentEpoch] = await Promise.all([ accounts(client, { address: options.clientAddress }), - getServicePrice(client), + getPriceList(client), isFwssMaxApproved(client, { clientAddress: options.clientAddress }), getBlockNumber(client, { cacheTime: 0 }), ]) @@ -79,9 +101,9 @@ export async function getUploadCosts( const totalSize = currentDataSetSize + options.dataSize const rate = calculateEffectiveRate({ sizeInBytes: totalSize, - pricePerTiBPerMonth: pricing.pricePerTiBPerMonthNoCDN, - minimumPricePerMonth: pricing.minimumPricePerMonth, - epochsPerMonth: pricing.epochsPerMonth, + storagePerTibPerMonth: priceList.rates.storagePerTibPerMonth, + provingServicePerMonth: priceList.rates.datasetFeePerMonth, + epochsPerMonth: TIME_CONSTANTS.EPOCHS_PER_MONTH, }) const accountParams = { @@ -94,16 +116,16 @@ export async function getUploadCosts( const debt = calculateAccountDebt(accountParams) const { availableFunds, runwayInEpochs } = resolveAccountState(accountParams) - // Calculate deposit needed - const depositNeeded = calculateDepositNeeded({ + // Deposit, plus the lockup and fee breakdowns it was computed from. + const { depositNeeded, lockup, fees } = calculateDepositNeeded({ dataSize: options.dataSize, currentDataSetSize, - pricePerTiBPerMonth: pricing.pricePerTiBPerMonthNoCDN, - minimumPricePerMonth: pricing.minimumPricePerMonth, - epochsPerMonth: pricing.epochsPerMonth, - lockupEpochs: LOCKUP_PERIOD, + priceList, + epochsPerMonth: TIME_CONSTANTS.EPOCHS_PER_MONTH, isNewDataSet, withCDN, + pieceCount, + addPiecesOperationCount, currentLockupRate: accountInfo.lockupRate, extraRunwayEpochs, debt, @@ -113,11 +135,21 @@ export async function getUploadCosts( }) const needsFwssMaxApproval = !approved + const rates = { + perEpoch: rate.ratePerEpoch, + perMonth: rate.ratePerMonth, + } return { - rate: { - perEpoch: rate.ratePerEpoch, - perMonth: rate.ratePerMonth, + rates, + rate: rates, + fees, + lockups: { + lifecycleLockup: lockup.lifecycleLockup, + streamingLockup: lockup.streamingLockup, + cdnLockup: lockup.cdnLockup, + cacheMissLockup: lockup.cacheMissLockup, + total: lockup.total, }, depositNeeded, needsFwssMaxApproval, diff --git a/packages/synapse-core/src/warm-storage/index.ts b/packages/synapse-core/src/warm-storage/index.ts index 2b1378342..f8d682604 100644 --- a/packages/synapse-core/src/warm-storage/index.ts +++ b/packages/synapse-core/src/warm-storage/index.ts @@ -13,6 +13,7 @@ export * from './add-approved-provider.ts' export * from './calculate-additional-lockup-required.ts' export * from './calculate-deposit-needed.ts' export * from './calculate-effective-rate.ts' +export * from './calculate-operation-fees.ts' export * from './fetch-provider-selection-input.ts' export * from './find-matching-data-sets.ts' export * from './get-account-total-storage-size.ts' @@ -28,6 +29,7 @@ export * from './get-pdp-data-sets.ts' export * from './get-service-price.ts' export * from './get-upload-costs.ts' export * from './location-types.ts' +export * from './price-list.ts' export * from './read-addresses.ts' export * from './remove-approved-provider.ts' export * from './select-providers.ts' diff --git a/packages/synapse-core/src/warm-storage/price-list.ts b/packages/synapse-core/src/warm-storage/price-list.ts new file mode 100644 index 000000000..4fbb5fa00 --- /dev/null +++ b/packages/synapse-core/src/warm-storage/price-list.ts @@ -0,0 +1,88 @@ +import type { Address, Chain, Client, Transport } from 'viem' +import { getServicePrice } from './get-service-price.ts' + +export namespace getPriceList { + export type OptionsType = getServicePrice.OptionsType + + /** + * The canonical warm storage price list. Matches the on-chain `PriceList` + * struct from `FilecoinWarmStorageServiceStateView.getPriceList()` + * ([filecoin-services#501](https://github.com/FilOzone/filecoin-services/issues/501)). + * Amounts are in the token's smallest unit; rates are per-month (divide by + * `EPOCHS_PER_MONTH` for per-epoch values). + */ + export type OutputType = { + token: Address + rates: { + storagePerTibPerMonth: bigint + datasetFeePerMonth: bigint + cdnEgressPerTib: bigint + cacheMissEgressPerTib: bigint + } + fees: { + createDataSetFee: bigint + addPiecesBaseFee: bigint + addPiecesPerPieceFee: bigint + schedulePieceRemovalsFee: bigint + terminateFee: bigint + } + lockups: { + lifecycleReserveTarget: bigint + replenishThreshold: bigint + defaultLockupPeriod: bigint + cdnLockupAmount: bigint + cacheMissLockupAmount: bigint + cdnLockupPeriod: bigint + } + } +} + +/** + * FWSS operation fees and lockup defaults, sourced from `PriceListUSDFC.sol`. + * Storage, egress, and token come from the contract via {@link getServicePrice}. + */ +const expectedFees = { + createDataSetFee: 25_000_000_000_000_000n, + addPiecesBaseFee: 500_000_000_000_000n, + addPiecesPerPieceFee: 300_000_000_000_000n, + schedulePieceRemovalsFee: 2_000_000_000_000_000n, + terminateFee: 1_120_000_000_000_000n, +} as const + +const expectedLockups = { + lifecycleReserveTarget: 100_000_000_000_000_000n, + replenishThreshold: 5_000_000_000_000_000n, + defaultLockupPeriod: 86_400n, // EPOCHS_PER_DAY * 30 + cdnLockupAmount: 700_000_000_000_000_000n, + cacheMissLockupAmount: 300_000_000_000_000_000n, + cdnLockupPeriod: 14_400n, // EPOCHS_PER_DAY * 5 +} as const + +const DATASET_FEE_PER_MONTH = 24_000_000_000_000_000n // $0.024 + +/** + * Read pricing through the canonical SDK shape. + * + * Storage and egress rates and the token come from the contract via + * {@link getServicePrice}. The proving (dataset) rate, operation fees, and + * lockup amounts come from {@link expectedFees} / {@link expectedLockups}. + */ +export async function getPriceList( + client: Client, + options: getPriceList.OptionsType = {} +): Promise { + const servicePrice = await getServicePrice(client, options) + + return { + token: servicePrice.tokenAddress, + rates: { + storagePerTibPerMonth: servicePrice.pricePerTiBPerMonthNoCDN, + datasetFeePerMonth: DATASET_FEE_PER_MONTH, + cdnEgressPerTib: servicePrice.pricePerTiBCdnEgress, + cacheMissEgressPerTib: servicePrice.pricePerTiBCacheMissEgress, + }, + // Spread so callers can't mutate the shared module-level constants. + fees: { ...expectedFees }, + lockups: { ...expectedLockups }, + } +} diff --git a/packages/synapse-core/test/calculate-additional-lockup-required.test.ts b/packages/synapse-core/test/calculate-additional-lockup-required.test.ts index 6a60f11f2..4fcd16a1e 100644 --- a/packages/synapse-core/test/calculate-additional-lockup-required.test.ts +++ b/packages/synapse-core/test/calculate-additional-lockup-required.test.ts @@ -1,92 +1,147 @@ /* globals describe it */ import assert from 'assert' -import { USDFC_SYBIL_FEE } from '../src/utils/constants.ts' import { calculateAdditionalLockupRequired } from '../src/warm-storage/calculate-additional-lockup-required.ts' +import { calculateEffectiveRate } from '../src/warm-storage/calculate-effective-rate.ts' +import type { getPriceList } from '../src/warm-storage/price-list.ts' -const pricing = { - pricePerTiBPerMonth: 2_500_000_000_000_000_000n, // 2.5 USDFC - minimumPricePerMonth: 60_000_000_000_000_000n, // 0.06 USDFC - epochsPerMonth: 86400n, -} +const priceList = { + token: '0x0000000000000000000000000000000000000001', + rates: { + storagePerTibPerMonth: 2_500_000_000_000_000_000n, + datasetFeePerMonth: 24_000_000_000_000_000n, + cdnEgressPerTib: 0n, + cacheMissEgressPerTib: 0n, + }, + fees: { + createDataSetFee: 25_000_000_000_000_000n, + addPiecesBaseFee: 500_000_000_000_000n, + addPiecesPerPieceFee: 300_000_000_000_000n, + schedulePieceRemovalsFee: 2_000_000_000_000_000n, + terminateFee: 1_120_000_000_000_000n, + }, + lockups: { + lifecycleReserveTarget: 100_000_000_000_000_000n, + replenishThreshold: 5_000_000_000_000_000n, + defaultLockupPeriod: 86_400n, + cdnLockupAmount: 700_000_000_000_000_000n, + cacheMissLockupAmount: 300_000_000_000_000_000n, + cdnLockupPeriod: 14_400n, + }, +} satisfies getPriceList.OutputType const lockupEpochs = 86400n // 30 days describe('calculateAdditionalLockupRequired', () => { - it('new dataset without CDN: no CDN fixed lockup', () => { + it('new dataset without CDN: includes lifecycle lockup only', () => { const result = calculateAdditionalLockupRequired({ dataSize: 1000n, currentDataSetSize: 0n, - ...pricing, + priceList, lockupEpochs, isNewDataSet: true, withCDN: false, }) - assert.equal(result.cdnFixedLockup, 0n) - assert.equal(result.sybilFee, USDFC_SYBIL_FEE) - // For a small file, should use floor rate - const minimumPerEpoch = pricing.minimumPricePerMonth / pricing.epochsPerMonth - assert.equal(result.rateDeltaPerEpoch, minimumPerEpoch) - assert.equal(result.rateLockupDelta, minimumPerEpoch * lockupEpochs) - assert.equal(result.total, result.rateLockupDelta + result.sybilFee) + // Additive model: rate delta for a new dataset is the storage rate for the + // added bytes plus the proving service rate. + const expectedRatePerEpoch = calculateEffectiveRate({ + sizeInBytes: 1000n, + storagePerTibPerMonth: priceList.rates.storagePerTibPerMonth, + provingServicePerMonth: priceList.rates.datasetFeePerMonth, + epochsPerMonth: 86400n, + }).ratePerEpoch + assert.equal(result.lifecycleLockup, priceList.lockups.lifecycleReserveTarget) + assert.equal(result.cdnLockup, 0n) + assert.equal(result.cacheMissLockup, 0n) + assert.equal(result.rateDeltaPerEpoch, expectedRatePerEpoch) + assert.equal(result.streamingLockup, expectedRatePerEpoch * lockupEpochs) + assert.equal(result.total, result.streamingLockup + result.lifecycleLockup) }) - it('new dataset with CDN: includes CDN fixed lockup of 1 USDFC', () => { + it('new dataset with CDN: includes CDN and cache-miss lockups', () => { const result = calculateAdditionalLockupRequired({ dataSize: 1000n, currentDataSetSize: 0n, - ...pricing, + priceList, lockupEpochs, isNewDataSet: true, withCDN: true, }) - const cdnFixedLockup = 1_000_000_000_000_000_000n // 1 USDFC - assert.equal(result.cdnFixedLockup, cdnFixedLockup) - assert.equal(result.sybilFee, USDFC_SYBIL_FEE) - assert.equal(result.total, result.rateLockupDelta + cdnFixedLockup + result.sybilFee) + assert.equal(result.lifecycleLockup, priceList.lockups.lifecycleReserveTarget) + assert.equal(result.cdnLockup, priceList.lockups.cdnLockupAmount) + assert.equal(result.cacheMissLockup, priceList.lockups.cacheMissLockupAmount) + assert.equal( + result.total, + result.streamingLockup + result.lifecycleLockup + result.cdnLockup + result.cacheMissLockup + ) }) - it('existing dataset floor-to-floor: rate delta = 0 when both sizes are below floor', () => { - // Both 100 bytes and 200 bytes are well below floor threshold + it('existing dataset keeps the proving rate and only locks up storage delta', () => { const result = calculateAdditionalLockupRequired({ dataSize: 100n, currentDataSetSize: 100n, - ...pricing, + priceList, lockupEpochs, isNewDataSet: false, withCDN: false, }) - // Both sizes produce floor rate, so delta = 0 - assert.equal(result.rateDeltaPerEpoch, 0n) - assert.equal(result.rateLockupDelta, 0n) - assert.equal(result.cdnFixedLockup, 0n) - assert.equal(result.sybilFee, 0n) - assert.equal(result.total, 0n) + // Proving rate cancels between current and new size; only the storage rate + // delta for the added bytes is locked up. + const rateParams = { + storagePerTibPerMonth: priceList.rates.storagePerTibPerMonth, + provingServicePerMonth: priceList.rates.datasetFeePerMonth, + epochsPerMonth: 86400n, + } + const expectedDelta = + calculateEffectiveRate({ ...rateParams, sizeInBytes: 200n }).ratePerEpoch - + calculateEffectiveRate({ ...rateParams, sizeInBytes: 100n }).ratePerEpoch + assert.ok(expectedDelta > 0n) + assert.equal(result.rateDeltaPerEpoch, expectedDelta) + assert.equal(result.streamingLockup, expectedDelta * lockupEpochs) + assert.equal(result.lifecycleLockup, 0n) + assert.equal(result.cdnLockup, 0n) + assert.equal(result.cacheMissLockup, 0n) + assert.equal(result.total, result.streamingLockup) }) - it('existing dataset crossing floor threshold: rate delta > 0', () => { + it('existing dataset with added storage has a positive rate delta', () => { const TiB = 1n << 40n - // Start with 0 (treated as new since isNewDataSet=false but currentDataSetSize=0 - // triggers the else branch... actually currentDataSetSize > 0n check fails so it - // goes to the else branch). Use a large currentDataSetSize instead. + // Non-zero existing dataset size so the existing-dataset delta path runs. const result = calculateAdditionalLockupRequired({ dataSize: TiB, - currentDataSetSize: 1n, // tiny existing dataset at floor - ...pricing, + currentDataSetSize: 1n, + priceList, lockupEpochs, isNewDataSet: false, withCDN: false, }) - // Adding 1 TiB to a 1-byte dataset: new rate will be well above floor - // while current rate is at the floor, so delta should be positive assert.ok(result.rateDeltaPerEpoch > 0n) - assert.equal(result.rateLockupDelta, result.rateDeltaPerEpoch * lockupEpochs) - assert.equal(result.cdnFixedLockup, 0n) - assert.equal(result.sybilFee, 0n) - assert.equal(result.total, result.rateLockupDelta) + assert.equal(result.streamingLockup, result.rateDeltaPerEpoch * lockupEpochs) + assert.equal(result.lifecycleLockup, 0n) + assert.equal(result.cdnLockup, 0n) + assert.equal(result.cacheMissLockup, 0n) + assert.equal(result.total, result.streamingLockup) + }) + + it('sources the lockup period from priceList.lockups.defaultLockupPeriod when lockupEpochs is omitted', () => { + const customPeriod = 1234n + const customPriceList = { + ...priceList, + lockups: { ...priceList.lockups, defaultLockupPeriod: customPeriod }, + } + + const result = calculateAdditionalLockupRequired({ + dataSize: 1000n, + currentDataSetSize: 0n, + priceList: customPriceList, + isNewDataSet: true, + withCDN: false, + }) + + assert.equal(result.streamingLockup, result.rateDeltaPerEpoch * customPeriod) }) }) diff --git a/packages/synapse-core/test/calculate-deposit-needed.test.ts b/packages/synapse-core/test/calculate-deposit-needed.test.ts index 05ce1bc41..0e20b3075 100644 --- a/packages/synapse-core/test/calculate-deposit-needed.test.ts +++ b/packages/synapse-core/test/calculate-deposit-needed.test.ts @@ -7,6 +7,32 @@ import { calculateDepositNeeded, calculateRunwayAmount, } from '../src/warm-storage/calculate-deposit-needed.ts' +import type { getPriceList } from '../src/warm-storage/price-list.ts' + +const priceList = { + token: '0x0000000000000000000000000000000000000001', + rates: { + storagePerTibPerMonth: 2_500_000_000_000_000_000n, + datasetFeePerMonth: 24_000_000_000_000_000n, + cdnEgressPerTib: 0n, + cacheMissEgressPerTib: 0n, + }, + fees: { + createDataSetFee: 25_000_000_000_000_000n, + addPiecesBaseFee: 500_000_000_000_000n, + addPiecesPerPieceFee: 300_000_000_000_000n, + schedulePieceRemovalsFee: 2_000_000_000_000_000n, + terminateFee: 1_120_000_000_000_000n, + }, + lockups: { + lifecycleReserveTarget: 100_000_000_000_000_000n, + replenishThreshold: 5_000_000_000_000_000n, + defaultLockupPeriod: 86_400n, + cdnLockupAmount: 700_000_000_000_000_000n, + cacheMissLockupAmount: 300_000_000_000_000_000n, + cdnLockupPeriod: 14_400n, + }, +} satisfies getPriceList.OutputType describe('calculateRunwayAmount', () => { it('computes netRateAfterUpload * extraRunwayEpochs', () => { @@ -90,17 +116,11 @@ describe('calculateBufferAmount', () => { }) describe('calculateDepositNeeded', () => { - const pricing = { - pricePerTiBPerMonth: 2_500_000_000_000_000_000n, - minimumPricePerMonth: 60_000_000_000_000_000n, - epochsPerMonth: 86400n, - } - it('healthy account, no debt, sufficient funds: returns 0', () => { const result = calculateDepositNeeded({ dataSize: 1000n, currentDataSetSize: 0n, - ...pricing, + priceList, lockupEpochs: 86400n, isNewDataSet: true, withCDN: false, @@ -112,14 +132,14 @@ describe('calculateDepositNeeded', () => { bufferEpochs: 10n, }) - assert.equal(result, 0n) + assert.equal(result.depositNeeded, 0n) }) it('new dataset + no existing rails: buffer skipped', () => { const base = { dataSize: 1000n, currentDataSetSize: 0n, - ...pricing, + priceList, lockupEpochs: 86400n, isNewDataSet: true, withCDN: false, @@ -134,15 +154,15 @@ describe('calculateDepositNeeded', () => { const withoutBuffer = calculateDepositNeeded({ ...base, bufferEpochs: 0n }) // No existing rails (currentLockupRate=0) + new dataset, buffer skipped - assert.equal(withBuffer, withoutBuffer) - assert.ok(withBuffer > 0n) // still requires the lockup deposit + assert.equal(withBuffer.depositNeeded, withoutBuffer.depositNeeded) + assert.ok(withBuffer.depositNeeded > 0n) // still requires the lockup deposit }) it('new dataset + existing rails: buffer still applies', () => { const base = { dataSize: 1000n, currentDataSetSize: 0n, - ...pricing, + priceList, lockupEpochs: 86400n, isNewDataSet: true, withCDN: false, @@ -157,7 +177,7 @@ describe('calculateDepositNeeded', () => { const withoutBuffer = calculateDepositNeeded({ ...base, bufferEpochs: 0n }) // Existing rails draining, buffer must apply even for new dataset - assert.ok(withBuffer > withoutBuffer) + assert.ok(withBuffer.depositNeeded > withoutBuffer.depositNeeded) }) it('underfunded account with debt: includes debt in deposit', () => { @@ -165,7 +185,7 @@ describe('calculateDepositNeeded', () => { const result = calculateDepositNeeded({ dataSize: 1000n, currentDataSetSize: 0n, - ...pricing, + priceList, lockupEpochs: 86400n, isNewDataSet: true, withCDN: false, @@ -177,7 +197,8 @@ describe('calculateDepositNeeded', () => { bufferEpochs: 10n, }) - // Result should include the debt - assert.ok(result >= debt) + // Result should include the debt, and the deposit covers debt + fees + lockup. + assert.ok(result.fees.total > 0n) + assert.ok(result.depositNeeded >= debt + result.fees.total + result.lockup.total) }) }) diff --git a/packages/synapse-core/test/calculate-effective-rate.test.ts b/packages/synapse-core/test/calculate-effective-rate.test.ts index 7e4db4314..b8862e857 100644 --- a/packages/synapse-core/test/calculate-effective-rate.test.ts +++ b/packages/synapse-core/test/calculate-effective-rate.test.ts @@ -4,42 +4,51 @@ import assert from 'assert' import { calculateEffectiveRate } from '../src/warm-storage/calculate-effective-rate.ts' const TiB = 1n << 40n -const pricePerTiBPerMonth = 2_500_000_000_000_000_000n // 2.5 USDFC -const minimumPricePerMonth = 60_000_000_000_000_000n // 0.06 USDFC +const storagePerTibPerMonth = 2_500_000_000_000_000_000n // 2.5 USDFC +const provingServicePerMonth = 24_000_000_000_000_000n // 0.024 USDFC const epochsPerMonth = 86400n describe('calculateEffectiveRate', () => { - it('floor pricing: tiny file uses the minimum rate', () => { + it('empty dataset has no recurring rate', () => { + const result = calculateEffectiveRate({ + sizeInBytes: 0n, + storagePerTibPerMonth, + provingServicePerMonth, + epochsPerMonth, + }) + + assert.equal(result.ratePerEpoch, 0n) + assert.equal(result.ratePerMonth, 0n) + }) + + it('tiny non-empty dataset pays storage plus the proving service rate', () => { const result = calculateEffectiveRate({ sizeInBytes: 1n, - pricePerTiBPerMonth, - minimumPricePerMonth, + storagePerTibPerMonth, + provingServicePerMonth, epochsPerMonth, }) - // naturalPerEpoch = (2.5e18 * 1) / (TiB * 86400) = 0 (truncated to 0) - // minimumPerEpoch = 60_000_000_000_000_000 / 86400 = 694_444_444_444 - const minimumPerEpoch = minimumPricePerMonth / epochsPerMonth - assert.equal(result.ratePerEpoch, minimumPerEpoch) - assert.equal(result.ratePerMonth, minimumPricePerMonth) + // Additive: even a 1-byte dataset pays a (tiny) storage rate on top of proving. + const storagePerEpoch = (storagePerTibPerMonth * 1n) / (TiB * epochsPerMonth) + const storagePerMonth = (storagePerTibPerMonth * 1n) / TiB + assert.equal(result.ratePerEpoch, storagePerEpoch + provingServicePerMonth / epochsPerMonth) + assert.equal(result.ratePerMonth, storagePerMonth + provingServicePerMonth) }) - it('above floor: large file natural rate exceeds minimum', () => { + it('large dataset pays storage plus proving service rate', () => { const result = calculateEffectiveRate({ sizeInBytes: TiB, - pricePerTiBPerMonth, - minimumPricePerMonth, + storagePerTibPerMonth, + provingServicePerMonth, epochsPerMonth, }) - // naturalPerMonth = (2.5e18 * TiB) / TiB = 2.5e18 - // naturalPerEpoch = (2.5e18 * TiB) / (TiB * 86400) = 2.5e18 / 86400 - const expectedPerMonth = pricePerTiBPerMonth - const expectedPerEpoch = pricePerTiBPerMonth / epochsPerMonth + const expectedPerMonth = storagePerTibPerMonth + provingServicePerMonth + const expectedPerEpoch = storagePerTibPerMonth / epochsPerMonth + provingServicePerMonth / epochsPerMonth assert.equal(result.ratePerMonth, expectedPerMonth) assert.equal(result.ratePerEpoch, expectedPerEpoch) - assert.ok(result.ratePerEpoch > minimumPricePerMonth / epochsPerMonth) }) it('precision: perMonth !== perEpoch * epochsPerMonth due to truncation', () => { @@ -48,8 +57,8 @@ describe('calculateEffectiveRate', () => { const result = calculateEffectiveRate({ sizeInBytes, - pricePerTiBPerMonth, - minimumPricePerMonth, + storagePerTibPerMonth, + provingServicePerMonth, epochsPerMonth, }) diff --git a/packages/synapse-core/test/get-price-list.test.ts b/packages/synapse-core/test/get-price-list.test.ts new file mode 100644 index 000000000..8f201d20d --- /dev/null +++ b/packages/synapse-core/test/get-price-list.test.ts @@ -0,0 +1,105 @@ +import assert from 'assert' +import { setup } from 'iso-web/msw' +import { createPublicClient, http, parseUnits } from 'viem' +import { calibration } from '../src/chains.ts' +import { JSONRPC, presets } from '../src/mocks/jsonrpc/index.ts' +import { getPriceList } from '../src/warm-storage/price-list.ts' + +describe('getPriceList', () => { + const server = setup() + + before(async () => { + await server.start() + }) + + after(() => { + server.stop() + }) + + beforeEach(() => { + server.resetHandlers() + }) + + const makeClient = () => createPublicClient({ chain: calibration, transport: http() }) + + it('reads rates and token live from getServicePrice', async () => { + // Distinct values (not the defaults) prove these fields are plumbed from the + // contract read rather than hardcoded in the adapter. + const token = '0x00000000000000000000000000000000000000aa' + server.use( + JSONRPC({ + ...presets.basic, + warmStorage: { + ...presets.basic.warmStorage, + getServicePrice: () => [ + { + pricePerTiBPerMonthNoCDN: parseUnits('9.9', 18), + pricePerTiBCdnEgress: parseUnits('1.5', 18), + pricePerTiBCacheMissEgress: parseUnits('2.5', 18), + minimumPricePerMonth: parseUnits('6', 16), + tokenAddress: token, + epochsPerMonth: 86400n, + }, + ], + }, + }) + ) + + const priceList = await getPriceList(makeClient()) + + assert.equal(priceList.token.toLowerCase(), token) + assert.equal(priceList.rates.storagePerTibPerMonth, parseUnits('9.9', 18)) + assert.equal(priceList.rates.cdnEgressPerTib, parseUnits('1.5', 18)) + assert.equal(priceList.rates.cacheMissEgressPerTib, parseUnits('2.5', 18)) + }) + + it('supplies the dataset fee, which getServicePrice does not expose', async () => { + server.use(JSONRPC(presets.basic)) + + const priceList = await getPriceList(makeClient()) + + // The current ABI has no dataset fee field, so the adapter must inject it. + assert.equal(priceList.rates.datasetFeePerMonth, parseUnits('0.024', 18)) + }) + + it('returns the on-chain PriceList key shape', async () => { + server.use(JSONRPC(presets.basic)) + + const priceList = await getPriceList(makeClient()) + + assert.deepEqual(Object.keys(priceList).sort(), ['fees', 'lockups', 'rates', 'token']) + assert.deepEqual(Object.keys(priceList.rates).sort(), [ + 'cacheMissEgressPerTib', + 'cdnEgressPerTib', + 'datasetFeePerMonth', + 'storagePerTibPerMonth', + ]) + assert.deepEqual(Object.keys(priceList.fees).sort(), [ + 'addPiecesBaseFee', + 'addPiecesPerPieceFee', + 'createDataSetFee', + 'schedulePieceRemovalsFee', + 'terminateFee', + ]) + assert.deepEqual(Object.keys(priceList.lockups).sort(), [ + 'cacheMissLockupAmount', + 'cdnLockupAmount', + 'cdnLockupPeriod', + 'defaultLockupPeriod', + 'lifecycleReserveTarget', + 'replenishThreshold', + ]) + }) + + it('returns independent fee/lockup objects per call (callers cannot corrupt later reads)', async () => { + server.use(JSONRPC(presets.basic)) + + const first = await getPriceList(makeClient()) + first.fees.createDataSetFee = 0n + first.lockups.lifecycleReserveTarget = 0n + + const second = await getPriceList(makeClient()) + assert.notEqual(second.fees.createDataSetFee, 0n) + assert.notEqual(second.lockups.lifecycleReserveTarget, 0n) + }) +}) diff --git a/packages/synapse-core/test/get-upload-costs.test.ts b/packages/synapse-core/test/get-upload-costs.test.ts index 194f8be69..0372ee9d1 100644 --- a/packages/synapse-core/test/get-upload-costs.test.ts +++ b/packages/synapse-core/test/get-upload-costs.test.ts @@ -32,11 +32,14 @@ describe('getUploadCosts', () => { const result = await getUploadCosts(client, { clientAddress: ADDRESSES.client1, - dataSize: 1n, // tiny file → uses floor pricing + dataSize: 1n, }) assert.equal(typeof result.rate.perEpoch, 'bigint') assert.equal(typeof result.rate.perMonth, 'bigint') + assert.equal(result.rate, result.rates) + assert.equal(typeof result.fees.total, 'bigint') + assert.equal(typeof result.lockups.total, 'bigint') assert.equal(typeof result.depositNeeded, 'bigint') assert.equal(typeof result.needsFwssMaxApproval, 'boolean') assert.equal(typeof result.ready, 'boolean') @@ -115,7 +118,7 @@ describe('getUploadCosts', () => { assert.equal(result.ready, false) }) - it('should apply floor pricing for tiny files', async () => { + it('should apply proving service rate for tiny files', async () => { server.use( JSONRPC({ ...presets.basic, @@ -136,13 +139,15 @@ describe('getUploadCosts', () => { dataSize: 1n, }) - // Floor: minimumPricePerMonth = 0.06 USDFC - // perMonth should equal minimumPricePerMonth (floor) - const minimumPricePerMonth = parseUnits('6', 16) // 0.06 USDFC - assert.equal(result.rate.perMonth, minimumPricePerMonth) + // Additive: 1-byte dataset pays a tiny storage rate on top of proving. + const storagePerMonth1Byte = parseUnits('2.5', 18) / (1n << 40n) + assert.equal(result.rate.perMonth, parseUnits('0.024', 18) + storagePerMonth1Byte) + assert.equal(result.fees.createDataSetFee, parseUnits('0.025', 18)) + assert.equal(result.fees.addPiecesFee, parseUnits('0.0008', 18)) + assert.equal(result.lockups.lifecycleLockup, parseUnits('0.10', 18)) }) - it('should use natural rate for large files above floor', async () => { + it('should use storage plus proving rate for large files', async () => { server.use( JSONRPC({ ...presets.basic, @@ -158,16 +163,15 @@ describe('getUploadCosts', () => { transport: http(), }) - // 1 TiB should be above floor pricing const onetiB = 1n << 40n const result = await getUploadCosts(client, { clientAddress: ADDRESSES.client1, dataSize: onetiB, }) - // Natural rate for 1 TiB = pricePerTiBPerMonth = 2.5 USDFC + // 1 TiB storage plus proving service rate. const pricePerTiBPerMonth = parseUnits('2.5', 18) - assert.equal(result.rate.perMonth, pricePerTiBPerMonth) + assert.equal(result.rate.perMonth, pricePerTiBPerMonth + parseUnits('0.024', 18)) }) it('should include debt in deposit for account in debt', async () => { @@ -247,10 +251,9 @@ describe('getUploadCosts', () => { ) // runway = (currentLockupRate + rateDeltaPerEpoch) * extraRunwayEpochs - // currentLockupRate = 0, rateDeltaPerEpoch = floor rate = minimumPerEpoch - // minimumPerEpoch = 6e16 / 86400 = 694,444,444,444 (bigint truncation) - // runway = 694,444,444,444 * 10,000 = 6,944,444,444,440,000 - const expectedRunway = 6_944_444_444_440_000n + // currentLockupRate = 0; rateDeltaPerEpoch = storage(1 byte) + proving, per epoch + const ratePerEpoch1Byte = parseUnits('2.5', 18) / ((1n << 40n) * 86400n) + parseUnits('0.024', 18) / 86400n + const expectedRunway = ratePerEpoch1Byte * 10_000n assert.equal( withRunway.depositNeeded - baseline.depositNeeded, expectedRunway, @@ -300,9 +303,8 @@ describe('getUploadCosts', () => { ) // Buffer delta = netRate * bufferEpochs = (currentLockupRate + rateDelta) * 100 - // rateDelta = floor rate for 1-byte file = minimumPricePerMonth / epochsPerMonth - const floorRatePerEpoch = 60_000_000_000_000_000n / 86400n - const netRate = 100_000_000_000_000n + floorRatePerEpoch + const ratePerEpoch1Byte = parseUnits('2.5', 18) / ((1n << 40n) * 86400n) + parseUnits('0.024', 18) / 86400n + const netRate = 100_000_000_000_000n + ratePerEpoch1Byte const expectedBufferDelta = netRate * 100n assert.equal( largeBuffer.depositNeeded - smallBuffer.depositNeeded, @@ -344,8 +346,8 @@ describe('getUploadCosts', () => { isNewDataSet: true, }) - // 1 TiB rate = 2.5 USDFC/month, 0.5 TiB rate < 2.5 USDFC/month - assert.equal(existing.rate.perMonth, parseUnits('2.5', 18)) + // Existing dataset pays storage for 1 TiB plus one proving service rate. + assert.equal(existing.rate.perMonth, parseUnits('2.524', 18)) assert.ok( existing.rate.perMonth > newDs.rate.perMonth, `existing dataset rate (${existing.rate.perMonth}) should exceed new dataset rate (${newDs.rate.perMonth})` @@ -382,12 +384,40 @@ describe('getUploadCosts', () => { withCDN: true, }) - // CDN_FIXED_LOCKUP.total = 1 USDFC (cdn 0.7 + cacheMiss 0.3) const cdnFixedLockupTotal = 1_000_000_000_000_000_000n assert.equal( withCDN.depositNeeded - withoutCDN.depositNeeded, cdnFixedLockupTotal, - 'CDN deposit should exceed non-CDN deposit by exactly CDN_FIXED_LOCKUP.total (1 USDFC)' + 'CDN deposit should exceed non-CDN deposit by the CDN and cache-miss lockups' + ) + }) + + it('includes operation fees in the deposit for a new dataset', async () => { + // Fresh account (no funds, no existing rails) creating a new dataset: with + // default runway/buffer this isolates the deposit to lockups + fees, so it + // proves operation fees are actually counted in depositNeeded. + server.use( + JSONRPC({ + ...presets.basic, + payments: { + ...presets.basic.payments, + accounts: () => [0n, 0n, 0n, 0n], + operatorApprovals: () => [true, maxUint256, maxUint256, 0n, 0n, maxUint256], + }, + }) ) + + const client = createPublicClient({ + chain: calibration, + transport: http(), + }) + + const result = await getUploadCosts(client, { + clientAddress: ADDRESSES.client1, + dataSize: 1n, + }) + + assert.ok(result.fees.total > 0n) + assert.equal(result.depositNeeded, result.lockups.total + result.fees.total) }) }) From 84b3695e022132b61a40bab4d66f50376f0d6c9a Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:51:04 -0400 Subject: [PATCH 02/16] feat(storage): use price list for upload costs --- packages/synapse-sdk/src/storage/manager.ts | 86 +++++++++++++------ .../calculate-multi-context-costs.test.ts | 39 ++++----- packages/synapse-sdk/src/types.ts | 9 +- .../synapse-sdk/src/warm-storage/service.ts | 17 +++- 4 files changed, 99 insertions(+), 52 deletions(-) diff --git a/packages/synapse-sdk/src/storage/manager.ts b/packages/synapse-sdk/src/storage/manager.ts index 3e367f145..bba9b3151 100644 --- a/packages/synapse-sdk/src/storage/manager.ts +++ b/packages/synapse-sdk/src/storage/manager.ts @@ -30,14 +30,15 @@ import { getDataSetSizes } from '@filoz/synapse-core/pdp-verifier' import * as Piece from '@filoz/synapse-core/piece' import type { UploadPieceStreamingData } from '@filoz/synapse-core/sp' import { getPDPProviderByAddress } from '@filoz/synapse-core/sp-registry' -import { DEFAULT_BUFFER_EPOCHS, DEFAULT_RUNWAY_EPOCHS, LOCKUP_PERIOD } from '@filoz/synapse-core/utils' +import { DEFAULT_BUFFER_EPOCHS, DEFAULT_RUNWAY_EPOCHS } from '@filoz/synapse-core/utils' import { calculateAdditionalLockupRequired, calculateBufferAmount, calculateEffectiveRate, + calculateOperationFees, calculateRunwayAmount, getUploadCosts as coreGetUploadCosts, - getServicePrice, + getPriceList, metadataMatches, } from '@filoz/synapse-core/warm-storage' import { type Address, type Hash, type Hex, UserRejectedRequestError, zeroAddress } from 'viem' @@ -694,7 +695,7 @@ export class StorageManager { * and buffer only once (they are shared across all contexts from the same payer). * * Dataset sizes are fetched from chain for existing datasets to get accurate - * floor-aware rate deltas. + * rate deltas. * * @param contexts - Storage contexts to aggregate costs for * @param options - Upload options (dataSize, extraRunwayEpochs, bufferEpochs) @@ -713,9 +714,9 @@ export class StorageManager { const existingDataSetIds = contexts.filter((ctx) => ctx.dataSetId != null).map((ctx) => ctx.dataSetId as bigint) // Fetch all needed data in parallel - const [accountInfo, pricing, approved, currentEpoch, sizes] = await Promise.all([ + const [accountInfo, priceList, approved, currentEpoch, sizes] = await Promise.all([ payAccounts(client, { address: clientAddress }), - getServicePrice(client), + getPriceList(client), isFwssMaxApproved(client, { clientAddress }), getBlockNumber(client, { cacheTime: 0 }), existingDataSetIds.length > 0 ? getDataSetSizes(client, { dataSetIds: existingDataSetIds }) : [], @@ -730,8 +731,14 @@ export class StorageManager { // Per-context loop: calculate lockup for each context let totalRateDeltaPerEpoch = 0n let totalLockup = 0n + let totalLifecycleLockup = 0n + let totalStreamingLockup = 0n + let totalCdnLockup = 0n + let totalCacheMissLockup = 0n let totalRatePerEpoch = 0n let totalRatePerMonth = 0n + let totalCreateDataSetFee = 0n + let totalAddPiecesFee = 0n for (let i = 0; i < contexts.length; i++) { const ctx = contexts[i] @@ -741,24 +748,34 @@ export class StorageManager { const lockup = calculateAdditionalLockupRequired({ dataSize: options.dataSize, currentDataSetSize, - pricePerTiBPerMonth: pricing.pricePerTiBPerMonthNoCDN, - minimumPricePerMonth: pricing.minimumPricePerMonth, - epochsPerMonth: pricing.epochsPerMonth, - lockupEpochs: LOCKUP_PERIOD, + priceList, + epochsPerMonth: TIME_CONSTANTS.EPOCHS_PER_MONTH, isNewDataSet, withCDN: ctx.withCDN, }) + // Multi-context preview assumes one piece / one addPieces op per context; + // batched multi-piece uploads should price via getUploadCosts with explicit counts. + const fees = calculateOperationFees({ + priceList, + isNewDataSet, + }) totalRateDeltaPerEpoch += lockup.rateDeltaPerEpoch totalLockup += lockup.total + totalLifecycleLockup += lockup.lifecycleLockup + totalStreamingLockup += lockup.streamingLockup + totalCdnLockup += lockup.cdnLockup + totalCacheMissLockup += lockup.cacheMissLockup + totalCreateDataSetFee += fees.createDataSetFee + totalAddPiecesFee += fees.addPiecesFee // Calculate per-context effective rate for the rate output const totalSize = currentDataSetSize + options.dataSize const rate = calculateEffectiveRate({ sizeInBytes: totalSize, - pricePerTiBPerMonth: pricing.pricePerTiBPerMonthNoCDN, - minimumPricePerMonth: pricing.minimumPricePerMonth, - epochsPerMonth: pricing.epochsPerMonth, + storagePerTibPerMonth: priceList.rates.storagePerTibPerMonth, + provingServicePerMonth: priceList.rates.datasetFeePerMonth, + epochsPerMonth: TIME_CONSTANTS.EPOCHS_PER_MONTH, }) totalRatePerEpoch += rate.ratePerEpoch totalRatePerMonth += rate.ratePerMonth @@ -782,13 +799,12 @@ export class StorageManager { extraRunwayEpochs, }) - const rawDepositNeeded = totalLockup + runway + debt - availableFunds + const totalFees = totalCreateDataSetFee + totalAddPiecesFee + const rawDepositNeeded = totalLockup + totalFees + runway + debt - availableFunds // Skip buffer when no existing rails are draining and all contexts are new datasets. // The deposit lands before any rail is created, so nothing consumes funds // between balance check and tx execution. - // Minimum upload size is 1 GiB, well below the ~26 GiB floor threshold, so buffer is - // not needed for upto 26 contexts as of now which is reasonable. const allNewDatasets = contexts.every((ctx) => ctx.dataSetId == null) const skipBuffer = accountInfo.lockupRate === 0n && allNewDatasets @@ -806,10 +822,25 @@ export class StorageManager { const depositNeeded = clamped + buffer const needsFwssMaxApproval = !approved + const rates = { + perEpoch: totalRatePerEpoch, + perMonth: totalRatePerMonth, + } + return { - rate: { - perEpoch: totalRatePerEpoch, - perMonth: totalRatePerMonth, + rates, + rate: rates, + fees: { + createDataSetFee: totalCreateDataSetFee, + addPiecesFee: totalAddPiecesFee, + total: totalFees, + }, + lockups: { + lifecycleLockup: totalLifecycleLockup, + streamingLockup: totalStreamingLockup, + cdnLockup: totalCdnLockup, + cacheMissLockup: totalCacheMissLockup, + total: totalLockup, }, depositNeeded, needsFwssMaxApproval, @@ -1017,7 +1048,7 @@ export class StorageManager { // Fetch all data in parallel for performance const [pricingData, approvedIds, allowances] = await Promise.all([ - this._warmStorageService.getServicePrice(), + this._warmStorageService.getPriceList(), this._warmStorageService.getApprovedProviderIds(), getOptionalAllowances(), ]) @@ -1026,19 +1057,19 @@ export class StorageManager { const providers = await spRegistry.getProviders({ providerIds: approvedIds }) // Calculate pricing per different time units - const epochsPerMonth = BigInt(pricingData.epochsPerMonth) + const epochsPerMonth = TIME_CONSTANTS.EPOCHS_PER_MONTH // TODO: StorageInfo needs updating to reflect that CDN costs are usage-based // Calculate per-epoch pricing (base storage cost) - const noCDNPerEpoch = BigInt(pricingData.pricePerTiBPerMonthNoCDN) / epochsPerMonth + const noCDNPerEpoch = pricingData.rates.storagePerTibPerMonth / epochsPerMonth // CDN costs are usage-based (egress charges), so base storage cost is the same - const withCDNPerEpoch = BigInt(pricingData.pricePerTiBPerMonthNoCDN) / epochsPerMonth + const withCDNPerEpoch = pricingData.rates.storagePerTibPerMonth / epochsPerMonth // Calculate per-day pricing (base storage cost) - const noCDNPerDay = BigInt(pricingData.pricePerTiBPerMonthNoCDN) / TIME_CONSTANTS.DAYS_PER_MONTH + const noCDNPerDay = pricingData.rates.storagePerTibPerMonth / TIME_CONSTANTS.DAYS_PER_MONTH // CDN costs are usage-based (egress charges), so base storage cost is the same - const withCDNPerDay = BigInt(pricingData.pricePerTiBPerMonthNoCDN) / TIME_CONSTANTS.DAYS_PER_MONTH + const withCDNPerDay = pricingData.rates.storagePerTibPerMonth / TIME_CONSTANTS.DAYS_PER_MONTH // Filter out providers with zero addresses const validProviders = providers.filter((p: PDPProvider) => p.serviceProvider !== zeroAddress) @@ -1046,18 +1077,19 @@ export class StorageManager { return { pricing: { noCDN: { - perTiBPerMonth: BigInt(pricingData.pricePerTiBPerMonthNoCDN), + perTiBPerMonth: pricingData.rates.storagePerTibPerMonth, perTiBPerDay: noCDNPerDay, perTiBPerEpoch: noCDNPerEpoch, }, // CDN costs are usage-based (egress charges), base storage cost is the same withCDN: { - perTiBPerMonth: BigInt(pricingData.pricePerTiBPerMonthNoCDN), + perTiBPerMonth: pricingData.rates.storagePerTibPerMonth, perTiBPerDay: withCDNPerDay, perTiBPerEpoch: withCDNPerEpoch, }, - tokenAddress: pricingData.tokenAddress, + tokenAddress: pricingData.token, tokenSymbol: 'USDFC', // Hardcoded as we know it's always USDFC + priceList: pricingData, }, providers: validProviders, serviceParameters: { diff --git a/packages/synapse-sdk/src/test/calculate-multi-context-costs.test.ts b/packages/synapse-sdk/src/test/calculate-multi-context-costs.test.ts index 5f7cc753f..a6010b554 100644 --- a/packages/synapse-sdk/src/test/calculate-multi-context-costs.test.ts +++ b/packages/synapse-sdk/src/test/calculate-multi-context-costs.test.ts @@ -2,7 +2,7 @@ import { type Chain, calibration } from '@filoz/synapse-core/chains' import * as Mocks from '@filoz/synapse-core/mocks' -import { CDN_FIXED_LOCKUP, SIZE_CONSTANTS } from '@filoz/synapse-core/utils' +import { SIZE_CONSTANTS } from '@filoz/synapse-core/utils' import { assert } from 'chai' import { setup } from 'iso-web/msw' import { @@ -117,6 +117,9 @@ describe('calculateMultiContextCosts', () => { assert.equal(typeof result.rate.perEpoch, 'bigint') assert.equal(typeof result.rate.perMonth, 'bigint') + assert.equal(result.rate, result.rates) + assert.equal(typeof result.fees.total, 'bigint') + assert.equal(typeof result.lockups.total, 'bigint') assert.equal(typeof result.depositNeeded, 'bigint') assert.equal(typeof result.needsFwssMaxApproval, 'boolean') assert.equal(typeof result.ready, 'boolean') @@ -140,9 +143,9 @@ describe('calculateMultiContextCosts', () => { assert.equal(result.needsFwssMaxApproval, false) assert.equal(result.ready, true) - // Floor pricing for tiny file - const minimumPricePerMonth = parseUnits('6', 16) // 0.06 USDFC - assert.equal(result.rate.perMonth, minimumPricePerMonth) + // Additive: 1-byte dataset pays a tiny storage rate on top of proving. + const storagePerMonth1Byte = parseUnits('2.5', 18) / (1n << 40n) + assert.equal(result.rate.perMonth, parseUnits('0.024', 18) + storagePerMonth1Byte) }) it('should aggregate rates across two new contexts', async () => { @@ -201,8 +204,8 @@ describe('calculateMultiContextCosts', () => { // Existing 1 TiB + 1 TiB = 2 TiB rate, new 1 TiB = 1 TiB rate // pricePerTiBPerMonth = 2.5 USDFC const pricePerTiBPerMonth = parseUnits('2.5', 18) - assert.equal(resultNew.rate.perMonth, pricePerTiBPerMonth) // 1 TiB = 2.5 USDFC/month - assert.equal(resultExisting.rate.perMonth, pricePerTiBPerMonth * 2n) // 2 TiB = 5 USDFC/month + assert.equal(resultNew.rate.perMonth, pricePerTiBPerMonth + parseUnits('0.024', 18)) + assert.equal(resultExisting.rate.perMonth, pricePerTiBPerMonth * 2n + parseUnits('0.024', 18)) }) it('should handle mixed new + existing contexts', async () => { @@ -236,9 +239,9 @@ describe('calculateMultiContextCosts', () => { dataSize: oneTiB, }) - // Combined rate: 1 TiB (2.5 USDFC) + 2 TiB (5 USDFC) = 7.5 USDFC/month + // Combined rate: storage rates plus one proving service rate per context. const pricePerTiBPerMonth = parseUnits('2.5', 18) - assert.equal(result.rate.perMonth, pricePerTiBPerMonth * 3n) + assert.equal(result.rate.perMonth, pricePerTiBPerMonth * 3n + parseUnits('0.024', 18) * 2n) }) it('should include debt in deposit for account in debt', async () => { @@ -301,11 +304,8 @@ describe('calculateMultiContextCosts', () => { `deposit with runway (${withRunway.depositNeeded}) should exceed baseline (${baseline.depositNeeded})` ) - // runway = (currentLockupRate + totalRateDelta) * extraRunwayEpochs - // currentLockupRate = 0, totalRateDelta = 2 * floor rate per epoch - // floor per epoch = 6e16 / 86400 = 694,444,444,444 - // runway = 2 * 694,444,444,444 * 10,000 = 13,888,888,888,880,000 - const expectedRunway = 13_888_888_888_880_000n + const ratePerEpoch1Byte = parseUnits('2.5', 18) / ((1n << 40n) * 86400n) + parseUnits('0.024', 18) / 86400n + const expectedRunway = 2n * ratePerEpoch1Byte * 10_000n assert.equal( withRunway.depositNeeded - baseline.depositNeeded, expectedRunway, @@ -387,9 +387,8 @@ describe('calculateMultiContextCosts', () => { ) // buffer delta = netRate * bufferEpochs = (currentLockupRate + rateDelta) * 100 - // rateDelta = floor rate for 1-byte file = minimumPricePerMonth / epochsPerMonth - const floorRatePerEpoch = 60_000_000_000_000_000n / 86400n - const netRate = 100_000_000_000_000n + floorRatePerEpoch + const ratePerEpoch1Byte = parseUnits('2.5', 18) / ((1n << 40n) * 86400n) + parseUnits('0.024', 18) / 86400n + const netRate = 100_000_000_000_000n + ratePerEpoch1Byte const expectedDelta = netRate * 100n assert.equal( withBuffer.depositNeeded - noBuffer.depositNeeded, @@ -424,11 +423,11 @@ describe('calculateMultiContextCosts', () => { dataSize: 1n, }) - // Difference should be exactly CDN_FIXED_LOCKUP.total (1 USDFC) + const cdnLockupTotal = parseUnits('1', 18) assert.equal( mixedResult.depositNeeded - baselineResult.depositNeeded, - CDN_FIXED_LOCKUP.total, - `CDN context should add exactly ${CDN_FIXED_LOCKUP.total} to deposit` + cdnLockupTotal, + `CDN context should add exactly ${cdnLockupTotal} to deposit` ) }) @@ -493,6 +492,6 @@ describe('calculateMultiContextCosts', () => { const ctx = makeContext(synapse, warmStorageService, {}) const result = await manager.calculateMultiContextCosts([ctx], { dataSize: oneTiB }) - assert.equal(result.rate.perMonth, pricePerTiBPerMonth) + assert.equal(result.rate.perMonth, pricePerTiBPerMonth + parseUnits('0.024', 18)) }) }) diff --git a/packages/synapse-sdk/src/types.ts b/packages/synapse-sdk/src/types.ts index 46b8c710a..7d3a3dff6 100644 --- a/packages/synapse-sdk/src/types.ts +++ b/packages/synapse-sdk/src/types.ts @@ -11,6 +11,7 @@ import type { SessionKey, SessionKeyAccount } from '@filoz/synapse-core/session- import type { pullPiecesApiRequest } from '@filoz/synapse-core/sp' import type { PDPProvider } from '@filoz/synapse-core/sp-registry' import type { MetadataObject } from '@filoz/synapse-core/utils' +import type { getPriceList, getUploadCosts } from '@filoz/synapse-core/warm-storage' import type { Account, Address, Client, Hash, Hex, Transport } from 'viem' import type { Synapse } from './synapse.ts' import type { WarmStorageService } from './warm-storage/service.ts' @@ -27,12 +28,12 @@ export type { RailInfo } from '@filoz/synapse-core/pay' export type { MetadataEntry, MetadataObject } from '@filoz/synapse-core/utils' // Re-export upload cost types from synapse-core -export type { getUploadCosts } from '@filoz/synapse-core/warm-storage' +export type { getPriceList, getUploadCosts } /** Alias for the upload costs return type */ -export type UploadCosts = import('@filoz/synapse-core/warm-storage').getUploadCosts.OutputType +export type UploadCosts = getUploadCosts.OutputType /** Alias for the upload costs options type */ -export type GetUploadCostsOptions = import('@filoz/synapse-core/warm-storage').getUploadCosts.OptionsType +export type GetUploadCostsOptions = getUploadCosts.OptionsType /** * Options for the fund() method on PaymentsService. @@ -648,6 +649,8 @@ export interface StorageInfo { tokenAddress: Address /** Token symbol (always USDFC for now) */ tokenSymbol: string + /** Canonical warm storage price list */ + priceList: getPriceList.OutputType } /** List of approved service providers */ diff --git a/packages/synapse-sdk/src/warm-storage/service.ts b/packages/synapse-sdk/src/warm-storage/service.ts index 9af8d964b..b2f806416 100644 --- a/packages/synapse-sdk/src/warm-storage/service.ts +++ b/packages/synapse-sdk/src/warm-storage/service.ts @@ -32,6 +32,7 @@ import { getClientDataSets, getClientDataSetsLength, getDataSet, + getPriceList, getServicePrice, removeApprovedProvider, terminateService, @@ -357,8 +358,20 @@ export class WarmStorageService { // ========== Storage Cost Operations ========== /** - * Get the current service price per TiB per month - * @returns Service price information for both CDN and non-CDN options + * Get the current warm storage price list. + * @returns Recurring rates, operation fees, and lockups. + */ + async getPriceList(): Promise { + return getPriceList(this._client) + } + + /** + * Get the current service price from the current FWSS ABI. + * + * @deprecated Use {@link WarmStorageService.getPriceList} for the latest + * pricing. This returns only the legacy `minimumPricePerMonth` floor plus + * storage/egress rates and does not reflect the per-operation fee model. + * @returns Service price information from the current FWSS ABI. */ async getServicePrice(): Promise { return getServicePrice(this._client) From 79118bbcdad8b3970dfcde320827c80ce0d22629 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:51:04 -0400 Subject: [PATCH 03/16] feat(react): add usePriceList hook --- .../synapse-react/src/warm-storage/index.ts | 1 + .../src/warm-storage/use-price-list.ts | 34 +++++++++++++++++++ .../src/warm-storage/use-service-price.ts | 3 ++ 3 files changed, 38 insertions(+) create mode 100644 packages/synapse-react/src/warm-storage/use-price-list.ts diff --git a/packages/synapse-react/src/warm-storage/index.ts b/packages/synapse-react/src/warm-storage/index.ts index 081817fef..1436214a2 100644 --- a/packages/synapse-react/src/warm-storage/index.ts +++ b/packages/synapse-react/src/warm-storage/index.ts @@ -1,6 +1,7 @@ export * from './use-create-data-set.ts' export * from './use-data-sets.ts' export * from './use-delete-piece.ts' +export * from './use-price-list.ts' export * from './use-providers.ts' export * from './use-service-price.ts' export * from './use-upload.ts' diff --git a/packages/synapse-react/src/warm-storage/use-price-list.ts b/packages/synapse-react/src/warm-storage/use-price-list.ts new file mode 100644 index 000000000..b10e670ae --- /dev/null +++ b/packages/synapse-react/src/warm-storage/use-price-list.ts @@ -0,0 +1,34 @@ +import { getPriceList } from '@filoz/synapse-core/warm-storage' +import { type UseQueryOptions, useQuery } from '@tanstack/react-query' +import { useConfig } from 'wagmi' + +/** + * The result for the usePriceList hook. + */ +export type UsePriceListResult = getPriceList.OutputType + +/** + * The props for the usePriceList hook. + */ +export interface UsePriceListProps { + query?: Omit, 'queryKey' | 'queryFn'> +} + +/** + * Get the warm storage price list. + * + * @param props - The props to use. + * @returns The price list. + */ +export function usePriceList(props?: UsePriceListProps) { + const config = useConfig() + + return useQuery({ + ...props?.query, + queryKey: ['synapse-warm-storage-get-price-list', config.getClient().chain.id], + queryFn: async () => { + const result = await getPriceList(config.getClient()) + return result + }, + }) +} diff --git a/packages/synapse-react/src/warm-storage/use-service-price.ts b/packages/synapse-react/src/warm-storage/use-service-price.ts index 2ce8d1c49..358da506c 100644 --- a/packages/synapse-react/src/warm-storage/use-service-price.ts +++ b/packages/synapse-react/src/warm-storage/use-service-price.ts @@ -17,6 +17,9 @@ export interface UseServicePriceProps { /** * Get the service price for the warm storage. * + * @deprecated Use {@link usePriceList} for the latest pricing. This returns only + * the legacy `minimumPricePerMonth` floor plus storage/egress rates and does not + * reflect the per-operation fee model. * @param props - The props to use. * @returns The service price. */ From 314d1980323a43fd3b7f8ae1f3dfd106f339164c Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:51:08 -0400 Subject: [PATCH 04/16] feat(costs): read price list from chain view --- packages/synapse-core/src/abis/index.ts | 7 +- packages/synapse-core/src/abis/price-list.ts | 68 ++++++++ .../synapse-core/src/mocks/jsonrpc/index.ts | 26 +++ .../src/mocks/jsonrpc/warm-storage.ts | 11 ++ packages/synapse-core/src/utils/constants.ts | 8 + .../calculate-additional-lockup-required.ts | 2 +- .../warm-storage/calculate-deposit-needed.ts | 6 +- .../warm-storage/calculate-effective-rate.ts | 17 +- ...ation-fees.ts => calculate-upload-fees.ts} | 22 ++- .../src/warm-storage/get-upload-costs.ts | 14 +- .../synapse-core/src/warm-storage/index.ts | 2 +- .../src/warm-storage/price-list.ts | 164 +++++++++++++----- ...lculate-additional-lockup-required.test.ts | 4 +- .../test/calculate-effective-rate.test.ts | 18 +- .../test/calculate-upload-fees.test.ts | 71 ++++++++ .../synapse-core/test/get-price-list.test.ts | 77 ++++---- .../test/get-upload-costs.test.ts | 15 +- 17 files changed, 410 insertions(+), 122 deletions(-) create mode 100644 packages/synapse-core/src/abis/price-list.ts rename packages/synapse-core/src/warm-storage/{calculate-operation-fees.ts => calculate-upload-fees.ts} (51%) create mode 100644 packages/synapse-core/test/calculate-upload-fees.test.ts diff --git a/packages/synapse-core/src/abis/index.ts b/packages/synapse-core/src/abis/index.ts index 3bcf0bb8f..3b98bf675 100644 --- a/packages/synapse-core/src/abis/index.ts +++ b/packages/synapse-core/src/abis/index.ts @@ -13,14 +13,19 @@ export * from './erc20.ts' export * as generated from './generated.ts' import * as generated from './generated.ts' +import { priceListAbi } from './price-list.ts' // Merge the storage and errors ABIs export const fwss = [...generated.filecoinWarmStorageServiceAbi, ...generated.errorsAbi] as const export const serviceProviderRegistry = [...generated.serviceProviderRegistryAbi, ...generated.errorsAbi] as const +// Merge the generated view ABI with the standalone getPriceList fragment, which +// is not yet on the generated ABI's pinned release. See abis/price-list.ts. +// TODO: drop the priceListAbi merge and re-export filecoinWarmStorageServiceStateViewAbi +// as fwssView once the generated ABI ref includes getPriceList. +export const fwssView = [...generated.filecoinWarmStorageServiceStateViewAbi, ...priceListAbi] as const export { filecoinPayV1Abi as filecoinPay, - filecoinWarmStorageServiceStateViewAbi as fwssView, pdpVerifierAbi as pdp, providerIdSetAbi as providerIdSet, sessionKeyRegistryAbi as sessionKeyRegistry, diff --git a/packages/synapse-core/src/abis/price-list.ts b/packages/synapse-core/src/abis/price-list.ts new file mode 100644 index 000000000..12ec32fb0 --- /dev/null +++ b/packages/synapse-core/src/abis/price-list.ts @@ -0,0 +1,68 @@ +/** + * Standalone `getPriceList()` view fragment for + * `FilecoinWarmStorageServiceStateView`. + * + * Mirrors the on-chain `PriceList` struct added in + * [FilOzone/filecoin-services#501](https://github.com/FilOzone/filecoin-services/pull/501). + * Kept separate from the wagmi-generated ABI so the price list can be read from + * the chain without bumping the generated ABI ref onto an unreleased commit. + * + * TODO: remove this file and the `fwssView` merge in `abis/index.ts` once the + * generated ABI ref (`FILECOIN_SERVICES_GIT_REF` in `wagmi.config.ts`) is bumped + * to a release that includes `getPriceList`; the generated view ABI will expose + * it directly. + */ +export const priceListAbi = [ + { + type: 'function', + inputs: [], + name: 'getPriceList', + outputs: [ + { + name: 'list', + internalType: 'struct PriceList', + type: 'tuple', + components: [ + { name: 'token', internalType: 'contract IERC20', type: 'address' }, + { + name: 'rates', + internalType: 'struct PriceListRates', + type: 'tuple', + components: [ + { name: 'storagePerTibPerMonth', internalType: 'uint256', type: 'uint256' }, + { name: 'datasetFeePerMonth', internalType: 'uint256', type: 'uint256' }, + { name: 'cdnEgressPerTib', internalType: 'uint256', type: 'uint256' }, + { name: 'cacheMissEgressPerTib', internalType: 'uint256', type: 'uint256' }, + ], + }, + { + name: 'fees', + internalType: 'struct PriceListFees', + type: 'tuple', + components: [ + { name: 'createDataSetFee', internalType: 'uint256', type: 'uint256' }, + { name: 'addPiecesBaseFee', internalType: 'uint256', type: 'uint256' }, + { name: 'addPiecesPerPieceFee', internalType: 'uint256', type: 'uint256' }, + { name: 'schedulePieceRemovalsFee', internalType: 'uint256', type: 'uint256' }, + { name: 'terminateFee', internalType: 'uint256', type: 'uint256' }, + ], + }, + { + name: 'lockups', + internalType: 'struct PriceListLockups', + type: 'tuple', + components: [ + { name: 'lifecycleReserveTarget', internalType: 'uint256', type: 'uint256' }, + { name: 'replenishThreshold', internalType: 'uint256', type: 'uint256' }, + { name: 'defaultLockupPeriod', internalType: 'uint256', type: 'uint256' }, + { name: 'cdnLockupAmount', internalType: 'uint256', type: 'uint256' }, + { name: 'cacheMissLockupAmount', internalType: 'uint256', type: 'uint256' }, + { name: 'cdnLockupPeriod', internalType: 'uint256', type: 'uint256' }, + ], + }, + ], + }, + ], + stateMutability: 'view', + }, +] as const diff --git a/packages/synapse-core/src/mocks/jsonrpc/index.ts b/packages/synapse-core/src/mocks/jsonrpc/index.ts index 74e9daea6..25021d488 100644 --- a/packages/synapse-core/src/mocks/jsonrpc/index.ts +++ b/packages/synapse-core/src/mocks/jsonrpc/index.ts @@ -517,6 +517,32 @@ export const presets = { getClientDataSetsLength: () => { return [1n] }, + getPriceList: () => [ + { + token: ADDRESSES.calibration.usdfcToken, + rates: { + storagePerTibPerMonth: parseUnits('2.5', 18), + datasetFeePerMonth: parseUnits('0.024', 18), + cdnEgressPerTib: parseUnits('7', 18), + cacheMissEgressPerTib: parseUnits('7', 18), + }, + fees: { + createDataSetFee: parseUnits('0.025', 18), + addPiecesBaseFee: parseUnits('0.0005', 18), + addPiecesPerPieceFee: parseUnits('0.0003', 18), + schedulePieceRemovalsFee: parseUnits('0.002', 18), + terminateFee: parseUnits('0.00112', 18), + }, + lockups: { + lifecycleReserveTarget: parseUnits('0.1', 18), + replenishThreshold: parseUnits('0.005', 18), + defaultLockupPeriod: TIME_CONSTANTS.DEFAULT_LOCKUP_DAYS * TIME_CONSTANTS.EPOCHS_PER_DAY, + cdnLockupAmount: parseUnits('0.7', 18), + cacheMissLockupAmount: parseUnits('0.3', 18), + cdnLockupPeriod: 5n * TIME_CONSTANTS.EPOCHS_PER_DAY, + }, + }, + ], }, pdpVerifier: { dataSetLive: () => [true], diff --git a/packages/synapse-core/src/mocks/jsonrpc/warm-storage.ts b/packages/synapse-core/src/mocks/jsonrpc/warm-storage.ts index ecb4a992c..d3727a4cd 100644 --- a/packages/synapse-core/src/mocks/jsonrpc/warm-storage.ts +++ b/packages/synapse-core/src/mocks/jsonrpc/warm-storage.ts @@ -21,6 +21,7 @@ type getPieceMetadata = ExtractAbiFunction type getPDPConfig = ExtractAbiFunction type getClientDataSetsLength = ExtractAbiFunction +type getPriceList = ExtractAbiFunction export interface WarmStorageViewOptions { isProviderApproved?: (args: AbiToType) => AbiToType @@ -40,6 +41,7 @@ export interface WarmStorageViewOptions { getClientDataSetsLength?: ( args: AbiToType ) => AbiToType + getPriceList?: (args: AbiToType) => AbiToType } /** @@ -359,6 +361,15 @@ export function warmStorageViewCallHandler(data: Hex, options: JSONRPCOptions): options.warmStorageView.getClientDataSetsLength(args) ) } + case 'getPriceList': { + if (!options.warmStorageView?.getPriceList) { + throw new Error('Warm Storage View: getPriceList is not defined') + } + return encodeAbiParameters( + Abis.fwssView.find((abi) => abi.type === 'function' && abi.name === 'getPriceList')!.outputs, + options.warmStorageView.getPriceList(args) + ) + } default: { throw new Error(`Warm Storage View: unknown function: ${functionName} with args: ${args}`) diff --git a/packages/synapse-core/src/utils/constants.ts b/packages/synapse-core/src/utils/constants.ts index 404c49156..693150e59 100644 --- a/packages/synapse-core/src/utils/constants.ts +++ b/packages/synapse-core/src/utils/constants.ts @@ -106,6 +106,14 @@ export const SIZE_CONSTANTS = { BYTES_PER_LEAF: 32n, } as const +/** + * Operator-approval allowance ceiling, in epochs. + * + * This is the `maxLockupPeriod` granted to FWSS when approving it as an + * operator, not a pricing input. The pricing lockup period is read from the + * chain via `getPriceList().lockups.defaultLockupPeriod`; granting a fixed, + * larger approval window stays valid even if the chain lockup period shrinks. + */ export const LOCKUP_PERIOD = TIME_CONSTANTS.DEFAULT_LOCKUP_DAYS * TIME_CONSTANTS.EPOCHS_PER_DAY /** diff --git a/packages/synapse-core/src/warm-storage/calculate-additional-lockup-required.ts b/packages/synapse-core/src/warm-storage/calculate-additional-lockup-required.ts index a7168fd85..f7628ad83 100644 --- a/packages/synapse-core/src/warm-storage/calculate-additional-lockup-required.ts +++ b/packages/synapse-core/src/warm-storage/calculate-additional-lockup-required.ts @@ -63,7 +63,7 @@ export function calculateAdditionalLockupRequired( const rateParams = { storagePerTibPerMonth: priceList.rates.storagePerTibPerMonth, - provingServicePerMonth: priceList.rates.datasetFeePerMonth, + datasetFeePerMonth: priceList.rates.datasetFeePerMonth, epochsPerMonth, } diff --git a/packages/synapse-core/src/warm-storage/calculate-deposit-needed.ts b/packages/synapse-core/src/warm-storage/calculate-deposit-needed.ts index 3ebf6d3b2..3f55fa3f2 100644 --- a/packages/synapse-core/src/warm-storage/calculate-deposit-needed.ts +++ b/packages/synapse-core/src/warm-storage/calculate-deposit-needed.ts @@ -1,6 +1,6 @@ import { DEFAULT_BUFFER_EPOCHS, DEFAULT_RUNWAY_EPOCHS } from '../utils/constants.ts' import { calculateAdditionalLockupRequired } from './calculate-additional-lockup-required.ts' -import { calculateOperationFees } from './calculate-operation-fees.ts' +import { calculateUploadFees } from './calculate-upload-fees.ts' import type { getPriceList } from './price-list.ts' export namespace calculateRunwayAmount { @@ -106,7 +106,7 @@ export namespace calculateDepositNeeded { /** Lockup breakdown the deposit was computed from. */ lockup: calculateAdditionalLockupRequired.OutputType /** Operation fee breakdown the deposit was computed from. */ - fees: calculateOperationFees.OutputType + fees: calculateUploadFees.OutputType } } @@ -129,7 +129,7 @@ export function calculateDepositNeeded(params: calculateDepositNeeded.ParamsType isNewDataSet: params.isNewDataSet, withCDN: params.withCDN, }) - const fees = calculateOperationFees({ + const fees = calculateUploadFees({ priceList: params.priceList, isNewDataSet: params.isNewDataSet, pieceCount: params.pieceCount, diff --git a/packages/synapse-core/src/warm-storage/calculate-effective-rate.ts b/packages/synapse-core/src/warm-storage/calculate-effective-rate.ts index 6032f7093..e5f3d7493 100644 --- a/packages/synapse-core/src/warm-storage/calculate-effective-rate.ts +++ b/packages/synapse-core/src/warm-storage/calculate-effective-rate.ts @@ -6,8 +6,11 @@ export namespace calculateEffectiveRate { sizeInBytes: bigint /** Storage price per TiB per month. */ storagePerTibPerMonth: bigint - /** Monthly proving service rate for non-empty datasets. */ - provingServicePerMonth: bigint + /** + * Per-dataset monthly fee (the contract's `datasetFeePerMonth`), charged as + * a flat additive proving service fee on non-empty datasets. + */ + datasetFeePerMonth: bigint /** Epochs per month. */ epochsPerMonth: bigint } @@ -17,7 +20,7 @@ export namespace calculateEffectiveRate { * Rate per epoch — matches the contract's additive per-epoch rate * (`calculateStorageSizeBasedRatePerEpoch`): the size-based storage rate plus * the per-epoch dataset fee, each truncated independently then summed: - * `(totalBytes * storagePerTibPerMonth) / (TiB * EPOCHS_PER_MONTH) + provingServicePerMonth / EPOCHS_PER_MONTH` + * `(totalBytes * storagePerTibPerMonth) / (TiB * EPOCHS_PER_MONTH) + datasetFeePerMonth / EPOCHS_PER_MONTH` * * Because truncation depends on totalBytes, this value is only valid for * the exact size it was computed for; you cannot scale it linearly to @@ -51,13 +54,13 @@ export namespace calculateEffectiveRate { * - `ratePerMonth` — higher precision, linearly scalable (use for display) * * Empty datasets have no recurring rate. Non-empty datasets pay the - * size-based storage rate plus the proving service rate. + * size-based storage rate plus the per-dataset proving service fee. * * @param params - {@link calculateEffectiveRate.ParamsType} * @returns {@link calculateEffectiveRate.OutputType} */ export function calculateEffectiveRate(params: calculateEffectiveRate.ParamsType): calculateEffectiveRate.OutputType { - const { sizeInBytes, storagePerTibPerMonth, provingServicePerMonth, epochsPerMonth } = params + const { sizeInBytes, storagePerTibPerMonth, datasetFeePerMonth, epochsPerMonth } = params if (sizeInBytes === 0n) { return { ratePerEpoch: 0n, ratePerMonth: 0n } @@ -70,8 +73,8 @@ export function calculateEffectiveRate(params: calculateEffectiveRate.ParamsType // truncation is size-dependent so this value is only valid for this exact sizeInBytes const storagePerEpoch = (storagePerTibPerMonth * sizeInBytes) / (SIZE_CONSTANTS.TiB * epochsPerMonth) - const ratePerMonth = storagePerMonth + provingServicePerMonth - const ratePerEpoch = storagePerEpoch + provingServicePerMonth / epochsPerMonth + const ratePerMonth = storagePerMonth + datasetFeePerMonth + const ratePerEpoch = storagePerEpoch + datasetFeePerMonth / epochsPerMonth return { ratePerEpoch, ratePerMonth } } diff --git a/packages/synapse-core/src/warm-storage/calculate-operation-fees.ts b/packages/synapse-core/src/warm-storage/calculate-upload-fees.ts similarity index 51% rename from packages/synapse-core/src/warm-storage/calculate-operation-fees.ts rename to packages/synapse-core/src/warm-storage/calculate-upload-fees.ts index 067a39455..ffd6e18da 100644 --- a/packages/synapse-core/src/warm-storage/calculate-operation-fees.ts +++ b/packages/synapse-core/src/warm-storage/calculate-upload-fees.ts @@ -1,10 +1,16 @@ +import { SIZE_CONSTANTS } from '../utils/constants.ts' import type { getPriceList } from './price-list.ts' -export namespace calculateOperationFees { +export namespace calculateUploadFees { export type ParamsType = { priceList: getPriceList.OutputType isNewDataSet: boolean pieceCount?: bigint + /** + * Number of addPieces operations the upload is split across. Defaults to + * `ceil(pieceCount / MAX_ADD_PIECES_BATCH_SIZE)`, since a single addPieces + * call cannot exceed the batch limit and pieces beyond it span more calls. + */ addPiecesOperationCount?: bigint } @@ -21,10 +27,20 @@ export namespace calculateOperationFees { * Scope is intentionally limited to upload-time fees: create-data-set (new * datasets only) and add-pieces. Schedule-removals, terminate, and delete are * post-upload lifecycle operations and are not part of an upload cost preview. + * + * When `addPiecesOperationCount` is omitted it is derived from `pieceCount` and + * the `MAX_ADD_PIECES_BATCH_SIZE` batch limit: a batch of `pieceCount` pieces + * is split into `ceil(pieceCount / MAX_ADD_PIECES_BATCH_SIZE)` addPieces calls, + * each charged the base fee. + * + * @param params - {@link calculateUploadFees.ParamsType} + * @returns {@link calculateUploadFees.OutputType} */ -export function calculateOperationFees(params: calculateOperationFees.ParamsType): calculateOperationFees.OutputType { +export function calculateUploadFees(params: calculateUploadFees.ParamsType): calculateUploadFees.OutputType { const pieceCount = params.pieceCount ?? 1n - const addPiecesOperationCount = params.addPiecesOperationCount ?? 1n + const maxBatch = BigInt(SIZE_CONSTANTS.MAX_ADD_PIECES_BATCH_SIZE) + const derivedOperationCount = (pieceCount + maxBatch - 1n) / maxBatch + const addPiecesOperationCount = params.addPiecesOperationCount ?? derivedOperationCount const createDataSetFee = params.isNewDataSet ? params.priceList.fees.createDataSetFee : 0n const addPiecesFee = params.priceList.fees.addPiecesBaseFee * addPiecesOperationCount + diff --git a/packages/synapse-core/src/warm-storage/get-upload-costs.ts b/packages/synapse-core/src/warm-storage/get-upload-costs.ts index fd715fb90..23f5e12aa 100644 --- a/packages/synapse-core/src/warm-storage/get-upload-costs.ts +++ b/packages/synapse-core/src/warm-storage/get-upload-costs.ts @@ -7,7 +7,7 @@ import { resolveAccountState } from '../pay/resolve-account-state.ts' import { DEFAULT_BUFFER_EPOCHS, DEFAULT_RUNWAY_EPOCHS, TIME_CONSTANTS } from '../utils/constants.ts' import { calculateDepositNeeded } from './calculate-deposit-needed.ts' import { calculateEffectiveRate } from './calculate-effective-rate.ts' -import type { calculateOperationFees } from './calculate-operation-fees.ts' +import type { calculateUploadFees } from './calculate-upload-fees.ts' import { getPriceList } from './price-list.ts' export namespace getUploadCosts { @@ -43,14 +43,7 @@ export namespace getUploadCosts { /** Rate per month — full precision for display. */ perMonth: bigint } - /** @deprecated Use rates. */ - rate: { - /** Rate per epoch — matches on-chain PDP rail rate. */ - perEpoch: bigint - /** Rate per month — full precision for display. */ - perMonth: bigint - } - fees: calculateOperationFees.OutputType + fees: calculateUploadFees.OutputType lockups: { lifecycleLockup: bigint streamingLockup: bigint @@ -102,7 +95,7 @@ export async function getUploadCosts( const rate = calculateEffectiveRate({ sizeInBytes: totalSize, storagePerTibPerMonth: priceList.rates.storagePerTibPerMonth, - provingServicePerMonth: priceList.rates.datasetFeePerMonth, + datasetFeePerMonth: priceList.rates.datasetFeePerMonth, epochsPerMonth: TIME_CONSTANTS.EPOCHS_PER_MONTH, }) @@ -142,7 +135,6 @@ export async function getUploadCosts( return { rates, - rate: rates, fees, lockups: { lifecycleLockup: lockup.lifecycleLockup, diff --git a/packages/synapse-core/src/warm-storage/index.ts b/packages/synapse-core/src/warm-storage/index.ts index f8d682604..fad7f1a31 100644 --- a/packages/synapse-core/src/warm-storage/index.ts +++ b/packages/synapse-core/src/warm-storage/index.ts @@ -13,7 +13,7 @@ export * from './add-approved-provider.ts' export * from './calculate-additional-lockup-required.ts' export * from './calculate-deposit-needed.ts' export * from './calculate-effective-rate.ts' -export * from './calculate-operation-fees.ts' +export * from './calculate-upload-fees.ts' export * from './fetch-provider-selection-input.ts' export * from './find-matching-data-sets.ts' export * from './get-account-total-storage-size.ts' diff --git a/packages/synapse-core/src/warm-storage/price-list.ts b/packages/synapse-core/src/warm-storage/price-list.ts index 4fbb5fa00..a220d5118 100644 --- a/packages/synapse-core/src/warm-storage/price-list.ts +++ b/packages/synapse-core/src/warm-storage/price-list.ts @@ -1,13 +1,29 @@ -import type { Address, Chain, Client, Transport } from 'viem' -import { getServicePrice } from './get-service-price.ts' +import type { Simplify } from 'type-fest' +import type { + Address, + Chain, + Client, + ContractFunctionParameters, + ContractFunctionReturnType, + ReadContractErrorType, + Transport, +} from 'viem' +import { readContract } from 'viem/actions' +import type { fwssView as fwssViewAbi } from '../abis/index.ts' +import { asChain } from '../chains.ts' +import type { ActionCallChain } from '../types.ts' export namespace getPriceList { - export type OptionsType = getServicePrice.OptionsType + export type OptionsType = { + /** Warm storage view contract address. Defaults to the chain's view contract. */ + contractAddress?: Address + } + + export type ContractOutputType = ContractFunctionReturnType /** * The canonical warm storage price list. Matches the on-chain `PriceList` - * struct from `FilecoinWarmStorageServiceStateView.getPriceList()` - * ([filecoin-services#501](https://github.com/FilOzone/filecoin-services/issues/501)). + * struct from `FilecoinWarmStorageServiceStateView.getPriceList()`. * Amounts are in the token's smallest unit; rates are per-month (divide by * `EPOCHS_PER_MONTH` for per-epoch values). */ @@ -35,54 +51,120 @@ export namespace getPriceList { cdnLockupPeriod: bigint } } -} - -/** - * FWSS operation fees and lockup defaults, sourced from `PriceListUSDFC.sol`. - * Storage, egress, and token come from the contract via {@link getServicePrice}. - */ -const expectedFees = { - createDataSetFee: 25_000_000_000_000_000n, - addPiecesBaseFee: 500_000_000_000_000n, - addPiecesPerPieceFee: 300_000_000_000_000n, - schedulePieceRemovalsFee: 2_000_000_000_000_000n, - terminateFee: 1_120_000_000_000_000n, -} as const -const expectedLockups = { - lifecycleReserveTarget: 100_000_000_000_000_000n, - replenishThreshold: 5_000_000_000_000_000n, - defaultLockupPeriod: 86_400n, // EPOCHS_PER_DAY * 30 - cdnLockupAmount: 700_000_000_000_000_000n, - cacheMissLockupAmount: 300_000_000_000_000_000n, - cdnLockupPeriod: 14_400n, // EPOCHS_PER_DAY * 5 -} as const - -const DATASET_FEE_PER_MONTH = 24_000_000_000_000_000n // $0.024 + export type ErrorType = asChain.ErrorType | ReadContractErrorType +} /** - * Read pricing through the canonical SDK shape. + * Read the full warm storage price list from the chain. * - * Storage and egress rates and the token come from the contract via - * {@link getServicePrice}. The proving (dataset) rate, operation fees, and - * lockup amounts come from {@link expectedFees} / {@link expectedLockups}. + * Calls `getPriceList()` on the `FilecoinWarmStorageServiceStateView` contract, + * which requires a deployment that includes + * [FilOzone/filecoin-services#501](https://github.com/FilOzone/filecoin-services/pull/501). + * + * @param client - The client to use to read the price list. + * @param options - {@link getPriceList.OptionsType} + * @returns The price list {@link getPriceList.OutputType} + * @throws Errors {@link getPriceList.ErrorType} + * + * @example + * ```ts + * import { getPriceList } from '@filoz/synapse-core/warm-storage' + * import { createPublicClient, http } from 'viem' + * import { calibration } from '@filoz/synapse-core/chains' + * + * const client = createPublicClient({ + * chain: calibration, + * transport: http(), + * }) + * + * const priceList = await getPriceList(client) + * + * console.log(priceList.rates.storagePerTibPerMonth) + * ``` */ export async function getPriceList( client: Client, options: getPriceList.OptionsType = {} ): Promise { - const servicePrice = await getServicePrice(client, options) + const list = await readContract( + client, + getPriceListCall({ + chain: client.chain, + contractAddress: options.contractAddress, + }) + ) + // Map into a fresh object so callers can't corrupt later reads and the shape + // is pinned to OutputType independent of the generated ABI tuple type. return { - token: servicePrice.tokenAddress, + token: list.token, rates: { - storagePerTibPerMonth: servicePrice.pricePerTiBPerMonthNoCDN, - datasetFeePerMonth: DATASET_FEE_PER_MONTH, - cdnEgressPerTib: servicePrice.pricePerTiBCdnEgress, - cacheMissEgressPerTib: servicePrice.pricePerTiBCacheMissEgress, + storagePerTibPerMonth: list.rates.storagePerTibPerMonth, + datasetFeePerMonth: list.rates.datasetFeePerMonth, + cdnEgressPerTib: list.rates.cdnEgressPerTib, + cacheMissEgressPerTib: list.rates.cacheMissEgressPerTib, + }, + fees: { + createDataSetFee: list.fees.createDataSetFee, + addPiecesBaseFee: list.fees.addPiecesBaseFee, + addPiecesPerPieceFee: list.fees.addPiecesPerPieceFee, + schedulePieceRemovalsFee: list.fees.schedulePieceRemovalsFee, + terminateFee: list.fees.terminateFee, + }, + lockups: { + lifecycleReserveTarget: list.lockups.lifecycleReserveTarget, + replenishThreshold: list.lockups.replenishThreshold, + defaultLockupPeriod: list.lockups.defaultLockupPeriod, + cdnLockupAmount: list.lockups.cdnLockupAmount, + cacheMissLockupAmount: list.lockups.cacheMissLockupAmount, + cdnLockupPeriod: list.lockups.cdnLockupPeriod, }, - // Spread so callers can't mutate the shared module-level constants. - fees: { ...expectedFees }, - lockups: { ...expectedLockups }, } } + +export namespace getPriceListCall { + export type OptionsType = Simplify + export type ErrorType = asChain.ErrorType + export type OutputType = ContractFunctionParameters +} + +/** + * Create a call to the getPriceList function + * + * This function is used to create a call to the getPriceList function for use with the multicall or readContract function. + * + * @param options - {@link getPriceListCall.OptionsType} + * @returns The call to the getPriceList function {@link getPriceListCall.OutputType} + * @throws Errors {@link getPriceListCall.ErrorType} + * + * @example + * ```ts + * import { getPriceListCall } from '@filoz/synapse-core/warm-storage' + * import { createPublicClient, http } from 'viem' + * import { multicall } from 'viem/actions' + * import { calibration } from '@filoz/synapse-core/chains' + * + * const client = createPublicClient({ + * chain: calibration, + * transport: http(), + * }) + * + * const results = await multicall(client, { + * contracts: [ + * getPriceListCall({ chain: calibration }), + * ], + * }) + * + * console.log(results[0]) + * ``` + */ +export function getPriceListCall(options: getPriceListCall.OptionsType) { + const chain = asChain(options.chain) + return { + abi: chain.contracts.fwssView.abi, + address: options.contractAddress ?? chain.contracts.fwssView.address, + functionName: 'getPriceList', + args: [], + } satisfies getPriceListCall.OutputType +} diff --git a/packages/synapse-core/test/calculate-additional-lockup-required.test.ts b/packages/synapse-core/test/calculate-additional-lockup-required.test.ts index 4fcd16a1e..fe949852e 100644 --- a/packages/synapse-core/test/calculate-additional-lockup-required.test.ts +++ b/packages/synapse-core/test/calculate-additional-lockup-required.test.ts @@ -48,7 +48,7 @@ describe('calculateAdditionalLockupRequired', () => { const expectedRatePerEpoch = calculateEffectiveRate({ sizeInBytes: 1000n, storagePerTibPerMonth: priceList.rates.storagePerTibPerMonth, - provingServicePerMonth: priceList.rates.datasetFeePerMonth, + datasetFeePerMonth: priceList.rates.datasetFeePerMonth, epochsPerMonth: 86400n, }).ratePerEpoch assert.equal(result.lifecycleLockup, priceList.lockups.lifecycleReserveTarget) @@ -92,7 +92,7 @@ describe('calculateAdditionalLockupRequired', () => { // delta for the added bytes is locked up. const rateParams = { storagePerTibPerMonth: priceList.rates.storagePerTibPerMonth, - provingServicePerMonth: priceList.rates.datasetFeePerMonth, + datasetFeePerMonth: priceList.rates.datasetFeePerMonth, epochsPerMonth: 86400n, } const expectedDelta = diff --git a/packages/synapse-core/test/calculate-effective-rate.test.ts b/packages/synapse-core/test/calculate-effective-rate.test.ts index b8862e857..cb43d5e27 100644 --- a/packages/synapse-core/test/calculate-effective-rate.test.ts +++ b/packages/synapse-core/test/calculate-effective-rate.test.ts @@ -5,7 +5,7 @@ import { calculateEffectiveRate } from '../src/warm-storage/calculate-effective- const TiB = 1n << 40n const storagePerTibPerMonth = 2_500_000_000_000_000_000n // 2.5 USDFC -const provingServicePerMonth = 24_000_000_000_000_000n // 0.024 USDFC +const datasetFeePerMonth = 24_000_000_000_000_000n // 0.024 USDFC const epochsPerMonth = 86400n describe('calculateEffectiveRate', () => { @@ -13,7 +13,7 @@ describe('calculateEffectiveRate', () => { const result = calculateEffectiveRate({ sizeInBytes: 0n, storagePerTibPerMonth, - provingServicePerMonth, + datasetFeePerMonth, epochsPerMonth, }) @@ -25,27 +25,27 @@ describe('calculateEffectiveRate', () => { const result = calculateEffectiveRate({ sizeInBytes: 1n, storagePerTibPerMonth, - provingServicePerMonth, + datasetFeePerMonth, epochsPerMonth, }) // Additive: even a 1-byte dataset pays a (tiny) storage rate on top of proving. const storagePerEpoch = (storagePerTibPerMonth * 1n) / (TiB * epochsPerMonth) const storagePerMonth = (storagePerTibPerMonth * 1n) / TiB - assert.equal(result.ratePerEpoch, storagePerEpoch + provingServicePerMonth / epochsPerMonth) - assert.equal(result.ratePerMonth, storagePerMonth + provingServicePerMonth) + assert.equal(result.ratePerEpoch, storagePerEpoch + datasetFeePerMonth / epochsPerMonth) + assert.equal(result.ratePerMonth, storagePerMonth + datasetFeePerMonth) }) it('large dataset pays storage plus proving service rate', () => { const result = calculateEffectiveRate({ sizeInBytes: TiB, storagePerTibPerMonth, - provingServicePerMonth, + datasetFeePerMonth, epochsPerMonth, }) - const expectedPerMonth = storagePerTibPerMonth + provingServicePerMonth - const expectedPerEpoch = storagePerTibPerMonth / epochsPerMonth + provingServicePerMonth / epochsPerMonth + const expectedPerMonth = storagePerTibPerMonth + datasetFeePerMonth + const expectedPerEpoch = storagePerTibPerMonth / epochsPerMonth + datasetFeePerMonth / epochsPerMonth assert.equal(result.ratePerMonth, expectedPerMonth) assert.equal(result.ratePerEpoch, expectedPerEpoch) @@ -58,7 +58,7 @@ describe('calculateEffectiveRate', () => { const result = calculateEffectiveRate({ sizeInBytes, storagePerTibPerMonth, - provingServicePerMonth, + datasetFeePerMonth, epochsPerMonth, }) diff --git a/packages/synapse-core/test/calculate-upload-fees.test.ts b/packages/synapse-core/test/calculate-upload-fees.test.ts new file mode 100644 index 000000000..bc2f78eed --- /dev/null +++ b/packages/synapse-core/test/calculate-upload-fees.test.ts @@ -0,0 +1,71 @@ +/* globals describe it */ + +import assert from 'assert' +import { SIZE_CONSTANTS } from '../src/utils/constants.ts' +import { calculateUploadFees } from '../src/warm-storage/calculate-upload-fees.ts' + +const priceList = { + token: '0x00000000000000000000000000000000000000aa' as const, + rates: { + storagePerTibPerMonth: 0n, + datasetFeePerMonth: 0n, + cdnEgressPerTib: 0n, + cacheMissEgressPerTib: 0n, + }, + fees: { + createDataSetFee: 100n, + addPiecesBaseFee: 10n, + addPiecesPerPieceFee: 1n, + schedulePieceRemovalsFee: 0n, + terminateFee: 0n, + }, + lockups: { + lifecycleReserveTarget: 0n, + replenishThreshold: 0n, + defaultLockupPeriod: 0n, + cdnLockupAmount: 0n, + cacheMissLockupAmount: 0n, + cdnLockupPeriod: 0n, + }, +} + +const maxBatch = BigInt(SIZE_CONSTANTS.MAX_ADD_PIECES_BATCH_SIZE) + +describe('calculateUploadFees', () => { + it('charges the create fee only for new datasets', () => { + const existing = calculateUploadFees({ priceList, isNewDataSet: false }) + const fresh = calculateUploadFees({ priceList, isNewDataSet: true }) + + assert.equal(existing.createDataSetFee, 0n) + assert.equal(fresh.createDataSetFee, priceList.fees.createDataSetFee) + }) + + it('derives addPieces operation count from the batch limit when not provided', () => { + // One full batch is a single addPieces operation. + const oneBatch = calculateUploadFees({ priceList, isNewDataSet: false, pieceCount: maxBatch }) + assert.equal( + oneBatch.addPiecesFee, + priceList.fees.addPiecesBaseFee + priceList.fees.addPiecesPerPieceFee * maxBatch + ) + + // One piece over the limit spills into a second operation. + const spill = calculateUploadFees({ priceList, isNewDataSet: false, pieceCount: maxBatch + 1n }) + assert.equal( + spill.addPiecesFee, + priceList.fees.addPiecesBaseFee * 2n + priceList.fees.addPiecesPerPieceFee * (maxBatch + 1n) + ) + }) + + it('uses an explicit addPiecesOperationCount over the derived value', () => { + const result = calculateUploadFees({ + priceList, + isNewDataSet: false, + pieceCount: maxBatch + 1n, + addPiecesOperationCount: 1n, + }) + assert.equal( + result.addPiecesFee, + priceList.fees.addPiecesBaseFee + priceList.fees.addPiecesPerPieceFee * (maxBatch + 1n) + ) + }) +}) diff --git a/packages/synapse-core/test/get-price-list.test.ts b/packages/synapse-core/test/get-price-list.test.ts index 8f201d20d..3f32b0677 100644 --- a/packages/synapse-core/test/get-price-list.test.ts +++ b/packages/synapse-core/test/get-price-list.test.ts @@ -22,44 +22,51 @@ describe('getPriceList', () => { const makeClient = () => createPublicClient({ chain: calibration, transport: http() }) - it('reads rates and token live from getServicePrice', async () => { - // Distinct values (not the defaults) prove these fields are plumbed from the - // contract read rather than hardcoded in the adapter. - const token = '0x00000000000000000000000000000000000000aa' - server.use( - JSONRPC({ - ...presets.basic, - warmStorage: { - ...presets.basic.warmStorage, - getServicePrice: () => [ - { - pricePerTiBPerMonthNoCDN: parseUnits('9.9', 18), - pricePerTiBCdnEgress: parseUnits('1.5', 18), - pricePerTiBCacheMissEgress: parseUnits('2.5', 18), - minimumPricePerMonth: parseUnits('6', 16), - tokenAddress: token, - epochsPerMonth: 86400n, - }, - ], - }, - }) - ) + // A full PriceList with distinct values per field so a misplumbed field is + // caught by an assertion rather than coinciding with another field's value. + const distinctPriceList = { + token: '0x00000000000000000000000000000000000000aa' as const, + rates: { + storagePerTibPerMonth: parseUnits('9.9', 18), + datasetFeePerMonth: parseUnits('0.123', 18), + cdnEgressPerTib: parseUnits('1.5', 18), + cacheMissEgressPerTib: parseUnits('2.5', 18), + }, + fees: { + createDataSetFee: parseUnits('0.011', 18), + addPiecesBaseFee: parseUnits('0.012', 18), + addPiecesPerPieceFee: parseUnits('0.013', 18), + schedulePieceRemovalsFee: parseUnits('0.014', 18), + terminateFee: parseUnits('0.015', 18), + }, + lockups: { + lifecycleReserveTarget: parseUnits('0.21', 18), + replenishThreshold: parseUnits('0.022', 18), + defaultLockupPeriod: 1234n, + cdnLockupAmount: parseUnits('0.23', 18), + cacheMissLockupAmount: parseUnits('0.24', 18), + cdnLockupPeriod: 5678n, + }, + } + + const withPriceList = (list: typeof distinctPriceList) => + JSONRPC({ + ...presets.basic, + warmStorageView: { + ...presets.basic.warmStorageView, + getPriceList: () => [list], + }, + }) + + it('plumbs every field from the on-chain getPriceList', async () => { + server.use(withPriceList(distinctPriceList)) const priceList = await getPriceList(makeClient()) - assert.equal(priceList.token.toLowerCase(), token) - assert.equal(priceList.rates.storagePerTibPerMonth, parseUnits('9.9', 18)) - assert.equal(priceList.rates.cdnEgressPerTib, parseUnits('1.5', 18)) - assert.equal(priceList.rates.cacheMissEgressPerTib, parseUnits('2.5', 18)) - }) - - it('supplies the dataset fee, which getServicePrice does not expose', async () => { - server.use(JSONRPC(presets.basic)) - - const priceList = await getPriceList(makeClient()) - - // The current ABI has no dataset fee field, so the adapter must inject it. - assert.equal(priceList.rates.datasetFeePerMonth, parseUnits('0.024', 18)) + assert.equal(priceList.token.toLowerCase(), distinctPriceList.token) + assert.deepEqual(priceList.rates, distinctPriceList.rates) + assert.deepEqual(priceList.fees, distinctPriceList.fees) + assert.deepEqual(priceList.lockups, distinctPriceList.lockups) }) it('returns the on-chain PriceList key shape', async () => { diff --git a/packages/synapse-core/test/get-upload-costs.test.ts b/packages/synapse-core/test/get-upload-costs.test.ts index 0372ee9d1..fc3b4ca2a 100644 --- a/packages/synapse-core/test/get-upload-costs.test.ts +++ b/packages/synapse-core/test/get-upload-costs.test.ts @@ -35,9 +35,8 @@ describe('getUploadCosts', () => { dataSize: 1n, }) - assert.equal(typeof result.rate.perEpoch, 'bigint') - assert.equal(typeof result.rate.perMonth, 'bigint') - assert.equal(result.rate, result.rates) + assert.equal(typeof result.rates.perEpoch, 'bigint') + assert.equal(typeof result.rates.perMonth, 'bigint') assert.equal(typeof result.fees.total, 'bigint') assert.equal(typeof result.lockups.total, 'bigint') assert.equal(typeof result.depositNeeded, 'bigint') @@ -141,7 +140,7 @@ describe('getUploadCosts', () => { // Additive: 1-byte dataset pays a tiny storage rate on top of proving. const storagePerMonth1Byte = parseUnits('2.5', 18) / (1n << 40n) - assert.equal(result.rate.perMonth, parseUnits('0.024', 18) + storagePerMonth1Byte) + assert.equal(result.rates.perMonth, parseUnits('0.024', 18) + storagePerMonth1Byte) assert.equal(result.fees.createDataSetFee, parseUnits('0.025', 18)) assert.equal(result.fees.addPiecesFee, parseUnits('0.0008', 18)) assert.equal(result.lockups.lifecycleLockup, parseUnits('0.10', 18)) @@ -171,7 +170,7 @@ describe('getUploadCosts', () => { // 1 TiB storage plus proving service rate. const pricePerTiBPerMonth = parseUnits('2.5', 18) - assert.equal(result.rate.perMonth, pricePerTiBPerMonth + parseUnits('0.024', 18)) + assert.equal(result.rates.perMonth, pricePerTiBPerMonth + parseUnits('0.024', 18)) }) it('should include debt in deposit for account in debt', async () => { @@ -347,10 +346,10 @@ describe('getUploadCosts', () => { }) // Existing dataset pays storage for 1 TiB plus one proving service rate. - assert.equal(existing.rate.perMonth, parseUnits('2.524', 18)) + assert.equal(existing.rates.perMonth, parseUnits('2.524', 18)) assert.ok( - existing.rate.perMonth > newDs.rate.perMonth, - `existing dataset rate (${existing.rate.perMonth}) should exceed new dataset rate (${newDs.rate.perMonth})` + existing.rates.perMonth > newDs.rates.perMonth, + `existing dataset rate (${existing.rates.perMonth}) should exceed new dataset rate (${newDs.rates.perMonth})` ) }) From 927ac87d47cc6eff4b12d41723141f38bcf7a414 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:51:13 -0400 Subject: [PATCH 05/16] refactor(storage): drop deprecated price APIs --- packages/synapse-sdk/src/filbeam/service.ts | 2 +- packages/synapse-sdk/src/storage/manager.ts | 7 +++---- .../calculate-multi-context-costs.test.ts | 19 +++++++++---------- packages/synapse-sdk/src/test/synapse.test.ts | 6 +++--- .../synapse-sdk/src/warm-storage/service.ts | 13 ------------- 5 files changed, 16 insertions(+), 31 deletions(-) diff --git a/packages/synapse-sdk/src/filbeam/service.ts b/packages/synapse-sdk/src/filbeam/service.ts index 9f195dcf3..b9c81d7d0 100644 --- a/packages/synapse-sdk/src/filbeam/service.ts +++ b/packages/synapse-sdk/src/filbeam/service.ts @@ -99,7 +99,7 @@ export class FilBeamService { * - **Cache Miss Egress Quota**: Remaining bytes that can be retrieved from storage providers (triggers caching) * * Both types of egress are billed based on volume. Query current pricing via - * {@link WarmStorageService.getServicePrice} or see https://docs.filbeam.com for rates. + * {@link WarmStorageService.getPriceList} or see https://docs.filbeam.com for rates. * * @param dataSetId - The unique identifier of the data set to query * @returns A promise that resolves to the data set statistics with remaining quotas as BigInt values diff --git a/packages/synapse-sdk/src/storage/manager.ts b/packages/synapse-sdk/src/storage/manager.ts index bba9b3151..818b1c5b9 100644 --- a/packages/synapse-sdk/src/storage/manager.ts +++ b/packages/synapse-sdk/src/storage/manager.ts @@ -35,8 +35,8 @@ import { calculateAdditionalLockupRequired, calculateBufferAmount, calculateEffectiveRate, - calculateOperationFees, calculateRunwayAmount, + calculateUploadFees, getUploadCosts as coreGetUploadCosts, getPriceList, metadataMatches, @@ -755,7 +755,7 @@ export class StorageManager { }) // Multi-context preview assumes one piece / one addPieces op per context; // batched multi-piece uploads should price via getUploadCosts with explicit counts. - const fees = calculateOperationFees({ + const fees = calculateUploadFees({ priceList, isNewDataSet, }) @@ -774,7 +774,7 @@ export class StorageManager { const rate = calculateEffectiveRate({ sizeInBytes: totalSize, storagePerTibPerMonth: priceList.rates.storagePerTibPerMonth, - provingServicePerMonth: priceList.rates.datasetFeePerMonth, + datasetFeePerMonth: priceList.rates.datasetFeePerMonth, epochsPerMonth: TIME_CONSTANTS.EPOCHS_PER_MONTH, }) totalRatePerEpoch += rate.ratePerEpoch @@ -829,7 +829,6 @@ export class StorageManager { return { rates, - rate: rates, fees: { createDataSetFee: totalCreateDataSetFee, addPiecesFee: totalAddPiecesFee, diff --git a/packages/synapse-sdk/src/test/calculate-multi-context-costs.test.ts b/packages/synapse-sdk/src/test/calculate-multi-context-costs.test.ts index a6010b554..71d7f606d 100644 --- a/packages/synapse-sdk/src/test/calculate-multi-context-costs.test.ts +++ b/packages/synapse-sdk/src/test/calculate-multi-context-costs.test.ts @@ -115,9 +115,8 @@ describe('calculateMultiContextCosts', () => { const ctx = makeContext(synapse, warmStorageService, {}) const result = await manager.calculateMultiContextCosts([ctx], { dataSize: 1n }) - assert.equal(typeof result.rate.perEpoch, 'bigint') - assert.equal(typeof result.rate.perMonth, 'bigint') - assert.equal(result.rate, result.rates) + assert.equal(typeof result.rates.perEpoch, 'bigint') + assert.equal(typeof result.rates.perMonth, 'bigint') assert.equal(typeof result.fees.total, 'bigint') assert.equal(typeof result.lockups.total, 'bigint') assert.equal(typeof result.depositNeeded, 'bigint') @@ -145,7 +144,7 @@ describe('calculateMultiContextCosts', () => { // Additive: 1-byte dataset pays a tiny storage rate on top of proving. const storagePerMonth1Byte = parseUnits('2.5', 18) / (1n << 40n) - assert.equal(result.rate.perMonth, parseUnits('0.024', 18) + storagePerMonth1Byte) + assert.equal(result.rates.perMonth, parseUnits('0.024', 18) + storagePerMonth1Byte) }) it('should aggregate rates across two new contexts', async () => { @@ -169,8 +168,8 @@ describe('calculateMultiContextCosts', () => { const double = await manager.calculateMultiContextCosts([ctxA, ctxB], { dataSize: 1n }) // Rates should be exactly 2x single context - assert.equal(double.rate.perEpoch, single.rate.perEpoch * 2n) - assert.equal(double.rate.perMonth, single.rate.perMonth * 2n) + assert.equal(double.rates.perEpoch, single.rates.perEpoch * 2n) + assert.equal(double.rates.perMonth, single.rates.perMonth * 2n) }) it('should fetch dataset size for existing contexts', async () => { @@ -204,8 +203,8 @@ describe('calculateMultiContextCosts', () => { // Existing 1 TiB + 1 TiB = 2 TiB rate, new 1 TiB = 1 TiB rate // pricePerTiBPerMonth = 2.5 USDFC const pricePerTiBPerMonth = parseUnits('2.5', 18) - assert.equal(resultNew.rate.perMonth, pricePerTiBPerMonth + parseUnits('0.024', 18)) - assert.equal(resultExisting.rate.perMonth, pricePerTiBPerMonth * 2n + parseUnits('0.024', 18)) + assert.equal(resultNew.rates.perMonth, pricePerTiBPerMonth + parseUnits('0.024', 18)) + assert.equal(resultExisting.rates.perMonth, pricePerTiBPerMonth * 2n + parseUnits('0.024', 18)) }) it('should handle mixed new + existing contexts', async () => { @@ -241,7 +240,7 @@ describe('calculateMultiContextCosts', () => { // Combined rate: storage rates plus one proving service rate per context. const pricePerTiBPerMonth = parseUnits('2.5', 18) - assert.equal(result.rate.perMonth, pricePerTiBPerMonth * 3n + parseUnits('0.024', 18) * 2n) + assert.equal(result.rates.perMonth, pricePerTiBPerMonth * 3n + parseUnits('0.024', 18) * 2n) }) it('should include debt in deposit for account in debt', async () => { @@ -492,6 +491,6 @@ describe('calculateMultiContextCosts', () => { const ctx = makeContext(synapse, warmStorageService, {}) const result = await manager.calculateMultiContextCosts([ctx], { dataSize: oneTiB }) - assert.equal(result.rate.perMonth, pricePerTiBPerMonth + parseUnits('0.024', 18)) + assert.equal(result.rates.perMonth, pricePerTiBPerMonth + parseUnits('0.024', 18)) }) }) diff --git a/packages/synapse-sdk/src/test/synapse.test.ts b/packages/synapse-sdk/src/test/synapse.test.ts index b5788c271..b2b2ecf8c 100644 --- a/packages/synapse-sdk/src/test/synapse.test.ts +++ b/packages/synapse-sdk/src/test/synapse.test.ts @@ -385,9 +385,9 @@ describe('Synapse', () => { server.use( Mocks.JSONRPC({ ...Mocks.presets.basic, - warmStorage: { - ...Mocks.presets.basic.warmStorage, - getServicePrice: () => { + warmStorageView: { + ...Mocks.presets.basic.warmStorageView, + getPriceList: () => { throw new Error('RPC error') }, }, diff --git a/packages/synapse-sdk/src/warm-storage/service.ts b/packages/synapse-sdk/src/warm-storage/service.ts index b2f806416..381a44b0a 100644 --- a/packages/synapse-sdk/src/warm-storage/service.ts +++ b/packages/synapse-sdk/src/warm-storage/service.ts @@ -33,7 +33,6 @@ import { getClientDataSetsLength, getDataSet, getPriceList, - getServicePrice, removeApprovedProvider, terminateService, } from '@filoz/synapse-core/warm-storage' @@ -365,18 +364,6 @@ export class WarmStorageService { return getPriceList(this._client) } - /** - * Get the current service price from the current FWSS ABI. - * - * @deprecated Use {@link WarmStorageService.getPriceList} for the latest - * pricing. This returns only the legacy `minimumPricePerMonth` floor plus - * storage/egress rates and does not reflect the per-operation fee model. - * @returns Service price information from the current FWSS ABI. - */ - async getServicePrice(): Promise { - return getServicePrice(this._client) - } - // ========== Data Set Operations ========== /** From fd643a45c50a893245115dd643305c2c2502c4ac Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:51:26 -0400 Subject: [PATCH 06/16] refactor(react): remove useServicePrice hook --- .../synapse-react/src/warm-storage/index.ts | 1 - .../src/warm-storage/use-service-price.ts | 37 ------------------- 2 files changed, 38 deletions(-) delete mode 100644 packages/synapse-react/src/warm-storage/use-service-price.ts diff --git a/packages/synapse-react/src/warm-storage/index.ts b/packages/synapse-react/src/warm-storage/index.ts index 1436214a2..a9cadf42b 100644 --- a/packages/synapse-react/src/warm-storage/index.ts +++ b/packages/synapse-react/src/warm-storage/index.ts @@ -3,6 +3,5 @@ export * from './use-data-sets.ts' export * from './use-delete-piece.ts' export * from './use-price-list.ts' export * from './use-providers.ts' -export * from './use-service-price.ts' export * from './use-upload.ts' export * from './use-upload-simple.ts' diff --git a/packages/synapse-react/src/warm-storage/use-service-price.ts b/packages/synapse-react/src/warm-storage/use-service-price.ts deleted file mode 100644 index 358da506c..000000000 --- a/packages/synapse-react/src/warm-storage/use-service-price.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { getServicePrice } from '@filoz/synapse-core/warm-storage' -import { type UseQueryOptions, useQuery } from '@tanstack/react-query' -import { useConfig } from 'wagmi' - -/** - * The result for the useServicePrice hook. - */ -export type UseServicePriceResult = getServicePrice.OutputType - -/** - * The props for the useServicePrice hook. - */ -export interface UseServicePriceProps { - query?: Omit, 'queryKey' | 'queryFn'> -} - -/** - * Get the service price for the warm storage. - * - * @deprecated Use {@link usePriceList} for the latest pricing. This returns only - * the legacy `minimumPricePerMonth` floor plus storage/egress rates and does not - * reflect the per-operation fee model. - * @param props - The props to use. - * @returns The service price. - */ -export function useServicePrice(props?: UseServicePriceProps) { - const config = useConfig() - - return useQuery({ - ...props?.query, - queryKey: ['synapse-warm-storage-get-service-price', config.getClient().chain.id], - queryFn: async () => { - const result = await getServicePrice(config.getClient()) - return result - }, - }) -} From 5f958b87db34b0cf620e7999acd7362bf1edc39d Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:25:06 -0400 Subject: [PATCH 07/16] refactor(costs): remove getServicePrice helper --- .../synapse-core/src/mocks/jsonrpc/index.ts | 10 -- .../src/mocks/jsonrpc/warm-storage.ts | 12 -- .../src/warm-storage/get-service-price.ts | 127 ------------------ .../synapse-core/src/warm-storage/index.ts | 1 - .../test/get-service-price.test.ts | 89 ------------ 5 files changed, 239 deletions(-) delete mode 100644 packages/synapse-core/src/warm-storage/get-service-price.ts delete mode 100644 packages/synapse-core/test/get-service-price.test.ts diff --git a/packages/synapse-core/src/mocks/jsonrpc/index.ts b/packages/synapse-core/src/mocks/jsonrpc/index.ts index 25021d488..0f8380036 100644 --- a/packages/synapse-core/src/mocks/jsonrpc/index.ts +++ b/packages/synapse-core/src/mocks/jsonrpc/index.ts @@ -401,16 +401,6 @@ export const presets = { viewContractAddress: () => [ADDRESSES.calibration.viewContract], serviceProviderRegistry: () => [ADDRESSES.calibration.spRegistry], sessionKeyRegistry: () => [ADDRESSES.calibration.sessionKeyRegistry], - getServicePrice: () => [ - { - pricePerTiBPerMonthNoCDN: parseUnits('2.5', 18), - pricePerTiBCdnEgress: parseUnits('7', 18), - pricePerTiBCacheMissEgress: parseUnits('7', 18), - minimumPricePerMonth: parseUnits('6', 16), - tokenAddress: ADDRESSES.calibration.usdfcToken, - epochsPerMonth: TIME_CONSTANTS.EPOCHS_PER_MONTH, - }, - ], owner: () => [ADDRESSES.client1], terminateService: () => [], topUpCDNPaymentRails: () => [], diff --git a/packages/synapse-core/src/mocks/jsonrpc/warm-storage.ts b/packages/synapse-core/src/mocks/jsonrpc/warm-storage.ts index d3727a4cd..a8634b632 100644 --- a/packages/synapse-core/src/mocks/jsonrpc/warm-storage.ts +++ b/packages/synapse-core/src/mocks/jsonrpc/warm-storage.ts @@ -57,7 +57,6 @@ type filBeamBeneficiaryAddress = ExtractAbiFunction type serviceProviderRegistry = ExtractAbiFunction type sessionKeyRegistry = ExtractAbiFunction -type getServicePrice = ExtractAbiFunction type owner = ExtractAbiFunction type terminateService = ExtractAbiFunction type topUpCDNPaymentRails = ExtractAbiFunction @@ -80,7 +79,6 @@ export interface WarmStorageOptions { args: AbiToType ) => AbiToType sessionKeyRegistry?: (args: AbiToType) => AbiToType - getServicePrice?: (args: AbiToType) => AbiToType owner?: (args: AbiToType) => AbiToType terminateService?: (args: AbiToType) => AbiToType topUpCDNPaymentRails?: (args: AbiToType) => AbiToType @@ -183,16 +181,6 @@ export function warmStorageCallHandler(data: Hex, options: JSONRPCOptions): Hex ) } - case 'getServicePrice': { - if (!options.warmStorage?.getServicePrice) { - throw new Error('Warm Storage: getServicePrice is not defined') - } - return encodeAbiParameters( - Abis.fwss.find((abi) => abi.type === 'function' && abi.name === 'getServicePrice')!.outputs, - options.warmStorage.getServicePrice(args) - ) - } - case 'owner': { if (!options.warmStorage?.owner) { throw new Error('Warm Storage: owner is not defined') diff --git a/packages/synapse-core/src/warm-storage/get-service-price.ts b/packages/synapse-core/src/warm-storage/get-service-price.ts deleted file mode 100644 index 7872d5e4a..000000000 --- a/packages/synapse-core/src/warm-storage/get-service-price.ts +++ /dev/null @@ -1,127 +0,0 @@ -import type { Simplify } from 'type-fest' -import type { - Address, - Chain, - Client, - ContractFunctionParameters, - ContractFunctionReturnType, - ReadContractErrorType, - Transport, -} from 'viem' -import { readContract } from 'viem/actions' -import type { fwss as storageAbi } from '../abis/index.ts' -import { asChain } from '../chains.ts' -import type { ActionCallChain } from '../types.ts' - -export namespace getServicePrice { - export type OptionsType = { - /** Warm storage contract address. If not provided, the default is the storage contract address for the chain. */ - contractAddress?: Address - } - - export type ContractOutputType = ContractFunctionReturnType - - /** - * The service price for the warm storage. - */ - export type OutputType = { - /** Price per TiB per month without CDN (in base units) */ - pricePerTiBPerMonthNoCDN: bigint - /** CDN egress price per TiB (usage-based, in base units) */ - pricePerTiBCdnEgress: bigint - /** Cache miss egress price per TiB (usage-based, in base units) */ - pricePerTiBCacheMissEgress: bigint - /** Token address for payments */ - tokenAddress: Address - /** Number of epochs per month */ - epochsPerMonth: bigint - /** Minimum monthly charge for any dataset size (in base units) */ - minimumPricePerMonth: bigint - } - - export type ErrorType = asChain.ErrorType | ReadContractErrorType -} - -/** - * Get the service price for the warm storage - * - * @param client - The client to use to get the service price. - * @param options - {@link getServicePrice.OptionsType} - * @returns The service price {@link getServicePrice.OutputType} - * @throws Errors {@link getServicePrice.ErrorType} - * - * @example - * ```ts - * import { getServicePrice } from '@filoz/synapse-core/warm-storage' - * import { createPublicClient, http } from 'viem' - * import { calibration } from '@filoz/synapse-core/chains' - * - * const client = createPublicClient({ - * chain: calibration, - * transport: http(), - * }) - * - * const price = await getServicePrice(client, {}) - * - * console.log(price.pricePerTiBPerMonthNoCDN) - * ``` - */ -export async function getServicePrice( - client: Client, - options: getServicePrice.OptionsType = {} -): Promise { - const data = await readContract( - client, - getServicePriceCall({ - chain: client.chain, - contractAddress: options.contractAddress, - }) - ) - return data -} - -export namespace getServicePriceCall { - export type OptionsType = Simplify - export type ErrorType = asChain.ErrorType - export type OutputType = ContractFunctionParameters -} - -/** - * Create a call to the getServicePrice function - * - * This function is used to create a call to the getServicePrice function for use with the multicall or readContract function. - * - * @param options - {@link getServicePriceCall.OptionsType} - * @returns The call to the getServicePrice function {@link getServicePriceCall.OutputType} - * @throws Errors {@link getServicePriceCall.ErrorType} - * - * @example - * ```ts - * import { getServicePriceCall } from '@filoz/synapse-core/warm-storage' - * import { createPublicClient, http } from 'viem' - * import { multicall } from 'viem/actions' - * import { calibration } from '@filoz/synapse-core/chains' - * - * const client = createPublicClient({ - * chain: calibration, - * transport: http(), - * }) - * - * const results = await multicall(client, { - * contracts: [ - * getServicePriceCall({ chain: calibration }), - * ], - * }) - * - * console.log(results[0]) - * ``` - */ -export function getServicePriceCall(options: getServicePriceCall.OptionsType) { - const chain = asChain(options.chain) - return { - abi: chain.contracts.fwss.abi, - address: options.contractAddress ?? chain.contracts.fwss.address, - functionName: 'getServicePrice', - args: [], - } satisfies getServicePriceCall.OutputType -} diff --git a/packages/synapse-core/src/warm-storage/index.ts b/packages/synapse-core/src/warm-storage/index.ts index fad7f1a31..dc29b5bf6 100644 --- a/packages/synapse-core/src/warm-storage/index.ts +++ b/packages/synapse-core/src/warm-storage/index.ts @@ -26,7 +26,6 @@ export * from './get-client-data-sets-length.ts' export * from './get-data-set.ts' export * from './get-pdp-data-set.ts' export * from './get-pdp-data-sets.ts' -export * from './get-service-price.ts' export * from './get-upload-costs.ts' export * from './location-types.ts' export * from './price-list.ts' diff --git a/packages/synapse-core/test/get-service-price.test.ts b/packages/synapse-core/test/get-service-price.test.ts deleted file mode 100644 index 1cf11d9a2..000000000 --- a/packages/synapse-core/test/get-service-price.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import assert from 'assert' -import { setup } from 'iso-web/msw' -import { createPublicClient, http } from 'viem' -import { calibration, mainnet } from '../src/chains.ts' -import { JSONRPC, presets } from '../src/mocks/jsonrpc/index.ts' -import { getServicePrice, getServicePriceCall } from '../src/warm-storage/get-service-price.ts' - -describe('getServicePrice', () => { - const server = setup() - - before(async () => { - await server.start() - }) - - after(() => { - server.stop() - }) - - beforeEach(() => { - server.resetHandlers() - }) - - describe('getServicePriceCall', () => { - it('should create call with calibration chain defaults', () => { - const call = getServicePriceCall({ - chain: calibration, - }) - - assert.equal(call.functionName, 'getServicePrice') - assert.deepEqual(call.args, []) - assert.equal(call.address, calibration.contracts.fwss.address) - assert.equal(call.abi, calibration.contracts.fwss.abi) - }) - - it('should create call with mainnet chain defaults', () => { - const call = getServicePriceCall({ - chain: mainnet, - }) - - assert.equal(call.functionName, 'getServicePrice') - assert.deepEqual(call.args, []) - assert.equal(call.address, mainnet.contracts.fwss.address) - assert.equal(call.abi, mainnet.contracts.fwss.abi) - }) - - it('should use custom address when provided', () => { - const customAddress = '0x1234567890123456789012345678901234567890' - const call = getServicePriceCall({ - chain: calibration, - contractAddress: customAddress, - }) - - assert.equal(call.address, customAddress) - }) - }) - - describe('getServicePrice (with mocked RPC)', () => { - it('should fetch service price', async () => { - server.use(JSONRPC(presets.basic)) - - const client = createPublicClient({ - chain: calibration, - transport: http(), - }) - - const price = await getServicePrice(client) - - assert.equal(typeof price.pricePerTiBPerMonthNoCDN, 'bigint') - assert.equal(typeof price.pricePerTiBCdnEgress, 'bigint') - assert.equal(typeof price.pricePerTiBCacheMissEgress, 'bigint') - assert.equal(typeof price.minimumPricePerMonth, 'bigint') - assert.equal(typeof price.tokenAddress, 'string') - assert.equal(typeof price.epochsPerMonth, 'bigint') - }) - - it('should fetch service price with empty options', async () => { - server.use(JSONRPC(presets.basic)) - - const client = createPublicClient({ - chain: calibration, - transport: http(), - }) - - const price = await getServicePrice(client, {}) - - assert.ok(price.pricePerTiBPerMonthNoCDN > 0n) - }) - }) -}) From 0bcda11f2d5be4f6c49a4d454b3707b7b1377859 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:04:20 -0400 Subject: [PATCH 08/16] fix(costs): derive addPieces op count in upload costs --- .../src/warm-storage/get-upload-costs.ts | 5 +++-- .../synapse-core/test/get-upload-costs.test.ts | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/synapse-core/src/warm-storage/get-upload-costs.ts b/packages/synapse-core/src/warm-storage/get-upload-costs.ts index 23f5e12aa..071cb6e73 100644 --- a/packages/synapse-core/src/warm-storage/get-upload-costs.ts +++ b/packages/synapse-core/src/warm-storage/get-upload-costs.ts @@ -26,7 +26,7 @@ export namespace getUploadCosts { dataSize: bigint /** Number of pieces added by this operation. Default: 1 */ pieceCount?: bigint - /** Number of addPieces operations. Default: 1 */ + /** Number of addPieces operations. Defaults to `ceil(pieceCount / MAX_ADD_PIECES_BATCH_SIZE)`. */ addPiecesOperationCount?: bigint /** Extra runway in epochs beyond the required lockup. */ @@ -78,7 +78,8 @@ export async function getUploadCosts( const withCDN = options.withCDN ?? false const currentDataSetSize = options.currentDataSetSize ?? 0n const pieceCount = options.pieceCount ?? 1n - const addPiecesOperationCount = options.addPiecesOperationCount ?? 1n + // Left undefined so calculateUploadFees derives it from pieceCount and the batch limit. + const addPiecesOperationCount = options.addPiecesOperationCount const extraRunwayEpochs = options.extraRunwayEpochs ?? DEFAULT_RUNWAY_EPOCHS const bufferEpochs = options.bufferEpochs ?? DEFAULT_BUFFER_EPOCHS diff --git a/packages/synapse-core/test/get-upload-costs.test.ts b/packages/synapse-core/test/get-upload-costs.test.ts index fc3b4ca2a..517037799 100644 --- a/packages/synapse-core/test/get-upload-costs.test.ts +++ b/packages/synapse-core/test/get-upload-costs.test.ts @@ -419,4 +419,20 @@ describe('getUploadCosts', () => { assert.ok(result.fees.total > 0n) assert.equal(result.depositNeeded, result.lockups.total + result.fees.total) }) + + it('derives an extra addPieces operation fee when pieceCount exceeds the batch limit', async () => { + server.use(JSONRPC(presets.basic)) + + const client = createPublicClient({ chain: calibration, transport: http() }) + + const within = await getUploadCosts(client, { clientAddress: ADDRESSES.client1, dataSize: 1n, pieceCount: 40n }) + const spill = await getUploadCosts(client, { clientAddress: ADDRESSES.client1, dataSize: 1n, pieceCount: 41n }) + + // 41 pieces span two addPieces ops (ceil(41/40) = 2), so the 41-piece cost + // adds exactly one extra base fee plus one extra per-piece fee over 40. + assert.equal( + spill.fees.addPiecesFee - within.fees.addPiecesFee, + parseUnits('0.0005', 18) + parseUnits('0.0003', 18) + ) + }) }) From 94098bd6c0b52f0debfeae7b47100cacc5b13ba1 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:04:20 -0400 Subject: [PATCH 09/16] refactor(pay): read approval lockup period from chain --- packages/synapse-core/src/pay/fund.ts | 15 +++++-- .../src/pay/is-fwss-max-approved.ts | 19 +++++--- packages/synapse-core/src/pay/payments.ts | 8 ++-- .../src/pay/set-operator-approval.ts | 32 +++++++++++--- packages/synapse-core/src/utils/constants.ts | 10 ----- packages/synapse-core/test/fund.test.ts | 11 +++-- .../test/is-fwss-max-approved.test.ts | 44 ++++++++++++++++--- .../test/set-operator-approval.test.ts | 42 +++++++++++------- packages/synapse-sdk/src/payments/service.ts | 6 +-- 9 files changed, 129 insertions(+), 58 deletions(-) diff --git a/packages/synapse-core/src/pay/fund.ts b/packages/synapse-core/src/pay/fund.ts index 33f49227a..98662056d 100644 --- a/packages/synapse-core/src/pay/fund.ts +++ b/packages/synapse-core/src/pay/fund.ts @@ -11,7 +11,7 @@ import type { import { maxUint256 } from 'viem' import { waitForTransactionReceipt } from 'viem/actions' import type { ActionSyncCallback } from '../types.ts' -import { LOCKUP_PERIOD } from '../utils/constants.ts' +import { getPriceList } from '../warm-storage/price-list.ts' import { depositWithPermit } from './deposit-with-permit.ts' import { isFwssMaxApproved } from './is-fwss-max-approved.ts' import { depositAndApprove } from './payments.ts' @@ -70,8 +70,15 @@ export namespace fund { * ``` */ export async function fund(client: Client, options: fund.OptionsType): Promise { + // Resolve the approval lockup period once from the chain and reuse it for both + // the readiness check and the approval call, so the path needs a single read. + const maxLockupPeriod = (await getPriceList(client)).lockups.defaultLockupPeriod const needsApproval = - options.needsFwssMaxApproval ?? !(await isFwssMaxApproved(client, { clientAddress: client.account.address })) + options.needsFwssMaxApproval ?? + !(await isFwssMaxApproved(client, { + clientAddress: client.account.address, + requiredMaxLockupPeriod: maxLockupPeriod, + })) if (needsApproval) { if (options.amount > 0n) { @@ -79,14 +86,14 @@ export async function fund(client: Client, options: f amount: options.amount, rateAllowance: maxUint256, lockupAllowance: maxUint256, - maxLockupPeriod: LOCKUP_PERIOD, + maxLockupPeriod, }) } else { return setOperatorApproval(client, { approve: true, rateAllowance: maxUint256, lockupAllowance: maxUint256, - maxLockupPeriod: LOCKUP_PERIOD, + maxLockupPeriod, }) } } else if (options.amount > 0n) { diff --git a/packages/synapse-core/src/pay/is-fwss-max-approved.ts b/packages/synapse-core/src/pay/is-fwss-max-approved.ts index 924ae78eb..bad3c6655 100644 --- a/packages/synapse-core/src/pay/is-fwss-max-approved.ts +++ b/packages/synapse-core/src/pay/is-fwss-max-approved.ts @@ -1,21 +1,27 @@ import type { Address, Chain, Client, ReadContractErrorType, Transport } from 'viem' import { maxUint256 } from 'viem' import type { asChain } from '../chains.ts' -import { LOCKUP_PERIOD } from '../utils/constants.ts' +import { getPriceList } from '../warm-storage/price-list.ts' import { operatorApprovals } from './operator-approvals.ts' export namespace isFwssMaxApproved { export type OptionsType = { /** The address of the client to check approval for. */ clientAddress: Address + /** + * The lockup period the approval must cover. Defaults to the chain's + * `getPriceList().lockups.defaultLockupPeriod`. Callers that already hold + * the price list can pass it to skip the extra read. + */ + requiredMaxLockupPeriod?: bigint } - export type ErrorType = asChain.ErrorType | ReadContractErrorType + export type ErrorType = asChain.ErrorType | ReadContractErrorType | getPriceList.ErrorType } /** - * Check whether FWSS is approved with sufficient rate/lockup allowances - * and at least LOCKUP_PERIOD (30 days) for maxLockupPeriod. + * Check whether FWSS is approved with sufficient rate/lockup allowances and a + * `maxLockupPeriod` covering the chain's default lockup period. * * rateAllowance is checked for exact maxUint256 since the contract never * decrements it — it only tracks usage separately via rateUsage. @@ -37,6 +43,9 @@ export async function isFwssMaxApproved( client: Client, options: isFwssMaxApproved.OptionsType ): Promise { + const requiredMaxLockupPeriod = + options.requiredMaxLockupPeriod ?? (await getPriceList(client)).lockups.defaultLockupPeriod + const approval = await operatorApprovals(client, { address: options.clientAddress, }) @@ -45,6 +54,6 @@ export async function isFwssMaxApproved( approval.isApproved && approval.rateAllowance === maxUint256 && approval.lockupAllowance >= maxUint256 / 2n && - approval.maxLockupPeriod >= LOCKUP_PERIOD + approval.maxLockupPeriod >= requiredMaxLockupPeriod ) } diff --git a/packages/synapse-core/src/pay/payments.ts b/packages/synapse-core/src/pay/payments.ts index a298d40c0..f147df898 100644 --- a/packages/synapse-core/src/pay/payments.ts +++ b/packages/synapse-core/src/pay/payments.ts @@ -19,7 +19,8 @@ import * as erc20 from '../erc20/index.ts' import { ValidationError } from '../errors/base.ts' import { DepositAmountError, InsufficientBalanceError } from '../errors/pay.ts' import { signErc20Permit } from '../typed-data/sign-erc20-permit.ts' -import { LOCKUP_PERIOD, TIME_CONSTANTS } from '../utils/constants.ts' +import { TIME_CONSTANTS } from '../utils/constants.ts' +import { getPriceList } from '../warm-storage/price-list.ts' export type DepositAndApproveOptions = { /** @@ -55,7 +56,8 @@ export type DepositAndApproveOptions = { */ lockupAllowance?: bigint /** - * The max lockup period to approve. If not provided, the LOCKUP_PERIOD will be used. + * The max lockup period to approve. If not provided, the chain's + * `getPriceList().lockups.defaultLockupPeriod` will be used. */ maxLockupPeriod?: bigint } @@ -91,7 +93,7 @@ export async function depositAndApprove(client: Client, options: setOperatorApproval.OptionsType ): Promise { + // maxLockupPeriod has no hardcoded default; resolve it from the chain price + // list when approving so the call builder (which is synchronous) gets a value. + const maxLockupPeriod = + options.maxLockupPeriod ?? (options.approve ? (await getPriceList(client)).lockups.defaultLockupPeriod : 0n) + const { request } = await simulateContract( client, setOperatorApprovalCall({ @@ -94,7 +103,7 @@ export async function setOperatorApproval( approve: options.approve, rateAllowance: options.rateAllowance, lockupAllowance: options.lockupAllowance, - maxLockupPeriod: options.maxLockupPeriod, + maxLockupPeriod, contractAddress: options.contractAddress, }) ) @@ -222,10 +231,19 @@ export function setOperatorApprovalCall( } } + // maxLockupPeriod has no hardcoded default. It must come from the chain price + // list (getPriceList().lockups.defaultLockupPeriod), which this synchronous + // builder cannot read, so approving callers must resolve and pass it. + if (options.approve && options.maxLockupPeriod === undefined) { + throw new ValidationError( + 'maxLockupPeriod is required when approving; resolve it from getPriceList().lockups.defaultLockupPeriod' + ) + } + // Defaults based on approve flag const rateAllowance = options.rateAllowance ?? (options.approve ? maxUint256 : 0n) const lockupAllowance = options.lockupAllowance ?? (options.approve ? maxUint256 : 0n) - const maxLockupPeriod = options.maxLockupPeriod ?? (options.approve ? LOCKUP_PERIOD : 0n) + const maxLockupPeriod = options.maxLockupPeriod ?? 0n if (rateAllowance < 0n || lockupAllowance < 0n || maxLockupPeriod < 0n) { throw new ValidationError('Allowance or lockup period values cannot be negative') diff --git a/packages/synapse-core/src/utils/constants.ts b/packages/synapse-core/src/utils/constants.ts index 693150e59..42f4a2887 100644 --- a/packages/synapse-core/src/utils/constants.ts +++ b/packages/synapse-core/src/utils/constants.ts @@ -106,16 +106,6 @@ export const SIZE_CONSTANTS = { BYTES_PER_LEAF: 32n, } as const -/** - * Operator-approval allowance ceiling, in epochs. - * - * This is the `maxLockupPeriod` granted to FWSS when approving it as an - * operator, not a pricing input. The pricing lockup period is read from the - * chain via `getPriceList().lockups.defaultLockupPeriod`; granting a fixed, - * larger approval window stays valid even if the chain lockup period shrinks. - */ -export const LOCKUP_PERIOD = TIME_CONSTANTS.DEFAULT_LOCKUP_DAYS * TIME_CONSTANTS.EPOCHS_PER_DAY - /** * Default safety margin in epochs when calculating deposit amounts. * Accounts for epoch drift between balance check and on-chain execution. diff --git a/packages/synapse-core/test/fund.test.ts b/packages/synapse-core/test/fund.test.ts index c2773cd40..ae63abbd2 100644 --- a/packages/synapse-core/test/fund.test.ts +++ b/packages/synapse-core/test/fund.test.ts @@ -5,7 +5,10 @@ import { privateKeyToAccount } from 'viem/accounts' import { calibration } from '../src/chains.ts' import { JSONRPC, PRIVATE_KEYS, presets } from '../src/mocks/jsonrpc/index.ts' import { fund } from '../src/pay/fund.ts' -import { LOCKUP_PERIOD } from '../src/utils/constants.ts' +import { TIME_CONSTANTS } from '../src/utils/constants.ts' + +// Matches the lockup period returned by the basic preset's getPriceList mock. +const CHAIN_LOCKUP_PERIOD = TIME_CONSTANTS.DEFAULT_LOCKUP_DAYS * TIME_CONSTANTS.EPOCHS_PER_DAY describe('fund', () => { const server = setup() @@ -107,7 +110,7 @@ describe('fund', () => { }, payments: { ...presets.basic.payments, - operatorApprovals: () => [true, maxUint256, maxUint256, 0n, 0n, LOCKUP_PERIOD], + operatorApprovals: () => [true, maxUint256, maxUint256, 0n, 0n, CHAIN_LOCKUP_PERIOD], depositWithPermit: () => { depositWithPermitCalled = true return [] @@ -136,7 +139,7 @@ describe('fund', () => { ...presets.basic, payments: { ...presets.basic.payments, - operatorApprovals: () => [true, maxUint256, maxUint256, 0n, 0n, LOCKUP_PERIOD], + operatorApprovals: () => [true, maxUint256, maxUint256, 0n, 0n, CHAIN_LOCKUP_PERIOD], }, }) ) @@ -167,7 +170,7 @@ describe('fund', () => { payments: { ...presets.basic.payments, // Even though operatorApprovals says approved, the override forces approval flow - operatorApprovals: () => [true, maxUint256, maxUint256, 0n, 0n, LOCKUP_PERIOD], + operatorApprovals: () => [true, maxUint256, maxUint256, 0n, 0n, CHAIN_LOCKUP_PERIOD], depositWithPermitAndApproveOperator: () => { depositAndApproveCalled = true return [] diff --git a/packages/synapse-core/test/is-fwss-max-approved.test.ts b/packages/synapse-core/test/is-fwss-max-approved.test.ts index 29290d0fe..8eebb4a0b 100644 --- a/packages/synapse-core/test/is-fwss-max-approved.test.ts +++ b/packages/synapse-core/test/is-fwss-max-approved.test.ts @@ -6,7 +6,10 @@ import { createPublicClient, http, maxUint256 } from 'viem' import { calibration } from '../src/chains.ts' import { ADDRESSES, JSONRPC, presets } from '../src/mocks/jsonrpc/index.ts' import { isFwssMaxApproved } from '../src/pay/is-fwss-max-approved.ts' -import { LOCKUP_PERIOD } from '../src/utils/constants.ts' +import { TIME_CONSTANTS } from '../src/utils/constants.ts' + +// Matches the lockup period returned by the basic preset's getPriceList mock. +const CHAIN_LOCKUP_PERIOD = TIME_CONSTANTS.DEFAULT_LOCKUP_DAYS * TIME_CONSTANTS.EPOCHS_PER_DAY describe('isFwssMaxApproved', () => { const server = setup() @@ -92,13 +95,13 @@ describe('isFwssMaxApproved', () => { assert.equal(result, false) }) - it('should return false when approved but maxLockupPeriod is below LOCKUP_PERIOD', async () => { + it('should return false when approved but maxLockupPeriod is below the chain lockup period', async () => { server.use( JSONRPC({ ...presets.basic, payments: { ...presets.basic.payments, - operatorApprovals: () => [true, maxUint256, maxUint256, 0n, 0n, LOCKUP_PERIOD - 1n], + operatorApprovals: () => [true, maxUint256, maxUint256, 0n, 0n, CHAIN_LOCKUP_PERIOD - 1n], }, }) ) @@ -115,13 +118,13 @@ describe('isFwssMaxApproved', () => { assert.equal(result, false) }) - it('should return true when maxLockupPeriod equals LOCKUP_PERIOD', async () => { + it('should return true when maxLockupPeriod equals the chain lockup period', async () => { server.use( JSONRPC({ ...presets.basic, payments: { ...presets.basic.payments, - operatorApprovals: () => [true, maxUint256, maxUint256, 0n, 0n, LOCKUP_PERIOD], + operatorApprovals: () => [true, maxUint256, maxUint256, 0n, 0n, CHAIN_LOCKUP_PERIOD], }, }) ) @@ -138,6 +141,37 @@ describe('isFwssMaxApproved', () => { assert.equal(result, true) }) + it('compares against an explicit requiredMaxLockupPeriod when provided', async () => { + // Approval is below the chain default but at/above the explicit requirement, + // proving the override is used instead of the chain read. + server.use( + JSONRPC({ + ...presets.basic, + payments: { + ...presets.basic.payments, + operatorApprovals: () => [true, maxUint256, maxUint256, 0n, 0n, CHAIN_LOCKUP_PERIOD - 10n], + }, + }) + ) + + const client = createPublicClient({ chain: calibration, transport: http() }) + + assert.equal( + await isFwssMaxApproved(client, { + clientAddress: ADDRESSES.client1, + requiredMaxLockupPeriod: CHAIN_LOCKUP_PERIOD - 10n, + }), + true + ) + assert.equal( + await isFwssMaxApproved(client, { + clientAddress: ADDRESSES.client1, + requiredMaxLockupPeriod: CHAIN_LOCKUP_PERIOD, + }), + false + ) + }) + it('should return true when all allowances are maxUint256', async () => { server.use( JSONRPC({ diff --git a/packages/synapse-core/test/set-operator-approval.test.ts b/packages/synapse-core/test/set-operator-approval.test.ts index 21c4178ec..776128478 100644 --- a/packages/synapse-core/test/set-operator-approval.test.ts +++ b/packages/synapse-core/test/set-operator-approval.test.ts @@ -20,7 +20,11 @@ import { setOperatorApprovalCall, setOperatorApprovalSync, } from '../src/pay/set-operator-approval.ts' -import { LOCKUP_PERIOD } from '../src/utils/constants.ts' +import { TIME_CONSTANTS } from '../src/utils/constants.ts' + +// Matches the lockup period returned by the basic preset's getPriceList mock, +// which the async approval path resolves from the chain. +const CHAIN_LOCKUP_PERIOD = TIME_CONSTANTS.DEFAULT_LOCKUP_DAYS * TIME_CONSTANTS.EPOCHS_PER_DAY // Type for captured args from setOperatorApproval mock type SetOperatorApprovalArgs = readonly [Address, Address, boolean, bigint, bigint, bigint] @@ -45,6 +49,7 @@ describe('setOperatorApproval', () => { const call = setOperatorApprovalCall({ chain: calibration, approve: true, + maxLockupPeriod: CHAIN_LOCKUP_PERIOD, }) assert.equal(call.functionName, 'setOperatorApproval') @@ -57,13 +62,14 @@ describe('setOperatorApproval', () => { assert.equal(call.args[2], true) // approved assert.equal(call.args[3], maxUint256) // rateAllowance assert.equal(call.args[4], maxUint256) // lockupAllowance - assert.equal(call.args[5], LOCKUP_PERIOD) // maxLockupPeriod (30 days in epochs) + assert.equal(call.args[5], CHAIN_LOCKUP_PERIOD) // maxLockupPeriod }) it('should create call with mainnet chain defaults when approving', () => { const call = setOperatorApprovalCall({ chain: mainnet, approve: true, + maxLockupPeriod: CHAIN_LOCKUP_PERIOD, }) assert.equal(call.functionName, 'setOperatorApproval') @@ -75,7 +81,16 @@ describe('setOperatorApproval', () => { assert.equal(call.args[2], true) assert.equal(call.args[3], maxUint256) assert.equal(call.args[4], maxUint256) - assert.equal(call.args[5], LOCKUP_PERIOD) + assert.equal(call.args[5], CHAIN_LOCKUP_PERIOD) + }) + + it('should throw when approving without an explicit maxLockupPeriod', () => { + // The synchronous builder has no hardcoded default; approving callers must + // resolve the period from the chain price list and pass it in. + assert.throws( + () => setOperatorApprovalCall({ chain: calibration, approve: true }), + /maxLockupPeriod is required when approving/ + ) }) it('should create call with zero defaults when revoking', () => { @@ -96,6 +111,7 @@ describe('setOperatorApproval', () => { const call = setOperatorApprovalCall({ chain: calibration, approve: true, + maxLockupPeriod: CHAIN_LOCKUP_PERIOD, contractAddress: customAddress, }) @@ -107,6 +123,7 @@ describe('setOperatorApproval', () => { const call = setOperatorApprovalCall({ chain: calibration, approve: true, + maxLockupPeriod: CHAIN_LOCKUP_PERIOD, token: customToken, }) @@ -223,7 +240,7 @@ describe('setOperatorApproval', () => { { name: 'lockupAllowance', type: 'uint256' }, { name: 'maxLockupPeriod', type: 'uint256' }, ], - [true, maxUint256, maxUint256, LOCKUP_PERIOD] + [true, maxUint256, maxUint256, CHAIN_LOCKUP_PERIOD] ) const logs: Log[] = [ @@ -246,7 +263,7 @@ describe('setOperatorApproval', () => { assert.equal(event.args.approved, true) assert.equal(event.args.rateAllowance, maxUint256) assert.equal(event.args.lockupAllowance, maxUint256) - assert.equal(event.args.maxLockupPeriod, LOCKUP_PERIOD) + assert.equal(event.args.maxLockupPeriod, CHAIN_LOCKUP_PERIOD) }) }) @@ -289,7 +306,7 @@ describe('setOperatorApproval', () => { assert.equal(capturedArgs[2], true) // approved assert.equal(capturedArgs[3], maxUint256) // rateAllowance assert.equal(capturedArgs[4], maxUint256) // lockupAllowance - assert.equal(capturedArgs[5], LOCKUP_PERIOD) // maxLockupPeriod + assert.equal(capturedArgs[5], CHAIN_LOCKUP_PERIOD) // maxLockupPeriod }) it('should send revoke transaction with zero defaults', async () => { @@ -426,7 +443,7 @@ describe('setOperatorApproval', () => { { name: 'lockupAllowance', type: 'uint256' }, { name: 'maxLockupPeriod', type: 'uint256' }, ], - [true, maxUint256, maxUint256, LOCKUP_PERIOD] + [true, maxUint256, maxUint256, CHAIN_LOCKUP_PERIOD] ) server.use( @@ -496,7 +513,7 @@ describe('setOperatorApproval', () => { assert.equal(event.args.approved, true) assert.equal(event.args.rateAllowance, maxUint256) assert.equal(event.args.lockupAllowance, maxUint256) - assert.equal(event.args.maxLockupPeriod, LOCKUP_PERIOD) + assert.equal(event.args.maxLockupPeriod, CHAIN_LOCKUP_PERIOD) }) it('should work without onHash callback', async () => { @@ -583,13 +600,4 @@ describe('setOperatorApproval', () => { assert.equal(event.args.maxLockupPeriod, 0n) }) }) - - describe('LOCKUP_PERIOD constant', () => { - it('should be 30 days in epochs', () => { - // 30 days * 24 hours * 60 minutes * 2 epochs per minute = 86400 epochs - const expectedEpochs = 30n * 24n * 60n * 2n - assert.equal(LOCKUP_PERIOD, expectedEpochs) - assert.equal(LOCKUP_PERIOD, 86400n) - }) - }) }) diff --git a/packages/synapse-sdk/src/payments/service.ts b/packages/synapse-sdk/src/payments/service.ts index d49573b00..b75981896 100644 --- a/packages/synapse-sdk/src/payments/service.ts +++ b/packages/synapse-sdk/src/payments/service.ts @@ -254,7 +254,7 @@ export class PaymentsService { * @param options.service - The service contract address to approve (defaults to Warm Storage contract address) * @param options.rateAllowance - Maximum payment rate per epoch the operator can set (defaults to maxUint256) * @param options.lockupAllowance - Maximum lockup amount the operator can set (defaults to maxUint256) - * @param options.maxLockupPeriod - Maximum lockup period in epochs the operator can set (defaults to 30 days in epochs) + * @param options.maxLockupPeriod - Maximum lockup period in epochs the operator can set (defaults to the chain's getPriceList().lockups.defaultLockupPeriod) * @param options.token - The token to approve for (defaults to USDFC) * @returns Transaction hash {@link Hash} * @throws Errors {@link Pay.setOperatorApproval.ErrorType} @@ -522,8 +522,8 @@ export class PaymentsService { /** * Smart deposit method that picks the right contract call based on FWSS approval state. * - * - If FWSS needs approval AND amount > 0: calls `depositWithPermitAndApproveOperator` with maxUint256 rate/lockup allowances and LOCKUP_PERIOD. - * - If FWSS needs approval AND amount === 0: calls `approveService` with maxUint256 rate/lockup allowances and LOCKUP_PERIOD. + * - If FWSS needs approval AND amount > 0: calls `depositWithPermitAndApproveOperator` with maxUint256 rate/lockup allowances and the chain's default lockup period. + * - If FWSS needs approval AND amount === 0: calls `approveService` with maxUint256 rate/lockup allowances and the chain's default lockup period. * - If FWSS is approved AND amount > 0: calls `depositWithPermit`. * - If FWSS is approved AND amount === 0: no-op, returns empty hash. * From 471adf1068323f7b2d0d0f0963db94f4e1edacb3 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:05:17 -0400 Subject: [PATCH 10/16] docs(costs): update price API references --- docs/src/content/docs/developer-guides/index.md | 2 +- docs/src/content/docs/getting-started/index.mdx | 2 +- utils/example-storage-e2e.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/content/docs/developer-guides/index.md b/docs/src/content/docs/developer-guides/index.md index a7c4096e2..0ae0362d9 100644 --- a/docs/src/content/docs/developer-guides/index.md +++ b/docs/src/content/docs/developer-guides/index.md @@ -68,7 +68,7 @@ sequenceDiagram Note over Client,Payments: Step 2: Payment Setup Client->>SDK: Check allowances - SDK->>WarmStorage: getServicePrice() + SDK->>WarmStorage: getPriceList() SDK->>Payments: accountInfo(client) alt Needs setup Client->>Payments: depositWithPermitAndApproveOperator() diff --git a/docs/src/content/docs/getting-started/index.mdx b/docs/src/content/docs/getting-started/index.mdx index 845a61531..791359ffc 100644 --- a/docs/src/content/docs/getting-started/index.mdx +++ b/docs/src/content/docs/getting-started/index.mdx @@ -169,7 +169,7 @@ Now let's break down each step... }); console.log("Deposit needed:", prep.costs.depositNeeded); - console.log("Rate per month:", prep.costs.rate.perMonth); + console.log("Rate per month:", prep.costs.rates.perMonth); console.log("Ready to upload:", prep.costs.ready); // Execute the transaction if needed (handles deposit + approval in one tx) diff --git a/utils/example-storage-e2e.js b/utils/example-storage-e2e.js index 7491d8b1c..a2013333d 100644 --- a/utils/example-storage-e2e.js +++ b/utils/example-storage-e2e.js @@ -81,8 +81,8 @@ async function main() { const { costs, transaction } = await synapse.storage.prepare({ dataSize: BigInt(totalSize) }) console.log('Estimated costs:') - console.log(` Per epoch (30s): ${formatUSDFC(costs.rate.perEpoch)}`) - console.log(` Per month: ${formatUSDFC(costs.rate.perMonth)}`) + console.log(` Per epoch (30s): ${formatUSDFC(costs.rates.perEpoch)}`) + console.log(` Per month: ${formatUSDFC(costs.rates.perMonth)}`) console.log(` Deposit needed: ${formatUSDFC(costs.depositNeeded)}`) console.log(` Ready: ${costs.ready}`) From be8ba1b89159dc119fc881179e1b0a3d48f39d04 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:22:12 -0400 Subject: [PATCH 11/16] docs(costs): update action guide example to getPriceList --- packages/synapse-core/AGENTS.md | 58 ++++++++++++++++----------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/packages/synapse-core/AGENTS.md b/packages/synapse-core/AGENTS.md index 5384b38af..62c565c91 100644 --- a/packages/synapse-core/AGENTS.md +++ b/packages/synapse-core/AGENTS.md @@ -159,24 +159,22 @@ export type OutputType = ContractFunctionReturnType< When the contract function return type is an array try to convert into an object using the contract source code to choose the best descritive property names. When the return type is already an object inline it with documentation for each property. ```ts - export type ContractOutputType = ContractFunctionReturnType + export type ContractOutputType = ContractFunctionReturnType /** - * The service price for the warm storage. + * The canonical warm storage price list. */ export type OutputType = { - /** Price per TiB per month without CDN (in base units) */ - pricePerTiBPerMonthNoCDN: bigint - /** CDN egress price per TiB (usage-based, in base units) */ - pricePerTiBCdnEgress: bigint - /** Cache miss egress price per TiB (usage-based, in base units) */ - pricePerTiBCacheMissEgress: bigint /** Token address for payments */ - tokenAddress: string - /** Number of epochs per month */ - epochsPerMonth: bigint - /** Minimum monthly charge for any dataset size (in base units) */ - minimumPricePerMonth: bigint + token: Address + rates: { + /** Storage price per TiB per month (in base units) */ + storagePerTibPerMonth: bigint + /** Per-dataset monthly proving service fee (in base units) */ + datasetFeePerMonth: bigint + // ...remaining rate fields documented the same way + } + // ...fees and lockups objects follow the same inline-with-docs pattern } ``` @@ -201,15 +199,15 @@ All read and write action require a call function to enable composition with oth ```ts /** - * Create a call to the {@link getServicePrice} function for use with the Viem multicall, readContract, or simulateContract functions. + * Create a call to the {@link getPriceList} function for use with the Viem multicall, readContract, or simulateContract functions. * - * @param options - {@link getServicePriceCall.OptionsType} - * @returns Call object {@link getServicePriceCall.OutputType} - * @throws Errors {@link getServicePriceCall.ErrorType} + * @param options - {@link getPriceListCall.OptionsType} + * @returns Call object {@link getPriceListCall.OutputType} + * @throws Errors {@link getPriceListCall.ErrorType} * * @example * ```ts - * import { getServicePriceCall } from '@filoz/synapse-core/warm-storage' + * import { getPriceListCall } from '@filoz/synapse-core/warm-storage' * import { createPublicClient, http } from 'viem' * import { multicall } from 'viem/actions' * import { calibration } from '@filoz/synapse-core/chains' @@ -221,21 +219,21 @@ All read and write action require a call function to enable composition with oth * * const results = await multicall(client, { * contracts: [ - * getServicePriceCall({ chain: calibration }), + * getPriceListCall({ chain: calibration }), * ], * }) * * console.log(results[0]) * ``` */ -export function getServicePriceCall(options: getServicePriceCall.OptionsType) { +export function getPriceListCall(options: getPriceListCall.OptionsType) { const chain = asChain(options.chain) return { - abi: chain.contracts.storage.abi, - address: options.address ?? chain.contracts.storage.address, - functionName: 'getServicePrice', + abi: chain.contracts.fwssView.abi, + address: options.contractAddress ?? chain.contracts.fwssView.address, + functionName: 'getPriceList', args: [], - } satisfies getServicePriceCall.OutputType + } satisfies getPriceListCall.OutputType } ``` @@ -243,10 +241,10 @@ export function getServicePriceCall(options: getServicePriceCall.OptionsType) { They should have their own namespaced types ```ts -export namespace getServicePriceCall { - export type OptionsType = Simplify +export namespace getPriceListCall { + export type OptionsType = Simplify export type ErrorType = asChain.ErrorType - export type OutputType = ContractFunctionParameters + export type OutputType = ContractFunctionParameters } ``` @@ -314,7 +312,7 @@ export function extractSetOperatorApprovalEvent(logs: Log[]) { #### Parse function -When the `ContractOutputType` is different from the `OutputType` we need a parse function to transform the contract output into the action output. It should called `parse` .ie `parseGetServicePrice`. +When the `ContractOutputType` is different from the `OutputType` we need a parse function to transform the contract output into the action output. It should called `parse` .ie `parseGetPriceList`. ## Decision-Making @@ -339,6 +337,6 @@ Reference contract source code for expected behavior and always create tests for Use mocks and constants inside `/mocks` to test the actions. -See `test/get-service-price.test.ts` for a comprehensive example of test patterns and structure. +See `test/get-price-list.test.ts` for a comprehensive example of test patterns and structure. -Run the tests with `pnpm exec playwright-test "test/get-service-price.test.ts" --mode node` +Run the tests with `pnpm exec playwright-test "test/get-price-list.test.ts" --mode node` From da378d96c1e434938aa96efd05976225a3df49d9 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:42:20 -0400 Subject: [PATCH 12/16] refactor(costs): derive addPieces op count internally --- .../warm-storage/calculate-deposit-needed.ts | 2 -- .../src/warm-storage/calculate-upload-fees.ts | 18 ++++++------------ .../src/warm-storage/get-upload-costs.ts | 5 ----- .../test/calculate-upload-fees.test.ts | 13 ------------- 4 files changed, 6 insertions(+), 32 deletions(-) diff --git a/packages/synapse-core/src/warm-storage/calculate-deposit-needed.ts b/packages/synapse-core/src/warm-storage/calculate-deposit-needed.ts index 3f55fa3f2..f989e9178 100644 --- a/packages/synapse-core/src/warm-storage/calculate-deposit-needed.ts +++ b/packages/synapse-core/src/warm-storage/calculate-deposit-needed.ts @@ -83,7 +83,6 @@ export namespace calculateDepositNeeded { isNewDataSet: boolean withCDN: boolean pieceCount?: bigint - addPiecesOperationCount?: bigint // Runway parameters currentLockupRate: bigint @@ -133,7 +132,6 @@ export function calculateDepositNeeded(params: calculateDepositNeeded.ParamsType priceList: params.priceList, isNewDataSet: params.isNewDataSet, pieceCount: params.pieceCount, - addPiecesOperationCount: params.addPiecesOperationCount, }) const netRateAfterUpload = params.currentLockupRate + lockup.rateDeltaPerEpoch diff --git a/packages/synapse-core/src/warm-storage/calculate-upload-fees.ts b/packages/synapse-core/src/warm-storage/calculate-upload-fees.ts index ffd6e18da..3126adf00 100644 --- a/packages/synapse-core/src/warm-storage/calculate-upload-fees.ts +++ b/packages/synapse-core/src/warm-storage/calculate-upload-fees.ts @@ -5,13 +5,8 @@ export namespace calculateUploadFees { export type ParamsType = { priceList: getPriceList.OutputType isNewDataSet: boolean + /** Number of pieces added by this upload. Defaults to 1. */ pieceCount?: bigint - /** - * Number of addPieces operations the upload is split across. Defaults to - * `ceil(pieceCount / MAX_ADD_PIECES_BATCH_SIZE)`, since a single addPieces - * call cannot exceed the batch limit and pieces beyond it span more calls. - */ - addPiecesOperationCount?: bigint } export type OutputType = { @@ -28,10 +23,10 @@ export namespace calculateUploadFees { * datasets only) and add-pieces. Schedule-removals, terminate, and delete are * post-upload lifecycle operations and are not part of an upload cost preview. * - * When `addPiecesOperationCount` is omitted it is derived from `pieceCount` and - * the `MAX_ADD_PIECES_BATCH_SIZE` batch limit: a batch of `pieceCount` pieces - * is split into `ceil(pieceCount / MAX_ADD_PIECES_BATCH_SIZE)` addPieces calls, - * each charged the base fee. + * The number of addPieces operations is derived from `pieceCount` and the + * `MAX_ADD_PIECES_BATCH_SIZE` batch limit: a single addPieces call cannot + * exceed the limit, so `pieceCount` pieces span `ceil(pieceCount / limit)` + * calls, each charged the base fee. * * @param params - {@link calculateUploadFees.ParamsType} * @returns {@link calculateUploadFees.OutputType} @@ -39,8 +34,7 @@ export namespace calculateUploadFees { export function calculateUploadFees(params: calculateUploadFees.ParamsType): calculateUploadFees.OutputType { const pieceCount = params.pieceCount ?? 1n const maxBatch = BigInt(SIZE_CONSTANTS.MAX_ADD_PIECES_BATCH_SIZE) - const derivedOperationCount = (pieceCount + maxBatch - 1n) / maxBatch - const addPiecesOperationCount = params.addPiecesOperationCount ?? derivedOperationCount + const addPiecesOperationCount = (pieceCount + maxBatch - 1n) / maxBatch const createDataSetFee = params.isNewDataSet ? params.priceList.fees.createDataSetFee : 0n const addPiecesFee = params.priceList.fees.addPiecesBaseFee * addPiecesOperationCount + diff --git a/packages/synapse-core/src/warm-storage/get-upload-costs.ts b/packages/synapse-core/src/warm-storage/get-upload-costs.ts index 071cb6e73..0cd4ebe3b 100644 --- a/packages/synapse-core/src/warm-storage/get-upload-costs.ts +++ b/packages/synapse-core/src/warm-storage/get-upload-costs.ts @@ -26,8 +26,6 @@ export namespace getUploadCosts { dataSize: bigint /** Number of pieces added by this operation. Default: 1 */ pieceCount?: bigint - /** Number of addPieces operations. Defaults to `ceil(pieceCount / MAX_ADD_PIECES_BATCH_SIZE)`. */ - addPiecesOperationCount?: bigint /** Extra runway in epochs beyond the required lockup. */ extraRunwayEpochs?: bigint @@ -78,8 +76,6 @@ export async function getUploadCosts( const withCDN = options.withCDN ?? false const currentDataSetSize = options.currentDataSetSize ?? 0n const pieceCount = options.pieceCount ?? 1n - // Left undefined so calculateUploadFees derives it from pieceCount and the batch limit. - const addPiecesOperationCount = options.addPiecesOperationCount const extraRunwayEpochs = options.extraRunwayEpochs ?? DEFAULT_RUNWAY_EPOCHS const bufferEpochs = options.bufferEpochs ?? DEFAULT_BUFFER_EPOCHS @@ -119,7 +115,6 @@ export async function getUploadCosts( isNewDataSet, withCDN, pieceCount, - addPiecesOperationCount, currentLockupRate: accountInfo.lockupRate, extraRunwayEpochs, debt, diff --git a/packages/synapse-core/test/calculate-upload-fees.test.ts b/packages/synapse-core/test/calculate-upload-fees.test.ts index bc2f78eed..73b8356a9 100644 --- a/packages/synapse-core/test/calculate-upload-fees.test.ts +++ b/packages/synapse-core/test/calculate-upload-fees.test.ts @@ -55,17 +55,4 @@ describe('calculateUploadFees', () => { priceList.fees.addPiecesBaseFee * 2n + priceList.fees.addPiecesPerPieceFee * (maxBatch + 1n) ) }) - - it('uses an explicit addPiecesOperationCount over the derived value', () => { - const result = calculateUploadFees({ - priceList, - isNewDataSet: false, - pieceCount: maxBatch + 1n, - addPiecesOperationCount: 1n, - }) - assert.equal( - result.addPiecesFee, - priceList.fees.addPiecesBaseFee + priceList.fees.addPiecesPerPieceFee * (maxBatch + 1n) - ) - }) }) From fcc31c825e1a7e60c3fa38eec48f97662d411571 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:54:16 -0400 Subject: [PATCH 13/16] docs(costs): make pricing comments future-oriented --- packages/synapse-core/src/abis/index.ts | 3 +-- packages/synapse-core/src/abis/price-list.ts | 15 +++++---------- packages/synapse-core/src/pay/fund.ts | 4 ++-- .../synapse-core/src/pay/set-operator-approval.ts | 10 +++++----- .../synapse-core/src/warm-storage/price-list.ts | 6 ++---- 5 files changed, 15 insertions(+), 23 deletions(-) diff --git a/packages/synapse-core/src/abis/index.ts b/packages/synapse-core/src/abis/index.ts index 3b98bf675..37fd8b77c 100644 --- a/packages/synapse-core/src/abis/index.ts +++ b/packages/synapse-core/src/abis/index.ts @@ -18,8 +18,7 @@ import { priceListAbi } from './price-list.ts' // Merge the storage and errors ABIs export const fwss = [...generated.filecoinWarmStorageServiceAbi, ...generated.errorsAbi] as const export const serviceProviderRegistry = [...generated.serviceProviderRegistryAbi, ...generated.errorsAbi] as const -// Merge the generated view ABI with the standalone getPriceList fragment, which -// is not yet on the generated ABI's pinned release. See abis/price-list.ts. +// The view ABI plus the standalone getPriceList fragment. See abis/price-list.ts. // TODO: drop the priceListAbi merge and re-export filecoinWarmStorageServiceStateViewAbi // as fwssView once the generated ABI ref includes getPriceList. export const fwssView = [...generated.filecoinWarmStorageServiceStateViewAbi, ...priceListAbi] as const diff --git a/packages/synapse-core/src/abis/price-list.ts b/packages/synapse-core/src/abis/price-list.ts index 12ec32fb0..381aa0b97 100644 --- a/packages/synapse-core/src/abis/price-list.ts +++ b/packages/synapse-core/src/abis/price-list.ts @@ -1,16 +1,11 @@ /** - * Standalone `getPriceList()` view fragment for - * `FilecoinWarmStorageServiceStateView`. - * - * Mirrors the on-chain `PriceList` struct added in + * `getPriceList()` view fragment for `FilecoinWarmStorageServiceStateView`, + * mirroring the on-chain `PriceList` struct from * [FilOzone/filecoin-services#501](https://github.com/FilOzone/filecoin-services/pull/501). - * Kept separate from the wagmi-generated ABI so the price list can be read from - * the chain without bumping the generated ABI ref onto an unreleased commit. * - * TODO: remove this file and the `fwssView` merge in `abis/index.ts` once the - * generated ABI ref (`FILECOIN_SERVICES_GIT_REF` in `wagmi.config.ts`) is bumped - * to a release that includes `getPriceList`; the generated view ABI will expose - * it directly. + * TODO: remove this file and the `fwssView` merge in `abis/index.ts` once + * `FILECOIN_SERVICES_GIT_REF` (`wagmi.config.ts`) points at a release whose + * generated view ABI exposes `getPriceList`. */ export const priceListAbi = [ { diff --git a/packages/synapse-core/src/pay/fund.ts b/packages/synapse-core/src/pay/fund.ts index 98662056d..84d612e46 100644 --- a/packages/synapse-core/src/pay/fund.ts +++ b/packages/synapse-core/src/pay/fund.ts @@ -70,8 +70,8 @@ export namespace fund { * ``` */ export async function fund(client: Client, options: fund.OptionsType): Promise { - // Resolve the approval lockup period once from the chain and reuse it for both - // the readiness check and the approval call, so the path needs a single read. + // Resolve the approval lockup period from the chain once and reuse it for the + // readiness check and the approval call. const maxLockupPeriod = (await getPriceList(client)).lockups.defaultLockupPeriod const needsApproval = options.needsFwssMaxApproval ?? diff --git a/packages/synapse-core/src/pay/set-operator-approval.ts b/packages/synapse-core/src/pay/set-operator-approval.ts index fcfef897a..6e9f99a48 100644 --- a/packages/synapse-core/src/pay/set-operator-approval.ts +++ b/packages/synapse-core/src/pay/set-operator-approval.ts @@ -89,8 +89,8 @@ export async function setOperatorApproval( client: Client, options: setOperatorApproval.OptionsType ): Promise { - // maxLockupPeriod has no hardcoded default; resolve it from the chain price - // list when approving so the call builder (which is synchronous) gets a value. + // The synchronous call builder cannot read the chain, so resolve maxLockupPeriod + // from the price list here when approving and pass it down. const maxLockupPeriod = options.maxLockupPeriod ?? (options.approve ? (await getPriceList(client)).lockups.defaultLockupPeriod : 0n) @@ -231,9 +231,9 @@ export function setOperatorApprovalCall( } } - // maxLockupPeriod has no hardcoded default. It must come from the chain price - // list (getPriceList().lockups.defaultLockupPeriod), which this synchronous - // builder cannot read, so approving callers must resolve and pass it. + // maxLockupPeriod comes from the chain price list + // (getPriceList().lockups.defaultLockupPeriod), which this synchronous builder + // cannot read, so approving callers must resolve and pass it. if (options.approve && options.maxLockupPeriod === undefined) { throw new ValidationError( 'maxLockupPeriod is required when approving; resolve it from getPriceList().lockups.defaultLockupPeriod' diff --git a/packages/synapse-core/src/warm-storage/price-list.ts b/packages/synapse-core/src/warm-storage/price-list.ts index a220d5118..68b900546 100644 --- a/packages/synapse-core/src/warm-storage/price-list.ts +++ b/packages/synapse-core/src/warm-storage/price-list.ts @@ -56,11 +56,9 @@ export namespace getPriceList { } /** - * Read the full warm storage price list from the chain. + * Read the warm storage price list. * - * Calls `getPriceList()` on the `FilecoinWarmStorageServiceStateView` contract, - * which requires a deployment that includes - * [FilOzone/filecoin-services#501](https://github.com/FilOzone/filecoin-services/pull/501). + * Reads the `getPriceList()` view on `FilecoinWarmStorageServiceStateView`. * * @param client - The client to use to read the price list. * @param options - {@link getPriceList.OptionsType} From f5730e5fff4335a614cdad2072d467fdf23d4bf7 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:20:55 -0400 Subject: [PATCH 14/16] docs(costs): use rates in cost doc samples --- .../docs/cookbooks/payments-and-storage.mdx | 2 +- .../developer-guides/storage/storage-costs.mdx | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/src/content/docs/cookbooks/payments-and-storage.mdx b/docs/src/content/docs/cookbooks/payments-and-storage.mdx index 16a027825..92338d9a6 100644 --- a/docs/src/content/docs/cookbooks/payments-and-storage.mdx +++ b/docs/src/content/docs/cookbooks/payments-and-storage.mdx @@ -113,7 +113,7 @@ Most developers won't need this section. The SDK abstracts these mechanics: `pre ### Rate Precision -Storage prices are stored per-month on-chain but rails operate per-epoch. Integer division during conversion means `perEpoch × EPOCHS_PER_MONTH` is slightly less than the true monthly rate due to truncation. The SDK returns both values: use `rate.perMonth` for display and `rate.perEpoch` for on-chain math. +Storage prices are stored per-month on-chain but rails operate per-epoch. Integer division during conversion means `perEpoch × EPOCHS_PER_MONTH` is slightly less than the true monthly rate due to truncation. The SDK returns both values: use `rates.perMonth` for display and `rates.perEpoch` for on-chain math. ### Deposit Components diff --git a/docs/src/content/docs/developer-guides/storage/storage-costs.mdx b/docs/src/content/docs/developer-guides/storage/storage-costs.mdx index 0b3d427ec..2128a5b94 100644 --- a/docs/src/content/docs/developer-guides/storage/storage-costs.mdx +++ b/docs/src/content/docs/developer-guides/storage/storage-costs.mdx @@ -109,7 +109,7 @@ import { privateKeyToAccount } from 'viem/accounts' const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' }) // ---cut--- // With currentDataSetSize — accurate floor-aware delta -const { rate, depositNeeded } = await synapse.storage.getUploadCosts({ +const { rates, depositNeeded } = await synapse.storage.getUploadCosts({ isNewDataSet: false, currentDataSetSize: 50n * SIZE_CONSTANTS.MiB, dataSize: 100n * SIZE_CONSTANTS.MiB, @@ -133,16 +133,16 @@ import { Synapse, formatUnits, SIZE_CONSTANTS } from "@filoz/synapse-sdk" import { privateKeyToAccount } from 'viem/accounts' const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' }) // ---cut--- -const { rate, depositNeeded, needsFwssMaxApproval, ready } = await synapse.storage.getUploadCosts({ +const { rates, depositNeeded, needsFwssMaxApproval, ready } = await synapse.storage.getUploadCosts({ dataSize: 100n * SIZE_CONSTANTS.MiB, isNewDataSet: true, withCDN: true, }) // Storage rate per epoch (30 seconds) -console.log("Rate per epoch:", formatUnits(rate.perEpoch), "USDFC") +console.log("Rate per epoch:", formatUnits(rates.perEpoch), "USDFC") // Storage rate per month -console.log("Rate per month:", formatUnits(rate.perMonth), "USDFC") +console.log("Rate per month:", formatUnits(rates.perMonth), "USDFC") // USDFC to deposit console.log("Deposit needed:", formatUnits(depositNeeded), "USDFC") // Whether FWSS needs to be approved @@ -172,8 +172,8 @@ const { costs, transaction } = await synapse.storage.prepare({ }) // Inspect costs -const { rate, depositNeeded } = costs // Upload costs breakdown -console.log("Rate per month:", formatUnits(rate.perMonth)) +const { rates, depositNeeded } = costs // Upload costs breakdown +console.log("Rate per month:", formatUnits(rates.perMonth)) console.log("Deposit needed:", formatUnits(depositNeeded)) // Execute if the account isn't ready @@ -209,8 +209,8 @@ const { costs, transaction } = await synapse.storage.prepare({ extraRunwayEpochs: oneYear, }) -const { rate, depositNeeded, ready } = costs -console.log("Monthly rate (per copy):", formatUnits(rate.perMonth), "USDFC") +const { rates, depositNeeded, ready } = costs +console.log("Monthly rate (per copy):", formatUnits(rates.perMonth), "USDFC") console.log("Total deposit needed:", formatUnits(depositNeeded), "USDFC") console.log("Account ready:", ready) @@ -251,7 +251,7 @@ const { totalSizeBytes, datasetCount } = await getAccountTotalStorageSize(client }) ``` -All rate values come in both per-epoch (contract-native) and per-month (`ratePerEpoch * EPOCHS_PER_MONTH`) units. Note that `ratePerMonth` from `totalAccountRate()` is computed as `ratePerEpoch * EPOCHS_PER_MONTH` — this is slightly less than the full-precision `rate.perMonth` returned by `getUploadCosts()` due to integer truncation. See [Rate Precision](/cookbooks/payments-and-storage/#rate-precision) for details. +All rate values come in both per-epoch (contract-native) and per-month (`ratePerEpoch * EPOCHS_PER_MONTH`) units. Note that `ratePerMonth` from `totalAccountRate()` is computed as `ratePerEpoch * EPOCHS_PER_MONTH` — this is slightly less than the full-precision `rates.perMonth` returned by `getUploadCosts()` due to integer truncation. See [Rate Precision](/cookbooks/payments-and-storage/#rate-precision) for details. :::tip[API Reference] For the full API — including pure calculation functions and lower-level utilities — see the [synapse-core pay reference](/reference/filoz/synapse-core/pay/toc/) and [synapse-core warm-storage reference](/reference/filoz/synapse-core/warm-storage/toc/). From 604f2e105d3bb7d305a6ef1ba7a8fbf70d1dd39d Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:40:27 -0400 Subject: [PATCH 15/16] docs: add 0.42.0 pricing migration notes --- .../docs/developer-guides/migration-guide.md | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/docs/src/content/docs/developer-guides/migration-guide.md b/docs/src/content/docs/developer-guides/migration-guide.md index 2c284f6be..168a48bd1 100644 --- a/docs/src/content/docs/developer-guides/migration-guide.md +++ b/docs/src/content/docs/developer-guides/migration-guide.md @@ -27,6 +27,54 @@ SessionKey.TerminateServicePermission TypedData.signTerminateService(client, { dataSetId }) ``` +### Action: Read pricing with `getPriceList()` + +`getServicePrice()` was removed from both `@filoz/synapse-core` and `WarmStorageService`. Use `getPriceList()`, which returns the full on-chain price catalogue (`token`, `rates`, `fees`, `lockups`). + +```ts +// before +const price = await warmStorage.getServicePrice() +price.pricePerTiBPerMonthNoCDN +price.pricePerTiBCdnEgress + +// after +const priceList = await warmStorage.getPriceList() +priceList.rates.storagePerTibPerMonth +priceList.rates.cdnEgressPerTib +``` + +The React `useServicePrice()` hook was removed in favor of `usePriceList()`. + +```tsx +// before +import { useServicePrice } from '@filoz/synapse-react' +const { data } = useServicePrice() +data?.pricePerTiBPerMonthNoCDN + +// after +import { usePriceList } from '@filoz/synapse-react' +const { data } = usePriceList() +data?.rates.storagePerTibPerMonth +``` + +### Action: Read upload rates from `costs.rates` + +The `rate` alias on upload-cost results was removed. Use `rates`. + +```ts +// before +const { costs } = await synapse.storage.prepare({ dataSize }) +costs.rate.perMonth + +// after +const { costs } = await synapse.storage.prepare({ dataSize }) +costs.rates.perMonth +``` + +### Action: Replace the `LOCKUP_PERIOD` constant + +The `LOCKUP_PERIOD` export was removed from `@filoz/synapse-core`. The lockup period is now read from the chain; use `getPriceList().lockups.defaultLockupPeriod` if you need the value. + ## 0.37.0 `synapse-sdk` moved to a viem-first API, removed deprecated modules/methods, and standardized method signatures around options objects plus `bigint` identifiers. From 064fe62b6c3a848ec0083ec9b93f1462f9b91260 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Fri, 5 Jun 2026 13:21:58 +1000 Subject: [PATCH 16/16] feat(abi)!: update latest filecoin-services main branch ABI This is not final, there will need to be at least another update before FWSS deploy. --- packages/synapse-core/src/abis/generated.ts | 518 ++++++++---------- packages/synapse-core/src/abis/index.ts | 6 +- packages/synapse-core/src/abis/price-list.ts | 63 --- .../synapse-core/src/mocks/jsonrpc/index.ts | 6 + .../get-account-total-storage-size.test.ts | 2 + .../test/get-client-data-sets.test.ts | 2 + .../synapse-core/test/get-data-set.test.ts | 2 + .../test/resolve-piece-url.test.ts | 6 + .../test/terminate-service.test.ts | 10 +- packages/synapse-core/wagmi.config.ts | 2 +- .../src/test/metadata-selection.test.ts | 6 + packages/synapse-sdk/src/test/storage.test.ts | 18 + packages/synapse-sdk/src/test/synapse.test.ts | 2 + .../src/test/warm-storage-service.test.ts | 18 + 14 files changed, 291 insertions(+), 370 deletions(-) delete mode 100644 packages/synapse-core/src/abis/price-list.ts diff --git a/packages/synapse-core/src/abis/generated.ts b/packages/synapse-core/src/abis/generated.ts index dbedf5b44..6ad638ed8 100644 --- a/packages/synapse-core/src/abis/generated.ts +++ b/packages/synapse-core/src/abis/generated.ts @@ -18,7 +18,6 @@ export const errorsAbi = [ ], name: 'AddressAlreadySet', }, - { type: 'error', inputs: [], name: 'AtLeastOnePriceMustBeNonZero' }, { type: 'error', inputs: [{ name: 'dataSetId', internalType: 'uint256', type: 'uint256' }], @@ -85,6 +84,15 @@ export const errorsAbi = [ ], name: 'CommissionExceedsMaximum', }, + { + type: 'error', + inputs: [ + { name: 'dataSetId', internalType: 'uint256', type: 'uint256' }, + { name: 'requiredEpoch', internalType: 'uint256', type: 'uint256' }, + { name: 'currentBlock', internalType: 'uint256', type: 'uint256' }, + ], + name: 'DataSetNotAbandoned', + }, { type: 'error', inputs: [{ name: 'railId', internalType: 'uint256', type: 'uint256' }], @@ -109,7 +117,6 @@ export const errorsAbi = [ ], name: 'DataSetPaymentBeyondEndEpoch', }, - { type: 'error', inputs: [], name: 'DivisionByZero' }, { type: 'error', inputs: [ @@ -150,11 +157,7 @@ export const errorsAbi = [ { name: 'operator', internalType: 'address', type: 'address' }, { name: 'lockupAllowance', internalType: 'uint256', type: 'uint256' }, { name: 'lockupUsage', internalType: 'uint256', type: 'uint256' }, - { - name: 'minimumLockupRequired', - internalType: 'uint256', - type: 'uint256', - }, + { name: 'lockupRequired', internalType: 'uint256', type: 'uint256' }, ], name: 'InsufficientLockupAllowance', }, @@ -162,7 +165,7 @@ export const errorsAbi = [ type: 'error', inputs: [ { name: 'payer', internalType: 'address', type: 'address' }, - { name: 'minimumRequired', internalType: 'uint256', type: 'uint256' }, + { name: 'required', internalType: 'uint256', type: 'uint256' }, { name: 'available', internalType: 'uint256', type: 'uint256' }, ], name: 'InsufficientLockupFunds', @@ -188,7 +191,7 @@ export const errorsAbi = [ { name: 'operator', internalType: 'address', type: 'address' }, { name: 'rateAllowance', internalType: 'uint256', type: 'uint256' }, { name: 'rateUsage', internalType: 'uint256', type: 'uint256' }, - { name: 'minimumRateRequired', internalType: 'uint256', type: 'uint256' }, + { name: 'rateRequired', internalType: 'uint256', type: 'uint256' }, ], name: 'InsufficientRateAllowance', }, @@ -361,19 +364,6 @@ export const errorsAbi = [ ], name: 'PaymentRailsNotFinalized', }, - { - type: 'error', - inputs: [ - { - name: 'priceType', - internalType: 'enum Errors.PriceType', - type: 'uint8', - }, - { name: 'maxAllowed', internalType: 'uint256', type: 'uint256' }, - { name: 'actual', internalType: 'uint256', type: 'uint256' }, - ], - name: 'PriceExceedsMaximum', - }, { type: 'error', inputs: [{ name: 'dataSetId', internalType: 'uint256', type: 'uint256' }], @@ -1805,15 +1795,6 @@ export const filecoinWarmStorageServiceAbi = [ outputs: [], stateMutability: 'nonpayable', }, - { - type: 'function', - inputs: [{ name: 'totalBytes', internalType: 'uint256', type: 'uint256' }], - name: 'calculateRatePerEpoch', - outputs: [ - { name: 'storageRate', internalType: 'uint256', type: 'uint256' }, - ], - stateMutability: 'view', - }, { type: 'function', inputs: [ @@ -1897,7 +1878,7 @@ export const filecoinWarmStorageServiceAbi = [ { name: 'serviceFee', internalType: 'uint256', type: 'uint256' }, { name: 'spPayment', internalType: 'uint256', type: 'uint256' }, ], - stateMutability: 'view', + stateMutability: 'pure', }, { type: 'function', @@ -1941,7 +1922,7 @@ export const filecoinWarmStorageServiceAbi = [ }, { name: 'epochsPerMonth', internalType: 'uint256', type: 'uint256' }, { - name: 'minimumPricePerMonth', + name: 'datasetFeePerMonth', internalType: 'uint256', type: 'uint256', }, @@ -2160,6 +2141,16 @@ export const filecoinWarmStorageServiceAbi = [ outputs: [], stateMutability: 'nonpayable', }, + { + type: 'function', + inputs: [ + { name: 'dataSetId', internalType: 'uint256', type: 'uint256' }, + { name: 'extraData', internalType: 'bytes', type: 'bytes' }, + ], + name: 'terminateService', + outputs: [], + stateMutability: 'nonpayable', + }, { type: 'function', inputs: [ @@ -2178,35 +2169,26 @@ export const filecoinWarmStorageServiceAbi = [ { type: 'function', inputs: [ - { name: 'newController', internalType: 'address', type: 'address' }, + { name: 'dataSetId', internalType: 'uint256', type: 'uint256' }, + { name: 'amount', internalType: 'uint256', type: 'uint256' }, ], - name: 'transferFilBeamController', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [{ name: 'newOwner', internalType: 'address', type: 'address' }], - name: 'transferOwnership', + name: 'topUpLifecycleReserve', outputs: [], stateMutability: 'nonpayable', }, { type: 'function', inputs: [ - { name: 'newStoragePrice', internalType: 'uint256', type: 'uint256' }, - { name: 'newMinimumRate', internalType: 'uint256', type: 'uint256' }, + { name: 'newController', internalType: 'address', type: 'address' }, ], - name: 'updatePricing', + name: 'transferFilBeamController', outputs: [], stateMutability: 'nonpayable', }, { type: 'function', - inputs: [ - { name: 'newCommissionBps', internalType: 'uint256', type: 'uint256' }, - ], - name: 'updateServiceCommission', + inputs: [{ name: 'newOwner', internalType: 'address', type: 'address' }], + name: 'transferOwnership', outputs: [], stateMutability: 'nonpayable', }, @@ -2260,43 +2242,6 @@ export const filecoinWarmStorageServiceAbi = [ outputs: [{ name: '', internalType: 'address', type: 'address' }], stateMutability: 'view', }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'dataSetId', - internalType: 'uint256', - type: 'uint256', - indexed: true, - }, - { - name: 'cdnAmountAdded', - internalType: 'uint256', - type: 'uint256', - indexed: false, - }, - { - name: 'totalCdnLockup', - internalType: 'uint256', - type: 'uint256', - indexed: false, - }, - { - name: 'cacheMissAmountAdded', - internalType: 'uint256', - type: 'uint256', - indexed: false, - }, - { - name: 'totalCacheMissLockup', - internalType: 'uint256', - type: 'uint256', - indexed: false, - }, - ], - name: 'CDNPaymentRailsToppedUp', - }, { type: 'event', anonymous: false, @@ -2328,37 +2273,6 @@ export const filecoinWarmStorageServiceAbi = [ ], name: 'CDNPaymentTerminated', }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'caller', - internalType: 'address', - type: 'address', - indexed: true, - }, - { - name: 'dataSetId', - internalType: 'uint256', - type: 'uint256', - indexed: true, - }, - { - name: 'cacheMissRailId', - internalType: 'uint256', - type: 'uint256', - indexed: false, - }, - { - name: 'cdnRailId', - internalType: 'uint256', - type: 'uint256', - indexed: false, - }, - ], - name: 'CDNServiceTerminated', - }, { type: 'event', anonymous: false, @@ -2624,25 +2538,6 @@ export const filecoinWarmStorageServiceAbi = [ ], name: 'PieceAdded', }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'storagePrice', - internalType: 'uint256', - type: 'uint256', - indexed: false, - }, - { - name: 'minimumRate', - internalType: 'uint256', - type: 'uint256', - indexed: false, - }, - ], - name: 'PricingUpdated', - }, { type: 'event', anonymous: false, @@ -2674,32 +2569,7 @@ export const filecoinWarmStorageServiceAbi = [ anonymous: false, inputs: [ { - name: 'dataSetId', - internalType: 'uint256', - type: 'uint256', - indexed: true, - }, - { - name: 'railId', - internalType: 'uint256', - type: 'uint256', - indexed: false, - }, - { - name: 'newRate', - internalType: 'uint256', - type: 'uint256', - indexed: false, - }, - ], - name: 'RailRateUpdated', - }, - { - type: 'event', - anonymous: false, - inputs: [ - { - name: 'caller', + name: 'approver', internalType: 'address', type: 'address', indexed: true, @@ -2783,17 +2653,6 @@ export const filecoinWarmStorageServiceAbi = [ inputs: [{ name: 'target', internalType: 'address', type: 'address' }], name: 'AddressEmptyCode', }, - { type: 'error', inputs: [], name: 'AtLeastOnePriceMustBeNonZero' }, - { - type: 'error', - inputs: [{ name: 'dataSetId', internalType: 'uint256', type: 'uint256' }], - name: 'CDNPaymentAlreadyTerminated', - }, - { - type: 'error', - inputs: [{ name: 'dataSetId', internalType: 'uint256', type: 'uint256' }], - name: 'CacheMissPaymentAlreadyTerminated', - }, { type: 'error', inputs: [ @@ -2840,15 +2699,11 @@ export const filecoinWarmStorageServiceAbi = [ { type: 'error', inputs: [ - { - name: 'commissionType', - internalType: 'enum Errors.CommissionType', - type: 'uint8', - }, - { name: 'max', internalType: 'uint256', type: 'uint256' }, - { name: 'actual', internalType: 'uint256', type: 'uint256' }, + { name: 'dataSetId', internalType: 'uint256', type: 'uint256' }, + { name: 'requiredEpoch', internalType: 'uint256', type: 'uint256' }, + { name: 'currentBlock', internalType: 'uint256', type: 'uint256' }, ], - name: 'CommissionExceedsMaximum', + name: 'DataSetNotAbandoned', }, { type: 'error', @@ -2874,7 +2729,6 @@ export const filecoinWarmStorageServiceAbi = [ ], name: 'DataSetPaymentBeyondEndEpoch', }, - { type: 'error', inputs: [], name: 'DivisionByZero' }, { type: 'error', inputs: [ @@ -2906,55 +2760,6 @@ export const filecoinWarmStorageServiceAbi = [ inputs: [{ name: 'dataSetId', internalType: 'uint256', type: 'uint256' }], name: 'FilBeamServiceNotConfigured', }, - { - type: 'error', - inputs: [ - { name: 'payer', internalType: 'address', type: 'address' }, - { name: 'operator', internalType: 'address', type: 'address' }, - { name: 'lockupAllowance', internalType: 'uint256', type: 'uint256' }, - { name: 'lockupUsage', internalType: 'uint256', type: 'uint256' }, - { - name: 'minimumLockupRequired', - internalType: 'uint256', - type: 'uint256', - }, - ], - name: 'InsufficientLockupAllowance', - }, - { - type: 'error', - inputs: [ - { name: 'payer', internalType: 'address', type: 'address' }, - { name: 'minimumRequired', internalType: 'uint256', type: 'uint256' }, - { name: 'available', internalType: 'uint256', type: 'uint256' }, - ], - name: 'InsufficientLockupFunds', - }, - { - type: 'error', - inputs: [ - { name: 'payer', internalType: 'address', type: 'address' }, - { name: 'operator', internalType: 'address', type: 'address' }, - { name: 'maxLockupPeriod', internalType: 'uint256', type: 'uint256' }, - { - name: 'requiredLockupPeriod', - internalType: 'uint256', - type: 'uint256', - }, - ], - name: 'InsufficientMaxLockupPeriod', - }, - { - type: 'error', - inputs: [ - { name: 'payer', internalType: 'address', type: 'address' }, - { name: 'operator', internalType: 'address', type: 'address' }, - { name: 'rateAllowance', internalType: 'uint256', type: 'uint256' }, - { name: 'rateUsage', internalType: 'uint256', type: 'uint256' }, - { name: 'minimumRateRequired', internalType: 'uint256', type: 'uint256' }, - ], - name: 'InsufficientRateAllowance', - }, { type: 'error', inputs: [ @@ -3006,11 +2811,6 @@ export const filecoinWarmStorageServiceAbi = [ inputs: [{ name: 'length', internalType: 'uint256', type: 'uint256' }], name: 'InvalidServiceNameLength', }, - { - type: 'error', - inputs: [{ name: 'dataSetId', internalType: 'uint256', type: 'uint256' }], - name: 'InvalidTopUpAmount', - }, { type: 'error', inputs: [], name: 'MaxProvingPeriodZero' }, { type: 'error', @@ -3077,14 +2877,6 @@ export const filecoinWarmStorageServiceAbi = [ ], name: 'OnlyPDPVerifierAllowed', }, - { - type: 'error', - inputs: [ - { name: 'payer', internalType: 'address', type: 'address' }, - { name: 'operator', internalType: 'address', type: 'address' }, - ], - name: 'OperatorNotApproved', - }, { type: 'error', inputs: [{ name: 'owner', internalType: 'address', type: 'address' }], @@ -3103,19 +2895,6 @@ export const filecoinWarmStorageServiceAbi = [ ], name: 'PaymentRailsNotFinalized', }, - { - type: 'error', - inputs: [ - { - name: 'priceType', - internalType: 'enum Errors.PriceType', - type: 'uint8', - }, - { name: 'maxAllowed', internalType: 'uint256', type: 'uint256' }, - { name: 'actual', internalType: 'uint256', type: 'uint256' }, - ], - name: 'PriceExceedsMaximum', - }, { type: 'error', inputs: [{ name: 'dataSetId', internalType: 'uint256', type: 'uint256' }], @@ -3344,6 +3123,16 @@ export const filecoinWarmStorageServiceStateViewAbi = [ { name: 'clientDataSetId', internalType: 'uint256', type: 'uint256' }, { name: 'pdpEndEpoch', internalType: 'uint256', type: 'uint256' }, { name: 'providerId', internalType: 'uint256', type: 'uint256' }, + { + name: 'pendingOneTimePayments', + internalType: 'uint96', + type: 'uint96', + }, + { + name: 'lifecycleReserveBalance', + internalType: 'uint96', + type: 'uint96', + }, { name: 'dataSetId', internalType: 'uint256', type: 'uint256' }, ], }, @@ -3370,6 +3159,16 @@ export const filecoinWarmStorageServiceStateViewAbi = [ { name: 'clientDataSetId', internalType: 'uint256', type: 'uint256' }, { name: 'pdpEndEpoch', internalType: 'uint256', type: 'uint256' }, { name: 'providerId', internalType: 'uint256', type: 'uint256' }, + { + name: 'pendingOneTimePayments', + internalType: 'uint96', + type: 'uint96', + }, + { + name: 'lifecycleReserveBalance', + internalType: 'uint96', + type: 'uint96', + }, { name: 'dataSetId', internalType: 'uint256', type: 'uint256' }, ], }, @@ -3389,7 +3188,7 @@ export const filecoinWarmStorageServiceStateViewAbi = [ name: 'getCurrentPricingRates', outputs: [ { name: 'storagePrice', internalType: 'uint256', type: 'uint256' }, - { name: 'minimumRate', internalType: 'uint256', type: 'uint256' }, + { name: 'datasetFee', internalType: 'uint256', type: 'uint256' }, ], stateMutability: 'view', }, @@ -3413,6 +3212,16 @@ export const filecoinWarmStorageServiceStateViewAbi = [ { name: 'clientDataSetId', internalType: 'uint256', type: 'uint256' }, { name: 'pdpEndEpoch', internalType: 'uint256', type: 'uint256' }, { name: 'providerId', internalType: 'uint256', type: 'uint256' }, + { + name: 'pendingOneTimePayments', + internalType: 'uint96', + type: 'uint96', + }, + { + name: 'lifecycleReserveBalance', + internalType: 'uint96', + type: 'uint96', + }, { name: 'dataSetId', internalType: 'uint256', type: 'uint256' }, ], }, @@ -3482,6 +3291,118 @@ export const filecoinWarmStorageServiceStateViewAbi = [ ], stateMutability: 'view', }, + { + type: 'function', + inputs: [], + name: 'getPriceList', + outputs: [ + { + name: 'list', + internalType: 'struct PriceList', + type: 'tuple', + components: [ + { name: 'token', internalType: 'contract IERC20', type: 'address' }, + { + name: 'rates', + internalType: 'struct PriceListRates', + type: 'tuple', + components: [ + { + name: 'storagePerTibPerMonth', + internalType: 'uint256', + type: 'uint256', + }, + { + name: 'datasetFeePerMonth', + internalType: 'uint256', + type: 'uint256', + }, + { + name: 'cdnEgressPerTib', + internalType: 'uint256', + type: 'uint256', + }, + { + name: 'cacheMissEgressPerTib', + internalType: 'uint256', + type: 'uint256', + }, + ], + }, + { + name: 'fees', + internalType: 'struct PriceListFees', + type: 'tuple', + components: [ + { + name: 'createDataSetFee', + internalType: 'uint256', + type: 'uint256', + }, + { + name: 'addPiecesBaseFee', + internalType: 'uint256', + type: 'uint256', + }, + { + name: 'addPiecesPerPieceFee', + internalType: 'uint256', + type: 'uint256', + }, + { + name: 'schedulePieceRemovalsFee', + internalType: 'uint256', + type: 'uint256', + }, + { + name: 'terminateFee', + internalType: 'uint256', + type: 'uint256', + }, + ], + }, + { + name: 'lockups', + internalType: 'struct PriceListLockups', + type: 'tuple', + components: [ + { + name: 'lifecycleReserveTarget', + internalType: 'uint256', + type: 'uint256', + }, + { + name: 'replenishThreshold', + internalType: 'uint256', + type: 'uint256', + }, + { + name: 'defaultLockupPeriod', + internalType: 'uint256', + type: 'uint256', + }, + { + name: 'cdnLockupAmount', + internalType: 'uint256', + type: 'uint256', + }, + { + name: 'cacheMissLockupAmount', + internalType: 'uint256', + type: 'uint256', + }, + { + name: 'cdnLockupPeriod', + internalType: 'uint256', + type: 'uint256', + }, + ], + }, + ], + }, + ], + stateMutability: 'view', + }, { type: 'function', inputs: [{ name: 'providerId', internalType: 'uint256', type: 'uint256' }], @@ -3602,58 +3523,45 @@ export const pdpVerifierAbi = [ type: 'constructor', inputs: [ { name: '_initializerVersion', internalType: 'uint64', type: 'uint64' }, - { name: '_usdfcTokenAddress', internalType: 'address', type: 'address' }, - { name: '_usdfcSybilFee', internalType: 'uint256', type: 'uint256' }, - { - name: '_paymentsContractAddress', - internalType: 'address', - type: 'address', - }, + { name: '_challengeFinality', internalType: 'uint256', type: 'uint256' }, ], stateMutability: 'nonpayable', }, { type: 'function', inputs: [], - name: 'FIL_SYBIL_FEE', + name: 'FIL_CLEANUP_DEPOSIT', outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], stateMutability: 'pure', }, { type: 'function', inputs: [], - name: 'MAX_ENQUEUED_REMOVALS', + name: 'INACTIVITY_WINDOW', outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], stateMutability: 'view', }, { type: 'function', inputs: [], - name: 'MAX_PIECE_SIZE_LOG2', + name: 'LEGACY_ACTIVITY_EPOCH', outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], stateMutability: 'view', }, { type: 'function', inputs: [], - name: 'NO_CHALLENGE_SCHEDULED', + name: 'MAX_ENQUEUED_REMOVALS', outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], stateMutability: 'view', }, { type: 'function', inputs: [], - name: 'NO_PROVEN_EPOCH', + name: 'MAX_PIECE_SIZE_LOG2', outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], stateMutability: 'view', }, - { - type: 'function', - inputs: [], - name: 'PAYMENTS_CONTRACT_ADDRESS', - outputs: [{ name: '', internalType: 'address', type: 'address' }], - stateMutability: 'view', - }, { type: 'function', inputs: [], @@ -3661,20 +3569,6 @@ export const pdpVerifierAbi = [ outputs: [{ name: '', internalType: 'string', type: 'string' }], stateMutability: 'view', }, - { - type: 'function', - inputs: [], - name: 'USDFC_SYBIL_FEE', - outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'function', - inputs: [], - name: 'USDFC_TOKEN_ADDRESS', - outputs: [{ name: '', internalType: 'address', type: 'address' }], - stateMutability: 'view', - }, { type: 'function', inputs: [], @@ -3744,6 +3638,16 @@ export const pdpVerifierAbi = [ outputs: [], stateMutability: 'nonpayable', }, + { + type: 'function', + inputs: [ + { name: 'setId', internalType: 'uint256', type: 'uint256' }, + { name: 'maxPieces', internalType: 'uint256', type: 'uint256' }, + ], + name: 'cleanupPieces', + outputs: [{ name: 'done', internalType: 'bool', type: 'bool' }], + stateMutability: 'nonpayable', + }, { type: 'function', inputs: [ @@ -3982,9 +3886,7 @@ export const pdpVerifierAbi = [ }, { type: 'function', - inputs: [ - { name: '_challengeFinality', internalType: 'uint256', type: 'uint256' }, - ], + inputs: [], name: 'initialize', outputs: [], stateMutability: 'nonpayable', @@ -4428,6 +4330,12 @@ export const pdpVerifierAbi = [ inputs: [{ name: 'target', internalType: 'address', type: 'address' }], name: 'AddressEmptyCode', }, + { type: 'error', inputs: [], name: 'CleanupDepositRequired' }, + { type: 'error', inputs: [], name: 'DataSetAlreadyInCleanup' }, + { type: 'error', inputs: [], name: 'DataSetNotFound' }, + { type: 'error', inputs: [], name: 'DataSetNotInCleanupMode' }, + { type: 'error', inputs: [], name: 'DataSetNotLive' }, + { type: 'error', inputs: [], name: 'DepositTransferFailed' }, { type: 'error', inputs: [ @@ -4436,8 +4344,15 @@ export const pdpVerifierAbi = [ name: 'ERC1967InvalidImplementation', }, { type: 'error', inputs: [], name: 'ERC1967NonPayable' }, + { + type: 'error', + inputs: [ + { name: 'epochs', internalType: 'uint256', type: 'uint256' }, + { name: 'maxDelay', internalType: 'uint256', type: 'uint256' }, + ], + name: 'ExcessiveChallengeDelay', + }, { type: 'error', inputs: [], name: 'FailedCall' }, - { type: 'error', inputs: [], name: 'FilRefundFailed' }, { type: 'error', inputs: [ @@ -4446,8 +4361,19 @@ export const pdpVerifierAbi = [ ], name: 'IndexedError', }, + { + type: 'error', + inputs: [ + { name: 'epochs', internalType: 'uint256', type: 'uint256' }, + { name: 'minDelay', internalType: 'uint256', type: 'uint256' }, + ], + name: 'InsufficientChallengeDelay', + }, { type: 'error', inputs: [], name: 'InvalidInitialization' }, + { type: 'error', inputs: [], name: 'MaxPiecesMustBePositive' }, { type: 'error', inputs: [], name: 'NotInitializing' }, + { type: 'error', inputs: [], name: 'OnlyStorageProviderCanCleanupPieces' }, + { type: 'error', inputs: [], name: 'OnlyStorageProviderCanDelete' }, { type: 'error', inputs: [{ name: 'owner', internalType: 'address', type: 'address' }], @@ -4458,13 +4384,13 @@ export const pdpVerifierAbi = [ inputs: [{ name: 'account', internalType: 'address', type: 'address' }], name: 'OwnableUnauthorizedAccount', }, + { type: 'error', inputs: [], name: 'TransferFailed' }, { type: 'error', inputs: [], name: 'UUPSUnauthorizedCallContext' }, { type: 'error', inputs: [{ name: 'slot', internalType: 'bytes32', type: 'bytes32' }], name: 'UUPSUnsupportedProxiableUUID', }, - { type: 'error', inputs: [], name: 'UsdfcSybilFeeNotMet' }, ] as const /** diff --git a/packages/synapse-core/src/abis/index.ts b/packages/synapse-core/src/abis/index.ts index 37fd8b77c..a92798bab 100644 --- a/packages/synapse-core/src/abis/index.ts +++ b/packages/synapse-core/src/abis/index.ts @@ -13,15 +13,11 @@ export * from './erc20.ts' export * as generated from './generated.ts' import * as generated from './generated.ts' -import { priceListAbi } from './price-list.ts' // Merge the storage and errors ABIs export const fwss = [...generated.filecoinWarmStorageServiceAbi, ...generated.errorsAbi] as const export const serviceProviderRegistry = [...generated.serviceProviderRegistryAbi, ...generated.errorsAbi] as const -// The view ABI plus the standalone getPriceList fragment. See abis/price-list.ts. -// TODO: drop the priceListAbi merge and re-export filecoinWarmStorageServiceStateViewAbi -// as fwssView once the generated ABI ref includes getPriceList. -export const fwssView = [...generated.filecoinWarmStorageServiceStateViewAbi, ...priceListAbi] as const +export const fwssView = generated.filecoinWarmStorageServiceStateViewAbi export { filecoinPayV1Abi as filecoinPay, diff --git a/packages/synapse-core/src/abis/price-list.ts b/packages/synapse-core/src/abis/price-list.ts deleted file mode 100644 index 381aa0b97..000000000 --- a/packages/synapse-core/src/abis/price-list.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * `getPriceList()` view fragment for `FilecoinWarmStorageServiceStateView`, - * mirroring the on-chain `PriceList` struct from - * [FilOzone/filecoin-services#501](https://github.com/FilOzone/filecoin-services/pull/501). - * - * TODO: remove this file and the `fwssView` merge in `abis/index.ts` once - * `FILECOIN_SERVICES_GIT_REF` (`wagmi.config.ts`) points at a release whose - * generated view ABI exposes `getPriceList`. - */ -export const priceListAbi = [ - { - type: 'function', - inputs: [], - name: 'getPriceList', - outputs: [ - { - name: 'list', - internalType: 'struct PriceList', - type: 'tuple', - components: [ - { name: 'token', internalType: 'contract IERC20', type: 'address' }, - { - name: 'rates', - internalType: 'struct PriceListRates', - type: 'tuple', - components: [ - { name: 'storagePerTibPerMonth', internalType: 'uint256', type: 'uint256' }, - { name: 'datasetFeePerMonth', internalType: 'uint256', type: 'uint256' }, - { name: 'cdnEgressPerTib', internalType: 'uint256', type: 'uint256' }, - { name: 'cacheMissEgressPerTib', internalType: 'uint256', type: 'uint256' }, - ], - }, - { - name: 'fees', - internalType: 'struct PriceListFees', - type: 'tuple', - components: [ - { name: 'createDataSetFee', internalType: 'uint256', type: 'uint256' }, - { name: 'addPiecesBaseFee', internalType: 'uint256', type: 'uint256' }, - { name: 'addPiecesPerPieceFee', internalType: 'uint256', type: 'uint256' }, - { name: 'schedulePieceRemovalsFee', internalType: 'uint256', type: 'uint256' }, - { name: 'terminateFee', internalType: 'uint256', type: 'uint256' }, - ], - }, - { - name: 'lockups', - internalType: 'struct PriceListLockups', - type: 'tuple', - components: [ - { name: 'lifecycleReserveTarget', internalType: 'uint256', type: 'uint256' }, - { name: 'replenishThreshold', internalType: 'uint256', type: 'uint256' }, - { name: 'defaultLockupPeriod', internalType: 'uint256', type: 'uint256' }, - { name: 'cdnLockupAmount', internalType: 'uint256', type: 'uint256' }, - { name: 'cacheMissLockupAmount', internalType: 'uint256', type: 'uint256' }, - { name: 'cdnLockupPeriod', internalType: 'uint256', type: 'uint256' }, - ], - }, - ], - }, - ], - stateMutability: 'view', - }, -] as const diff --git a/packages/synapse-core/src/mocks/jsonrpc/index.ts b/packages/synapse-core/src/mocks/jsonrpc/index.ts index 0f8380036..67df3d1e8 100644 --- a/packages/synapse-core/src/mocks/jsonrpc/index.ts +++ b/packages/synapse-core/src/mocks/jsonrpc/index.ts @@ -421,6 +421,8 @@ export const presets = { clientDataSetId: 0n, pdpEndEpoch: 0n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, cdnEndEpoch: 0n, dataSetId: 1n, }, @@ -443,6 +445,8 @@ export const presets = { pdpEndEpoch: 0n, pdpRailId: 1n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, serviceProvider: ADDRESSES.serviceProvider1, }, ] @@ -459,6 +463,8 @@ export const presets = { pdpEndEpoch: 0n, pdpRailId: 0n, providerId: 0n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, serviceProvider: ADDRESSES.zero, }, ] diff --git a/packages/synapse-core/test/get-account-total-storage-size.test.ts b/packages/synapse-core/test/get-account-total-storage-size.test.ts index c39c3b9fa..4b390c55d 100644 --- a/packages/synapse-core/test/get-account-total-storage-size.test.ts +++ b/packages/synapse-core/test/get-account-total-storage-size.test.ts @@ -144,6 +144,8 @@ function makeDataSet(dataSetId: bigint) { clientDataSetId: 0n, pdpEndEpoch: 0n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, cdnEndEpoch: 0n, dataSetId, } diff --git a/packages/synapse-core/test/get-client-data-sets.test.ts b/packages/synapse-core/test/get-client-data-sets.test.ts index 1488abe1f..6a7e9819c 100644 --- a/packages/synapse-core/test/get-client-data-sets.test.ts +++ b/packages/synapse-core/test/get-client-data-sets.test.ts @@ -321,6 +321,8 @@ function makeDataSet(dataSetId: bigint) { clientDataSetId: 0n, pdpEndEpoch: 0n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, dataSetId, } } diff --git a/packages/synapse-core/test/get-data-set.test.ts b/packages/synapse-core/test/get-data-set.test.ts index 98d2467c3..2b8863d3b 100644 --- a/packages/synapse-core/test/get-data-set.test.ts +++ b/packages/synapse-core/test/get-data-set.test.ts @@ -81,6 +81,8 @@ describe('getDataSet', () => { pdpEndEpoch: 0n, pdpRailId: 1n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, serviceProvider: ADDRESSES.serviceProvider1, }) }) diff --git a/packages/synapse-core/test/resolve-piece-url.test.ts b/packages/synapse-core/test/resolve-piece-url.test.ts index a15bafb88..c56b4ae1f 100644 --- a/packages/synapse-core/test/resolve-piece-url.test.ts +++ b/packages/synapse-core/test/resolve-piece-url.test.ts @@ -319,6 +319,8 @@ describe('resolve-piece-url', () => { clientDataSetId: 0n, pdpEndEpoch: 0n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, cdnEndEpoch: 0n, dataSetId: 1n, }, @@ -333,6 +335,8 @@ describe('resolve-piece-url', () => { clientDataSetId: 1n, pdpEndEpoch: 0n, providerId: 2n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, cdnEndEpoch: 0n, dataSetId: 2n, }, @@ -347,6 +351,8 @@ describe('resolve-piece-url', () => { clientDataSetId: 2n, pdpEndEpoch: 1n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, cdnEndEpoch: 0n, dataSetId: 3n, }, diff --git a/packages/synapse-core/test/terminate-service.test.ts b/packages/synapse-core/test/terminate-service.test.ts index 308578e59..91dc9e41f 100644 --- a/packages/synapse-core/test/terminate-service.test.ts +++ b/packages/synapse-core/test/terminate-service.test.ts @@ -120,7 +120,7 @@ describe('terminateService', () => { abi: Abis.fwss, eventName: 'ServiceTerminated', args: { - caller: ADDRESSES.client1, + approver: ADDRESSES.client1, dataSetId, }, }) @@ -198,9 +198,9 @@ describe('terminateService', () => { assert.ok(event) assert.equal(event.eventName, 'ServiceTerminated') - assert.ok(event.args.caller) + assert.ok(event.args.approver) assert.equal(event.args.dataSetId, dataSetId) - assert.equal(event.args.caller.toLowerCase(), ADDRESSES.client1.toLowerCase()) + assert.equal(event.args.approver.toLowerCase(), ADDRESSES.client1.toLowerCase()) if (event.eventName === 'ServiceTerminated') { assert.equal(event.args.pdpRailId, pdpRailId) } @@ -218,7 +218,7 @@ describe('terminateService', () => { abi: Abis.fwss, eventName: 'ServiceTerminated', args: { - caller: ADDRESSES.client1, + approver: ADDRESSES.client1, dataSetId, }, }) @@ -305,7 +305,7 @@ describe('terminateService', () => { abi: Abis.fwss, eventName: 'ServiceTerminated', args: { - caller: ADDRESSES.client1, + approver: ADDRESSES.client1, dataSetId, }, }) diff --git a/packages/synapse-core/wagmi.config.ts b/packages/synapse-core/wagmi.config.ts index 102827621..f5aef1ee7 100644 --- a/packages/synapse-core/wagmi.config.ts +++ b/packages/synapse-core/wagmi.config.ts @@ -7,7 +7,7 @@ import { ZodValidationError } from './src/errors/base.ts' import { zAddress, zAddressLoose } from './src/utils/schemas.ts' // GIT_REF can be one of: '', '' or 'tags/' -const FILECOIN_SERVICES_GIT_REF = '4e548903095cfb46bc35af029f2ae0f39f18b8e4' // v1.2.1 +const FILECOIN_SERVICES_GIT_REF = '02de64a17847f59262b535ab548cae6be307917f' // main const FILECOIN_SERVICES_REF = FILECOIN_SERVICES_GIT_REF.replace(/^(?![a-f0-9]{40}$)/, 'refs/') const BASE_URL = `https://raw.githubusercontent.com/FilOzone/filecoin-services/${FILECOIN_SERVICES_REF}/service_contracts/abi` const DEPLOYMENTS_URL = `https://raw.githubusercontent.com/FilOzone/filecoin-services/${FILECOIN_SERVICES_REF}/service_contracts/deployments.json` diff --git a/packages/synapse-sdk/src/test/metadata-selection.test.ts b/packages/synapse-sdk/src/test/metadata-selection.test.ts index a732a6b20..ab676300f 100644 --- a/packages/synapse-sdk/src/test/metadata-selection.test.ts +++ b/packages/synapse-sdk/src/test/metadata-selection.test.ts @@ -44,6 +44,8 @@ describe('Metadata-based Data Set Selection', () => { clientDataSetId: 0n, pdpEndEpoch: 0n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, cdnEndEpoch: 0n, dataSetId: 1n, }, @@ -58,6 +60,8 @@ describe('Metadata-based Data Set Selection', () => { clientDataSetId: 1n, pdpEndEpoch: 0n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, cdnEndEpoch: 0n, dataSetId: 2n, }, @@ -72,6 +76,8 @@ describe('Metadata-based Data Set Selection', () => { clientDataSetId: 2n, pdpEndEpoch: 0n, providerId: 2n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, cdnEndEpoch: 0n, dataSetId: 3n, }, diff --git a/packages/synapse-sdk/src/test/storage.test.ts b/packages/synapse-sdk/src/test/storage.test.ts index 3110e2fe6..bc266490b 100644 --- a/packages/synapse-sdk/src/test/storage.test.ts +++ b/packages/synapse-sdk/src/test/storage.test.ts @@ -293,6 +293,8 @@ describe('StorageService', () => { pdpEndEpoch: 0n, pdpRailId: 1n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, serviceProvider: Mocks.ADDRESSES.serviceProvider1, } const expectedDataSets = [ @@ -408,6 +410,8 @@ describe('StorageService', () => { pdpEndEpoch: 0n, pdpRailId: 1n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, serviceProvider: Mocks.ADDRESSES.serviceProvider1, }, ] @@ -424,6 +428,8 @@ describe('StorageService', () => { pdpEndEpoch: 0n, pdpRailId: 2n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, serviceProvider: Mocks.ADDRESSES.serviceProvider1, }, ] @@ -567,6 +573,8 @@ describe('StorageService', () => { pdpEndEpoch: 0n, pdpRailId: 1n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, serviceProvider: Mocks.ADDRESSES.serviceProvider1, }, { @@ -580,6 +588,8 @@ describe('StorageService', () => { pdpEndEpoch: 0n, pdpRailId: 2n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, serviceProvider: Mocks.ADDRESSES.serviceProvider1, }, ], @@ -649,6 +659,8 @@ describe('StorageService', () => { pdpEndEpoch: 0n, pdpRailId: 1n, providerId: 3n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, serviceProvider: Mocks.ADDRESSES.serviceProvider1, }, ] @@ -694,6 +706,8 @@ describe('StorageService', () => { pdpEndEpoch: 0n, pdpRailId: 1n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, serviceProvider: Mocks.ADDRESSES.serviceProvider1, }, ] @@ -733,6 +747,8 @@ describe('StorageService', () => { pdpEndEpoch: 0n, pdpRailId: 1n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, serviceProvider: Mocks.ADDRESSES.serviceProvider1, }, ] @@ -793,6 +809,8 @@ describe('StorageService', () => { pdpEndEpoch: 0n, pdpRailId: 1n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, serviceProvider: Mocks.ADDRESSES.serviceProvider1, }, ] diff --git a/packages/synapse-sdk/src/test/synapse.test.ts b/packages/synapse-sdk/src/test/synapse.test.ts index b2b2ecf8c..57d9f2399 100644 --- a/packages/synapse-sdk/src/test/synapse.test.ts +++ b/packages/synapse-sdk/src/test/synapse.test.ts @@ -600,6 +600,8 @@ describe('Synapse', () => { pdpEndEpoch: 0n, pdpRailId: dataSetId, providerId: 1n, // Same provider for both + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, serviceProvider: Mocks.ADDRESSES.serviceProvider1, cdnEndEpoch: 0n, }, diff --git a/packages/synapse-sdk/src/test/warm-storage-service.test.ts b/packages/synapse-sdk/src/test/warm-storage-service.test.ts index 671464231..8f85b2da2 100644 --- a/packages/synapse-sdk/src/test/warm-storage-service.test.ts +++ b/packages/synapse-sdk/src/test/warm-storage-service.test.ts @@ -135,6 +135,8 @@ describe('WarmStorageService', () => { clientDataSetId: 0n, pdpEndEpoch: 0n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, cdnEndEpoch: 0n, dataSetId: 1n, }, @@ -149,6 +151,8 @@ describe('WarmStorageService', () => { clientDataSetId: 1n, pdpEndEpoch: 0n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, cdnEndEpoch: 0n, dataSetId: 2n, }, @@ -204,6 +208,8 @@ describe('WarmStorageService', () => { clientDataSetId: 0n, pdpEndEpoch: 0n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, cdnEndEpoch: 0n, dataSetId: 1n, }, @@ -225,6 +231,8 @@ describe('WarmStorageService', () => { clientDataSetId: 1n, pdpEndEpoch: 0n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, cdnEndEpoch: 0n, dataSetId: 2n, }, @@ -424,6 +432,8 @@ describe('WarmStorageService', () => { clientDataSetId: 0n, pdpEndEpoch: 0n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, dataSetId: 242n, }, ], @@ -472,6 +482,8 @@ describe('WarmStorageService', () => { clientDataSetId: 0n, pdpEndEpoch: 0n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, dataSetId: 242n, }, { @@ -485,6 +497,8 @@ describe('WarmStorageService', () => { clientDataSetId: 1n, pdpEndEpoch: 0n, providerId: 2n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, dataSetId: 243n, }, ], @@ -545,6 +559,8 @@ describe('WarmStorageService', () => { clientDataSetId: 0n, pdpEndEpoch: 0n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, dataSetId: 242n, }, ], @@ -593,6 +609,8 @@ describe('WarmStorageService', () => { clientDataSetId: 0n, pdpEndEpoch: 0n, providerId: 1n, + pendingOneTimePayments: 0n, + lifecycleReserveBalance: 0n, dataSetId: 242n, }, ],