Skip to content

Conversation

@rvagg
Copy link
Collaborator

@rvagg rvagg commented Oct 31, 2025

Two commits in here. I was originally solving for #360 and then the deeper I got the worse I realised it was. We have a lot of CDN stuff still in there, floor pricing impacts a lot of things, there's mismatch in arguments and return types. So I've tried to be consistent; I've removed "perDay" and "perEpoch" from a bunch of places, leaving just the standardised "perMonth" we do everywhere else.

I'm hoping that this will be able to replace a bunch of things in filecoin-pin. @SgtPooki when you have a spare moment would you mind having a quick think about the coverage of functionality we have here? filecoin-pin is currently not dealing with the floor price, although it is updated to the 30 day lockup. I've also pulled in all of the detailed service readiness checks in here.


refactor(sdk): decompose preflight into reusable primitives

  • Add PaymentsService.checkServiceReadiness() for any service
  • Rename calculateStorageCost() to calculateUploadCost(), expose floor pricing
  • Simplify prepareStorageUpload() signature (dataSize only, monthly cost only)
  • Remove checkAllowanceForStorage()
  • Add getContractAddress(), export ServiceReadinessCheck type

refactor(sdk): rename StorageInfo → ServiceInfo, restructure pricing

  • Rename getStorageInfo() → getServiceInfo()
  • Restructure pricing fields for clarity
  • Simplify allowance checks

Fixes: #360


So we can now get this kind of information from Synapse, this is from example-storage-info.js with my wallet that's not set up for current contracts:

--- Pricing Information ---
Token: USDFC (0xb3042734b608a1B16e9e86B374A3f3e389B4cDf0)

Base Storage:
  Per TiB per month:        2.500000 USDFC
  Minimum price per month:  0.060000 USDFC

CDN Usage-Based Pricing (only for data sets with CDN enabled):
  CDN egress per TiB:       7.000000 USDFC
  Cache miss egress per TiB: 7.000000 USDFC

--- Service Providers ---
Total providers: 2

Provider #2:
  Name:        ezpdpz-calib2
  Description: PDP Calibration node based in the UK
  Address:     0xbCdf1bdc1a97D071a5a8EF03F1F05225b6E2a1Ba
  Payee:       0xbCdf1bdc1a97D071a5a8EF03F1F05225b6E2a1Ba
  Active:      true
  Service URL: https://calib2.ezpdpz.net
  PDP Service:
    Min size:  1 MB
    Max size:  1 GB
    Price:     0.000000 USDFC/TiB/month
    Location:  C=GB;ST=Gloucestershire;L=Cheltenham

Provider #1:
  Name:        Kubuxu's dev node
  Description: Do not rely on it at all
  Address:     0x8c8c7a9BE47ed491B33B941fBc0276BD2ec25E7e
  Payee:       0x8c8c7a9BE47ed491B33B941fBc0276BD2ec25E7e
  Active:      true
  Service URL: https://pdp-dev.kubuxu.com
  PDP Service:
    Min size:  1 MB
    Max size:  64 GB
    Price:     0.000000 USDFC/TiB/month
    Location:  C=FI;ST=Uusimaa;L=Helsinki

--- Service Parameters ---
Network:          calibration
Epochs per month: 86,400
Epochs per day:   2,880
Epoch duration:   30 seconds
Min upload size:  127 Bytes
Max upload size:  200 MB

Contract Addresses:
  Warm Storage:                  0x02925630df557F957f70E112bA06e50965417CA0
  Payments:                      0x09a0fDc2723fAd1A7b8e3e00eE5DF73841df55a0
  PDP Verifier:                  0x85e366Cf9DD2c0aE37E963d9556F5f4718d6417C
  Service Provider Registry:     0x839e5c9988e4e9977d40708d0094103c0839Ac9D
  Session Key Registry:          0x97Dd879F5a97A8c761B94746d7F5cfF50AAd4452

--- Current Allowances ---
Service: 0x02925630df557F957f70E112bA06e50965417CA0

Rate:
  Allowance:  0.000000 USDFC
  Used:       0.000000 USDFC
  Available:  0.000000 USDFC

