@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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# State Schema Migrations
|
|
2
|
+
|
|
3
|
+
When you need to change the shape of your app's state (add a field, rename a field, change a type),
|
|
4
|
+
use `#[app::migrate]` to write a migration function that converts the old Borsh bytes to the new
|
|
5
|
+
state struct.
|
|
6
|
+
|
|
7
|
+
The migration runs **once** when the upgraded WASM is installed on a context that has existing
|
|
8
|
+
state. New contexts (no prior state) call `#[app::init]` as normal.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## How it works
|
|
13
|
+
|
|
14
|
+
1. Define the OLD state struct with `BorshDeserialize` only (for reading old bytes)
|
|
15
|
+
2. Define the NEW state struct (your updated `#[app::state]`)
|
|
16
|
+
3. Write a `#[app::migrate]` function that reads the old bytes and returns the new struct
|
|
17
|
+
4. Install the new WASM — the node calls the migrate function on the existing context
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Example: adding a field
|
|
22
|
+
|
|
23
|
+
```rust
|
|
24
|
+
use calimero_sdk::app;
|
|
25
|
+
use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
|
|
26
|
+
use calimero_sdk::state::read_raw;
|
|
27
|
+
use calimero_storage::collections::{LwwRegister, UnorderedMap};
|
|
28
|
+
|
|
29
|
+
// --- V1 (old schema, for deserialization only) ---
|
|
30
|
+
#[derive(BorshDeserialize)]
|
|
31
|
+
#[borsh(crate = "calimero_sdk::borsh")]
|
|
32
|
+
struct AppStateV1 {
|
|
33
|
+
items: UnorderedMap<String, LwwRegister<String>>,
|
|
34
|
+
counter: LwwRegister<u64>,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- V2 (new schema, adds a `notes` field) ---
|
|
38
|
+
#[app::state(emits = for<'a> Event<'a>)]
|
|
39
|
+
#[derive(Debug, BorshSerialize, BorshDeserialize)]
|
|
40
|
+
#[borsh(crate = "calimero_sdk::borsh")]
|
|
41
|
+
pub struct AppStateV2 {
|
|
42
|
+
items: UnorderedMap<String, LwwRegister<String>>,
|
|
43
|
+
counter: LwwRegister<u64>,
|
|
44
|
+
notes: LwwRegister<String>, // new field
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[app::event]
|
|
48
|
+
pub enum Event<'a> {
|
|
49
|
+
Migrated { from: &'a str, to: &'a str },
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- Migration function ---
|
|
53
|
+
#[app::migrate]
|
|
54
|
+
pub fn migrate_v1_to_v2() -> AppStateV2 {
|
|
55
|
+
let old_bytes = read_raw().unwrap_or_else(|| {
|
|
56
|
+
panic!("Migration error: no existing state found");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
let old: AppStateV1 = BorshDeserialize::deserialize(&mut &old_bytes[..])
|
|
60
|
+
.unwrap_or_else(|e| panic!("Migration error: deserialization failed: {:?}", e));
|
|
61
|
+
|
|
62
|
+
app::emit!(Event::Migrated { from: "1.0.0", to: "2.0.0" });
|
|
63
|
+
|
|
64
|
+
AppStateV2 {
|
|
65
|
+
items: old.items,
|
|
66
|
+
counter: old.counter,
|
|
67
|
+
notes: LwwRegister::new("added in v2".to_owned()),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- App logic (same as before, plus new notes methods) ---
|
|
72
|
+
#[app::logic]
|
|
73
|
+
impl AppStateV2 {
|
|
74
|
+
#[app::init]
|
|
75
|
+
pub fn init() -> AppStateV2 {
|
|
76
|
+
AppStateV2 {
|
|
77
|
+
items: UnorderedMap::new(),
|
|
78
|
+
counter: LwwRegister::new(0),
|
|
79
|
+
notes: LwwRegister::new(String::new()),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
pub fn set_notes(&mut self, notes: String) -> app::Result<()> {
|
|
84
|
+
self.notes.set(notes);
|
|
85
|
+
Ok(())
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## `read_raw()`
|
|
93
|
+
|
|
94
|
+
```rust
|
|
95
|
+
use calimero_sdk::state::read_raw;
|
|
96
|
+
|
|
97
|
+
let bytes: Option<Vec<u8>> = read_raw();
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Returns the raw Borsh-encoded bytes of the current state, or `None` if no state exists (fresh
|
|
101
|
+
context). Always `unwrap` with a clear panic message — a missing state during migration is a
|
|
102
|
+
programmer error.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Migration scenarios
|
|
107
|
+
|
|
108
|
+
| Change | How to handle |
|
|
109
|
+
| -------------- | -------------------------------------------------- |
|
|
110
|
+
| Add a field | New field with default value in migrate fn |
|
|
111
|
+
| Remove a field | Simply don't include it in new struct |
|
|
112
|
+
| Rename a field | Map old field name to new field name |
|
|
113
|
+
| Change a type | Deserialize old, convert value, assign to new type |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Rules
|
|
118
|
+
|
|
119
|
+
- The migration function must return the **exact** new state type decorated with `#[app::state]`
|
|
120
|
+
- The old struct needs only `BorshDeserialize` — do NOT put `#[app::state]` on it
|
|
121
|
+
- Keep old struct definitions in the source file (or a `migrations` module) as long as the migration
|
|
122
|
+
exists
|
|
123
|
+
- Do NOT call `read_raw()` outside of `#[app::migrate]` — it's only valid in that context
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Nested CRDTs and Mergeable
|
|
2
|
+
|
|
3
|
+
When you use a custom struct as a value in `UnorderedMap<K, V>` or `UserStorage<T>`, that struct
|
|
4
|
+
must implement `Mergeable` so the CRDT engine knows how to resolve concurrent writes.
|
|
5
|
+
|
|
6
|
+
Two ways: derive macro (easiest) or manual impl.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Option 1: `#[derive(Mergeable)]` (recommended when all fields are CRDTs)
|
|
11
|
+
|
|
12
|
+
Add `calimero-storage-macros` to your dependencies:
|
|
13
|
+
|
|
14
|
+
```toml
|
|
15
|
+
[dependencies]
|
|
16
|
+
calimero-sdk = "0.10"
|
|
17
|
+
calimero-storage = "0.10"
|
|
18
|
+
calimero-storage-macros = "0.10"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```rust
|
|
22
|
+
use calimero_sdk::app;
|
|
23
|
+
use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
|
|
24
|
+
use calimero_storage::collections::{Counter, UnorderedMap};
|
|
25
|
+
use calimero_storage_macros::Mergeable;
|
|
26
|
+
|
|
27
|
+
/// All fields are CRDTs — derive macro just calls merge() on each field
|
|
28
|
+
#[derive(Debug, Mergeable, BorshSerialize, BorshDeserialize)]
|
|
29
|
+
#[borsh(crate = "calimero_sdk::borsh")]
|
|
30
|
+
pub struct TeamStats {
|
|
31
|
+
pub wins: Counter,
|
|
32
|
+
pub losses: Counter,
|
|
33
|
+
pub draws: Counter,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[app::state(emits = MetricsEvent)]
|
|
37
|
+
#[derive(Debug, BorshSerialize, BorshDeserialize)]
|
|
38
|
+
#[borsh(crate = "calimero_sdk::borsh")]
|
|
39
|
+
pub struct AppState {
|
|
40
|
+
teams: UnorderedMap<String, TeamStats>,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[app::logic]
|
|
44
|
+
impl AppState {
|
|
45
|
+
pub fn record_win(&mut self, team: String) -> app::Result<u64> {
|
|
46
|
+
let mut stats = self.teams.get(&team)?.unwrap_or_else(|| TeamStats {
|
|
47
|
+
wins: Counter::new(),
|
|
48
|
+
losses: Counter::new(),
|
|
49
|
+
draws: Counter::new(),
|
|
50
|
+
});
|
|
51
|
+
stats.wins.increment()?;
|
|
52
|
+
let total = stats.wins.value()?;
|
|
53
|
+
self.teams.insert(team, stats)?;
|
|
54
|
+
Ok(total)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Use `#[derive(Mergeable)]` whenever all fields are CRDT types (`Counter`, `UnorderedMap`,
|
|
60
|
+
`LwwRegister`, `Vector`, etc.).
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Option 2: Manual `Mergeable` impl (when fields aren't all CRDTs)
|
|
65
|
+
|
|
66
|
+
```rust
|
|
67
|
+
use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
|
|
68
|
+
use calimero_storage::collections::{
|
|
69
|
+
crdt_meta::MergeError, LwwRegister, Mergeable, UnorderedMap,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
#[derive(Debug, BorshSerialize, BorshDeserialize, Default)]
|
|
73
|
+
#[borsh(crate = "calimero_sdk::borsh")]
|
|
74
|
+
pub struct UserProfile {
|
|
75
|
+
pub name: LwwRegister<String>,
|
|
76
|
+
pub score: LwwRegister<u64>,
|
|
77
|
+
pub tags: UnorderedMap<String, LwwRegister<bool>>,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
impl Mergeable for UserProfile {
|
|
81
|
+
fn merge(&mut self, other: &Self) -> Result<(), MergeError> {
|
|
82
|
+
self.name.merge(&other.name)?;
|
|
83
|
+
self.score.merge(&other.score)?;
|
|
84
|
+
self.tags.merge(&other.tags)?;
|
|
85
|
+
Ok(())
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For structs with non-CRDT primitive fields (e.g. `u64` timestamps), pick a merge strategy explicitly
|
|
91
|
+
— typically last-write-wins based on a timestamp field:
|
|
92
|
+
|
|
93
|
+
```rust
|
|
94
|
+
impl Mergeable for FileRecord {
|
|
95
|
+
fn merge(&mut self, other: &Self) -> Result<(), MergeError> {
|
|
96
|
+
// LWW: take the version with the later timestamp
|
|
97
|
+
if other.uploaded_at > self.uploaded_at {
|
|
98
|
+
*self = other.clone();
|
|
99
|
+
}
|
|
100
|
+
Ok(())
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Rules
|
|
108
|
+
|
|
109
|
+
- Custom value types in `UnorderedMap<K, V>` **must** implement `Mergeable`
|
|
110
|
+
- Custom types in `UserStorage<T>` **must** implement `Mergeable + Default`
|
|
111
|
+
- `#[derive(Mergeable)]` works only when every field already implements `Mergeable`
|
|
112
|
+
- All nested CRDT fields must still be initialized with `::new()` — there is no blanket `Default`
|
|
113
|
+
(exception: when `Default` is derived manually for use with `UserStorage`)
|
|
@@ -2,55 +2,97 @@
|
|
|
2
2
|
|
|
3
3
|
Calimero apps have two storage scopes:
|
|
4
4
|
|
|
5
|
-
| Scope
|
|
6
|
-
|
|
|
7
|
-
| Shared state (CRDT) | All context members
|
|
8
|
-
| Private storage
|
|
5
|
+
| Scope | Who can read | Synced across members? |
|
|
6
|
+
| ------------------- | -------------------------- | ---------------------- |
|
|
7
|
+
| Shared state (CRDT) | All context members | Yes — automatic |
|
|
8
|
+
| Private storage | Only the local node/member | No |
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Declaring private storage
|
|
11
|
+
|
|
12
|
+
Use `#[app::private]` on a separate struct. Private storage is **never broadcast** to other nodes.
|
|
11
13
|
|
|
12
14
|
```rust
|
|
13
|
-
use calimero_sdk::
|
|
15
|
+
use calimero_sdk::app;
|
|
16
|
+
use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
|
|
17
|
+
use calimero_storage::collections::UnorderedMap;
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
#[derive(BorshSerialize, BorshDeserialize, Debug)]
|
|
20
|
+
#[borsh(crate = "calimero_sdk::borsh")]
|
|
21
|
+
#[app::private]
|
|
22
|
+
pub struct PrivateData {
|
|
23
|
+
secrets: UnorderedMap<String, String>,
|
|
24
|
+
}
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
impl Default for PrivateData {
|
|
27
|
+
fn default() -> Self {
|
|
28
|
+
Self {
|
|
29
|
+
secrets: UnorderedMap::new(),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Reading and writing private storage
|
|
36
|
+
|
|
37
|
+
```rust
|
|
38
|
+
#[app::logic]
|
|
39
|
+
impl AppState {
|
|
40
|
+
pub fn set_secret(&mut self, key: String, value: String) -> app::Result<()> {
|
|
41
|
+
// Load (or create default) private storage
|
|
42
|
+
let mut priv_data = PrivateData::private_load_or_default()?;
|
|
43
|
+
// Get a mutable guard
|
|
44
|
+
let mut priv_mut = priv_data.as_mut();
|
|
45
|
+
|
|
46
|
+
priv_mut.secrets.insert(key, value)?;
|
|
47
|
+
Ok(())
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub fn get_secret(&self, key: &str) -> app::Result<Option<String>> {
|
|
51
|
+
let priv_data = PrivateData::private_load_or_default()?;
|
|
52
|
+
Ok(priv_data.secrets.get(key)?.cloned())
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Real example from battleships — private board storage
|
|
58
|
+
|
|
59
|
+
```rust
|
|
60
|
+
#[derive(BorshSerialize, BorshDeserialize, Debug)]
|
|
61
|
+
#[borsh(crate = "calimero_sdk::borsh")]
|
|
62
|
+
#[app::private]
|
|
63
|
+
pub struct PrivateBoards {
|
|
64
|
+
boards: UnorderedMap<String, PlayerBoard>,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
impl Default for PrivateBoards {
|
|
68
|
+
fn default() -> Self {
|
|
69
|
+
Self { boards: UnorderedMap::new() }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Usage inside a method:
|
|
74
|
+
let mut priv_boards = PrivateBoards::private_load_or_default()?;
|
|
75
|
+
let mut priv_mut = priv_boards.as_mut();
|
|
76
|
+
let mut pb = priv_mut.boards.get(&key)?.unwrap_or(PlayerBoard::new());
|
|
77
|
+
pb.place_ships(ships)?;
|
|
78
|
+
priv_mut.boards.insert(key, pb)?;
|
|
21
79
|
```
|
|
22
80
|
|
|
23
81
|
## When to use private storage
|
|
24
82
|
|
|
25
83
|
- Secrets, credentials, or keys that should not leave the local node
|
|
26
|
-
- Per-member preferences or
|
|
84
|
+
- Per-member preferences or game state (e.g. hidden ship positions in battleships)
|
|
27
85
|
- Draft data not yet ready to share with the context
|
|
28
86
|
- Caching computed values locally
|
|
29
87
|
|
|
30
88
|
## When NOT to use private storage
|
|
31
89
|
|
|
32
|
-
- Anything that must be visible to other context members — use CRDT state instead
|
|
90
|
+
- Anything that must be visible to other context members — use CRDT shared state instead
|
|
33
91
|
- Authoritative application state — always use shared CRDT collections for that
|
|
34
92
|
|
|
35
|
-
##
|
|
36
|
-
|
|
37
|
-
```rust
|
|
38
|
-
use calimero_sdk::borsh::{self, BorshSerialize, BorshDeserialize};
|
|
39
|
-
use calimero_sdk::env;
|
|
40
|
-
|
|
41
|
-
#[derive(BorshSerialize, BorshDeserialize)]
|
|
42
|
-
struct MyPrivateData {
|
|
43
|
-
token: String,
|
|
44
|
-
created_at: u64,
|
|
45
|
-
}
|
|
93
|
+
## Key points
|
|
46
94
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// Read
|
|
53
|
-
if let Some(bytes) = env::private_storage_read(b"my_data") {
|
|
54
|
-
let data: MyPrivateData = borsh::from_slice(&bytes).unwrap();
|
|
55
|
-
}
|
|
56
|
-
```
|
|
95
|
+
- `PrivateData::private_load_or_default()?` — loads or creates fresh instance
|
|
96
|
+
- `priv_data.as_mut()` — returns a mutable guard; changes are persisted when the guard is dropped
|
|
97
|
+
- Private storage uses `UnorderedMap` from `calimero_storage::collections`, same as shared state
|
|
98
|
+
- Multiple `#[app::private]` structs can coexist in the same app
|
|
@@ -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
|
+
```
|