Skip to content

Commit ea2a6c3

Browse files
feat!: New synapse costs API (#632)
Co-authored-by: Julian Gruber <julian@juliangruber.com>
1 parent 7d9bf30 commit ea2a6c3

47 files changed

Lines changed: 4468 additions & 1016 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/src/content/docs/developer-guides/payments/payment-operations.mdx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,29 @@ This requires two transactions and higher gas costs. Use `depositWithPermit` ins
7878

7979
</details>
8080

81+
### Account Summary
82+
83+
Get a comprehensive snapshot of your account's payment state in a single call. This is the recommended way to check account health — it returns derived values like debt, lockup breakdown, and funded-until epoch.
84+
85+
```ts twoslash
86+
// @lib: esnext,dom
87+
import { Synapse, formatUnits } from "@filoz/synapse-sdk";
88+
import { privateKeyToAccount } from 'viem/accounts'
89+
const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' });
90+
// ---cut---
91+
const summary = await synapse.payments.accountSummary();
92+
93+
console.log("Funds:", formatUnits(summary.funds));
94+
console.log("Available:", formatUnits(summary.availableFunds));
95+
console.log("Debt:", formatUnits(summary.debt));
96+
console.log("Rate per epoch:", summary.lockupRatePerEpoch);
97+
console.log("Rate per month:", formatUnits(summary.lockupRatePerMonth));
98+
console.log("Total lockup:", formatUnits(summary.totalLockup));
99+
console.log(" Fixed lockup:", formatUnits(summary.totalFixedLockup));
100+
console.log(" Rate-based lockup:", formatUnits(summary.totalRateBasedLockup));
101+
console.log("Funded until epoch:", summary.fundedUntilEpoch);
102+
```
103+
81104
### Account Health Monitoring
82105

83106
**Important**: Monitor your account health regularly. Insufficient balance causes payment failures and service interruptions.
@@ -90,13 +113,13 @@ const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source:
90113
// ---cut---
91114
import { TIME_CONSTANTS } from "@filoz/synapse-sdk";
92115

93-
const info = await synapse.payments.accountInfo();
94-
const epochsRemaining = info.availableFunds / info.lockupRate;
116+
const summary = await synapse.payments.accountSummary();
117+
const epochsRemaining = summary.availableFunds / summary.lockupRatePerEpoch;
95118
const daysRemaining =
96119
Number(epochsRemaining) / Number(TIME_CONSTANTS.EPOCHS_PER_DAY);
97120

98121
console.log(`Days remaining: ${daysRemaining.toFixed(1)}`);
99-
if (daysRemaining < 7) console.warn("⚠️ Low balance!");
122+
if (daysRemaining < 7) console.warn("Low balance!");
100123
```
101124

102125
### Withdrawing Unlocked Funds

docs/src/content/docs/developer-guides/storage/storage-costs.mdx

Lines changed: 49 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -92,127 +92,47 @@ const totalCostFor24Months = totalCostPerMonth * 24;
9292

9393
## Warm Storage Service Approvals
9494

95-
Before uploading, approve the **WarmStorage operator** and fund your account. FWSS requires a 30-day prepayment bufferwhen your balance drops below 30 days, the provider may remove your data.
95+
Before uploading, the **WarmStorage operator** must be approved and your account must be funded. FWSS requires a 30-day prepayment bufferwhen your balance drops below 30 days, the provider may remove your data.
9696

97-
**Approval Components:**
98-
99-
| Component | Purpose | Formula When Adding Storage |
100-
| --------------------- | ------------------------------------- | ---------------------------------------- |
101-
| **Deposit Amount** | USDFC tokens for storage duration | Total cost for desired months |
102-
| **Rate Allowance** | Max spending per epoch (cumulative) | `currentRateUsed + newRate` |
103-
| **Lockup Allowance** | 30-day prepayment buffer (cumulative) | `currentLockupUsed + (newRate × 86,400)` |
104-
| **Max Lockup Period** | Safety limit on locked funds | constant of 86,400 epochs (30 days) |
97+
The SDK's `prepare()` method handles all of this automatically. It computes the exact deposit needed, checks whether the FWSS operator is approved, and returns a single transaction that handles both.
10598

10699
---
107100

108-
## Detailed Calculator Guide
109-
110-
Learn how to calculate storage costs, required operator allowances and fund your account for future storage needs.
111-
112-
In this guide we will calculate the costs for a 1 GiB storage capacity for 12 months.
113-
114-
### Step 1: Calculate Base Storage Costs
101+
## Querying Upload Costs
115102

116-
Get pricing information and calculate the cost per epoch for your storage capacity:
103+
Use `getUploadCosts()` to preview costs without executing any transaction. This is useful for displaying pricing in a UI or letting users confirm before proceeding.
117104

118105
```ts twoslash
119106
// @lib: esnext,dom
120-
import {
121-
Synapse,
122-
SIZE_CONSTANTS,
123-
calibration
124-
} from "@filoz/synapse-sdk"
125-
import { WarmStorageService } from '@filoz/synapse-sdk/warm-storage'
107+
import { Synapse, formatUnits } from "@filoz/synapse-sdk";
126108
import { privateKeyToAccount } from 'viem/accounts'
109+
const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' });
127110
// ---cut---
128-
// Get pricing structure
129-
const warmStorageService = WarmStorageService.create({ account: privateKeyToAccount('0x...') })
130-
131-
const { minimumPricePerMonth, epochsPerMonth, pricePerTiBPerMonthNoCDN } =
132-
await warmStorageService.getServicePrice()
111+
const costs = await synapse.storage.getUploadCosts({
112+
dataSize: 1073741824n, // 1 GiB in bytes
113+
})
133114

134-
// Calculate base cost per month
135-
const bytesToStore = SIZE_CONSTANTS.GiB // 1 GiB
136-
let pricePerMonth =
137-
(pricePerTiBPerMonthNoCDN * BigInt(bytesToStore)) /
138-
BigInt(SIZE_CONSTANTS.TiB)
139-
140-
// Apply minimum pricing if needed
141-
if (pricePerMonth < minimumPricePerMonth) {
142-
pricePerMonth = minimumPricePerMonth
143-
}
144-
145-
// Calculate per-epoch cost
146-
const pricePerEpoch = pricePerMonth / epochsPerMonth
147-
148-
console.log("Monthly cost:", pricePerMonth)
149-
console.log("Per-epoch cost:", pricePerEpoch)
150115
```
151116

152-
### Step 2: Calculate Required Allowances
153-
154-
Calculate cumulative allowances for your new storage, accounting for existing usage.
155-
156-
:::note[Understanding Cumulative Allowances]
157-
Allowances are **cumulative** - you must add your new storage requirements to any existing usage. This is because the WarmStorage operator needs permission to spend on behalf of all your active datasets, not just the new one. Think of it like updating a credit limit to cover both old and new subscriptions.
158-
159-
- **Rate Allowance**: Total per-epoch spending across all datasets
160-
- **Lockup Allowance**: Total 30-day buffer across all datasets
161-
162-
:::
163-
164-
```ts twoslash
165-
// @lib: esnext,dom
166-
import { Synapse, TOKENS, TIME_CONSTANTS, calibration } from "@filoz/synapse-sdk";
167-
import { privateKeyToAccount } from 'viem/accounts'
168-
const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' });
169-
const pricePerMonth = null as unknown as bigint;
170-
const pricePerEpoch = null as unknown as bigint;
171-
// ---cut---
172-
// Define storage duration in months (minimum 1 month)
173-
const persistencePeriodInMonths = 12n;
174-
175-
// Check if creating a new CDN dataset (creates context to determine if one exists)
176-
const storageContext = await synapse.storage.createContext({
177-
withCDN: true,
178-
metadata: { Application: "MyApp", Version: "1.0.0" },
179-
});
180-
const newCDNDataSetNeeded = storageContext.dataSetId === null;
181-
182-
const warmStorageDefaultLockupPeriod = TIME_CONSTANTS.DEFAULT_LOCKUP_DAYS; // 30 days
183-
184-
// Calculate total cost and lockup needed for THIS storage addition
185-
const cdnDataSetCreationCost = 10n ** 18n; // 1 USDFC
186-
let totalCostNeeded = pricePerMonth * persistencePeriodInMonths;
187-
let lockupNeeded = pricePerMonth * warmStorageDefaultLockupPeriod;
188-
if (newCDNDataSetNeeded) {
189-
lockupNeeded += cdnDataSetCreationCost;
190-
totalCostNeeded += cdnDataSetCreationCost;
191-
}
192-
193-
// Get current approval status (what's already being used)
194-
const [approval, accountInfo] = await Promise.all([
195-
synapse.payments.serviceApproval(),
196-
synapse.payments.accountInfo(),
197-
]);
117+
The returned `UploadCosts` object contains:
198118

199-
// Calculate cumulative allowances: existing usage + new requirements
200-
// This ensures the operator can spend for ALL your datasets
201-
const lockupAllowanceNeeded = approval.lockupUsage + lockupNeeded;
202-
const rateAllowanceNeeded = approval.rateUsage + pricePerEpoch;
119+
| Field | Type | Description |
120+
| --- | --- | --- |
121+
| `rate.perEpoch` | `bigint` | Storage rate per epoch (30 seconds) |
122+
| `rate.perMonth` | `bigint` | Storage rate per month (for display) |
123+
| `depositNeeded` | `bigint` | USDFC to deposit (`0n` if sufficient funds) |
124+
| `needsFwssMaxApproval` | `boolean` | Whether FWSS operator approval is needed |
125+
| `ready` | `boolean` | `true` when no deposit or approval is needed |
203126

204-
console.log("New lockup needed:", lockupNeeded);
205-
console.log("Cumulative lockup allowance needed:", lockupAllowanceNeeded);
206-
console.log("Cumulative rate allowance needed:", rateAllowanceNeeded);
207-
```
127+
---
208128

209-
### Step 3: Fund Your Account
129+
## Funding Your Account
210130

211-
Check balances, calculate deposit amount, and execute the appropriate transaction:
131+
Use `prepare()` to compute costs and get a ready-to-execute transaction. This is the recommended approach for most use cases — it replaces the previous manual flow of calculating allowances, checking approvals, and branching between deposit/approve methods.
212132

213133
```ts twoslash
214134
// @lib: esnext,dom
215-
import { Synapse, TIME_CONSTANTS, calibration, TOKENS } from "@filoz/synapse-sdk";
135+
import { Synapse, formatUnits } from "@filoz/synapse-sdk";
216136
import { privateKeyToAccount } from 'viem/accounts'
217137
const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' });
218138
const totalCostNeeded = null as unknown as bigint;
@@ -221,38 +141,38 @@ const lockupAllowanceNeeded = null as unknown as bigint;
221141
const approval = await synapse.payments.serviceApproval();
222142
const accountInfo = await synapse.payments.accountInfo();
223143
// ---cut---
224-
// Calculate deposit amount needed
225-
const depositAmountNeeded =
226-
totalCostNeeded > accountInfo.availableFunds
227-
? totalCostNeeded - accountInfo.availableFunds
228-
: 0n;
229-
230-
// Check if allowances are sufficient
231-
const sufficient =
232-
rateAllowanceNeeded <= approval.rateAllowance &&
233-
lockupAllowanceNeeded <= approval.lockupAllowance;
234-
235-
// Verify wallet balance
236-
const walletBalance = await synapse.payments.walletBalance({ token: TOKENS.USDFC });
237-
if (walletBalance < depositAmountNeeded) {
238-
throw new Error("Insufficient USDFC balance in wallet");
239-
}
240-
241-
// Execute appropriate transaction
242-
if (!sufficient && depositAmountNeeded > 0n) {
243-
// Need both deposit and approval
244-
await synapse.payments.depositWithPermitAndApproveOperator({ amount: depositAmountNeeded });
245-
} else if (!sufficient) {
246-
// Only need approval update
247-
await synapse.payments.approveService();
248-
} else if (depositAmountNeeded > 0n) {
249-
// Only need deposit
250-
await synapse.payments.depositWithPermit({ amount: depositAmountNeeded });
144+
// Compute costs and get a transaction (if needed)
145+
const prep = await synapse.storage.prepare({
146+
dataSize: 1073741824n, // 1 GiB
147+
})
148+
149+
// Inspect costs
150+
console.log("Rate per month:", formatUnits(prep.costs.rate.perMonth))
151+
console.log("Deposit needed:", formatUnits(prep.costs.depositNeeded))
152+
153+
// Execute if the account isn't ready
154+
if (prep.transaction) {
155+
console.log("Deposit amount:", formatUnits(prep.transaction.depositAmount))
156+
console.log("Includes approval:", prep.transaction.includesApproval)
157+
158+
const { hash } = await prep.transaction.execute()
159+
console.log("Transaction confirmed:", hash)
251160
}
252161

253-
console.log("Account funded successfully");
162+
// Now safe to upload
254163
```
255164

165+
`prepare()` returns:
166+
167+
- **`costs`**: The full `UploadCosts` breakdown
168+
- **`transaction`**: A transaction object with `execute()`, or `null` if the account is already ready
169+
170+
The transaction automatically picks the right contract call:
171+
- **Needs deposit + approval**: calls `depositWithPermitAndApproveOperator`
172+
- **Needs approval only**: calls `approveService`
173+
- **Needs deposit only**: calls `depositWithPermit`
174+
- **Already ready**: returns `transaction: null`
175+
256176
## Next Steps
257177

258178
- [Storage Operations](/developer-guides/storage/storage-operations/) - Storage concepts and workflows

docs/src/content/docs/developer-guides/storage/storage-operations.mdx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,25 @@ console.log("Uploaded:", result.pieceCid.toString())
102102

103103
Subsequent uploads with the same `metadata` reuse the same data sets and payment rails.
104104

105-
#### Upload with Callbacks
105+
:::tip[Prepare Before Uploading]
106+
Before your first upload, call `prepare()` to ensure your account is funded and the storage service is approved. It computes the exact deposit needed and returns a single transaction to execute:
107+
108+
```ts
109+
import { Synapse } from "@filoz/synapse-sdk"
110+
import { privateKeyToAccount } from "viem/accounts"
111+
112+
const synapse = Synapse.create({ account: privateKeyToAccount("0x...") })
113+
114+
const prep = await synapse.storage.prepare({ dataSize: 1073741824n }) // 1 GiB
115+
if (prep.transaction) {
116+
await prep.transaction.execute()
117+
}
118+
```
119+
120+
See the [Storage Costs guide](/developer-guides/storage/storage-costs/) for a full breakdown.
121+
:::
122+
123+
## Upload with Callbacks
106124

107125
Track the lifecycle of a multi-copy upload with callbacks:
108126

docs/src/content/docs/getting-started/index.mdx

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ Get started with storage in just a few lines of code.
7474

7575
```ts twoslash
7676
// @lib: esnext,dom
77-
import { Synapse, parseUnits, mainnet, calibration } from "@filoz/synapse-sdk"
77+
import { Synapse } from "@filoz/synapse-sdk"
78+
import { mainnet } from "@filoz/synapse-core/chains"
7879
import { privateKeyToAccount } from 'viem/accounts'
7980

8081
async function main() {
@@ -87,20 +88,22 @@ async function main() {
8788
// withCDN: true
8889
})
8990

90-
// 2) Fund & approve (single tx)
91-
const hash = await synapse.payments.depositWithPermitAndApproveOperator({
92-
amount: parseUnits("2.5"), // 2.5 USDFC (covers 1TiB of storage for 30 days)
93-
})
94-
await synapse.client.waitForTransactionReceipt({ hash })
95-
console.log(`✅ USDFC deposit and Warm Storage service approval successful!`);
96-
97-
// 3) Upload — stores 2 copies across independent providers for durability
91+
// 2) Prepare account (single tx handles deposit + approval)
9892
const file = new TextEncoder().encode(
9993
`🚀 Welcome to decentralized storage on Filecoin Onchain Cloud!
10094
Your data is safe here.
10195
🌍 You need to make sure to meet the minimum size
10296
requirement of 127 bytes per upload.`
10397
);
98+
const prep = await synapse.storage.prepare({
99+
dataSize: BigInt(file.byteLength),
100+
})
101+
if (prep.transaction) {
102+
const { hash } = await prep.transaction.execute()
103+
console.log(`✅ Account funded and approved (tx: ${hash})`)
104+
}
105+
106+
// 3) Upload — stores 2 copies across independent providers for durability
104107
const { pieceCid, size, copies, failures } = await synapse.storage.upload(file)
105108
console.log(`✅ Upload complete!`);
106109
console.log(`PieceCID: ${pieceCid}`);
@@ -127,9 +130,9 @@ main().then(() => {
127130

128131
What you just did:
129132

130-
- ✅ Initialized synapse SDK for Filecoin Calibration
131-
-Deposited USDFC as payment tokens
132-
-Authorized the storage service to use the token
133+
- ✅ Initialized Synapse SDK for Filecoin Calibration
134+
-Computed the exact deposit and approval needed for your data size
135+
-Executed a single transaction to fund and approve (if needed)
133136
- ✅ Uploaded data with 2 copies across independent providers for durability
134137
- ✅ Retrieved it using only the content address (from any provider that has it)
135138

@@ -152,28 +155,37 @@ The `source` parameter identifies your application. It is stored as metadata on
152155

153156
### 2: Payment Setup
154157

155-
Before storing data, you need to deposit USDFC tokens in your payments account. The amount you deposit is entirely up to your anticipated storage and retrieval needs.
158+
Before storing data, you need to ensure your account is funded and the storage service is approved. The `prepare()` method calculates the exact deposit needed for your data size and returns a single transaction that handles both funding and approval.
159+
156160
:::note[Pricing]
157-
To size your deposit, check the up‑to‑date rates in [**Pricing**](/introduction/about/#pricing) and use the [storage costs calculator](/developer-guides/storage/storage-costs/#detailed-calculator-guide) for a precise estimate.
161+
To size your deposit, check the up‑to‑date rates in [**Pricing**](/introduction/about/#pricing) and use the [storage costs guide](/developer-guides/storage/storage-costs/) for a detailed breakdown.
158162
:::
159163

160164
```ts twoslash
161165
// @lib: esnext,dom
162-
import { Synapse, TOKENS, formatUnits, parseUnits } from "@filoz/synapse-sdk";
166+
import { Synapse, TOKENS, formatUnits } from "@filoz/synapse-sdk";
163167
import { privateKeyToAccount } from 'viem/accounts'
164168
const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' })
165169

166170
// ---cut---
167171
// Check current USDFC balance
168-
const walletBalance = await synapse.payments.walletBalance({ token:TOKENS.USDFC });
172+
const walletBalance = await synapse.payments.walletBalance({ token: TOKENS.USDFC });
169173
const formattedBalance = formatUnits(walletBalance);
170174

171-
// Deposit USDFC for payment and approve Warm Storage service
172-
const hash = await synapse.payments.depositWithPermitAndApproveOperator({
173-
amount: parseUnits("2.5"), // Deposit amount: 2.5 USDFC (covers 1TiB of storage for 30 days)
175+
// Prepare account — computes exact deposit + approval for your data size
176+
const prep = await synapse.storage.prepare({
177+
dataSize: 1073741824n, // 1 GiB
174178
});
175-
await synapse.client.waitForTransactionReceipt({ hash });
176-
console.log(`✅ USDFC deposit and Warm Storage service approval successful!`);
179+
180+
console.log("Deposit needed:", prep.costs.depositNeeded);
181+
console.log("Rate per month:", prep.costs.rate.perMonth);
182+
console.log("Ready to upload:", prep.costs.ready);
183+
184+
// Execute the transaction if needed (handles deposit + approval in one tx)
185+
if (prep.transaction) {
186+
const { hash } = await prep.transaction.execute();
187+
console.log(`✅ Account funded and approved (tx: ${hash})`);
188+
}
177189
```
178190

179191
### 3: Store and Download Data

packages/synapse-core/src/mocks/jsonrpc/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ export const presets = {
394394
sessionKeyRegistry: () => [ADDRESSES.calibration.sessionKeyRegistry],
395395
getServicePrice: () => [
396396
{
397-
pricePerTiBPerMonthNoCDN: parseUnits('2', 18),
397+
pricePerTiBPerMonthNoCDN: parseUnits('2.5', 18),
398398
pricePerTiBCdnEgress: parseUnits('7', 18),
399399
pricePerTiBCacheMissEgress: parseUnits('7', 18),
400400
minimumPricePerMonth: parseUnits('6', 16),

0 commit comments

Comments
 (0)