diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1bfaeed..1e192d8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -186,6 +186,18 @@ This must produce zero warnings. - No `unsafe` code is permitted in any contract. - No external crate dependencies beyond `soroban-sdk` are permitted without prior discussion with maintainers. +### Storage Type Selection + +Soroban provides three storage tiers. When adding a new `DataKey` variant to any contract, pick the right one: + +| Storage type | Use when… | +| :--- | :--- | +| `instance()` | Small scalars (counters, config) that are read on almost every call and can share the contract instance TTL | +| `persistent()` | Per-user or per-entity data (streams, vesting configs, proposals) that must survive beyond the instance TTL | +| `temporary()` | Short-lived data that can expire without consequence (e.g. nonces, rate-limit windows) | + +Always add a comment above the variant in the `DataKey` enum documenting which storage type it uses and why. See `contracts/forge-stream/src/lib.rs` for an example. + --- ## Pre-Commit Hook (Optional but Recommended) diff --git a/contracts/forge-stream/README.md b/contracts/forge-stream/README.md index 382a292..6777977 100644 --- a/contracts/forge-stream/README.md +++ b/contracts/forge-stream/README.md @@ -1,5 +1,28 @@ # Forge Stream +## Storage Strategy + +`forge-stream` uses two Soroban storage tiers. The rule of thumb: use +**persistent** for data that must survive beyond a single transaction or +contract instance TTL; use **instance** for small, frequently-accessed +scalars that are always read together with the contract instance. + +| `DataKey` variant | Storage type | Rationale | +| :--- | :--- | :--- | +| `Stream(u64)` | `persistent` | Stream data must outlive the instance TTL while tokens remain unclaimed | +| `NextId` | `instance` | Small scalar always read on `create_stream`; co-located with instance for efficiency | +| `ActiveStreamsCount` | `instance` | Updated on every create/cancel/finish; always accessed with other instance data | +| `SenderStreams(Address)` | `persistent` | Grows with each stream; must survive beyond instance TTL for historical lookups | +| `RecipientStreams(Address)` | `persistent` | Same rationale as `SenderStreams` | + +When adding a new `DataKey` variant, choose the storage type using this +checklist: +- Does the data need to survive after the contract instance TTL expires? → **persistent** +- Is it a small scalar read on almost every call? → **instance** +- Is it keyed per-user or per-stream (unbounded growth)? → **persistent** + +--- + ## Resource Usage > **Note:** Resource usage estimates are approximate and may vary based on contract state and input sizes. Run `stellar contract invoke` with `--cost` flag to measure actual usage for your specific use case. diff --git a/contracts/forge-stream/src/lib.rs b/contracts/forge-stream/src/lib.rs index 4b03d2e..b289ac5 100644 --- a/contracts/forge-stream/src/lib.rs +++ b/contracts/forge-stream/src/lib.rs @@ -16,10 +16,24 @@ use soroban_sdk::{ #[contracttype] pub enum DataKey { + /// Per-stream data (token, sender, recipient, rate, timestamps, state). + /// Uses **persistent** storage — must outlive the contract instance TTL + /// for as long as the stream has unclaimed tokens. Stream(u64), + /// Monotonically increasing counter used to assign the next stream ID. + /// Uses **instance** storage — small scalar that is always read on + /// `create_stream`, so co-locating it with the instance is efficient. NextId, + /// Count of streams that are currently active (not cancelled/finished). + /// Uses **instance** storage — updated on every create/cancel/finish, + /// always accessed together with other instance data. ActiveStreamsCount, + /// List of stream IDs created by a given sender address. + /// Uses **persistent** storage — the list grows with each stream and + /// must survive beyond the instance TTL for historical lookups. SenderStreams(Address), + /// List of stream IDs where a given address is the recipient. + /// Uses **persistent** storage — same rationale as `SenderStreams`. RecipientStreams(Address), }