@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,39 +1,120 @@
|
|
|
1
1
|
# CRDT State Collections
|
|
2
2
|
|
|
3
|
-
Calimero provides conflict-free replicated data types for application state.
|
|
3
|
+
Calimero provides conflict-free replicated data types for application state. All collections are
|
|
4
|
+
imported from `calimero_storage::collections`.
|
|
5
|
+
|
|
6
|
+
> **Critical:** Do NOT use `calimero_sdk::state::*` — that path no longer exists. The correct import
|
|
7
|
+
> is `calimero_storage::collections::*`.
|
|
4
8
|
|
|
5
9
|
## Available Collections
|
|
6
10
|
|
|
7
|
-
| Type
|
|
8
|
-
|
|
|
9
|
-
| `
|
|
10
|
-
| `
|
|
11
|
-
| `
|
|
12
|
-
| `
|
|
11
|
+
| Type | Use for | Notes |
|
|
12
|
+
| ------------------------- | --------------------------------------- | -------------------------------------------------- |
|
|
13
|
+
| `UnorderedMap<K, V>` | Key-value mapping | Most collection ops return `Result<>` — use `?` |
|
|
14
|
+
| `UnorderedSet<T>` | Unique value set | |
|
|
15
|
+
| `Vector<T>` | Ordered list (append-only) | |
|
|
16
|
+
| `LwwRegister<T>` | Single last-write-wins value | Wrap map values: `UnorderedMap<K, LwwRegister<V>>` |
|
|
17
|
+
| `Counter` | Grow-only counter (GCounter by default) | `.increment()`, `.value()` |
|
|
18
|
+
| `Counter<true>` | PN-Counter (supports decrement) | Same API + `.decrement()` |
|
|
19
|
+
| `FrozenStorage<T>` | Immutable content-addressed entries | |
|
|
20
|
+
| `UserStorage<T>` | Per-member isolated storage | Not synced to other members |
|
|
21
|
+
| `ReplicatedGrowableArray` | CRDT text / ordered sequence | Collaborative editing |
|
|
22
|
+
|
|
23
|
+
## Import
|
|
24
|
+
|
|
25
|
+
```rust
|
|
26
|
+
use calimero_storage::collections::{
|
|
27
|
+
Counter, FrozenStorage, LwwRegister, ReplicatedGrowableArray,
|
|
28
|
+
UnorderedMap, UnorderedSet, UserStorage, Vector,
|
|
29
|
+
};
|
|
30
|
+
```
|
|
13
31
|
|
|
14
32
|
## Usage
|
|
15
33
|
|
|
16
34
|
```rust
|
|
17
|
-
use calimero_sdk::
|
|
35
|
+
use calimero_sdk::app;
|
|
36
|
+
use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
|
|
37
|
+
use calimero_storage::collections::{LwwRegister, UnorderedMap, Vector, Counter};
|
|
18
38
|
|
|
19
|
-
#[
|
|
39
|
+
#[app::state]
|
|
40
|
+
#[derive(Debug, BorshSerialize, BorshDeserialize)]
|
|
41
|
+
#[borsh(crate = "calimero_sdk::borsh")]
|
|
20
42
|
pub struct AppState {
|
|
21
|
-
|
|
22
|
-
|
|
43
|
+
// Map values wrapped in LwwRegister for CRDT merge
|
|
44
|
+
items: UnorderedMap<String, LwwRegister<String>>,
|
|
45
|
+
log: Vector<String>,
|
|
46
|
+
counter: Counter,
|
|
23
47
|
}
|
|
24
48
|
|
|
25
49
|
#[app::logic]
|
|
26
50
|
impl AppState {
|
|
27
|
-
pub fn set(&mut self, key: String, value: String) {
|
|
28
|
-
self.items.insert(key, value)
|
|
51
|
+
pub fn set(&mut self, key: String, value: String) -> app::Result<()> {
|
|
52
|
+
self.items.insert(key, value.into())?;
|
|
53
|
+
Ok(())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
pub fn get(&self, key: &str) -> app::Result<Option<String>> {
|
|
57
|
+
Ok(self.items.get(key)?.map(|v| v.get().clone()))
|
|
29
58
|
}
|
|
30
59
|
|
|
31
|
-
pub fn
|
|
32
|
-
self.
|
|
60
|
+
pub fn append(&mut self, entry: String) -> app::Result<()> {
|
|
61
|
+
self.log.push(entry)?;
|
|
62
|
+
Ok(())
|
|
33
63
|
}
|
|
34
64
|
|
|
35
|
-
pub fn
|
|
36
|
-
self.
|
|
65
|
+
pub fn increment(&mut self) -> app::Result<()> {
|
|
66
|
+
self.counter.increment()?;
|
|
67
|
+
Ok(())
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## UnorderedMap patterns
|
|
73
|
+
|
|
74
|
+
```rust
|
|
75
|
+
// Insert
|
|
76
|
+
self.items.insert(key, value.into())?;
|
|
77
|
+
|
|
78
|
+
// Get (returns Option<LwwRegister<V>>)
|
|
79
|
+
let val = self.items.get(&key)?.map(|v| v.get().clone());
|
|
80
|
+
|
|
81
|
+
// Contains
|
|
82
|
+
let exists = self.items.contains(&key)?;
|
|
83
|
+
|
|
84
|
+
// Remove
|
|
85
|
+
let old = self.items.remove(&key)?;
|
|
86
|
+
|
|
87
|
+
// All entries
|
|
88
|
+
let all: Vec<(K, V)> = self.items.entries()?.collect();
|
|
89
|
+
|
|
90
|
+
// In-place mutation via guard
|
|
91
|
+
if let Some(mut guard) = self.items.get_mut(&key)? {
|
|
92
|
+
guard.set(new_value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Entry API (like HashMap::entry)
|
|
96
|
+
use calimero_storage::collections::unordered_map::Entry;
|
|
97
|
+
let entry = self.items.entry(key)?;
|
|
98
|
+
let val = entry.or_insert(LwwRegister::new(default_value))?;
|
|
99
|
+
|
|
100
|
+
// Length
|
|
101
|
+
let n = self.items.len()?;
|
|
102
|
+
|
|
103
|
+
// Clear
|
|
104
|
+
self.items.clear()?;
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Constructor
|
|
108
|
+
|
|
109
|
+
Collections must be initialized with `::new()` — there is no Default impl:
|
|
110
|
+
|
|
111
|
+
```rust
|
|
112
|
+
#[app::init]
|
|
113
|
+
pub fn init() -> AppState {
|
|
114
|
+
AppState {
|
|
115
|
+
items: UnorderedMap::new(),
|
|
116
|
+
log: Vector::new(),
|
|
117
|
+
counter: Counter::new(),
|
|
37
118
|
}
|
|
38
119
|
}
|
|
39
120
|
```
|
|
@@ -41,10 +122,14 @@ impl AppState {
|
|
|
41
122
|
## Keys and values must implement
|
|
42
123
|
|
|
43
124
|
- `BorshSerialize + BorshDeserialize`
|
|
44
|
-
- `
|
|
125
|
+
- Values in UnorderedMap are typically wrapped in `LwwRegister<V>` — this handles conflict
|
|
126
|
+
resolution automatically
|
|
45
127
|
|
|
46
128
|
## Important
|
|
47
129
|
|
|
48
|
-
CRDT collections handle concurrent writes from different context members automatically —
|
|
49
|
-
|
|
50
|
-
|
|
130
|
+
CRDT collections handle concurrent writes from different context members automatically — you never
|
|
131
|
+
need to manually resolve conflicts. Writes from different members are merged deterministically using
|
|
132
|
+
the DAG-based sync engine.
|
|
133
|
+
|
|
134
|
+
Collection operations are **fallible** — always propagate errors with `?`. Panicking inside a WASM
|
|
135
|
+
method aborts the execution and rolls back state.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# UserStorage and FrozenStorage
|
|
2
|
+
|
|
3
|
+
Two specialised CRDT collection types for per-member and immutable data.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## UserStorage\<T\>
|
|
8
|
+
|
|
9
|
+
Stores a separate value of type `T` per context member (keyed by public key). Each member can only
|
|
10
|
+
read/write their own value via the current executor identity. Reading another member's value
|
|
11
|
+
requires explicitly passing their public key.
|
|
12
|
+
|
|
13
|
+
`T` must implement `BorshSerialize + BorshDeserialize + Mergeable + Default`.
|
|
14
|
+
|
|
15
|
+
### Simple example (single value per user)
|
|
16
|
+
|
|
17
|
+
```rust
|
|
18
|
+
use calimero_storage::collections::{LwwRegister, UserStorage};
|
|
19
|
+
|
|
20
|
+
#[app::state]
|
|
21
|
+
pub struct AppState {
|
|
22
|
+
// Each member stores their own string value
|
|
23
|
+
user_names: UserStorage<LwwRegister<String>>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[app::logic]
|
|
27
|
+
impl AppState {
|
|
28
|
+
// Write the current executor's value
|
|
29
|
+
pub fn set_my_name(&mut self, name: String) -> app::Result<()> {
|
|
30
|
+
self.user_names.insert(name.into())?;
|
|
31
|
+
Ok(())
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Read the current executor's value
|
|
35
|
+
pub fn get_my_name(&self) -> app::Result<Option<String>> {
|
|
36
|
+
Ok(self.user_names.get()?.map(|v| v.get().clone()))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Read a specific user's value
|
|
40
|
+
pub fn get_name_for(&self, user_key: calimero_sdk::PublicKey) -> app::Result<Option<String>> {
|
|
41
|
+
Ok(self.user_names.get_for_user(&user_key)?.map(|v| v.get().clone()))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Nested example (map per user)
|
|
47
|
+
|
|
48
|
+
When `T` is a collection, it must implement `Mergeable` manually (or via `#[derive(Mergeable)]`):
|
|
49
|
+
|
|
50
|
+
```rust
|
|
51
|
+
use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
|
|
52
|
+
use calimero_storage::collections::{LwwRegister, Mergeable, UnorderedMap, UserStorage};
|
|
53
|
+
|
|
54
|
+
#[derive(Debug, BorshSerialize, BorshDeserialize, Default)]
|
|
55
|
+
#[borsh(crate = "calimero_sdk::borsh")]
|
|
56
|
+
struct UserKvMap {
|
|
57
|
+
map: UnorderedMap<String, LwwRegister<String>>,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
impl Mergeable for UserKvMap {
|
|
61
|
+
fn merge(&mut self, other: &Self)
|
|
62
|
+
-> Result<(), calimero_storage::collections::crdt_meta::MergeError>
|
|
63
|
+
{
|
|
64
|
+
self.map.merge(&other.map)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[app::state]
|
|
69
|
+
pub struct AppState {
|
|
70
|
+
user_data: UserStorage<UserKvMap>,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#[app::logic]
|
|
74
|
+
impl AppState {
|
|
75
|
+
pub fn set_user_kv(&mut self, key: String, value: String) -> app::Result<()> {
|
|
76
|
+
// get-modify-put pattern
|
|
77
|
+
let mut data = self.user_data.get()?.unwrap_or_default();
|
|
78
|
+
data.map.insert(key, value.into())?;
|
|
79
|
+
self.user_data.insert(data)?;
|
|
80
|
+
Ok(())
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
pub fn get_user_kv(&self, key: &str) -> app::Result<Option<String>> {
|
|
84
|
+
let data = self.user_data.get()?;
|
|
85
|
+
match data {
|
|
86
|
+
Some(d) => Ok(d.map.get(key)?.map(|v| v.get().clone())),
|
|
87
|
+
None => Ok(None),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Key methods
|
|
94
|
+
|
|
95
|
+
| Method | Description |
|
|
96
|
+
| -------------------------------- | -------------------------------- |
|
|
97
|
+
| `insert(value: T)?` | Store value for current executor |
|
|
98
|
+
| `get()?` | Get value for current executor |
|
|
99
|
+
| `get_for_user(key: &PublicKey)?` | Get value for a specific user |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## FrozenStorage\<T\>
|
|
104
|
+
|
|
105
|
+
Content-addressed, **immutable** storage. Values are keyed by their SHA-256 hash. Once inserted, a
|
|
106
|
+
value can never be modified or deleted. All members share the same frozen entries (unlike
|
|
107
|
+
`UserStorage`).
|
|
108
|
+
|
|
109
|
+
`T` must implement `BorshSerialize + BorshDeserialize + Into<FrozenValue<T>>`. Typically `T` is
|
|
110
|
+
`String` or a `Vec<u8>`.
|
|
111
|
+
|
|
112
|
+
```rust
|
|
113
|
+
use calimero_storage::collections::FrozenStorage;
|
|
114
|
+
|
|
115
|
+
#[app::state]
|
|
116
|
+
pub struct AppState {
|
|
117
|
+
frozen: FrozenStorage<String>,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#[app::logic]
|
|
121
|
+
impl AppState {
|
|
122
|
+
/// Insert a value — returns its 32-byte SHA-256 hash
|
|
123
|
+
pub fn add_frozen(&mut self, value: String) -> app::Result<[u8; 32]> {
|
|
124
|
+
let hash = self.frozen.insert(value.into())?;
|
|
125
|
+
Ok(hash)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Retrieve by hash
|
|
129
|
+
pub fn get_frozen(&self, hash: [u8; 32]) -> app::Result<Option<String>> {
|
|
130
|
+
Ok(self.frozen.get(&hash)?.map(|v| v.clone()))
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Typical usage pattern
|
|
136
|
+
|
|
137
|
+
Since hashes are `[u8; 32]`, encode them as hex or base58 strings for JSON:
|
|
138
|
+
|
|
139
|
+
```rust
|
|
140
|
+
use hex;
|
|
141
|
+
|
|
142
|
+
pub fn add_frozen(&mut self, value: String) -> app::Result<String> {
|
|
143
|
+
let hash = self.frozen.insert(value.into())?;
|
|
144
|
+
Ok(hex::encode(hash)) // return as hex string for JSON clients
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
pub fn get_frozen(&self, hash_hex: String) -> app::Result<Option<String>> {
|
|
148
|
+
let mut hash = [0u8; 32];
|
|
149
|
+
hex::decode_to_slice(&hash_hex, &mut hash)
|
|
150
|
+
.map_err(|_| /* your error type */)?;
|
|
151
|
+
Ok(self.frozen.get(&hash)?.map(|v| v.clone()))
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Frozen storage key methods
|
|
156
|
+
|
|
157
|
+
| Method | Description |
|
|
158
|
+
| -------------------------- | ------------------------------------ |
|
|
159
|
+
| `insert(value: T.into())?` | Store value, returns `[u8; 32]` hash |
|
|
160
|
+
| `get(&hash)?` | Retrieve value by hash |
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Cargo.toml — add hex for hash encoding
|
|
165
|
+
|
|
166
|
+
```toml
|
|
167
|
+
[dependencies]
|
|
168
|
+
hex = "0.4"
|
|
169
|
+
```
|
|
@@ -29,6 +29,9 @@ impl AppState {
|
|
|
29
29
|
}
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
**Why:** `#[app::state]` and `#[app::logic]` are proc macros that transform impl blocks to register
|
|
32
|
+
**Why:** `#[app::state]` and `#[app::logic]` are proc macros that transform impl blocks to register
|
|
33
|
+
the type with the SDK runtime. The struct itself only needs the derive macros (`Default`,
|
|
34
|
+
`BorshDeserialize`, `BorshSerialize`).
|
|
33
35
|
|
|
34
|
-
**Also:** `#[app::init]` marks the constructor — called once when the context is created. It must
|
|
36
|
+
**Also:** `#[app::init]` marks the constructor — called once when the context is created. It must
|
|
37
|
+
return the state type, not `Self`.
|
|
@@ -22,6 +22,9 @@ pub struct AppState {
|
|
|
22
22
|
}
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
**Why:** `std::collections` types do not participate in the CRDT merge process. Data written to them
|
|
25
|
+
**Why:** `std::collections` types do not participate in the CRDT merge process. Data written to them
|
|
26
|
+
by one node will never reach other context members. The bug is silent — the app compiles and runs
|
|
27
|
+
correctly on one node, but state diverges in multi-member contexts.
|
|
26
28
|
|
|
27
|
-
**Applies to:** `HashMap`, `BTreeMap`, `HashSet`, `BTreeSet`, `Vec` (for shared state). `Vec` is
|
|
29
|
+
**Applies to:** `HashMap`, `BTreeMap`, `HashSet`, `BTreeSet`, `Vec` (for shared state). `Vec` is
|
|
30
|
+
fine for temporary local values inside a method, just not as a field in the state struct.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Rule: State struct must derive Default, BorshDeserialize, BorshSerialize
|
|
2
|
+
|
|
3
|
+
The three derives are all required. Missing any one of them causes a **runtime panic**, not a
|
|
4
|
+
compile error — the app will install and start, then crash when the context is first accessed.
|
|
5
|
+
|
|
6
|
+
## WRONG — missing derives:
|
|
7
|
+
|
|
8
|
+
```rust
|
|
9
|
+
// ✗ Missing all derives — runtime panic on first call
|
|
10
|
+
pub struct AppState {
|
|
11
|
+
items: UnorderedMap<String, String>,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ✗ Missing BorshSerialize — state cannot be persisted
|
|
15
|
+
#[derive(Default, BorshDeserialize)]
|
|
16
|
+
pub struct AppState {
|
|
17
|
+
items: UnorderedMap<String, String>,
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## CORRECT:
|
|
22
|
+
|
|
23
|
+
```rust
|
|
24
|
+
#[derive(Default, BorshDeserialize, BorshSerialize)]
|
|
25
|
+
pub struct AppState {
|
|
26
|
+
items: UnorderedMap<String, String>,
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## What each derive does
|
|
31
|
+
|
|
32
|
+
| Derive | Required for |
|
|
33
|
+
| ------------------ | ---------------------------------------------------------- |
|
|
34
|
+
| `Default` | `#[app::init]` calls `AppState::default()` as the baseline |
|
|
35
|
+
| `BorshDeserialize` | Loading state from node storage on every method call |
|
|
36
|
+
| `BorshSerialize` | Saving state to node storage after every mutation |
|
|
37
|
+
|
|
38
|
+
## Import
|
|
39
|
+
|
|
40
|
+
```rust
|
|
41
|
+
use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Do not use `borsh` crate directly — import from `calimero_sdk::borsh` to ensure version
|
|
45
|
+
compatibility.
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Calimero apps run in a sandboxed WASM environment. The following are **not available**:
|
|
4
4
|
|
|
5
|
-
| Forbidden
|
|
6
|
-
|
|
|
7
|
-
| `std::thread::spawn`
|
|
5
|
+
| Forbidden | Use instead |
|
|
6
|
+
| --------------------------------------------- | ----------------------------------------------- |
|
|
7
|
+
| `std::thread::spawn` | Not supported — WASM is single-threaded |
|
|
8
8
|
| `tokio`, `async-std`, `async fn` in app logic | Not supported — all app methods are synchronous |
|
|
9
|
-
| `std::fs`, file I/O
|
|
10
|
-
| `std::net`, HTTP calls outbound
|
|
11
|
-
| `println!`, `eprintln!`
|
|
12
|
-
| System time (`std::time::SystemTime`)
|
|
13
|
-
| Environment variables
|
|
14
|
-
| Random number generation via `rand`
|
|
9
|
+
| `std::fs`, file I/O | Use `env::private_storage_write/read` |
|
|
10
|
+
| `std::net`, HTTP calls outbound | Not supported in WASM runtime |
|
|
11
|
+
| `println!`, `eprintln!` | Use `calimero_sdk::env::log(&str)` |
|
|
12
|
+
| System time (`std::time::SystemTime`) | Use `calimero_sdk::env::block_timestamp()` |
|
|
13
|
+
| Environment variables | Not accessible from WASM |
|
|
14
|
+
| Random number generation via `rand` | Use `calimero_sdk::env::random_seed()` |
|
|
15
15
|
|
|
16
16
|
## Logging
|
|
17
17
|
|
|
@@ -32,4 +32,6 @@ let ts = env::block_timestamp(); // u64 nanoseconds
|
|
|
32
32
|
|
|
33
33
|
## Why
|
|
34
34
|
|
|
35
|
-
The `merod` WASM runtime is deterministic by design. Non-deterministic operations (threads, I/O,
|
|
35
|
+
The `merod` WASM runtime is deterministic by design. Non-deterministic operations (threads, I/O,
|
|
36
|
+
time, random) would break the CRDT synchronization guarantees — every node must reach the same state
|
|
37
|
+
given the same sequence of operations.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# calimero-sdk-js — Agent Instructions
|
|
2
|
+
|
|
3
|
+
You are helping a developer **build a Calimero P2P application in TypeScript** using
|
|
4
|
+
`@calimero-network/calimero-sdk-js`. The app compiles to WebAssembly and runs inside the `merod`
|
|
5
|
+
node runtime.
|
|
6
|
+
|
|
7
|
+
> **NOT this skill** if the developer is connecting a browser/Node.js _frontend_ to a node (that's
|
|
8
|
+
> `calimero-client-js` / `@calimero-network/calimero-client`). This skill is for writing the
|
|
9
|
+
> application logic that _runs on the node_.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @calimero-network/calimero-sdk-js
|
|
15
|
+
pnpm add -D @calimero-network/calimero-cli-js typescript
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The CLI's `postinstall` hook downloads QuickJS, WASI-SDK, and Binaryen automatically. If you used
|
|
19
|
+
`--ignore-scripts`, re-run with `pnpm install --ignore-scripts=false`.
|
|
20
|
+
|
|
21
|
+
## Core concepts
|
|
22
|
+
|
|
23
|
+
| Concept | What it is |
|
|
24
|
+
| -------------------------- | -------------------------------------------------------------------------------------- |
|
|
25
|
+
| `@State` class | Persisted data — fields must be CRDT types |
|
|
26
|
+
| `@Logic(StateClass)` class | Entry points callable via JSON-RPC; must extend the state class |
|
|
27
|
+
| `@Init` static method | Seeds the initial state when context is first created |
|
|
28
|
+
| `@View()` method | Read-only — skips persistence; required for query methods |
|
|
29
|
+
| CRDT collection | Conflict-free type (`Counter`, `UnorderedMap`, etc.) — all state fields must use these |
|
|
30
|
+
|
|
31
|
+
## Minimal app
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { State, Logic, Init, View } from '@calimero-network/calimero-sdk-js';
|
|
35
|
+
import { UnorderedMap } from '@calimero-network/calimero-sdk-js/collections';
|
|
36
|
+
import * as env from '@calimero-network/calimero-sdk-js/env';
|
|
37
|
+
|
|
38
|
+
@State
|
|
39
|
+
export class KvState {
|
|
40
|
+
items: UnorderedMap<string, string> = new UnorderedMap();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Logic(KvState)
|
|
44
|
+
export class KvLogic extends KvState {
|
|
45
|
+
@Init
|
|
46
|
+
static init(): KvState {
|
|
47
|
+
return new KvState();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
set(key: string, value: string): void {
|
|
51
|
+
env.log(`set ${key}`);
|
|
52
|
+
this.items.set(key, value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@View()
|
|
56
|
+
get(key: string): string | null {
|
|
57
|
+
return this.items.get(key) ?? null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## CRDT collections quick reference
|
|
63
|
+
|
|
64
|
+
| Type | Use case | Key ops |
|
|
65
|
+
| ------------------- | --------------------------------------- | -------------------------------------------------- |
|
|
66
|
+
| `Counter` | Distributed counting (returns `bigint`) | `increment()`, `incrementBy(n)`, `value()` |
|
|
67
|
+
| `UnorderedMap<K,V>` | Key-value store (LWW per key) | `set()`, `get()`, `has()`, `remove()`, `entries()` |
|
|
68
|
+
| `UnorderedSet<T>` | Unique membership (LWW per element) | `add()`, `has()`, `delete()`, `toArray()` |
|
|
69
|
+
| `Vector<T>` | Ordered list | `push()`, `get(i)`, `pop()`, `len()` |
|
|
70
|
+
| `LwwRegister<T>` | Single value (timestamp LWW) | `set()`, `get()` |
|
|
71
|
+
|
|
72
|
+
Nested collections (`Map<K, Set<V>>`) propagate changes automatically — no manual re-serialization.
|
|
73
|
+
|
|
74
|
+
## Build & deploy
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Build to WASM
|
|
78
|
+
npx calimero-sdk build src/index.ts -o build/service.wasm
|
|
79
|
+
|
|
80
|
+
# Install on node
|
|
81
|
+
meroctl --node-name <NODE> app install \
|
|
82
|
+
--path build/service.wasm \
|
|
83
|
+
--context-id <CONTEXT_ID>
|
|
84
|
+
|
|
85
|
+
# Call a method
|
|
86
|
+
meroctl --node-name <NODE> call \
|
|
87
|
+
--context-id <CONTEXT_ID> \
|
|
88
|
+
--method set \
|
|
89
|
+
--args '{"key":"hello","value":"world"}'
|
|
90
|
+
|
|
91
|
+
# Call a view
|
|
92
|
+
meroctl --node-name <NODE> call \
|
|
93
|
+
--context-id <CONTEXT_ID> \
|
|
94
|
+
--method get \
|
|
95
|
+
--args '{"key":"hello"}'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Key rules
|
|
99
|
+
|
|
100
|
+
- All `@State` fields must be CRDT types — never plain `Map`, `Set`, `Array`, or primitives
|
|
101
|
+
- `@View()` is **required** on every read-only method — omitting it causes unnecessary persistence
|
|
102
|
+
- Use `env.log()` not `console.log()` — `console` is not available in the WASM runtime
|
|
103
|
+
- `Counter.value()` returns `bigint`, not `number`
|
|
104
|
+
- `@Init` must be a static method that returns the state class instance
|
|
105
|
+
- `@Logic(StateClass)` must extend the state class
|
|
106
|
+
- No async, no I/O, no threads in app logic — the WASM runtime is synchronous
|
|
107
|
+
- **Windows: building is not supported natively — use WSL** (QuickJS/WASI-SDK toolchain requires
|
|
108
|
+
Linux/macOS)
|
|
109
|
+
|
|
110
|
+
## Events
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { emit } from '@calimero-network/calimero-sdk-js';
|
|
114
|
+
|
|
115
|
+
// Inside a mutation method:
|
|
116
|
+
emit({ type: 'ItemAdded', key: 'foo', value: 'bar' });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Events are pushed to all context members via WebSocket. Clients subscribe using
|
|
120
|
+
`WsSubscriptionsClient` from `@calimero-network/calimero-client`.
|
|
121
|
+
|
|
122
|
+
## Private storage (node-local)
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { createPrivateEntry } from '@calimero-network/calimero-sdk-js';
|
|
126
|
+
|
|
127
|
+
const secret = createPrivateEntry<string>();
|
|
128
|
+
secret.set('my-api-key');
|
|
129
|
+
const val = secret.get();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Private entries are never broadcast to other nodes.
|
|
133
|
+
|
|
134
|
+
## Related skills
|
|
135
|
+
|
|
136
|
+
- **`calimero-core`** — runtime concepts (context/app model, JSON-RPC protocol, WebSocket events,
|
|
137
|
+
CRDT type taxonomy)
|
|
138
|
+
- **`calimero-meroctl`** — full `meroctl` CLI reference for deploying and testing the app
|
|
139
|
+
- **`calimero-client-js`** — connecting a browser/React frontend to the node (not building app
|
|
140
|
+
logic)
|
|
141
|
+
|
|
142
|
+
## References
|
|
143
|
+
|
|
144
|
+
See `references/` for CRDT collections, events, and build pipeline details. See `rules/` for hard
|
|
145
|
+
constraints.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Build Pipeline
|
|
2
|
+
|
|
3
|
+
`@calimero-network/calimero-cli-js` compiles your TypeScript app to a `.wasm` binary.
|
|
4
|
+
|
|
5
|
+
## Pipeline stages
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
TypeScript → Rollup → QuickJS → WASI-SDK → Binaryen → .wasm
|
|
9
|
+
Source Bundle C bytecode WASM Optimized
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Install SDK and CLI
|
|
16
|
+
pnpm add @calimero-network/calimero-sdk-js
|
|
17
|
+
pnpm add -D @calimero-network/calimero-cli-js typescript
|
|
18
|
+
|
|
19
|
+
# If postinstall didn't run (--ignore-scripts was set):
|
|
20
|
+
pnpm install --ignore-scripts=false
|
|
21
|
+
# Or manually:
|
|
22
|
+
pnpm --filter @calimero-network/calimero-cli-js run install-deps
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## package.json (minimal)
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@calimero-network/calimero-sdk-js": "^0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@calimero-network/calimero-cli-js": "^0.1.0",
|
|
34
|
+
"typescript": "^5.0.0"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "calimero-sdk build src/index.ts -o build/service.wasm"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Build command
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Build
|
|
46
|
+
npx calimero-sdk build src/index.ts -o build/service.wasm
|
|
47
|
+
|
|
48
|
+
# Or via npm script
|
|
49
|
+
pnpm build
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Deploy to node
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Install the app
|
|
56
|
+
meroctl --node-name <NODE> app install \
|
|
57
|
+
--path build/service.wasm \
|
|
58
|
+
--context-id <CONTEXT_ID>
|
|
59
|
+
|
|
60
|
+
# Create a new context (runs @Init)
|
|
61
|
+
meroctl --node-name <NODE> context create \
|
|
62
|
+
--app-id <APP_ID>
|
|
63
|
+
|
|
64
|
+
# Call a mutation
|
|
65
|
+
meroctl --node-name <NODE> call \
|
|
66
|
+
--context-id <CONTEXT_ID> \
|
|
67
|
+
--method set \
|
|
68
|
+
--args '{"key":"hello","value":"world"}'
|
|
69
|
+
|
|
70
|
+
# Call a view
|
|
71
|
+
meroctl --node-name <NODE> call \
|
|
72
|
+
--context-id <CONTEXT_ID> \
|
|
73
|
+
--method get \
|
|
74
|
+
--args '{"key":"hello"}'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## End-to-end test with Merobox
|
|
78
|
+
|
|
79
|
+
Each example in the SDK ships a Merobox workflow for multi-node E2E testing:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
merobox bootstrap run examples/counter/workflows/counter-js.yml --log-level=trace
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Platform support
|
|
86
|
+
|
|
87
|
+
| Platform | Supported |
|
|
88
|
+
| ------------------ | ------------ |
|
|
89
|
+
| macOS (x64, arm64) | Yes |
|
|
90
|
+
| Linux (x64, arm64) | Yes |
|
|
91
|
+
| Windows (native) | No — use WSL |
|
|
92
|
+
|
|
93
|
+
## Definition of done (before PR)
|
|
94
|
+
|
|
95
|
+
1. `pnpm lint` passes
|
|
96
|
+
2. `pnpm format:check` passes
|
|
97
|
+
3. `pnpm test` passes
|
|
98
|
+
4. Example app builds: `cd examples/counter && pnpm build`
|