Lockup:
  Allowance:  0.000000 USDFC
  Used:       0.000000 USDFC
  Available:  0.000000 USDFC

--- Account Balance ---
Total funds:      1.000000 USDFC
Available funds:  1.000000 USDFC
Locked up:        0.000000 USDFC

--- Upload Cost Examples ---

1 MiB:
  Base monthly cost:  0.000002 USDFC
  With floor pricing: 0.060000 USDFC

100 MiB:
  Base monthly cost:  0.000238 USDFC
  With floor pricing: 0.060000 USDFC

1 GiB:
  Base monthly cost:  0.002441 USDFC
  With floor pricing: 0.060000 USDFC

10 GiB:
  Base monthly cost:  0.024414 USDFC
  With floor pricing: 0.060000 USDFC

--- Wallet Readiness Check ---
Checking readiness for 1 GiB upload (0.060000 USDFC/month)...

Readiness: ❌ NOT READY

Checks:
  ✓ Operator approved:     ❌
  ✓ Sufficient funds:      ✅
  ✓ Rate allowance:        ❌
  ✓ Lockup allowance:      ❌
  ✓ Valid lockup period:   ❌

Required actions:
  - Increase rate allowance by 0.000001 USDFC
  - Increase lockup allowance by 0.060000 USDFC
  - Extend lockup period by 86400 epochs

@github-project-automation github-project-automation bot moved this to 📌 Triage in FS Oct 31, 2025
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Oct 31, 2025

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
synapse-dev a94dde6 Commit Preview URL

Branch Preview URL
Nov 04 2025, 09:45 AM

@rjan90 rjan90 moved this from 📌 Triage to 🔎 Awaiting review in FS Oct 31, 2025
Comment on lines +827 to 856
if (readiness.gaps?.fundsNeeded) {
const fundsNeeded = readiness.gaps.fundsNeeded
actions.push({
type: 'deposit',
description: `Deposit ${depositAmount} USDFC to payments contract`,
execute: async () => await paymentsService.deposit(depositAmount, TOKENS.USDFC),
description: `Deposit ${fundsNeeded} USDFC to payments contract`,
execute: async () => await paymentsService.deposit(fundsNeeded, TOKENS.USDFC),
})
}

