@calimero-network/agent-skills 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +137 -17
  2. package/SKILL.md +31 -28
  3. package/package.json +1 -1
  4. package/scripts/install.js +3 -3
  5. package/scripts/test.js +6 -15
  6. package/skills/calimero-abi-codegen/SKILL.md +121 -22
  7. package/skills/calimero-abi-codegen/references/abi-format.md +3 -5
  8. package/skills/calimero-abi-codegen/references/generated-output.md +12 -4
  9. package/skills/calimero-abi-codegen/rules/schema-version.md +11 -4
  10. package/skills/calimero-abi-codegen/rules/unique-names.md +2 -6
  11. package/skills/calimero-client-js/SKILL.md +126 -31
  12. package/skills/calimero-client-js/references/auth.md +18 -10
  13. package/skills/calimero-client-js/references/rpc-calls.md +15 -21
  14. package/skills/calimero-client-js/references/sso.md +9 -9
  15. package/skills/calimero-client-js/references/websocket-events.md +73 -92
  16. package/skills/calimero-client-js/rules/camelcase-api.md +10 -7
  17. package/skills/calimero-client-js/rules/token-refresh.md +11 -11
  18. package/skills/calimero-client-py/SKILL.md +25 -13
  19. package/skills/calimero-client-py/references/api.md +41 -43
  20. package/skills/calimero-client-py/references/auth.md +7 -7
  21. package/skills/calimero-client-py/rules/async-usage.md +27 -31
  22. package/skills/calimero-client-py/rules/stable-node-name.md +7 -7
  23. package/skills/calimero-core/SKILL.md +135 -0
  24. package/skills/calimero-core/references/architecture.md +101 -0
  25. package/skills/calimero-core/references/jsonrpc-protocol.md +192 -0
  26. package/skills/calimero-core/references/namespaces-groups.md +94 -0
  27. package/skills/calimero-core/references/storage-types.md +118 -0
  28. package/skills/calimero-core/references/websocket-events.md +142 -0
  29. package/skills/calimero-core/rules/context-is-not-app.md +35 -0
  30. package/skills/calimero-core/rules/crdt-types-only.md +55 -0
  31. package/skills/calimero-desktop/SKILL.md +24 -19
  32. package/skills/calimero-desktop/references/sso-integration.md +2 -2
  33. package/skills/calimero-desktop/rules/sso-fallback.md +3 -2
  34. package/skills/calimero-merobox/SKILL.md +255 -28
  35. package/skills/calimero-merobox/references/ci-integration.md +3 -2
  36. package/skills/calimero-merobox/references/workflow-files.md +7 -5
  37. package/skills/calimero-merobox/rules/docker-required.md +7 -6
  38. package/skills/calimero-meroctl/SKILL.md +68 -0
  39. package/skills/calimero-meroctl/references/commands.md +177 -0
  40. package/skills/calimero-meroctl/references/scripting.md +80 -0
  41. package/skills/calimero-meroctl/rules/call-view-flag.md +28 -0
  42. package/skills/calimero-meroctl/rules/register-node-once.md +34 -0
  43. package/skills/calimero-merod/SKILL.md +49 -0
  44. package/skills/calimero-merod/references/health-endpoints.md +90 -0
  45. package/skills/calimero-merod/references/init-flags.md +84 -0
  46. package/skills/calimero-merod/rules/init-before-run.md +40 -0
  47. package/skills/calimero-merod/rules/port-assignments.md +33 -0
  48. package/skills/calimero-node/SKILL.md +50 -39
  49. package/skills/calimero-node/references/context-lifecycle.md +34 -17
  50. package/skills/calimero-node/references/meroctl-commands.md +89 -99
  51. package/skills/calimero-node/rules/app-vs-context.md +4 -4
  52. package/skills/calimero-registry/SKILL.md +110 -31
  53. package/skills/calimero-registry/references/bundle-and-push.md +99 -34
  54. package/skills/calimero-registry/references/manifest-format.md +56 -35
  55. package/skills/calimero-registry/references/mero-sign.md +10 -9
  56. package/skills/calimero-registry/rules/key-security.md +3 -2
  57. package/skills/calimero-registry/rules/sign-before-pack.md +5 -5
  58. package/skills/calimero-rust-sdk/SKILL.md +154 -44
  59. package/skills/calimero-rust-sdk/references/blob-api.md +119 -0
  60. package/skills/calimero-rust-sdk/references/event-handlers.md +122 -0
  61. package/skills/calimero-rust-sdk/references/events.md +2 -1
  62. package/skills/calimero-rust-sdk/references/examples.md +81 -29
  63. package/skills/calimero-rust-sdk/references/migrations.md +123 -0
  64. package/skills/calimero-rust-sdk/references/nested-crdts.md +113 -0
  65. package/skills/calimero-rust-sdk/references/private-storage.md +76 -34
  66. package/skills/calimero-rust-sdk/references/state-collections.md +106 -21
  67. package/skills/calimero-rust-sdk/references/user-and-frozen-storage.md +169 -0
  68. package/skills/calimero-rust-sdk/rules/app-macro-placement.md +5 -2
  69. package/skills/calimero-rust-sdk/rules/no-std-collections.md +5 -2
  70. package/skills/calimero-rust-sdk/rules/state-derives.md +9 -10
  71. package/skills/calimero-rust-sdk/rules/wasm-constraints.md +12 -10
  72. package/skills/calimero-sdk-js/SKILL.md +34 -26
  73. package/skills/calimero-sdk-js/references/build-pipeline.md +6 -6
  74. package/skills/calimero-sdk-js/references/collections.md +11 -11
  75. package/skills/calimero-sdk-js/references/events.md +7 -3
  76. package/skills/calimero-sdk-js/rules/crdt-only-state.md +18 -18
  77. package/skills/calimero-sdk-js/rules/no-console-log.md +6 -6
  78. package/skills/calimero-sdk-js/rules/view-decorator.md +6 -4
