@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
|
@@ -1,46 +1,47 @@
|
|
|
1
|
-
# Manifest Format
|
|
1
|
+
# Manifest Format (V2)
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
6
|
+
## Bundle create generates this automatically
|
|
6
7
|
|
|
7
|
-
|
|
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
|
-
##
|
|
11
|
+
## Manifest V2 format
|
|
19
12
|
|
|
20
13
|
```json
|
|
21
14
|
{
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
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://
|
|
30
|
-
"
|
|
29
|
+
"frontend": "https://example.com",
|
|
30
|
+
"github": "https://github.com/example/myapp",
|
|
31
|
+
"docs": "https://example.com/docs"
|
|
31
32
|
},
|
|
32
|
-
"
|
|
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
|
-
"
|
|
43
|
-
"
|
|
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
|
-
##
|
|
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
|
-
|
|
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":
|
|
29
|
-
"signer_id":
|
|
28
|
+
"public_key": "yuKE404BaldXazEIUC4XrVGFyXxxyoRVjrrGhcKk1P4",
|
|
29
|
+
"signer_id": "did:key:z6Mkt7Ejb12a1BxvRiUpd5YWkMrk8KVjaShW2vMt6trm7FGH"
|
|
30
30
|
}
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
| Field
|
|
34
|
-
|
|
|
35
|
-
| `private_key` | Base64url Ed25519 secret (32 bytes). Never share or commit.
|
|
36
|
-
| `public_key`
|
|
37
|
-
| `signer_id`
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
-
|
|
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
|
+
```
|