// Check if service approval is needed
if (!allowanceCheck.sufficient) {
if (
!readiness.checks.isOperatorApproved ||
readiness.gaps?.rateAllowanceNeeded ||
readiness.gaps?.lockupAllowanceNeeded
) {
const rateAllowanceNeeded = readiness.gaps?.rateAllowanceNeeded ?? 0n
const lockupAllowanceNeeded = readiness.gaps?.lockupAllowanceNeeded ?? 0n
actions.push({
type: 'approveService',
description: `Approve service with rate allowance ${allowanceCheck.rateAllowanceNeeded} and lockup allowance ${allowanceCheck.lockupAllowanceNeeded}`,
description: `Approve service with rate allowance ${rateAllowanceNeeded} and lockup allowance ${lockupAllowanceNeeded}`,
execute: async () =>
await paymentsService.approveService(
this._warmStorageAddress,
allowanceCheck.rateAllowanceNeeded,
allowanceCheck.lockupAllowanceNeeded,
TIME_CONSTANTS.EPOCHS_PER_MONTH, // 30 days max lockup period
rateAllowanceNeeded,
lockupAllowanceNeeded,
lockupEpochs,
TOKENS.USDFC
),
})
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actions consolidation using Permit: This will reduce actions to only one.

if (fundsNeeded && approvalNeeded) {
  actions.push({
    type: 'DepositAndApprove',
    description: `Deposit ${fundsNeeded} USDFC and approve service in one transaction`,
    execute: async () => await paymentsService.depositWithPermitAndApproveOperator(
      fundsNeeded, 
      this._warmStorageAddress, 
      rateAllowanceNeeded, 
      lockupAllowanceNeeded, 
      lockupEpochs, 
      TOKENS.USDFC
    ),
  })
}

if (approvalNeeded && !fundsNeeded) {
  actions.push({
    type: 'approveService',
    description: `Approve service with rate allowance ${rateAllowanceNeeded} and lockup allowance ${lockupAllowanceNeeded}`,
    execute: async () =>
      await paymentsService.approveService(
        this._warmStorageAddress,
        rateAllowanceNeeded,
        lockupAllowanceNeeded,
        lockupEpochs,
        TOKENS.USDFC
      ),
  })
}

if (fundsNeeded && !approvalNeeded) {
  actions.push({
    type: 'deposit',
    description: `Deposit ${fundsNeeded} USDFC to payments contract`,
    execute: async () => await paymentsService.deposit(fundsNeeded, TOKENS.USDFC),
  })
}

* }
* ```
*/
async prepareStorageUpload(
Copy link

@nijoe1 nijoe1 Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle prepayment for cdnDataSet creation

*improvement

  async prepareStorageUpload(
    dataSize: number,
    paymentsService: PaymentsService,
    // If true, adds 1 USDFC to lockup for CDN dataset creation (users must prepay egress credits)
    creationOfCDNDataset
  ): Promise<{
    estimatedCostPerMonth: bigint
    allowanceCheck: {
      sufficient: boolean
      message?: string
    }
    actions: Array<{
      type: 'deposit' | 'approve' | 'approveService'
      description: string
      execute: () => Promise<ethers.TransactionResponse>
    }>
  }> 

CDN Dataset Creation Logic

// Add 1 USDFC lockup for CDN dataset creation
// CDN datasets require users to prepay 1 USDFC for egress credits balance upfront
let additionalLockupNeeded = 0n
if (creationOfCDNDataset) {
   additionalLockupNeeded = 10n ** 18n // 1 USDFC in wei (18 decimals)
}

Lockup Calculation Update

// Calculate total lockup: (rate per epoch * lockup epochs) + CDN dataset fee (if applicable)
const lockupNeeded = ratePerEpoch * lockupEpochs + additionalLockupNeeded

Handles prepayment for egress credits on cdnDataSet creation

Problem

When creating a CDN-enabled dataset for the first time, users must prepay 1 USDFC for egress credits balance.
Without this flag, the upload fails with "Insufficient lockup" error.

Detection Pattern

// In user code (SDK usage):
const context = await synapse.storage.createContext({ withCDN: true })

// Detect first-time CDN dataset creation
const isCreatingNewCDNDataset = context.dataSetId === null && context.withCDN
//                               ^^^^^^^^^^^^^^^^^^^^^^    ^^^^^^^^^^^^^^
//                               No existing dataset       CDN requested

// Pass flag to prevent lockup calculation errors
const { actions } = await warmStorageService.prepareStorageUpload(
  dataSize,
  paymentsService,
  isCreatingNewCDNDataset  // ← Adds 1 USDFC to lockup requirements
)

for (const action of actions) {
  await action.execute();
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative idea create a similar method inside StorageContext

Additional Improvements

  • Check if userWalletBalance >= depositNeeded and return sufficientBalance
  • Bring back the lockupDays parameter that checkAllowanceForStorage had. This allows developers to pay for storage for more than 30 days, giving them this flexibility.
// Simple, clean API - context knows its own state
const context = await synapse.storage.createContext({ withCDN: true })

// Context automatically detects if it's creating a new CDN dataset
const preparation = await context.prepareStorageUpload(dataSize)

// Clear balance handling with descriptive response
if (!preparation.sufficientBalance) {
  throw new Error("Insufficient USDFC balance. Please top up your wallet.")
}

// Execute required actions
for (const action of preparation.actions) {
  await action.execute()
}

// Upload proceeds with confidence
await context.upload(data)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would rather deal with CDN in another PR, i still dont understand how that works and this 1 usdfc for cdn dataset seems like we going back..

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know about this CDN thing, it is kind of important. I've opened an issue for the contract here: FilOzone/filecoin-services#339 and I'll consider it here as well.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i scheduled a sync with the cdn team for today to understand cdn payments better

Copy link
Collaborator

@SgtPooki SgtPooki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few things here that will help reduce specifics in filecoin-pin:

  1. Readiness checkscheckServiceReadiness() can replace parts of our custom validatePaymentCapacity() and checkUploadReadiness() logic. We'll still need the runway-aware checks below.

  2. Cost calculationcalculateUploadCost() with floor pricing will replace our calculateRequiredAllowances() calls, once we integrate floor pricing.

What is missing that filecoin-pin needs (runway management):

filecoin-pin adds runway/duration management not covered here:

  1. Runway calculationscomputeTopUpForDuration() computes how many days current funding covers based on rateUsed + lockupUsed + balance.
  2. Adjustment calculationscomputeAdjustmentForExactDays() / computeAdjustmentForExactDaysWithPiece() calculate exact deposits needed to maintain N days runway (including after adding new content).
  3. Top-up orchestrationcalculateRequiredTopUp() combines piece upload needs + runway requirements with reason codes.

rateUsed: bigint
lockupAllowance: bigint
lockupUsed: bigint
maxLockupPeriod: bigint
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we explain these a little more here? What exactly is maxLockupPeriod? the max period I will allow a service to lockup funds for? or the maximum lockup period that a service provider requires?

Copy link
Collaborator

@hugomrdias hugomrdias left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This all seems very messy, i think we are trying to shoehorn the old methods into a system that changed a bit (plus cdn egress that i dont understand).
We should redesign from first principles a better api.

requirements: {
rateNeeded: bigint
lockupNeeded: bigint
lockupPeriodNeeded: bigint
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this as an option ?

isnt it a constant BigInt(TIME_CONSTANTS.DEFAULT_LOCKUP_DAYS * TIME_CONSTANTS.EPOCHS_PER_DAY) ?

Comment on lines +706 to +813
const pricing = await warmStorageService.getServicePrice()
const ratePerEpoch = cost.withFloorPerMonth / pricing.epochsPerMonth

// Calculate lockup requirements
const lockupEpochs = BigInt(TIME_CONSTANTS.DEFAULT_LOCKUP_DAYS * TIME_CONSTANTS.EPOCHS_PER_DAY)
const lockupNeeded = ratePerEpoch * lockupEpochs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really dont like this per month thing its inaccurate and forces us to do this back and forth stuff epoch<>month exchange.

also we just called getServicePrice in calculateUploadCost and we call it again just to get

https://github.com/FilOzone/filecoin-services/blob/980e25aeb920bb894eab71a3dba9e8c9bccddcae/service_contracts/src/FilecoinWarmStorageService.sol#L200

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

point taken on the double call to getServicePrice, could just do that once.

really dont like this per month thing

but the problem is twofold:

  1. the contract defines pricing per month
  2. downscaling to per-epoch loses a lot of precision (a surprising amount since we're dealing with fairly low numbers in the first place)

the logic here is intentionally keeping things at per-month for as long as possible to avoid the early precision loss of going per-epoch and then back again.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok makes sense the per month thing, but isnt the precision off already like, which month? the amount of 30s in a month varies a lot in different months ?

Comment on lines +724 to +846
// Build error message if not sufficient
let message: string | undefined
if (!readiness.sufficient) {
const issues: string[] = []
if (!readiness.checks.hasSufficientFunds && readiness.gaps?.fundsNeeded) {
issues.push(`Insufficient funds: need ${readiness.gaps.fundsNeeded} more`)
}
if (!readiness.checks.isOperatorApproved) {
issues.push('Operator not approved for service')
}
if (!readiness.checks.hasRateAllowance && readiness.gaps?.rateAllowanceNeeded) {
issues.push(`Insufficient rate allowance: need ${readiness.gaps.rateAllowanceNeeded} more`)
}
if (!readiness.checks.hasLockupAllowance && readiness.gaps?.lockupAllowanceNeeded) {
issues.push(`Insufficient lockup allowance: need ${readiness.gaps.lockupAllowanceNeeded} more`)
}
if (!readiness.checks.hasValidLockupPeriod && readiness.gaps?.lockupPeriodNeeded) {
issues.push(`Lockup period too short: need ${readiness.gaps.lockupPeriodNeeded} more epochs`)
}
message = issues.join('; ')
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo preflight and readiness can be merged into one function and throw and good error with most of this stuff in it.

* }
* ```
*/
async prepareStorageUpload(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would rather deal with CDN in another PR, i still dont understand how that works and this 1 usdfc for cdn dataset seems like we going back..

Comment on lines +801 to +805
const cost = await this.calculateUploadCost(dataSize)

// Calculate requirements
const pricing = await this.getServicePrice()
const ratePerEpoch = cost.withFloorPerMonth / pricing.epochsPerMonth
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again calculateUploadCost and getServicePrice just for pricing.epochsPerMonth

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ugh, yeah

@github-project-automation github-project-automation bot moved this from 🔎 Awaiting review to ⌨️ In Progress in FS Nov 3, 2025
rvagg added 2 commits November 4, 2025 20:16
- Rename getStorageInfo() → getServiceInfo()
- Restructure pricing fields for clarity
- Simplify allowance checks

Fixes: #360
- Add PaymentsService.checkServiceReadiness() for any service
- Rename calculateStorageCost() to calculateUploadCost(), expose floor pricing
- Simplify prepareStorageUpload() signature (dataSize only, monthly cost only)
- Remove checkAllowanceForStorage()
- Add getContractAddress(), export ServiceReadinessCheck type
@rvagg
Copy link
Collaborator Author

rvagg commented Nov 4, 2025

We should redesign from first principles a better api.

Yeah, I agree .. the more I look at this the more I hate it from a consumption perspective. A lot of this isn't as helpful as it should be.

From the experience with Filecoin Pin (and what we're trying to do with filecoin-project/filecoin-pin#76, and also see filecoin-project/filecoin-pin#202 (comment)) is solve for: do one transaction (if required) that sets me up to use the system. Taken further, we when we add storage to that, we should also be able to make it: do one transaction (if required) that sets me up to perform this upload.

The combination of deposit and depositWithPermitAndApproveOperator I think gives us just one transaction in each of the necessary cases. And we also have a bunch of failure conditions where we can't perform that operation (wallet not found, don't have USDFC, don't have enough USDFC).

How about we do this: strip out all of this complexity and replace it with a single method: synapse.storage.prepare(): Transaction (could be something more helpful like: { type: ['deposit', 'deposit-and-approve-operator', 'approve-operator'], Transaction }). It throws in the cases it can't solve, but where it can solve it it's allowed to submit one transaction. It can do the calculations you need based on what the system requires, and what your input requirements are and this can solve for. We take the various options we've added into Filecoin Pin wrt runway calculation and add them here. We also allow for proving a list of contexts (multi-context style, for multi-uploads) so it can perform calculations based on what you already have in those data sets and get you the runway you need.

Basically we want to take away the pain of figuring out all of this stuff for practical use: Set me up to store this thing, on top of what I'm already storing, and I want it to be paid for this far into the future.

@rvagg rvagg marked this pull request as draft November 4, 2025 09:43
@rvagg rvagg force-pushed the rvagg/service-pricing branch from 7fc2358 to a94dde6 Compare November 4, 2025 09:43
@hugomrdias
Copy link
Collaborator

hugomrdias commented Nov 4, 2025

storage.prepare sounds good! what about current costs and current prepared upload costs?

@rvagg
Copy link
Collaborator Author

rvagg commented Nov 5, 2025

what about current costs and current prepared upload costs

you mean in terms of current storage costs and up-coming storage costs? I think those just become options.

prepare({
  context?: StorageContext | StorageContext[], // existing data set(s) that may have pieces to account for
  dataSize?: number // new data to be added
  runwayEpochs?: number // number of epochs (or days?) to ensure we have enough funds to cover on top of the required lock-up
})

The "months" thing is just 30 days, we have a constant for it locally in constants.ts, and in the contract (as witnessed above) 2880*30. We just treat a storage "month" as 30 days. Anything fancier than that is on you. We can't align to calendar months like a web2 storage service would do for you. I guess we should make that crystal clear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ⌨️ In Progress

Development

Successfully merging this pull request may close these issues.

StorageInfo from getStorageInfo() should reflect new storage pricing

5 participants