@@ -0,0 +1,123 @@
1
+ # State Schema Migrations
2
+
3
+ When you need to change the shape of your app's state (add a field, rename a field, change a type),
4
+ use `#[app::migrate]` to write a migration function that converts the old Borsh bytes to the new
5
+ state struct.
6
+
7
+ The migration runs **once** when the upgraded WASM is installed on a context that has existing
8
+ state. New contexts (no prior state) call `#[app::init]` as normal.
9
+
10
+ ---
11
+
12
+ ## How it works
13
+
14
+ 1. Define the OLD state struct with `BorshDeserialize` only (for reading old bytes)
15
+ 2. Define the NEW state struct (your updated `#[app::state]`)
16
+ 3. Write a `#[app::migrate]` function that reads the old bytes and returns the new struct
17
+ 4. Install the new WASM — the node calls the migrate function on the existing context
18
+
19
+ ---
20
+
21
+ ## Example: adding a field
22
+
23
+ ```rust
24
+ use calimero_sdk::app;
25
+ use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
26
+ use calimero_sdk::state::read_raw;
27
+ use calimero_storage::collections::{LwwRegister, UnorderedMap};
28
+
29
+ // --- V1 (old schema, for deserialization only) ---
30
+ #[derive(BorshDeserialize)]
31
+ #[borsh(crate = "calimero_sdk::borsh")]
32
+ struct AppStateV1 {
33
+ items: UnorderedMap<String, LwwRegister<String>>,
34
+ counter: LwwRegister<u64>,
35
+ }
36
+
37
+ // --- V2 (new schema, adds a `notes` field) ---
38
+ #[app::state(emits = for<'a> Event<'a>)]
39
+ #[derive(Debug, BorshSerialize, BorshDeserialize)]
40
+ #[borsh(crate = "calimero_sdk::borsh")]
41
+ pub struct AppStateV2 {
42
+ items: UnorderedMap<String, LwwRegister<String>>,
43
+ counter: LwwRegister<u64>,
44
+ notes: LwwRegister<String>, // new field
45
+ }
46
+
47
+ #[app::event]
48
+ pub enum Event<'a> {
49
+ Migrated { from: &'a str, to: &'a str },
50
+ }
51
+
52
+ // --- Migration function ---
53
+ #[app::migrate]
54
+ pub fn migrate_v1_to_v2() -> AppStateV2 {
55
+ let old_bytes = read_raw().unwrap_or_else(|| {
56
+ panic!("Migration error: no existing state found");
57
+ });
58
+
59
+ let old: AppStateV1 = BorshDeserialize::deserialize(&mut &old_bytes[..])
60
+ .unwrap_or_else(|e| panic!("Migration error: deserialization failed: {:?}", e));
61
+
62
+ app::emit!(Event::Migrated { from: "1.0.0", to: "2.0.0" });
63
+
64
+ AppStateV2 {
65
+ items: old.items,
66
+ counter: old.counter,
67
+ notes: LwwRegister::new("added in v2".to_owned()),
68
+ }
69
+ }
70
+
71
+ // --- App logic (same as before, plus new notes methods) ---
72
+ #[app::logic]
73
+ impl AppStateV2 {
74
+ #[app::init]
75
+ pub fn init() -> AppStateV2 {
76
+ AppStateV2 {
77
+ items: UnorderedMap::new(),
78
+ counter: LwwRegister::new(0),
79
+ notes: LwwRegister::new(String::new()),
80
+ }
81
+ }
82
+
83
+ pub fn set_notes(&mut self, notes: String) -> app::Result<()> {
84
+ self.notes.set(notes);
85
+ Ok(())
86
+ }
87
+ }
88
+ ```
89
+
90
+ ---
91
+
92
+ ## `read_raw()`
93
+
94
+ ```rust
95
+ use calimero_sdk::state::read_raw;
96
+
97
+ let bytes: Option<Vec<u8>> = read_raw();
98
+ ```
99
+
100
+ Returns the raw Borsh-encoded bytes of the current state, or `None` if no state exists (fresh
101
+ context). Always `unwrap` with a clear panic message — a missing state during migration is a
102
+ programmer error.
103
+
104
+ ---
105
+
106
+ ## Migration scenarios
107
+
108
+ | Change | How to handle |
109
+ | -------------- | -------------------------------------------------- |
110
+ | Add a field | New field with default value in migrate fn |
111
+ | Remove a field | Simply don't include it in new struct |
112
+ | Rename a field | Map old field name to new field name |
113
+ | Change a type | Deserialize old, convert value, assign to new type |
114
+
115
+ ---
116
+
117
+ ## Rules
118
+
119
+ - The migration function must return the **exact** new state type decorated with `#[app::state]`
120
+ - The old struct needs only `BorshDeserialize` — do NOT put `#[app::state]` on it
121
+ - Keep old struct definitions in the source file (or a `migrations` module) as long as the migration
122
+ exists
123
+ - Do NOT call `read_raw()` outside of `#[app::migrate]` — it's only valid in that context
@@ -0,0 +1,113 @@
1
+ # Nested CRDTs and Mergeable
2
+
3
+ When you use a custom struct as a value in `UnorderedMap<K, V>` or `UserStorage<T>`, that struct
4
+ must implement `Mergeable` so the CRDT engine knows how to resolve concurrent writes.
5
+
6
+ Two ways: derive macro (easiest) or manual impl.
7
+
8
+ ---
9
+
10
+ ## Option 1: `#[derive(Mergeable)]` (recommended when all fields are CRDTs)
11
+
12
+ Add `calimero-storage-macros` to your dependencies:
13
+
14
+ ```toml
15
+ [dependencies]
16
+ calimero-sdk = "0.10"
17
+ calimero-storage = "0.10"
18
+ calimero-storage-macros = "0.10"
19
+ ```
20
+
21
+ ```rust
22
+ use calimero_sdk::app;
23
+ use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
24
+ use calimero_storage::collections::{Counter, UnorderedMap};
25
+ use calimero_storage_macros::Mergeable;
26
+
27
+ /// All fields are CRDTs — derive macro just calls merge() on each field
28
+ #[derive(Debug, Mergeable, BorshSerialize, BorshDeserialize)]
29
+ #[borsh(crate = "calimero_sdk::borsh")]
30
+ pub struct TeamStats {
31
+ pub wins: Counter,
32
+ pub losses: Counter,
33
+ pub draws: Counter,
34
+ }
35
+
36
+ #[app::state(emits = MetricsEvent)]
37
+ #[derive(Debug, BorshSerialize, BorshDeserialize)]
38
+ #[borsh(crate = "calimero_sdk::borsh")]
39
+ pub struct AppState {
40
+ teams: UnorderedMap<String, TeamStats>,
41
+ }
42
+
43
+ #[app::logic]
44
+ impl AppState {
45
+ pub fn record_win(&mut self, team: String) -> app::Result<u64> {
46
+ let mut stats = self.teams.get(&team)?.unwrap_or_else(|| TeamStats {
47
+ wins: Counter::new(),
48
+ losses: Counter::new(),
49
+ draws: Counter::new(),
50
+ });
51
+ stats.wins.increment()?;
52
+ let total = stats.wins.value()?;
53
+ self.teams.insert(team, stats)?;
54
+ Ok(total)
55
+ }
56
+ }
57
+ ```
58
+
59
+ Use `#[derive(Mergeable)]` whenever all fields are CRDT types (`Counter`, `UnorderedMap`,
60
+ `LwwRegister`, `Vector`, etc.).
61
+
62
+ ---
63
+
64
+ ## Option 2: Manual `Mergeable` impl (when fields aren't all CRDTs)
65
+
66
+ ```rust
67
+ use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
68
+ use calimero_storage::collections::{
69
+ crdt_meta::MergeError, LwwRegister, Mergeable, UnorderedMap,
70
+ };
71
+
72
+ #[derive(Debug, BorshSerialize, BorshDeserialize, Default)]
73
+ #[borsh(crate = "calimero_sdk::borsh")]
74
+ pub struct UserProfile {
75
+ pub name: LwwRegister<String>,
76
+ pub score: LwwRegister<u64>,
77
+ pub tags: UnorderedMap<String, LwwRegister<bool>>,
78
+ }
79
+
80
+ impl Mergeable for UserProfile {
81
+ fn merge(&mut self, other: &Self) -> Result<(), MergeError> {
82
+ self.name.merge(&other.name)?;
83
+ self.score.merge(&other.score)?;
84
+ self.tags.merge(&other.tags)?;
85
+ Ok(())
86
+ }
87
+ }
88
+ ```
89
+
90
+ For structs with non-CRDT primitive fields (e.g. `u64` timestamps), pick a merge strategy explicitly
91
+ — typically last-write-wins based on a timestamp field:
92
+
93
+ ```rust
94
+ impl Mergeable for FileRecord {
95
+ fn merge(&mut self, other: &Self) -> Result<(), MergeError> {
96
+ // LWW: take the version with the later timestamp
97
+ if other.uploaded_at > self.uploaded_at {
98
+ *self = other.clone();
99
+ }
100
+ Ok(())
101
+ }
102
+ }
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Rules
108
+
109
+ - Custom value types in `UnorderedMap<K, V>` **must** implement `Mergeable`
110
+ - Custom types in `UserStorage<T>` **must** implement `Mergeable + Default`
111
+ - `#[derive(Mergeable)]` works only when every field already implements `Mergeable`
112
+ - All nested CRDT fields must still be initialized with `::new()` — there is no blanket `Default`
113
+ (exception: when `Default` is derived manually for use with `UserStorage`)
@@ -2,55 +2,97 @@
2
2
 
