@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
@@ -1,46 +1,47 @@
1
- # Manifest Format
1
+ # Manifest Format (V2)
2
2
 
3
- `manifest.json` describes the app bundle. mero-sign reads and signs it.
3
+ The manifest describes the app bundle. It is generated by `calimero-registry bundle create` and can
4
+ optionally be signed with `mero-sign` before pushing.
4
5
 
5
- ## Minimal manifest
6
+ ## Bundle create generates this automatically
6
7
 
7
- ```json
8
- {
9
- "name": "My App",
10
- "version": "1.0.0",
11
- "description": "A short description of what this app does.",
12
- "repository": "https://github.com/yourorg/your-app",
13
- "authors": ["Your Name <you@example.com>"],
14
- "license": "MIT"
15
- }
16
- ```
8
+ When you run `calimero-registry bundle create`, the CLI generates a `manifest.json` inside the
9
+ bundle directory. You can also supply a manifest via `-m, --manifest <path>` to pre-fill fields.
17
10
 
18
- ## Full manifest with optional fields
11
+ ## Manifest V2 format
19
12
 
20
13
  ```json
21
14
  {
22
- "name": "My App",
23
- "version": "1.0.0",
24
- "description": "A short description.",
25
- "repository": "https://github.com/yourorg/your-app",
26
- "authors": ["Your Name <you@example.com>"],
27
- "license": "MIT",
15
+ "version": "1.0",
16
+ "package": "com.example.myapp",
17
+ "appVersion": "1.0.0",
18
+ "metadata": {
19
+ "name": "My Application",
20
+ "description": "Application description",
21
+ "author": "Your Name"
22
+ },
23
+ "wasm": {
24
+ "path": "app.wasm",
25
+ "hash": "sha256:...",
26
+ "size": 12345
27
+ },
28
28
  "links": {
29
- "frontend": "https://my-app-frontend.com",
30
- "docs": "https://docs.my-app.com"
29
+ "frontend": "https://example.com",
30
+ "github": "https://github.com/example/myapp",
31
+ "docs": "https://example.com/docs"
31
32
  },
32
- "min_runtime_version": "0.3.0"
33
+ "minRuntimeVersion": "0.3.0"
33
34
  }
34
35
  ```
35
36
 
36
- ## After signing
37
+ ## After signing with mero-sign
37
38
 
38
39
  mero-sign injects a `signature` block:
39
40
 
40
41
  ```json
41
42
  {
42
- "name": "My App",
43
- "version": "1.0.0",
43
+ "version": "1.0",
44
+ "package": "com.example.myapp",
44
45
  ...
45
46
  "signature": {
46
47
  "alg": "ed25519",
@@ -51,13 +52,33 @@ mero-sign injects a `signature` block:
51
52
  }
52
53
  ```
53
54
 
54
- ## Requirements
55
+ ## Package ownership
56
+
57
+ - The first push establishes the package owner via the Ed25519 `signature.pubkey`
58
+ - Only the owner (or keys in `manifest.owners`) can push subsequent versions
59
+ - For team publishing, add teammates' public keys to `manifest.owners`:
60
+
61
+ ```json
62
+ {
63
+ "version": "1.0",
64
+ "package": "com.example.myapp",
65
+ "owners": [
66
+ "yuKE404BaldXazEIUC4XrVGFyXxxyoRVjrrGhcKk1P4",
67
+ "anotherTeammatePubKey..."
68
+ ],
69
+ ...
70
+ }
71
+ ```
72
+
73
+ ## Package naming
74
+
75
+ | Rule | Example |
76
+ | --------------------------- | ------------------------ |
77
+ | Must be reverse-domain | `com.yourorg.appname` ✅ |
78
+ | Version must be full SemVer | `1.0.0` ✅ |
79
+ | No `v` prefix on version | `v1.0.0` ❌ |
80
+
81
+ ## `links.frontend` is used by Desktop
55
82
 
56
- | Field | Required | Notes |
57
- | --- | --- | --- |
58
- | `name` | Yes | Display name |
59
- | `version` | Yes | Semver — `MAJOR.MINOR.PATCH` |
60
- | `description` | Yes | Short description |
61
- | `repository` | Yes | GitHub or other source URL |
62
- | `links.frontend` | No | Used by Desktop to open the app UI |
63
- | `min_runtime_version` | No | Minimum `merod` version required |
83
+ The Desktop app reads `links.frontend` to know which URL to open when a user opens this app. Always
84
+ include it.
@@ -25,16 +25,16 @@ Produces:
25
25
  ```json
26
26
  {
27
27
  "private_key": "PZbZ5yM9t63qOHMM-CCzExbNv8u79XTxZT9UW8GQJ60",
28
- "public_key": "yuKE404BaldXazEIUC4XrVGFyXxxyoRVjrrGhcKk1P4",
29
- "signer_id": "did:key:z6Mkt7Ejb12a1BxvRiUpd5YWkMrk8KVjaShW2vMt6trm7FGH"
28
+ "public_key": "yuKE404BaldXazEIUC4XrVGFyXxxyoRVjrrGhcKk1P4",
29
+ "signer_id": "did:key:z6Mkt7Ejb12a1BxvRiUpd5YWkMrk8KVjaShW2vMt6trm7FGH"
30
30
  }
31
31
  ```
32
32
 
