@calimero-network/agent-skills 0.2.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 -23
  3. package/package.json +2 -2
  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 +127 -22
  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 -59
  16. package/skills/calimero-client-js/rules/camelcase-api.md +10 -7
  17. package/skills/calimero-client-js/rules/token-refresh.md +59 -21
  18. package/skills/calimero-client-py/SKILL.md +26 -10
  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 +25 -14
  32. package/skills/calimero-desktop/references/sso-integration.md +49 -22
  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 +52 -35
  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 +45 -0
  71. package/skills/calimero-rust-sdk/rules/wasm-constraints.md +12 -10
  72. package/skills/calimero-sdk-js/SKILL.md +145 -0
  73. package/skills/calimero-sdk-js/references/build-pipeline.md +98 -0
  74. package/skills/calimero-sdk-js/references/collections.md +132 -0
  75. package/skills/calimero-sdk-js/references/events.md +63 -0
  76. package/skills/calimero-sdk-js/rules/crdt-only-state.md +47 -0
  77. package/skills/calimero-sdk-js/rules/no-console-log.md +38 -0
  78. package/skills/calimero-sdk-js/rules/view-decorator.md +48 -0
@@ -0,0 +1,122 @@
1
+ # Event Handlers
2
+
3
+ Event handlers are app methods that run **automatically on every peer** when an event is emitted
4
+ with a named handler. The emitting peer runs the handler immediately; all other peers run it when
5
+ they receive the event during sync.
6
+
7
+ This lets you trigger side effects (CRDT updates, counters, bookkeeping) that happen consistently on
8
+ every node without requiring explicit RPC calls.
9
+
10
+ ---
11
+
12
+ ## Syntax
13
+
14
+ Pass a string method name as the second element of a tuple in `app::emit!`:
15
+
16
+ ```rust
17
+ // Without handler (normal event, subscribers only):
18
+ app::emit!(Event::Updated { key: &key, value: &value });
19
+
20
+ // With handler (also calls self.update_handler(key, value) on every peer):
21
+ app::emit!((Event::Updated { key: &key, value: &value }, "update_handler"));
22
+ ```
23
+
24
+ The handler method must exist on the same `#[app::logic]` impl with matching arguments.
25
+
26
+ ---
27
+
28
+ ## Full example
29
+
30
+ ```rust
31
+ use calimero_sdk::app;
32
+ use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
33
+ use calimero_storage::collections::{Counter, LwwRegister, UnorderedMap};
34
+
35
+ #[app::state(emits = for<'a> Event<'a>)]
36
+ #[derive(Debug, BorshSerialize, BorshDeserialize)]
37
+ #[borsh(crate = "calimero_sdk::borsh")]
38
+ pub struct AppState {
39
+ items: UnorderedMap<String, LwwRegister<String>>,
40
+ handler_counter: Counter,
41
+ }
42
+
43
+ #[app::event]
44
+ pub enum Event<'a> {
45
+ Inserted { key: &'a str, value: &'a str },
46
+ Updated { key: &'a str, value: &'a str },
47
+ Removed { key: &'a str },
48
+ }
49
+
50
+ #[app::logic]
51
+ impl AppState {
52
+ #[app::init]
53
+ pub fn init() -> AppState {
54
+ AppState {
55
+ items: UnorderedMap::new(),
56
+ handler_counter: Counter::new(),
57
+ }
58
+ }
59
+
60
+ pub fn set(&mut self, key: String, value: String) -> app::Result<()> {
61
+ if self.items.contains(&key)? {
62
+ app::emit!((Event::Updated { key: &key, value: &value }, "update_handler"));
63
+ } else {
64
+ app::emit!((Event::Inserted { key: &key, value: &value }, "insert_handler"));
65
+ }
66
+ self.items.insert(key, value.into())?;
67
+ Ok(())
68
+ }
69
+
70
+ pub fn remove(&mut self, key: &str) -> app::Result<Option<String>> {
71
+ app::emit!((Event::Removed { key }, "remove_handler"));
72
+ Ok(self.items.remove(key)?.map(|v| v.get().clone()))
73
+ }
74
+
75
+ // --- Handlers (called automatically on every peer when event fires) ---
76
+
77
+ pub fn insert_handler(&mut self, key: &str, value: &str) -> app::Result<()> {
78
+ app::log!("insert_handler: key={}, value={}", key, value);
79
+ self.handler_counter.increment()?;
80
+ Ok(())
81
+ }
82
+
83
+ pub fn update_handler(&mut self, key: &str, value: &str) -> app::Result<()> {
84
+ app::log!("update_handler: key={}, value={}", key, value);
85
+ self.handler_counter.increment()?;
86
+ Ok(())
87
+ }
88
+
89
+ pub fn remove_handler(&mut self, key: &str) -> app::Result<()> {
90
+ app::log!("remove_handler: key={}", key);
91
+ self.handler_counter.increment()?;
92
+ Ok(())
93
+ }
94
+
95
+ pub fn get_handler_count(&self) -> app::Result<u64> {
96
+ Ok(self.handler_counter.value()?)
97
+ }
98
+ }
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Handler constraints
104
+
105
+ Handlers must be:
106
+
107
+ - **Commutative** — order of execution should not matter
108
+ - **Idempotent** — safe to replay (sync may re-deliver events during catch-up)
109
+ - **Pure w.r.t. CRDT state** — only mutate CRDT fields; avoid logic that depends on exact ordering
110
+
111
+ Handlers receive the same arguments as the event variant fields. The method signature must match
112
+ exactly.
113
+
114
+ ---
115
+
116
+ ## When to use handlers vs regular methods
117
+
118
+ | Use handler when | Use regular method when |
119
+ | -------------------------------------------------------- | -------------------------------------------- |
120
+ | You want something to happen on every node automatically | You want a node to call something explicitly |
121
+ | Maintaining derived CRDT counters / aggregates | One-off mutations triggered by one actor |
122
+ | Bookkeeping that must stay consistent across peers | Operations that require authorization checks |
@@ -1,6 +1,7 @@
1
1
  # Events
2
2
 
3
- Events are emitted from app logic and pushed in real-time to all context members subscribed via WebSocket.
3
+ Events are emitted from app logic and pushed in real-time to all context members subscribed via
4
+ WebSocket.
4
5
 
5
6
  ## Define an event type
6
7
 
@@ -2,42 +2,58 @@
2
2
 
3
3
  ## KV Store (simplest app)
4
4
 
5
- Source: https://github.com/calimero-network/kv-store
5
+ Source: <https://github.com/calimero-network/kv-store>
6
6
 
7
7
  ```rust
