This document describes the contract's storage architecture, key design, and TTL (Time To Live) policies.
The Fluxora Stream contract uses Soroban's storage system with two storage types:
- Instance storage for global configuration and counters
- Persistent storage for individual stream data
All storage keys are defined in the DataKey enum:
#[contracttype]
pub enum DataKey {
Config, // Instance storage for global settings (admin/token).
NextStreamId, // Instance storage for the auto-incrementing ID counter.
Stream(u64), // Persistent storage for individual stream data (O(1) lookup).
}Instance storage is used for contract-wide configuration that applies to all streams:
| Key | Type | Description | Set By | Modified By |
|---|---|---|---|---|
Config |
Config struct |
Contains token address and admin address |
init() (one-shot, admin.require_auth()) |
set_admin() (admin key rotation only) |
NextStreamId |
u64 |
Auto-incrementing counter for stream IDs | init() (set to 0) |
create_stream() (incremented) |
Characteristics:
- Shared across all contract operations
- Low cardinality (only 2 keys)
- TTL extended on every read and write (see TTL Policy below)
- Accessed frequently by most contract functions
Persistent storage is used for individual stream records:
| Key Pattern | Type | Description | Set By | Modified By |
|---|---|---|---|---|
Stream(stream_id) |
Stream struct |
Complete stream state including participants, amounts, timing, and status | create_stream() |
pause_stream(), resume_stream(), cancel_stream(), withdraw() |
Characteristics:
- One entry per stream (unbounded growth)
- O(1) lookup by stream ID
- Contains all stream metadata and state
- TTL extended on every read and write operation
All TTL values are defined as named constants for maintainability:
/// Minimum remaining TTL (in ledgers) before we bump. ~1 day at 5 s/ledger.
const INSTANCE_LIFETIME_THRESHOLD: u32 = 17_280;
/// Extend to ~7 days of ledgers when bumping instance storage.
const INSTANCE_BUMP_AMOUNT: u32 = 120_960;
/// Minimum remaining TTL for persistent (stream) entries.
const PERSISTENT_LIFETIME_THRESHOLD: u32 = 17_280;
/// Extend persistent entries to ~7 days of ledgers.
const PERSISTENT_BUMP_AMOUNT: u32 = 120_960;Instance TTL is extended via the bump_instance_ttl() helper, which is called on every entry-point that reads or writes instance storage:
fn bump_instance_ttl(env: &Env) {
env.storage()
.instance()
.extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);
}Extension points (exhaustive):
| Function | Trigger |
|---|---|
init() |
After initial writes |
get_config() |
On every read of Config |
get_stream_count() |
On every read of NextStreamId |
set_stream_count() |
After writing NextStreamId |
set_admin() |
After updating Config with new admin |
- Threshold: 17,280 ledgers (~24 hours at 5s/ledger)
- Max extension: 120,960 ledgers (~7 days)
- Rationale: Any contract interaction — whether a read (e.g.,
get_config,calculate_accrued) or a write — refreshes the instance TTL. This prevents the contract from becoming unusable due to storage expiration on testnet or mainnet, even during periods of low activity.
Extended on every stream load (load_stream()) and save (save_stream()):
// On read:
env.storage().persistent().extend_ttl(&key, PERSISTENT_LIFETIME_THRESHOLD, PERSISTENT_BUMP_AMOUNT);
// On write:
env.storage().persistent().extend_ttl(&key, PERSISTENT_LIFETIME_THRESHOLD, PERSISTENT_BUMP_AMOUNT);- Threshold: 17,280 ledgers (~24 hours)
- Max extension: 120,960 ledgers (~7 days)
- Trigger: Every read or write (create, pause, resume, cancel, withdraw, get_stream_state, calculate_accrued)
- Rationale: Both active and queried streams remain accessible. A UI polling
calculate_accruedorget_stream_statewill keep the stream alive.
- Active streams: TTL refreshed on any interaction (reads or writes)
- Queried streams: TTL refreshed when viewed via
get_stream_stateorcalculate_accrued - Inactive streams: May expire after ~7 days with zero interaction
- Completed/Cancelled streams: TTL still refreshed when queried; expire only if nobody reads them for 7 days
- Recovery: Expired entries cannot be recovered; data is permanently lost
- Contract liveness: Because instance TTL is bumped on every entry-point, the contract itself (Config + NextStreamId) stays alive as long as any function is called at least once per 7 days
get_config()→ readsConfigfrom instance storage, bumps instance TTLget_stream_count()→ readsNextStreamIdfrom instance storage, bumps instance TTLget_stream_state(stream_id)→ readsStream(stream_id)from persistent storage, bumps stream TTLcalculate_accrued(stream_id)→ readsStream(stream_id)from persistent storage, bumps stream TTL
init()→ writesConfigandNextStreamIdonce (requires bootstrapadminauth), bumps instance TTLcreate_stream()→ reads/writesNextStreamId, writesStream(stream_id), bumps both TTLspause_stream()→ reads/writesStream(stream_id), bumps both stream and instance TTLsresume_stream()→ reads/writesStream(stream_id), bumps both stream and instance TTLscancel_stream()→ reads/writesStream(stream_id), bumps both stream and instance TTLswithdraw()→ reads/writesStream(stream_id), bumps both stream and instance TTLsset_admin()→ writesConfig, bumps instance TTL
- Fixed cost: 2 keys regardless of stream count
- Minimal storage footprint (~100 bytes total)
- Shared across all operations
- Linear growth: 1 key per stream
- Per-stream footprint: ~200-300 bytes (depends on address sizes)
- Unbounded growth potential
- TTL maintenance automatic through usage
- Stream IDs are sequential
u64values (efficient key space) - No secondary indexes (trade-off: no enumeration, but O(1) lookups)
- No stream deletion (terminal states remain in storage until TTL expiration)
- Consider archiving completed/cancelled streams off-chain for historical queries
- Admin rotation: Admin can be changed via
set_admin()with current-admin authorization - Atomic operations: All state changes are transactional (no partial updates)
- Key isolation: Each stream has independent storage (no cross-stream interference)
- TTL protection: Both reads and writes keep storage alive, preventing accidental expiration
- No stale state: TTL bumps on reads mean even monitoring/UI queries keep data fresh
Potential storage improvements for future versions:
- Stream enumeration support (e.g., by sender or recipient)
- Configurable TTL policies per stream
- Stream archival mechanism for completed streams
- Storage rent payment automation