@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.
- package/README.md +137 -17
- package/SKILL.md +31 -23
- package/package.json +2 -2
- package/scripts/install.js +3 -3
- package/scripts/test.js +6 -15
- package/skills/calimero-abi-codegen/SKILL.md +121 -22
- package/skills/calimero-abi-codegen/references/abi-format.md +3 -5
- package/skills/calimero-abi-codegen/references/generated-output.md +12 -4
- package/skills/calimero-abi-codegen/rules/schema-version.md +11 -4
- package/skills/calimero-abi-codegen/rules/unique-names.md +2 -6
- package/skills/calimero-client-js/SKILL.md +127 -22
- package/skills/calimero-client-js/references/auth.md +18 -10
- package/skills/calimero-client-js/references/rpc-calls.md +15 -21
- package/skills/calimero-client-js/references/sso.md +9 -9
- package/skills/calimero-client-js/references/websocket-events.md +73 -59
- package/skills/calimero-client-js/rules/camelcase-api.md +10 -7
- package/skills/calimero-client-js/rules/token-refresh.md +59 -21
- package/skills/calimero-client-py/SKILL.md +26 -10
- package/skills/calimero-client-py/references/api.md +41 -43
- package/skills/calimero-client-py/references/auth.md +7 -7
- package/skills/calimero-client-py/rules/async-usage.md +27 -31
- package/skills/calimero-client-py/rules/stable-node-name.md +7 -7
- package/skills/calimero-core/SKILL.md +135 -0
- package/skills/calimero-core/references/architecture.md +101 -0
- package/skills/calimero-core/references/jsonrpc-protocol.md +192 -0
- package/skills/calimero-core/references/namespaces-groups.md +94 -0
- package/skills/calimero-core/references/storage-types.md +118 -0
- package/skills/calimero-core/references/websocket-events.md +142 -0
- package/skills/calimero-core/rules/context-is-not-app.md +35 -0
- package/skills/calimero-core/rules/crdt-types-only.md +55 -0
- package/skills/calimero-desktop/SKILL.md +25 -14
- package/skills/calimero-desktop/references/sso-integration.md +49 -22
- package/skills/calimero-desktop/rules/sso-fallback.md +3 -2
- package/skills/calimero-merobox/SKILL.md +255 -28
- package/skills/calimero-merobox/references/ci-integration.md +3 -2
- package/skills/calimero-merobox/references/workflow-files.md +7 -5
- package/skills/calimero-merobox/rules/docker-required.md +7 -6
- package/skills/calimero-meroctl/SKILL.md +68 -0
- package/skills/calimero-meroctl/references/commands.md +177 -0
- package/skills/calimero-meroctl/references/scripting.md +80 -0
- package/skills/calimero-meroctl/rules/call-view-flag.md +28 -0
- package/skills/calimero-meroctl/rules/register-node-once.md +34 -0
- package/skills/calimero-merod/SKILL.md +49 -0
- package/skills/calimero-merod/references/health-endpoints.md +90 -0
- package/skills/calimero-merod/references/init-flags.md +84 -0
- package/skills/calimero-merod/rules/init-before-run.md +40 -0
- package/skills/calimero-merod/rules/port-assignments.md +33 -0
- package/skills/calimero-node/SKILL.md +52 -35
- package/skills/calimero-node/references/context-lifecycle.md +34 -17
- package/skills/calimero-node/references/meroctl-commands.md +89 -99
- package/skills/calimero-node/rules/app-vs-context.md +4 -4
- package/skills/calimero-registry/SKILL.md +110 -31
- package/skills/calimero-registry/references/bundle-and-push.md +99 -34
- package/skills/calimero-registry/references/manifest-format.md +56 -35
- package/skills/calimero-registry/references/mero-sign.md +10 -9
- package/skills/calimero-registry/rules/key-security.md +3 -2
- package/skills/calimero-registry/rules/sign-before-pack.md +5 -5
- package/skills/calimero-rust-sdk/SKILL.md +154 -44
- package/skills/calimero-rust-sdk/references/blob-api.md +119 -0
- package/skills/calimero-rust-sdk/references/event-handlers.md +122 -0
- package/skills/calimero-rust-sdk/references/events.md +2 -1
- package/skills/calimero-rust-sdk/references/examples.md +81 -29
- package/skills/calimero-rust-sdk/references/migrations.md +123 -0
- package/skills/calimero-rust-sdk/references/nested-crdts.md +113 -0
- package/skills/calimero-rust-sdk/references/private-storage.md +76 -34
- package/skills/calimero-rust-sdk/references/state-collections.md +106 -21
- package/skills/calimero-rust-sdk/references/user-and-frozen-storage.md +169 -0
- package/skills/calimero-rust-sdk/rules/app-macro-placement.md +5 -2
- package/skills/calimero-rust-sdk/rules/no-std-collections.md +5 -2
- package/skills/calimero-rust-sdk/rules/state-derives.md +45 -0
- package/skills/calimero-rust-sdk/rules/wasm-constraints.md +12 -10
- package/skills/calimero-sdk-js/SKILL.md +145 -0
- package/skills/calimero-sdk-js/references/build-pipeline.md +98 -0
- package/skills/calimero-sdk-js/references/collections.md +132 -0
- package/skills/calimero-sdk-js/references/events.md +63 -0
- package/skills/calimero-sdk-js/rules/crdt-only-state.md +47 -0
- package/skills/calimero-sdk-js/rules/no-console-log.md +38 -0
- 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 |
|
|
@@ -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::
|
|
10
|
+
use calimero_sdk::serde::Serialize;
|
|
11
|
+
use calimero_storage::collections::{LwwRegister, UnorderedMap};
|
|
11
12
|
|
|
12
|
-
#[
|
|
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
|
-
|
|
17
|
+
items: UnorderedMap<String, LwwRegister<String>>,
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
#[app::
|
|
18
|
-
|
|
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::
|
|
31
|
+
KvStore { items: UnorderedMap::new() }
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
pub fn set(&mut self, key: String, value: String) {
|
|
28
|
-
self.
|
|
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:
|
|
32
|
-
self.
|
|
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
|
|
36
|
-
|
|
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
|
|
40
|
-
self.entries
|
|
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
|
-
|
|
59
|
-
|
|
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(¶ms).unwrap();
|
|
89
|
+
calimero_sdk::env::xcall(&lobby_ctx_bytes, "on_match_finished", ¶ms_bytes);
|
|
61
90
|
```
|
|
62
91
|
|
|
63
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6
|
-
|
|
|
7
|
-
| Shared state (CRDT) | All context members
|
|
8
|
-
| Private storage
|
|
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
|
-
##
|
|
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::
|
|
15
|
+
use calimero_sdk::app;
|
|
16
|
+
use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
|
|
17
|
+
use calimero_storage::collections::UnorderedMap;
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|