3
3
  Calimero apps have two storage scopes:
4
4
 
5
- | Scope | Who can read | Synced across members? |
6
- | --- | --- | --- |
7
- | Shared state (CRDT) | All context members | Yes — automatic |
8
- | Private storage | Only the local member | No |
5
+ | Scope | Who can read | Synced across members? |
6
+ | ------------------- | -------------------------- | ---------------------- |
7
+ | Shared state (CRDT) | All context members | Yes — automatic |
8
+ | Private storage | Only the local node/member | No |
9
9
 
10
- ## Using private storage
10
+ ## Declaring private storage
11
+
12
+ Use `#[app::private]` on a separate struct. Private storage is **never broadcast** to other nodes.
11
13
 
12
14
  ```rust
13
- use calimero_sdk::env;
15
+ use calimero_sdk::app;
16
+ use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
17
+ use calimero_storage::collections::UnorderedMap;
14
18
 
15
- // Write private data
16
- env::private_storage_write(b"my_key", b"my_value");
19
+ #[derive(BorshSerialize, BorshDeserialize, Debug)]
20
+ #[borsh(crate = "calimero_sdk::borsh")]
21
+ #[app::private]
22
+ pub struct PrivateData {
23
+ secrets: UnorderedMap<String, String>,
24
+ }
17
25
 
18
- // Read private data
19
- let value = env::private_storage_read(b"my_key");
20
- // returns Option<Vec<u8>>
26
+ impl Default for PrivateData {
27
+ fn default() -> Self {
28
+ Self {
29
+ secrets: UnorderedMap::new(),
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ ## Reading and writing private storage
36
+
37
+ ```rust
38
+ #[app::logic]
39
+ impl AppState {
40
+ pub fn set_secret(&mut self, key: String, value: String) -> app::Result<()> {
41
+ // Load (or create default) private storage
42
+ let mut priv_data = PrivateData::private_load_or_default()?;
43
+ // Get a mutable guard
44
+ let mut priv_mut = priv_data.as_mut();
45
+
46
+ priv_mut.secrets.insert(key, value)?;
47
+ Ok(())
48
+ }
49
+
50
+ pub fn get_secret(&self, key: &str) -> app::Result<Option<String>> {
51
+ let priv_data = PrivateData::private_load_or_default()?;
52
+ Ok(priv_data.secrets.get(key)?.cloned())
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## Real example from battleships — private board storage
58
+
59
+ ```rust
60
+ #[derive(BorshSerialize, BorshDeserialize, Debug)]
61
+ #[borsh(crate = "calimero_sdk::borsh")]
62
+ #[app::private]
63
+ pub struct PrivateBoards {
64
+ boards: UnorderedMap<String, PlayerBoard>,
65
+ }
66
+
67
+ impl Default for PrivateBoards {
68
+ fn default() -> Self {
69
+ Self { boards: UnorderedMap::new() }
70
+ }
71
+ }
72
+
73
+ // Usage inside a method:
74
+ let mut priv_boards = PrivateBoards::private_load_or_default()?;
75
+ let mut priv_mut = priv_boards.as_mut();
76
+ let mut pb = priv_mut.boards.get(&key)?.unwrap_or(PlayerBoard::new());
77
+ pb.place_ships(ships)?;
78
+ priv_mut.boards.insert(key, pb)?;
21
79
  ```
22
80
 
23
81
  ## When to use private storage
24
82
 
25
83
  - Secrets, credentials, or keys that should not leave the local node
26
- - Per-member preferences or settings
84
+ - Per-member preferences or game state (e.g. hidden ship positions in battleships)
27
85
  - Draft data not yet ready to share with the context
28
86
  - Caching computed values locally
29
87
 
30
88
  ## When NOT to use private storage
31
89
 
32
- - Anything that must be visible to other context members — use CRDT state instead
90
+ - Anything that must be visible to other context members — use CRDT shared state instead
33
91
  - Authoritative application state — always use shared CRDT collections for that
34
92
 
35
- ## Serializing structured data to private storage
36
-
37
- ```rust
38
- use calimero_sdk::borsh::{self, BorshSerialize, BorshDeserialize};
39
- use calimero_sdk::env;
40
-
41
- #[derive(BorshSerialize, BorshDeserialize)]
42
- struct MyPrivateData {
43
- token: String,
44
- created_at: u64,
45
- }
93
+ ## Key points
46
94
 
47
- // Write
48
- let data = MyPrivateData { token: "secret".into(), created_at: 1234567890 };
49
- let bytes = borsh::to_vec(&data).unwrap();
50
- env::private_storage_write(b"my_data", &bytes);
51
-
52
- // Read
53
- if let Some(bytes) = env::private_storage_read(b"my_data") {
54
- let data: MyPrivateData = borsh::from_slice(&bytes).unwrap();
55
- }
56
- ```
95
+ - `PrivateData::private_load_or_default()?` — loads or creates fresh instance
96
+ - `priv_data.as_mut()` returns a mutable guard; changes are persisted when the guard is dropped
97
+ - Private storage uses `UnorderedMap` from `calimero_storage::collections`, same as shared state
98
+ - Multiple `#[app::private]` structs can coexist in the same app
@@ -1,39 +1,120 @@
1
1
  # CRDT State Collections
2
2
 
3
- Calimero provides conflict-free replicated data types for application state. These are the only correct way to store persistent, shared state.
3
+ Calimero provides conflict-free replicated data types for application state. All collections are
4
+ imported from `calimero_storage::collections`.
5
+
6
+ > **Critical:** Do NOT use `calimero_sdk::state::*` — that path no longer exists. The correct import
7
+ > is `calimero_storage::collections::*`.
4
8
 
5
9
  ## Available Collections
6
10
 
7
- | Type | Use for | std equivalent |
8
- | --- | --- | --- |
9
- | `calimero_sdk::state::Map<K, V>` | Key-value mapping | `HashMap<K, V>` |
10
- | `calimero_sdk::state::Set<T>` | Unique values | `HashSet<T>` |
11
- | `calimero_sdk::state::Vector<T>` | Ordered list | `Vec<T>` |
12
- | `calimero_sdk::state::UnorderedMap<K, V>` | Unordered map | `HashMap<K, V>` |
11
+ | Type | Use for | Notes |
12
+ | ------------------------- | --------------------------------------- | -------------------------------------------------- |
13
+ | `UnorderedMap<K, V>` | Key-value mapping | Most collection ops return `Result<>` use `?` |
14
+ | `UnorderedSet<T>` | Unique value set | |
15
+ | `Vector<T>` | Ordered list (append-only) | |
16
+ | `LwwRegister<T>` | Single last-write-wins value | Wrap map values: `UnorderedMap<K, LwwRegister<V>>` |
17
+ | `Counter` | Grow-only counter (GCounter by default) | `.increment()`, `.value()` |
18
+ | `Counter<true>` | PN-Counter (supports decrement) | Same API + `.decrement()` |
19
+ | `FrozenStorage<T>` | Immutable content-addressed entries | |
20
+ | `UserStorage<T>` | Per-member isolated storage | Not synced to other members |
21
+ | `ReplicatedGrowableArray` | CRDT text / ordered sequence | Collaborative editing |
22
+
23
+ ## Import
24
+
25
+ ```rust
26
+ use calimero_storage::collections::{
27
+ Counter, FrozenStorage, LwwRegister, ReplicatedGrowableArray,
28
+ UnorderedMap, UnorderedSet, UserStorage, Vector,
29
+ };
30
+ ```
13
31
 
14
32
  ## Usage
15
33
 
16
34
  ```rust
17
- use calimero_sdk::state::{Map, Vector};
35
+ use calimero_sdk::app;
36
+ use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
37
+ use calimero_storage::collections::{LwwRegister, UnorderedMap, Vector, Counter};
18
38
 
19
- #[derive(Default, BorshDeserialize, BorshSerialize)]
39
+ #[app::state]
40
+ #[derive(Debug, BorshSerialize, BorshDeserialize)]
41
+ #[borsh(crate = "calimero_sdk::borsh")]
20
42
  pub struct AppState {
21
- items: Map<String, String>,
22
- log: Vector<String>,
43
+ // Map values wrapped in LwwRegister for CRDT merge
44
+ items: UnorderedMap<String, LwwRegister<String>>,
45
+ log: Vector<String>,
46
+ counter: Counter,
23
47
  }
24
48
 
25
49
  #[app::logic]
26
50
  impl AppState {
27
- pub fn set(&mut self, key: String, value: String) {
28
- self.items.insert(key, value);
51
+ pub fn set(&mut self, key: String, value: String) -> app::Result<()> {
52
+ self.items.insert(key, value.into())?;
53
+ Ok(())
54
+ }
55
+
56
+ pub fn get(&self, key: &str) -> app::Result<Option<String>> {
57
+ Ok(self.items.get(key)?.map(|v| v.get().clone()))
29
58
  }
30
59
 
31
- pub fn get(&self, key: &str) -> Option<&String> {
32
- self.items.get(key)
60
+ pub fn append(&mut self, entry: String) -> app::Result<()> {
61
+ self.log.push(entry)?;
62
+ Ok(())
33
63
  }
34
64
 
35
- pub fn append(&mut self, entry: String) {
36
- self.log.push(entry);
65
+ pub fn increment(&mut self) -> app::Result<()> {
66
+ self.counter.increment()?;
67
+ Ok(())
68
+ }
69
+ }
70
+ ```
71
+
72
+ ## UnorderedMap patterns
73
+
74
+ ```rust
75
+ // Insert
76
+ self.items.insert(key, value.into())?;
77
+
78
+ // Get (returns Option<LwwRegister<V>>)
79
+ let val = self.items.get(&key)?.map(|v| v.get().clone());
80
+
81
+ // Contains
82
+ let exists = self.items.contains(&key)?;
83
+
84
+ // Remove
85
+ let old = self.items.remove(&key)?;
86
+
87
+ // All entries
88
+ let all: Vec<(K, V)> = self.items.entries()?.collect();
89
+
90
+ // In-place mutation via guard
91
+ if let Some(mut guard) = self.items.get_mut(&key)? {
92
+ guard.set(new_value);
93
+ }
94
+
95
+ // Entry API (like HashMap::entry)
96
+ use calimero_storage::collections::unordered_map::Entry;
97
+ let entry = self.items.entry(key)?;
98
+ let val = entry.or_insert(LwwRegister::new(default_value))?;
99
+
100
+ // Length
101
+ let n = self.items.len()?;
102
+
103
+ // Clear
104
+ self.items.clear()?;
105
+ ```
106
+
107
+ ## Constructor
108
+
109
+ Collections must be initialized with `::new()` — there is no Default impl:
110
+
111
+ ```rust
112
+ #[app::init]
113
+ pub fn init() -> AppState {
114
+ AppState {
115
+ items: UnorderedMap::new(),
116
+ log: Vector::new(),
117
+ counter: Counter::new(),
37
118
  }
38
119
  }
39
120
  ```
@@ -41,10 +122,14 @@ impl AppState {
41
122
  ## Keys and values must implement
42
123
 
43
124
  - `BorshSerialize + BorshDeserialize`
44
- - `Clone` (for most operations)
125
+ - Values in UnorderedMap are typically wrapped in `LwwRegister<V>` this handles conflict
126
+ resolution automatically
45
127
 
46
128
  ## Important
47
129
 
48
- CRDT collections handle concurrent writes from different context members automatically —
49
- you never need to manually resolve conflicts. Writes from different members are merged
50
- deterministically using the DAG-based sync engine.
130
+ CRDT collections handle concurrent writes from different context members automatically — you never
131
+ need to manually resolve conflicts. Writes from different members are merged deterministically using
132
+ the DAG-based sync engine.
133
+
134
+ Collection operations are **fallible** — always propagate errors with `?`. Panicking inside a WASM
135
+ method aborts the execution and rolls back state.
@@ -0,0 +1,169 @@
1
+ # UserStorage and FrozenStorage
2
+
3
+ Two specialised CRDT collection types for per-member and immutable data.
4
+
5
+ ---
6
+
7
+ ## UserStorage\<T\>
8
+
9
+ Stores a separate value of type `T` per context member (keyed by public key). Each member can only
10
+ read/write their own value via the current executor identity. Reading another member's value
11
+ requires explicitly passing their public key.
12
+
13
+ `T` must implement `BorshSerialize + BorshDeserialize + Mergeable + Default`.
14
+
15
+ ### Simple example (single value per user)
16
+
17
+ ```rust
18
+ use calimero_storage::collections::{LwwRegister, UserStorage};
19
+
20
+ #[app::state]
21
+ pub struct AppState {
22
+ // Each member stores their own string value
23
+ user_names: UserStorage<LwwRegister<String>>,
24
+ }
25
+
26
+ #[app::logic]
27
+ impl AppState {
28
+ // Write the current executor's value
29
+ pub fn set_my_name(&mut self, name: String) -> app::Result<()> {
30
+ self.user_names.insert(name.into())?;
31
+ Ok(())
32
+ }
33
+
34
+ // Read the current executor's value
35
+ pub fn get_my_name(&self) -> app::Result<Option<String>> {
36
+ Ok(self.user_names.get()?.map(|v| v.get().clone()))
37
+ }
38
+
39
+ // Read a specific user's value
40
+ pub fn get_name_for(&self, user_key: calimero_sdk::PublicKey) -> app::Result<Option<String>> {
41
+ Ok(self.user_names.get_for_user(&user_key)?.map(|v| v.get().clone()))
42
+ }
43
+ }
44
+ ```
45
+
46
+ ### Nested example (map per user)
47
+
48
+ When `T` is a collection, it must implement `Mergeable` manually (or via `#[derive(Mergeable)]`):
49
+
50
+ ```rust
51
+ use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
52
+ use calimero_storage::collections::{LwwRegister, Mergeable, UnorderedMap, UserStorage};
53
+
54
+ #[derive(Debug, BorshSerialize, BorshDeserialize, Default)]
55
+ #[borsh(crate = "calimero_sdk::borsh")]
56
+ struct UserKvMap {
57
+ map: UnorderedMap<String, LwwRegister<String>>,
58
+ }
59
+
60
+ impl Mergeable for UserKvMap {
61
+ fn merge(&mut self, other: &Self)
62
+ -> Result<(), calimero_storage::collections::crdt_meta::MergeError>
63
+ {
64
+ self.map.merge(&other.map)
65
+ }
66
+ }
67
+
68
+ #[app::state]
69
+ pub struct AppState {
70
+ user_data: UserStorage<UserKvMap>,
71
+ }
72
+
73
+ #[app::logic]
74
+ impl AppState {
75
+ pub fn set_user_kv(&mut self, key: String, value: String) -> app::Result<()> {
76
+ // get-modify-put pattern
77
+ let mut data = self.user_data.get()?.unwrap_or_default();
78
+ data.map.insert(key, value.into())?;
79
+ self.user_data.insert(data)?;
80
+ Ok(())
81
+ }
82
+
83
+ pub fn get_user_kv(&self, key: &str) -> app::Result<Option<String>> {
84
+ let data = self.user_data.get()?;
85
+ match data {
86
+ Some(d) => Ok(d.map.get(key)?.map(|v| v.get().clone())),
87
+ None => Ok(None),
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ ### Key methods
94
+
95
+ | Method | Description |
96
+ | -------------------------------- | -------------------------------- |
97
+ | `insert(value: T)?` | Store value for current executor |
98
+ | `get()?` | Get value for current executor |
99
+ | `get_for_user(key: &PublicKey)?` | Get value for a specific user |
100
+
101
+ ---
102
+
103
+ ## FrozenStorage\<T\>
104
+
105
+ Content-addressed, **immutable** storage. Values are keyed by their SHA-256 hash. Once inserted, a
106
+ value can never be modified or deleted. All members share the same frozen entries (unlike
107
+ `UserStorage`).
108
+
109
+ `T` must implement `BorshSerialize + BorshDeserialize + Into<FrozenValue<T>>`. Typically `T` is
110
+ `String` or a `Vec<u8>`.
111
+
112
+ ```rust
113
+ use calimero_storage::collections::FrozenStorage;
114
+
115
+ #[app::state]
116
+ pub struct AppState {
117
+ frozen: FrozenStorage<String>,
118
+ }
119
+
120
+ #[app::logic]
121
+ impl AppState {
122
+ /// Insert a value — returns its 32-byte SHA-256 hash
123
+ pub fn add_frozen(&mut self, value: String) -> app::Result<[u8; 32]> {
124
+ let hash = self.frozen.insert(value.into())?;
125
+ Ok(hash)
126
+ }
127
+
128
+ /// Retrieve by hash
129
+ pub fn get_frozen(&self, hash: [u8; 32]) -> app::Result<Option<String>> {
130
+ Ok(self.frozen.get(&hash)?.map(|v| v.clone()))
131
+ }
132
+ }
133
+ ```
134
+
135
+ ### Typical usage pattern
136
+
137
+ Since hashes are `[u8; 32]`, encode them as hex or base58 strings for JSON:
138
+
139
+ ```rust
140
+ use hex;
141
+
142
+ pub fn add_frozen(&mut self, value: String) -> app::Result<String> {
143
+ let hash = self.frozen.insert(value.into())?;
144
+ Ok(hex::encode(hash)) // return as hex string for JSON clients
145
+ }
146
+
147
+ pub fn get_frozen(&self, hash_hex: String) -> app::Result<Option<String>> {
148
+ let mut hash = [0u8; 32];
149
+ hex::decode_to_slice(&hash_hex, &mut hash)
150
+ .map_err(|_| /* your error type */)?;
151
+ Ok(self.frozen.get(&hash)?.map(|v| v.clone()))
152
+ }
153
+ ```
154
+
155
+ ### Frozen storage key methods
156
+
157
+ | Method | Description |
158
+ | -------------------------- | ------------------------------------ |
159
+ | `insert(value: T.into())?` | Store value, returns `[u8; 32]` hash |
160
+ | `get(&hash)?` | Retrieve value by hash |
161
+
162
+ ---
163
+
164
+ ## Cargo.toml — add hex for hash encoding
165
+
166
+ ```toml
167
+ [dependencies]
168
+ hex = "0.4"
169
+ ```