The Streaming Payments Contract enables continuous, per-ledger token distribution for real-time payroll, subscriptions, and vesting schedules. This implementation provides a complete solution from smart contract to backend API integration.
┌─────────────────┐
│ Frontend (UI) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Backend API │
│ (Express) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Soroban RPC │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Streaming │
│ Contract │
└─────────────────┘
contracts/streaming/
- Per-ledger streaming: Calculates
rate_per_ledger = total_amount / duration - Partial withdrawals: Recipients withdraw available balance anytime
- Cancellation with refunds: Sender cancels and reclaims unstreamed tokens
- Event emission: Tracks creation, withdrawals, and cancellations
Stream {
sender: Address,
recipient: Address,
token: Address,
rate_per_ledger: i128,
start_ledger: u32,
stop_ledger: u32,
withdrawn: i128,
}pub fn create_stream(
e: Env,
sender: Address,
recipient: Address,
token: Address,
total_amount: i128,
start_ledger: u32,
stop_ledger: u32,
) -> u64- Validates parameters
- Transfers tokens to contract
- Calculates rate per ledger
- Returns unique stream ID
pub fn withdraw(e: Env, stream_id: u64, amount: i128)- Requires recipient authorization
- Checks available balance
- Transfers tokens to recipient
- Updates withdrawn amount
pub fn cancel_stream(e: Env, stream_id: u64)- Requires sender authorization
- Transfers available balance to recipient
- Refunds unstreamed tokens to sender
- Removes stream from storage
pub fn balance_of(e: Env, stream_id: u64) -> i128- Calculates elapsed ledgers
- Returns
streamed - withdrawn
server/services/streaming-service.js
Handles Soroban RPC communication:
- Transaction building
- Contract invocation
- Result parsing
- Transaction polling
server/routes/streaming-routes.js
| Method | Endpoint | Description |
|---|---|---|
| POST | /streams |
Create new stream |
| POST | /streams/:id/withdraw |
Withdraw from stream |
| DELETE | /streams/:id |
Cancel stream |
| GET | /streams/:id |
Get stream details |
| GET | /streams/:id/balance |
Get available balance |
server/models/Stream.js
Tracks stream metadata:
- Stream ID and contract address
- Sender and recipient addresses
- Token details and amounts
- Ledger range
- Status (active/completed/canceled)
- Transaction hashes
// Create 30-day salary stream
const currentLedger = await getCurrentLedger();
const monthInLedgers = 518_400; // ~30 days at 5s/ledger
const stream = await streamingService.createStream(
contractId,
employerKeypair,
employerAddress,
employeeAddress,
usdcTokenAddress,
'5000000000000', // 5000 USDC (7 decimals)
currentLedger,
currentLedger + monthInLedgers
);
// Employee withdraws weekly
const weekInLedgers = 120_960;
await streamingService.withdraw(
contractId,
employeeKeypair,
stream.streamId,
'1250000000000' // ~1250 USDC
);// Monthly subscription: 100 tokens
const subscription = await streamingService.createStream(
contractId,
subscriberKeypair,
subscriberAddress,
serviceProviderAddress,
paymentTokenAddress,
'1000000000', // 100 tokens
currentLedger,
currentLedger + 518_400
);
// Service provider withdraws daily
const dayInLedgers = 17_280;
const dailyAmount = '3333333'; // ~3.33 tokens/day
await streamingService.withdraw(
contractId,
providerKeypair,
subscription.streamId,
dailyAmount
);// 1-year vesting with 3-month cliff
const yearInLedgers = 6_307_200;
const cliffInLedgers = 1_576_800;
const vesting = await streamingService.createStream(
contractId,
companyKeypair,
companyAddress,
founderAddress,
companyTokenAddress,
'1000000000000000', // 1M tokens
currentLedger + cliffInLedgers,
currentLedger + yearInLedgers
);Stellar ledgers close approximately every 5 seconds:
| Duration | Ledgers | Calculation |
|---|---|---|
| 1 minute | 12 | 60 / 5 |
| 1 hour | 720 | 3600 / 5 |
| 1 day | 17,280 | 86400 / 5 |
| 1 week | 120,960 | 604800 / 5 |
| 30 days | 518,400 | 2592000 / 5 |
| 1 year | 6,307,200 | 31536000 / 5 |
Add to server/.env:
STREAMING_CONTRACT_ID=C...cd contracts/streaming
cargo testcd server
npm test -- streamingcd contracts/streaming
cargo build --target wasm32-unknown-unknown --release
soroban contract optimize --wasm target/wasm32-unknown-unknown/release/soromint_streaming.wasmsoroban contract deploy \
--wasm target/wasm32-unknown-unknown/release/soromint_streaming.wasm \
--source DEPLOYER_SECRET \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase "Test SDF Network ; September 2015"echo "STREAMING_CONTRACT_ID=<deployed_contract_id>" >> server/.envAdd to server/index.js:
const streamingRoutes = require('./routes/streaming-routes');
app.use('/api/v1', streamingRoutes);- Authorization: All operations require proper signatures
- Balance Validation: Prevents over-withdrawal
- Atomic Operations: Token transfers are atomic with state updates
- Refund Safety: Cancellation properly handles all balances
- Rate Limiting: API endpoints should be rate-limited
- Input Validation: All inputs validated before contract calls
- Uses persistent storage for streams (cheaper than instance)
- Minimal storage keys (stream ID only)
- Efficient balance calculation (no iteration)
- Events for off-chain indexing
Track these metrics:
- Active streams count
- Total value locked
- Withdrawal frequency
- Cancellation rate
- Failed transactions
- Pause/Resume: Temporarily halt streaming
- Multi-recipient: Split stream to multiple addresses
- Dynamic Rate: Adjust rate during stream
- Cliff Period: Delay before streaming starts
- Batch Operations: Create/cancel multiple streams
- NFT Integration: Stream NFT royalties
For issues or questions:
- GitHub: EDOHWARES/SoroMint
- Issue: #188 Streaming Payments Contract
Part of the SoroMint project.