8
8
  use calimero_sdk::app;
9
9
  use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
10
- use calimero_sdk::state::UnorderedMap;
10
+ use calimero_sdk::serde::Serialize;
11
+ use calimero_storage::collections::{LwwRegister, UnorderedMap};
11
12
 
12
- #[derive(Default, BorshDeserialize, BorshSerialize)]
13
+ #[app::state(emits = for<'a> Event<'a>)]
14
+ #[derive(Debug, BorshSerialize, BorshDeserialize)]
15
+ #[borsh(crate = "calimero_sdk::borsh")]
13
16
  pub struct KvStore {
14
- entries: UnorderedMap<String, String>,
17
+ items: UnorderedMap<String, LwwRegister<String>>,
15
18
  }
16
19
 
17
- #[app::state]
18
- impl KvStore {}
20
+ #[app::event]
21
+ pub enum Event<'a> {
22
+ Inserted { key: &'a str, value: &'a str },
23
+ Updated { key: &'a str, value: &'a str },
24
+ Removed { key: &'a str },
25
+ }
19
26
 
20
27
  #[app::logic]
21
28
  impl KvStore {
22
29
  #[app::init]
23
30
  pub fn init() -> KvStore {
24
- KvStore::default()
31
+ KvStore { items: UnorderedMap::new() }
25
32
  }
26
33
 
27
- pub fn set(&mut self, key: String, value: String) {
28
- self.entries.insert(key, value);
34
+ pub fn set(&mut self, key: String, value: String) -> app::Result<()> {
35
+ if self.items.contains(&key)? {
36
+ app::emit!(Event::Updated { key: &key, value: &value });
37
+ } else {
38
+ app::emit!(Event::Inserted { key: &key, value: &value });
39
+ }
40
+ self.items.insert(key, value.into())?;
41
+ Ok(())
29
42
  }
30
43
 
31
- pub fn get(&self, key: String) -> Option<String> {
32
- self.entries.get(&key).cloned()
44
+ pub fn get(&self, key: &str) -> app::Result<Option<String>> {
45
+ Ok(self.items.get(key)?.map(|v| v.get().clone()))
33
46
  }
34
47
 
35
- pub fn entries(&self) -> Vec<(String, String)> {
36
- self.entries.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
48
+ pub fn remove(&mut self, key: &str) -> app::Result<Option<String>> {
49
+ app::emit!(Event::Removed { key });
50
+ Ok(self.items.remove(key)?.map(|v| v.get().clone()))
37
51
  }
38
52
 
39
- pub fn remove(&mut self, key: String) -> bool {
40
- self.entries.remove(&key).is_some()
53
+ pub fn entries(&self) -> app::Result<Vec<(String, String)>> {
54
+ Ok(self.items.entries()?
55
+ .map(|(k, v)| (k, v.get().clone()))
56
+ .collect())
41
57
  }
42
58
  }
43
59
  ```
@@ -45,26 +61,62 @@ impl KvStore {
45
61
  ## Calling from a client (JSON-RPC)
46
62
 
47
63
  Mutations (state changes):
64
+
48
65
  ```json
49
- {
50
- "method": "set",
51
- "args": { "key": "hello", "value": "world" }
52
- }
66
+ { "method": "set", "args": { "key": "hello", "value": "world" } }
53
67
  ```
54
68
 
55
69
  Views (read-only):
70
+
56
71
  ```json
57
- {
58
- "method": "get",
59
- "args": { "key": "hello" }
60
- }
72
+ { "method": "get", "args": { "key": "hello" } }
73
+ { "method": "entries", "args": {} }
74
+ ```
75
+
76
+ ## Lobby + Game pattern (battleships)
77
+
78
+ Two separate WASM crates — a Lobby context and a Game context. The Game context calls back into the
79
+ Lobby via `env::xcall()` when a match ends.
80
+
81
+ ```rust
82
+ // In game crate — notify lobby when match finishes
83
+ let params = calimero_sdk::serde_json::json!({
84
+ "match_id": mid,
85
+ "winner": winner_key,
86
+ "loser": loser_key,
87
+ });
88
+ let params_bytes = calimero_sdk::serde_json::to_vec(&params).unwrap();
89
+ calimero_sdk::env::xcall(&lobby_ctx_bytes, "on_match_finished", &params_bytes);
61
90
  ```
62
91
 
63
- ## Other reference apps in core/apps
92
+ ## Error handling pattern
93
+
94
+ ```rust
95
+ use thiserror::Error;
96
+ use calimero_sdk::serde::Serialize;
97
+
98
+ #[derive(Debug, Error, Serialize)]
99
+ #[serde(crate = "calimero_sdk::serde")]
100
+ #[serde(tag = "kind", content = "data")]
101
+ pub enum AppError {
102
+ #[error("not found: {0}")]
103
+ NotFound(String),
104
+ #[error("forbidden: {0}")]
105
+ Forbidden(&'static str),
106
+ }
107
+
108
+ // In a method:
109
+ pub fn get_item(&self, key: &str) -> app::Result<String> {
110
+ let Some(v) = self.items.get(key)? else {
111
+ app::bail!(AppError::NotFound(key.to_string()));
112
+ };
113
+ Ok(v.get().clone())
114
+ }
115
+ ```
64
116
 
65
- - `collaborative-editor` concurrent text editing with CRDT merge
66
- - `private-data` — mixing shared and private storage patterns
67
- - `team-metrics` — structured data with multiple CRDT collections
68
- - `blobs` — binary payload handling
117
+ ## Other reference apps
69
118
 
70
- Source: https://github.com/calimero-network/core/tree/master/apps
119
+ - `battleships` — multi-context game, private storage, xcall, event handlers
120
+ - `calimero-scaffolding-e2e-application` — exercises all CRDT types, blob API, user storage
121
+ - `curb` — chat app, event-driven UI updates
122
+ - Source: <https://github.com/calimero-network/>
@@ -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