@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.
- package/README.md +137 -17
- package/SKILL.md +31 -28
- package/package.json +1 -1
- 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 +126 -31
- 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 -92
- package/skills/calimero-client-js/rules/camelcase-api.md +10 -7
- package/skills/calimero-client-js/rules/token-refresh.md +11 -11
- package/skills/calimero-client-py/SKILL.md +25 -13
- 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 +24 -19
- package/skills/calimero-desktop/references/sso-integration.md +2 -2
- 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 +50 -39
- 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 +9 -10
- package/skills/calimero-rust-sdk/rules/wasm-constraints.md +12 -10
- package/skills/calimero-sdk-js/SKILL.md +34 -26
- package/skills/calimero-sdk-js/references/build-pipeline.md +6 -6
- package/skills/calimero-sdk-js/references/collections.md +11 -11
- package/skills/calimero-sdk-js/references/events.md +7 -3
- package/skills/calimero-sdk-js/rules/crdt-only-state.md +18 -18
- package/skills/calimero-sdk-js/rules/no-console-log.md +6 -6
- package/skills/calimero-sdk-js/rules/view-decorator.md +6 -4
|
@@ -5,11 +5,15 @@ You are helping a developer build a **Calimero WASM application** in Rust using
|
|
|
5
5
|
## What you need to know immediately
|
|
6
6
|
|
|
7
7
|
- Apps compile to WASM and run inside the `merod` node runtime
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
8
|
+
- `#[app::state]` goes on the **struct**, `#[app::logic]` goes on the **impl block**
|
|
9
|
+
- All CRDT collections are from `calimero_storage::collections::*` — NOT `calimero_sdk::state::*`
|
|
10
|
+
- Most collection operations return `Result<>` — always use `?`
|
|
11
|
+
- `app::Result<T>` is the correct return type for public methods (not `Result<T, String>`)
|
|
12
|
+
- Use `app::bail!(err)` to early-return errors
|
|
13
|
+
- Use `app::log!()` not `env::log!()` or `println!()`
|
|
14
|
+
- Events are declared with `#[app::event]` on an enum and declared on state with
|
|
15
|
+
`emits = for<'a> Event<'a>`
|
|
16
|
+
- Private storage uses `#[app::private]` struct + `StructName::private_load_or_default()?`
|
|
13
17
|
- There is no `main()` — the SDK provides the entry point
|
|
14
18
|
|
|
15
19
|
## Cargo.toml setup
|
|
@@ -19,38 +23,81 @@ You are helping a developer build a **Calimero WASM application** in Rust using
|
|
|
19
23
|
crate-type = ["cdylib"]
|
|
20
24
|
|
|
21
25
|
[dependencies]
|
|
22
|
-
calimero-sdk
|
|
26
|
+
calimero-sdk = "0.10"
|
|
27
|
+
calimero-storage = "0.10"
|
|
28
|
+
|
|
29
|
+
[build-dependencies]
|
|
30
|
+
calimero-wasm-abi = "0.10"
|
|
31
|
+
serde_json = "1"
|
|
32
|
+
|
|
33
|
+
[profile.app-release]
|
|
34
|
+
inherits = "release"
|
|
35
|
+
codegen-units = 1
|
|
36
|
+
opt-level = "z"
|
|
37
|
+
lto = true
|
|
38
|
+
debug = false
|
|
39
|
+
strip = "symbols"
|
|
40
|
+
panic = "abort"
|
|
41
|
+
overflow-checks = true
|
|
23
42
|
```
|
|
24
43
|
|
|
25
|
-
|
|
44
|
+
For the build script, add `build.rs`:
|
|
45
|
+
|
|
46
|
+
```rust
|
|
47
|
+
fn main() {
|
|
48
|
+
calimero_wasm_abi::export().unwrap();
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Minimal app skeleton (KV store)
|
|
26
53
|
|
|
27
54
|
```rust
|
|
28
55
|
use calimero_sdk::app;
|
|
29
56
|
use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
|
|
30
|
-
use calimero_sdk::
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
57
|
+
use calimero_sdk::serde::Serialize;
|
|
58
|
+
use calimero_storage::collections::{LwwRegister, UnorderedMap};
|
|
59
|
+
|
|
60
|
+
#[app::state(emits = for<'a> Event<'a>)]
|
|
61
|
+
#[derive(Debug, BorshSerialize, BorshDeserialize)]
|
|
62
|
+
#[borsh(crate = "calimero_sdk::borsh")]
|
|
63
|
+
pub struct KvStore {
|
|
64
|
+
items: UnorderedMap<String, LwwRegister<String>>,
|
|
35
65
|
}
|
|
36
66
|
|
|
37
|
-
#[app::
|
|
38
|
-
|
|
67
|
+
#[app::event]
|
|
68
|
+
pub enum Event<'a> {
|
|
69
|
+
Inserted { key: &'a str, value: &'a str },
|
|
70
|
+
Updated { key: &'a str, value: &'a str },
|
|
71
|
+
Removed { key: &'a str },
|
|
72
|
+
}
|
|
39
73
|
|
|
40
74
|
#[app::logic]
|
|
41
|
-
impl
|
|
75
|
+
impl KvStore {
|
|
42
76
|
#[app::init]
|
|
43
|
-
pub fn init() ->
|
|
44
|
-
|
|
77
|
+
pub fn init() -> KvStore {
|
|
78
|
+
KvStore {
|
|
79
|
+
items: UnorderedMap::new(),
|
|
80
|
+
}
|
|
45
81
|
}
|
|
46
82
|
|
|
47
|
-
pub fn set(&mut self, key: String, value: String) -> Result<()
|
|
48
|
-
|
|
83
|
+
pub fn set(&mut self, key: String, value: String) -> app::Result<()> {
|
|
84
|
+
app::log!("set {:?} = {:?}", key, value);
|
|
85
|
+
if self.items.contains(&key)? {
|
|
86
|
+
app::emit!(Event::Updated { key: &key, value: &value });
|
|
87
|
+
} else {
|
|
88
|
+
app::emit!(Event::Inserted { key: &key, value: &value });
|
|
89
|
+
}
|
|
90
|
+
self.items.insert(key, value.into())?;
|
|
49
91
|
Ok(())
|
|
50
92
|
}
|
|
51
93
|
|
|
52
|
-
pub fn get(&self, key:
|
|
53
|
-
self.items.get(
|
|
94
|
+
pub fn get(&self, key: &str) -> app::Result<Option<String>> {
|
|
95
|
+
Ok(self.items.get(key)?.map(|v| v.get().clone()))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
pub fn remove(&mut self, key: &str) -> app::Result<Option<String>> {
|
|
99
|
+
app::emit!(Event::Removed { key });
|
|
100
|
+
Ok(self.items.remove(key)?.map(|v| v.get().clone()))
|
|
54
101
|
}
|
|
55
102
|
}
|
|
56
103
|
```
|
|
@@ -61,51 +108,114 @@ impl AppState {
|
|
|
61
108
|
# Add WASM target (one-time)
|
|
62
109
|
rustup target add wasm32-unknown-unknown
|
|
63
110
|
|
|
64
|
-
# Build
|
|
65
|
-
cargo build --target wasm32-unknown-unknown --release
|
|
111
|
+
# Build with the app-release profile
|
|
112
|
+
cargo build --target wasm32-unknown-unknown --profile app-release
|
|
66
113
|
|
|
67
|
-
# Output: target/wasm32-unknown-unknown/release/<crate_name>.wasm
|
|
114
|
+
# Output: target/wasm32-unknown-unknown/app-release/<crate_name>.wasm
|
|
68
115
|
```
|
|
69
116
|
|
|
70
117
|
## Installing and running on a node (dev workflow)
|
|
71
118
|
|
|
72
119
|
```bash
|
|
120
|
+
# (Assumes: meroctl node add node1 ... && meroctl node use node1 already done)
|
|
121
|
+
|
|
73
122
|
# 1. Install app from WASM file
|
|
74
|
-
meroctl
|
|
75
|
-
--path target/wasm32-unknown-unknown/release/myapp.wasm
|
|
76
|
-
# Returns:
|
|
123
|
+
meroctl app install \
|
|
124
|
+
--path target/wasm32-unknown-unknown/app-release/myapp.wasm
|
|
125
|
+
# Returns: application-id
|
|
77
126
|
|
|
78
|
-
# 2. Create a context (instance of the app)
|
|
79
|
-
meroctl
|
|
127
|
+
# 2. Create a context (instance of the app — init() is called)
|
|
128
|
+
meroctl context create --application-id <application-id>
|
|
80
129
|
# Returns: context-id
|
|
81
130
|
|
|
82
|
-
# 3. Call a
|
|
83
|
-
meroctl
|
|
84
|
-
|
|
131
|
+
# 3. Call a mutation
|
|
132
|
+
meroctl call <context-id> set --args '{"key":"hello","value":"world"}'
|
|
133
|
+
|
|
134
|
+
# 4. Call a view (read-only)
|
|
135
|
+
meroctl call <context-id> get --args '{"key":"hello"}' --view
|
|
85
136
|
|
|
86
|
-
#
|
|
87
|
-
meroctl
|
|
88
|
-
--args '{"key":"hello"}' --view
|
|
137
|
+
# Dev mode: auto-reinstall when WASM changes
|
|
138
|
+
meroctl context create --watch target/wasm32-unknown-unknown/app-release/myapp.wasm
|
|
89
139
|
```
|
|
90
140
|
|
|
141
|
+
For `merod` setup and full `meroctl` reference, see `calimero-merod` and `calimero-meroctl` skills.
|
|
142
|
+
|
|
91
143
|
## Key rules
|
|
92
144
|
|
|
93
|
-
- State struct
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
145
|
+
- State struct derives `BorshDeserialize, BorshSerialize` — **not** `Default`
|
|
146
|
+
- Add `#[borsh(crate = "calimero_sdk::borsh")]` to all borsh types
|
|
147
|
+
- Add `#[serde(crate = "calimero_sdk::serde")]` to all serde types
|
|
148
|
+
- Never use `HashMap`, `Vec`, `BTreeMap` directly for **persisted shared state** — use CRDT
|
|
149
|
+
collections
|
|
150
|
+
- `Vec<T>` and `Option<T>` are fine for local / return types — just not for fields that need CRDT
|
|
151
|
+
sync
|
|
97
152
|
- Mutations use `&mut self`, views use `&self`
|
|
153
|
+
- Return `app::Result<T>` for all public methods; use `?` on collection ops
|
|
154
|
+
- Use `app::bail!(err)` to return errors early (like `return Err(err.into())`)
|
|
155
|
+
|
|
156
|
+
## Cross-context calls (xcall)
|
|
157
|
+
|
|
158
|
+
```rust
|
|
159
|
+
use calimero_sdk::env;
|
|
160
|
+
use calimero_sdk::serde_json;
|
|
161
|
+
|
|
162
|
+
// Call a method on another context
|
|
163
|
+
let ctx_bytes: [u8; 32] = /* context id bytes */;
|
|
164
|
+
let params = serde_json::json!({ "match_id": id, "winner": w });
|
|
165
|
+
let params_bytes = serde_json::to_vec(¶ms).unwrap();
|
|
166
|
+
env::xcall(&ctx_bytes, "on_match_finished", ¶ms_bytes);
|
|
167
|
+
```
|
|
98
168
|
|
|
99
|
-
##
|
|
169
|
+
## Environment functions
|
|
100
170
|
|
|
101
171
|
```rust
|
|
102
172
|
use calimero_sdk::env;
|
|
103
173
|
|
|
104
|
-
//
|
|
105
|
-
env::
|
|
174
|
+
env::executor_id() // [u8; 32] — caller's public key bytes
|
|
175
|
+
env::context_id() // [u8; 32] — current context ID
|
|
176
|
+
env::time_now() // u64 — current time in milliseconds
|
|
177
|
+
|
|
178
|
+
env::random_bytes(buf: &mut [u8]) // fill buf with random bytes
|
|
179
|
+
|
|
180
|
+
env::ed25519_verify(sig: &[u8; 64], pub_key: &[u8; 32], msg: &[u8]) -> bool
|
|
181
|
+
|
|
182
|
+
// Blob streaming API
|
|
183
|
+
let fd = env::blob_create(); // start writing a new blob
|
|
184
|
+
env::blob_write(fd, data: &[u8]) -> u64; // write bytes
|
|
185
|
+
let blob_id: [u8; 32] = env::blob_close(fd); // finalize, returns content-addressed ID
|
|
186
|
+
|
|
187
|
+
let fd = env::blob_open(blob_id: &[u8; 32]); // open blob for reading (returns 0 if not found)
|
|
188
|
+
env::blob_read(fd, buffer: &mut [u8]) -> u64; // read bytes (returns 0 at EOF)
|
|
189
|
+
env::blob_close(fd); // close read handle
|
|
190
|
+
|
|
191
|
+
// Make a blob discoverable to all context members
|
|
192
|
+
env::blob_announce_to_context(blob_id: &[u8; 32], context_id: &[u8; 32]) -> bool;
|
|
193
|
+
|
|
194
|
+
// Cross-context call
|
|
195
|
+
env::xcall(context_id: &[u8; 32], method: &str, params: &[u8]);
|
|
106
196
|
```
|
|
107
197
|
|
|
198
|
+
## Additional features
|
|
199
|
+
|
|
200
|
+
- **UserStorage** — per-member isolated storage (not synced to others): see
|
|
201
|
+
`references/user-and-frozen-storage.md`
|
|
202
|
+
- **FrozenStorage** — immutable content-addressed entries: see
|
|
203
|
+
`references/user-and-frozen-storage.md`
|
|
204
|
+
- **Event handlers** — named callbacks triggered when an event is emitted on any peer: see
|
|
205
|
+
`references/event-handlers.md`
|
|
206
|
+
- **Migrations** — `#[app::migrate]` for upgrading state schema: see `references/migrations.md`
|
|
207
|
+
- **Blob API** — streaming binary storage from app logic: see `references/blob-api.md`
|
|
208
|
+
- **Nested CRDTs** — `#[derive(Mergeable)]` for custom structs used as map values: see
|
|
209
|
+
`references/nested-crdts.md`
|
|
210
|
+
|
|
211
|
+
## Related skills
|
|
212
|
+
|
|
213
|
+
- **`calimero-core`** — runtime concepts (context/app model, JSON-RPC protocol, WebSocket events,
|
|
214
|
+
CRDT type taxonomy)
|
|
215
|
+
- **`calimero-meroctl`** — full `meroctl` CLI reference for deploying and testing the app
|
|
216
|
+
- **`calimero-merod`** — `merod` daemon setup and health checks
|
|
217
|
+
|
|
108
218
|
## References
|
|
109
219
|
|
|
110
|
-
See `references/` for CRDT collections, events, private storage, and examples.
|
|
111
|
-
|
|
220
|
+
See `references/` for CRDT collections, events, private storage, and examples. See `rules/` for hard
|
|
221
|
+
constraints the compiler won't catch.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Blob API
|
|
2
|
+
|
|
3
|
+
Blobs are binary objects stored outside the CRDT state — large files, images, documents. The app
|
|
4
|
+
stores a blob **ID** (32-byte hash) in state; the binary data lives in blob storage.
|
|
5
|
+
|
|
6
|
+
Two separate steps: **upload the binary** (client-side, before calling the app method), then **store
|
|
7
|
+
the metadata + announce** (inside the app method).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Streaming write (create a blob from inside the app)
|
|
12
|
+
|
|
13
|
+
```rust
|
|
14
|
+
use calimero_sdk::env;
|
|
15
|
+
|
|
16
|
+
// Write arbitrary bytes to a new blob
|
|
17
|
+
let fd = env::blob_create();
|
|
18
|
+
env::blob_write(fd, b"Hello, World!");
|
|
19
|
+
env::blob_write(fd, b" More data...");
|
|
20
|
+
let blob_id: [u8; 32] = env::blob_close(fd); // finalize; returns content-addressed ID
|
|
21
|
+
|
|
22
|
+
// Announce to context so other nodes can discover and download it
|
|
23
|
+
let ctx = env::context_id();
|
|
24
|
+
env::blob_announce_to_context(&blob_id, &ctx);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Streaming read (read a blob from inside the app)
|
|
28
|
+
|
|
29
|
+
```rust
|
|
30
|
+
let fd = env::blob_open(&blob_id); // returns 0 if blob not found
|
|
31
|
+
if fd == 0 {
|
|
32
|
+
app::bail!(/* not found error */);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let mut buf = [0u8; 4096];
|
|
36
|
+
let mut all_bytes = Vec::new();
|
|
37
|
+
loop {
|
|
38
|
+
let n = env::blob_read(fd, &mut buf);
|
|
39
|
+
if n == 0 { break; }
|
|
40
|
+
all_bytes.extend_from_slice(&buf[..n as usize]);
|
|
41
|
+
}
|
|
42
|
+
env::blob_close(fd);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Typical pattern: client uploads, app stores metadata
|
|
48
|
+
|
|
49
|
+
Most apps let the **client** upload binary data via `blobClient.uploadBlob()` (which returns a blob
|
|
50
|
+
ID), then call an app method that stores the metadata and announces the blob:
|
|
51
|
+
|
|
52
|
+
```rust
|
|
53
|
+
use calimero_sdk::{app, env};
|
|
54
|
+
use calimero_storage::collections::UnorderedMap;
|
|
55
|
+
|
|
56
|
+
#[app::state]
|
|
57
|
+
pub struct FileStore {
|
|
58
|
+
files: UnorderedMap<String, [u8; 32]>, // name → blob_id
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#[app::logic]
|
|
62
|
+
impl FileStore {
|
|
63
|
+
/// Called after client uploads the blob and gets back blob_id_bytes
|
|
64
|
+
pub fn store_file(
|
|
65
|
+
&mut self,
|
|
66
|
+
name: String,
|
|
67
|
+
blob_id: [u8; 32],
|
|
68
|
+
) -> app::Result<()> {
|
|
69
|
+
// Announce so all context peers can discover and download it
|
|
70
|
+
let ctx = env::context_id();
|
|
71
|
+
env::blob_announce_to_context(&blob_id, &ctx);
|
|
72
|
+
|
|
73
|
+
self.files.insert(name, blob_id)?;
|
|
74
|
+
Ok(())
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
pub fn get_blob_id(&self, name: &str) -> app::Result<Option<[u8; 32]>> {
|
|
78
|
+
Ok(self.files.get(name)?)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## env functions summary
|
|
86
|
+
|
|
87
|
+
| Function | Signature | Notes |
|
|
88
|
+
| ------------------------------------------- | -------------------------------- | -------------------------------------------------------------------- |
|
|
89
|
+
| `blob_create()` | `() -> u64` | Returns write fd |
|
|
90
|
+
| `blob_write(fd, data)` | `(u64, &[u8]) -> u64` | Returns bytes written |
|
|
91
|
+
| `blob_close(fd)` | `(u64) -> [u8; 32]` | Finalizes; returns blob ID |
|
|
92
|
+
| `blob_open(id)` | `(&[u8; 32]) -> u64` | Returns read fd, 0 if missing |
|
|
93
|
+
| `blob_read(fd, buf)` | `(u64, &mut [u8]) -> u64` | Returns bytes read, 0 at EOF |
|
|
94
|
+
| `blob_announce_to_context(blob_id, ctx_id)` | `(&[u8; 32], &[u8; 32]) -> bool` | Makes blob discoverable to peers; context must match current context |
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Encoding blob IDs for JSON
|
|
99
|
+
|
|
100
|
+
Blob IDs are `[u8; 32]`. For JSON-friendly responses, encode as hex or base58:
|
|
101
|
+
|
|
102
|
+
```rust
|
|
103
|
+
// hex (add hex = "0.4" to Cargo.toml)
|
|
104
|
+
let hex_id: String = hex::encode(&blob_id);
|
|
105
|
+
let blob_id: [u8; 32] = hex::decode(&hex_str).unwrap().try_into().unwrap();
|
|
106
|
+
|
|
107
|
+
// base58 (add bs58 to Cargo.toml)
|
|
108
|
+
let b58_id: String = bs58::encode(&blob_id).into_string();
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Cargo.toml additions
|
|
114
|
+
|
|
115
|
+
```toml
|
|
116
|
+
[dependencies]
|
|
117
|
+
hex = "0.4" # for hex encoding
|
|
118
|
+
bs58 = "0.5" # for base58 encoding (optional)
|
|
119
|
+
```
|
|
@@ -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/>
|