Skip to content

Commit 9a5503e

Browse files
authored
feat: payment rail settlement (#136)
Closes: #48
1 parent d3a3608 commit 9a5503e

18 files changed

Lines changed: 1801 additions & 248 deletions

README.md

Lines changed: 134 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ The Synapse SDK provides an interface to Filecoin's decentralized services ecosy
2020

2121
The SDK handles all the complexity of blockchain interactions, provider selection, and data management, so you can focus on building your application.
2222

23+
### Key Concepts
24+
25+
- **Service Contracts**: Smart contracts that manage specific services (like storage). Currently, **Warm Storage** is the primary service contract that handles storage operations and payment validation.
26+
- **Payment Rails**: Automated payment streams between clients and service providers, managed by the Payments contract. When you create a data set in Warm Storage, it automatically creates corresponding payment rails.
27+
- **Data Sets**: Collections of stored data managed by Warm Storage. Each data set has an associated payment rail that handles the ongoing storage payments.
28+
- **Pieces**: Individual units of data identified by PieceCID (content-addressed identifiers). Multiple pieces can be added to a data set for storage.
29+
- **PDP (Proof of Data Possession)**: The cryptographic protocol that verifies storage providers are actually storing the data they claim to store. Providers must periodically prove they possess the data.
30+
- **Validators**: Service contracts (like Warm Storage) act as validators for payment settlements, ensuring services are delivered before payments are released.
31+
2332
## Installation
2433

2534
```bash
@@ -59,6 +68,7 @@ Note: `ethers` v6 is a peer dependency and must be installed separately.
5968
* [Commit Message Guidelines](#commit-message-guidelines)
6069
* [Git hooks](#git-hooks)
6170
* [Testing](#testing)
71+
* [Generating ABIs](#generating-abis)
6272
* [Migration Guide](#migration-guide)
6373
* [Terminology Update (v0.24.0+)](#terminology-update-v0240)
6474
* [License](#license)
@@ -94,6 +104,18 @@ const data = await synapse.storage.download(uploadResult.pieceCid)
94104
console.log('Retrieved:', new TextDecoder().decode(data))
95105
```
96106

107+
#### Connection Management
108+
109+
When using WebSocket connections (recommended for better performance), it's important to properly clean up when your application is done:
110+
111+
```javascript
112+
// When you're done with the SDK, close the connection
113+
const provider = synapse.getProvider()
114+
if (provider && typeof provider.destroy === 'function') {
115+
await provider.destroy()
116+
}
117+
```
118+
97119
#### Payment Setup
98120

99121
Before uploading data, you'll need to deposit funds and approve the storage service:
@@ -106,7 +128,8 @@ import { ethers } from 'ethers'
106128
const amount = ethers.parseUnits('100', 18) // 100 USDFC
107129
await synapse.payments.deposit(amount)
108130

109-
// 2. Approve the Warm Storage service for automated payments
131+
// 2. Approve the Warm Storage service contract for automated payments
132+
// Warm Storage acts as both the storage coordinator and payment validator
110133
// The SDK automatically uses the correct service address for your network
111134
const warmStorageAddress = await synapse.getWarmStorageAddress()
112135
await synapse.payments.approveService(
@@ -300,6 +323,15 @@ await synapse.storage.download(pieceCid, { context: storageContext })
300323
- `revokeService(service, token?)` - Revoke service operator approval, returns `TransactionResponse`
301324
- `serviceApproval(service, token?)` - Check service approval status and allowances
302325

326+
**Rail Settlement:**
327+
- `getRailsAsPayer(token?)` - Get all payment rails where wallet is the payer, returns `RailInfo[]` with `{railId, isTerminated, endEpoch}` (endEpoch is 0 for active rails)
328+
- `getRailsAsPayee(token?)` - Get all payment rails where wallet is the payee (recipient), returns `RailInfo[]`
329+
- `getRail(railId)` - Get detailed rail information, returns `{token, from, to, operator, validator, paymentRate, lockupPeriod, lockupFixed, settledUpTo, endEpoch, commissionRateBps, serviceFeeRecipient}`. Throws if rail doesn't exist.
330+
- `settle(railId, untilEpoch?)` - Settle a payment rail up to specified epoch (must be <= current epoch; defaults to current if not specified), automatically includes settlement fee (0.0013 FIL), returns `TransactionResponse`
331+
- `settleTerminatedRail(railId)` - Emergency settlement for terminated rails only - bypasses Warm Storage (or other validator) validation to ensure payment even if the validator contract is buggy (pays in full), returns `TransactionResponse`
332+
- `getSettlementAmounts(railId, untilEpoch?)` - Preview settlement amounts without executing (untilEpoch must be <= current epoch; defaults to current), returns `SettlementResult` with `{totalSettledAmount, totalNetPayeeAmount, totalOperatorCommission, finalSettledEpoch, note}`
333+
- `settleAuto(railId, untilEpoch?)` - Automatically detect rail status and settle appropriately (untilEpoch must be <= current epoch for active rails)
334+
303335
#### Storage Context Methods
304336

305337
A `StorageContext` (previously `StorageService`) represents a connection to a specific service provider and data set. Create one with `synapse.storage.createContext()`.
@@ -437,16 +469,13 @@ const result = await context.upload(data, {
437469
console.log(`Upload complete! PieceCID: ${pieceCid}`)
438470
},
439471
onPieceAdded: (transaction) => {
440-
// For new servers: transaction object with details
441-
// For old servers: undefined (backward compatible)
442-
if (transaction) {
443-
console.log(`Transaction confirmed: ${transaction.hash}`)
444-
} else {
445-
console.log('Data added to data set (legacy server)')
446-
}
472+
// Called when the service provider has added the piece and submitted the
473+
// transaction to the chain
474+
console.log(`Transaction submitted: ${transaction.hash}`)
447475
},
448476
onPieceConfirmed: (pieceIds) => {
449-
// Only called for new servers with transaction tracking
477+
// Called when the service provider agrees that the piece addition is
478+
// confirmed on-chain
450479
console.log(`Piece IDs assigned: ${pieceIds.join(', ')}`)
451480
}
452481
})
@@ -593,6 +622,57 @@ All components can be imported and used independently for advanced use cases. Th
593622

594623
Direct interface to the Payments contract for token operations and operator approvals.
595624

625+
#### Understanding Payment Rails
626+
627+
Payment rails are continuous payment streams between clients and service providers that are created automatically when data sets are established. Each data set has associated payment rails (one for PDP storage, optionally additional ones for CDN services).
628+
629+
**How Rails Work:**
630+
631+
Rails ensure reliable payments through a simple lockup mechanism:
632+
633+
1. **The Lockup Requirement**: When you create a data set (storage), the system calculates how much balance you need to maintain:
634+
- Formula: `lockup = paymentRate × lockupPeriod` (e.g., 10 days worth of payments)
635+
- Example: Storing 1 GiB costs ~0.0000565 USDFC/epoch, requiring ~1.63 USDFC minimum balance
636+
- This protects the service provider by ensuring you always have enough for the next payment period
637+
638+
2. **How Your Balance Works**:
639+
- You deposit funds into the payments contract (e.g., 100 USDFC)
640+
- The lockup requirement reserves part of this balance (e.g., 1.63 USDFC for 1 GiB storage)
641+
- You can withdraw anything above the lockup requirement
642+
- When you settle, your total balance decreases by the payment amount (lockup requirement stays the same)
643+
644+
3. **Normal vs Abnormal Operations**:
645+
- **Normal Operation**: You keep settling regularly, lockup stays reserved but unused
646+
- **If you stop settling**: Service continues but unpaid amounts accumulate
647+
- **If balance gets too low**: Rail terminates when you can't cover future payments
648+
- **After termination**: The lockup NOW becomes available to pay the service provider for the period already provided
649+
650+
**Understanding Your Balance:**
651+
- **Total Funds**: All tokens you've deposited into the payments contract
652+
- **Lockup Requirement**: The minimum balance reserved to guarantee future payments
653+
- **Available Balance**: `totalFunds - lockupRequirement` (this is what you can withdraw)
654+
655+
**When Lockup Gets Used (The Safety Net):**
656+
657+
The lockup finally gets "used" when things go wrong:
658+
- **Rail terminates** (due to insufficient funds or manual termination)
659+
- **After termination**, the service provider can settle and claim payment from the lockup
660+
- **This ensures** the provider gets paid for services already delivered, even if the client disappears
661+
- **Example**: If you had 10 days of lockup and the rail terminates, the provider can claim up to 10 days of service payments from that locked amount
662+
663+
For more details on the payment mechanics, see [Filecoin Pay documentation](https://github.com/FilOzone/filecoin-pay)
664+
665+
**When to Settle:**
666+
- **Service Providers**: Periodically settle to receive accumulated earnings
667+
- **Clients**: Settle before withdrawing to update available balance
668+
- **Terminated Rails**: Must be settled to finalize and close the payment stream
669+
670+
**Settlement Fee:**
671+
- Settlement operations require sending a small amount of FIL as a settlement fee (0.0013 FIL)
672+
- The SDK automatically includes this fee when calling `settle()`
673+
- The fee is defined as `SETTLEMENT_FEE` constant (corresponds to NETWORK_FEE in the contract)
674+
- Make sure your wallet has sufficient FIL balance for the settlement fee
675+
596676
```javascript
597677
import { PaymentsService } from '@filoz/synapse-sdk/payments'
598678
import { ethers } from 'ethers'
@@ -619,6 +699,51 @@ const approveTx = await paymentsService.approveService(
619699
)
620700
console.log(`Service approval transaction: ${approveTx.hash}`)
621701
await approveTx.wait() // Wait for confirmation
702+
703+
// Rail Settlement - manage continuous payment streams for storage services
704+
705+
// As a CLIENT: Find and settle your payment obligations
706+
const payerRails = await paymentsService.getRailsAsPayer()
707+
console.log(`You have ${payerRails.length} payment rails as a payer`)
708+
709+
// Check settlement fee requirement (automatically included in settle())
710+
import { SETTLEMENT_FEE } from '@filoz/synapse-sdk'
711+
console.log(`Settlement fee per settlement: ${ethers.formatEther(SETTLEMENT_FEE)} FIL`)
712+
713+
for (const rail of payerRails) {
714+
console.log(`Rail ${rail.railId}: ${rail.isTerminated ? 'terminated' : 'active'}`)
715+
716+
// Preview what would be settled (useful before withdrawing funds)
717+
const preview = await paymentsService.getSettlementAmounts(rail.railId)
718+
console.log(` Accumulated payment: ${preview.totalSettledAmount}`)
719+
720+
// Settle to clear obligations and update available balance
721+
// Note: SDK automatically includes the network fee in the transaction
722+
if (!rail.isTerminated && preview.totalSettledAmount > 0n) {
723+
const settleTx = await paymentsService.settle(rail.railId)
724+
console.log(` Settling rail ${rail.railId}: ${settleTx.hash}`)
725+
await settleTx.wait()
726+
}
727+
}
728+
729+
// As a SERVICE PROVIDER: Find and collect earnings
730+
const payeeRails = await paymentsService.getRailsAsPayee()
731+
console.log(`You have ${payeeRails.length} payment rails as a payee`)
732+
733+
for (const rail of payeeRails) {
734+
// Check accumulated earnings
735+
const preview = await paymentsService.getSettlementAmounts(rail.railId)
736+
console.log(`Rail ${rail.railId} earnings: ${preview.totalNetPayeeAmount}`)
737+
738+
// Settle to receive payments
739+
if (preview.totalNetPayeeAmount > 0n) {
740+
const settleTx = rail.isTerminated
741+
? await paymentsService.settleTerminatedRail(rail.railId) // For ended storage
742+
: await paymentsService.settle(rail.railId) // For ongoing storage
743+
console.log(` Collecting payment: ${settleTx.hash}`)
744+
await settleTx.wait()
745+
}
746+
}
622747
```
623748

624749
### Service Provider Registry

0 commit comments

Comments
 (0)