33
- | Field | Description |
34
- | --- | --- |
35
- | `private_key` | Base64url Ed25519 secret (32 bytes). Never share or commit. |
36
- | `public_key` | Base64url public key (32 bytes). Embedded in every signed manifest. |
37
- | `signer_id` | `did:key` DID representation. Used as identity reference in the registry. |
33
+ | Field | Description |
34
+ | ------------- | ------------------------------------------------------------------------- |
35
+ | `private_key` | Base64url Ed25519 secret (32 bytes). Never share or commit. |
36
+ | `public_key` | Base64url public key (32 bytes). Embedded in every signed manifest. |
37
+ | `signer_id` | `did:key` DID representation. Used as identity reference in the registry. |
38
38
 
39
39
  ## Sign a manifest
40
40
 
@@ -45,7 +45,7 @@ mero-sign sign manifest.json --key key.json
45
45
 
46
46
  ## How signing works
47
47
 
48
- ```
48
+ ```text
49
49
  manifest.json (signature field absent or empty)
50
50
 
51
51
  ▼ Remove signature + all _* prefixed fields
@@ -63,7 +63,8 @@ mero-sign sign manifest.json --key key.json
63
63
 
64
64
  ## Team workflow
65
65
 
66
- Each developer keeps their own key — the registry validates org membership via authenticated email, not by which key was used.
66
+ Each developer keeps their own key — the registry validates org membership via authenticated email,
67
+ not by which key was used.
67
68
 
68
69
  ```bash
69
70
  # Each developer once:
@@ -1,7 +1,7 @@
1
1
  # Rule: Never commit key.json
2
2
 
3
- The signing key file contains your Ed25519 private key. If committed to version control,
4
- anyone with repo access can sign bundles as you and publish malicious apps under your identity.
3
+ The signing key file contains your Ed25519 private key. If committed to version control, anyone with
4
+ repo access can sign bundles as you and publish malicious apps under your identity.
5
5
 
6
6
  ## Required steps
7
7
 
@@ -34,6 +34,7 @@ Store `CALIMERO_SIGNING_KEY` as a repository secret (GitHub Secrets, etc.), neve
34
34
  ## If you accidentally committed a key
35
35
 
36
36
  Rotate immediately:
37
+
37
38
  ```bash
38
39
  mero-sign generate-key --output new-key.json
39
40
  # Update your public key in the registry
@@ -1,7 +1,7 @@
1
1
  # Rule: Sign the manifest BEFORE bundling
2
2
 
3
- mero-sign operates on a standalone `manifest.json` file — not on a `.mpk` archive.
4
- Signing after `bundle create` will not work because the manifest is already packed.
3
+ mero-sign operates on a standalone `manifest.json` file — not on a `.mpk` archive. Signing after
4
+ `bundle create` will not work because the manifest is already packed.
5
5
 
6
6
  ## WRONG order:
7
7
 
@@ -20,6 +20,6 @@ calimero-registry bundle push app.mpk --key key.json # ✓ then push
20
20
 
21
21
  ## Why
22
22
 
23
- The registry verifies the signature by re-running the RFC 8785 canonicalization on the
24
- manifest fields inside the bundle. If the manifest was modified after signing — including
25
- by the bundle tool itself — the signature check fails with `400 invalid_signature`.
23
+ The registry verifies the signature by re-running the RFC 8785 canonicalization on the manifest
24
+ fields inside the bundle. If the manifest was modified after signing — including by the bundle tool
25
+ itself — the signature check fails with `400 invalid_signature`.
@@ -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
- - All persistent state **must** use Calimero CRDT collections never `std::collections`
9
- - The `#[app]` macro goes on the **`impl` block**, not the struct
10
- - All `pub` methods on the `#[app]` impl are callable via JSON-RPC from clients
11
- - Events are emitted with `app::emit!()` and pushed to all context members
12
- - Private storage is per-member and isolated; shared state syncs across all members
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 = "0.x"
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
- ## Minimal app skeleton
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::state::UnorderedMap;
31
-
32
- #[derive(Default, BorshDeserialize, BorshSerialize)]
33
- pub struct AppState {
34
- items: UnorderedMap<String, String>,
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::state]
38
- impl AppState {}
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 AppState {
75
+ impl KvStore {
42
76
  #[app::init]
43
- pub fn init() -> AppState {
44
- AppState::default()
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<(), String> {
48
- self.items.insert(key, value);
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: String) -> Option<String> {
53
- self.items.get(&key).cloned()
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 --node-url http://localhost:2428 app install \
75
- --path target/wasm32-unknown-unknown/release/myapp.wasm
76
- # Returns: app-id
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 --node-url http://localhost:2428 context create --app-id <app-id>
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 method
83
- meroctl --node-url http://localhost:2428 call <context-id> set \
84
- --args '{"key":"hello","value":"world"}'
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
- # 4. Call a view
87
- meroctl --node-url http://localhost:2428 call <context-id> get \
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 **must** derive `Default`, `BorshDeserialize`, `BorshSerialize`
94
- - Never use `HashMap`, `Vec`, `BTreeMap` directly for state — use CRDT collections
95
- - No blocking I/O, no threads, no `async` in app logic
96
- - Use `calimero_sdk::env::log!()` not `println!`
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(&params).unwrap();
166
+ env::xcall(&ctx_bytes, "on_match_finished", &params_bytes);
167
+ ```
98
168
 
99
- ## Logging
169
+ ## Environment functions
100
170
 
101
171
  ```rust
102
172
  use calimero_sdk::env;
103
173
 
104
- // Inside any app method:
105
- env::log!("Processing key: {}", key);
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
- See `rules/` for hard constraints the compiler won't catch.
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
+ ```