@calimero-network/agent-skills 0.1.0 → 0.2.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 +1 -0
- package/package.json +4 -4
- package/skills/calimero-client-js/SKILL.md +47 -4
- package/skills/calimero-client-js/references/auth.md +141 -36
- package/skills/calimero-client-js/references/rpc-calls.md +121 -30
- package/skills/calimero-client-js/references/sso.md +17 -12
- package/skills/calimero-client-js/references/websocket-events.md +96 -36
- package/skills/calimero-node/SKILL.md +49 -11
- package/skills/calimero-node/references/meroctl-commands.md +139 -36
- package/skills/calimero-rust-sdk/SKILL.md +54 -16
package/README.md
CHANGED
|
@@ -234,5 +234,6 @@ Skills should be:
|
|
|
234
234
|
|
|
235
235
|
- [Calimero Network](https://calimero.network)
|
|
236
236
|
- [Documentation](https://docs.calimero.network)
|
|
237
|
+
- [Repository](https://github.com/calimero-network/calimero-skills)
|
|
237
238
|
- [GitHub org](https://github.com/calimero-network)
|
|
238
239
|
- [Discord](https://discord.gg/urJeMtRRMu)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@calimero-network/agent-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "AI agent skills for Calimero Network development — Rust SDK, JS client, registry publishing, desktop SSO, and more.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"calimero",
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
"mero-sign",
|
|
13
13
|
"calimero-sdk"
|
|
14
14
|
],
|
|
15
|
-
"homepage": "https://github.com/calimero-network/
|
|
15
|
+
"homepage": "https://github.com/calimero-network/calimero-skills",
|
|
16
16
|
"bugs": {
|
|
17
|
-
"url": "https://github.com/calimero-network/
|
|
17
|
+
"url": "https://github.com/calimero-network/calimero-skills/issues"
|
|
18
18
|
},
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "https://github.com/calimero-network/
|
|
21
|
+
"url": "https://github.com/calimero-network/calimero-skills.git"
|
|
22
22
|
},
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"bin": {
|
|
@@ -33,10 +33,53 @@ pnpm add @calimero-network/mero-js
|
|
|
33
33
|
|
|
34
34
|
## Core workflow
|
|
35
35
|
|
|
36
|
-
1.
|
|
37
|
-
2.
|
|
38
|
-
3. Call app methods via
|
|
39
|
-
4. Subscribe to events via
|
|
36
|
+
1. On startup: read SSO tokens from URL hash (if opened by Desktop), otherwise check `localStorage` for existing session, otherwise show login
|
|
37
|
+
2. Store tokens using the provided storage helpers (`setAppEndpointKey`, `setAccessToken`, etc.)
|
|
38
|
+
3. Call app methods via the `rpcClient` singleton using `rpcClient.execute()`
|
|
39
|
+
4. Subscribe to events via `WsSubscriptionsClient`
|
|
40
|
+
|
|
41
|
+
## Minimal working example
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import {
|
|
45
|
+
rpcClient,
|
|
46
|
+
getContextId,
|
|
47
|
+
getExecutorPublicKey,
|
|
48
|
+
getAppEndpointKey,
|
|
49
|
+
setAppEndpointKey,
|
|
50
|
+
setAccessToken,
|
|
51
|
+
setRefreshToken,
|
|
52
|
+
setContextAndIdentityFromJWT,
|
|
53
|
+
WsSubscriptionsClient,
|
|
54
|
+
} from '@calimero-network/calimero-client';
|
|
55
|
+
|
|
56
|
+
// 1. Store auth tokens (after SSO or login)
|
|
57
|
+
setAppEndpointKey('http://localhost:2428');
|
|
58
|
+
setAccessToken(accessToken);
|
|
59
|
+
setRefreshToken(refreshToken);
|
|
60
|
+
setContextAndIdentityFromJWT(accessToken); // extracts contextId + executorPublicKey
|
|
61
|
+
|
|
62
|
+
// 2. Call an app method
|
|
63
|
+
const response = await rpcClient.execute<{ key: string }, string | null>({
|
|
64
|
+
contextId: getContextId()!,
|
|
65
|
+
method: 'get',
|
|
66
|
+
argsJson: { key: 'hello' },
|
|
67
|
+
executorPublicKey: getExecutorPublicKey()!,
|
|
68
|
+
});
|
|
69
|
+
console.log(response.result?.output);
|
|
70
|
+
|
|
71
|
+
// 3. Subscribe to real-time events
|
|
72
|
+
const ws = new WsSubscriptionsClient(getAppEndpointKey()!, '/ws');
|
|
73
|
+
await ws.connect();
|
|
74
|
+
ws.subscribe([getContextId()!]);
|
|
75
|
+
ws.addCallback((event) => {
|
|
76
|
+
if (event.type === 'ExecutionEvent') {
|
|
77
|
+
for (const e of event.data.events) {
|
|
78
|
+
console.log(e.kind, e.data);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
```
|
|
40
83
|
|
|
41
84
|
## References
|
|
42
85
|
|
|
@@ -1,58 +1,163 @@
|
|
|
1
1
|
# Authentication
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Storage helpers
|
|
4
|
+
|
|
5
|
+
`@calimero-network/calimero-client` provides these storage functions (backed by `localStorage`):
|
|
4
6
|
|
|
5
7
|
```typescript
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
8
|
+
import {
|
|
9
|
+
// Node URL
|
|
10
|
+
setAppEndpointKey,
|
|
11
|
+
getAppEndpointKey,
|
|
12
|
+
// JWT tokens
|
|
13
|
+
setAccessToken,
|
|
14
|
+
getAccessToken,
|
|
15
|
+
setRefreshToken,
|
|
16
|
+
getRefreshToken,
|
|
17
|
+
// Context and identity (extracted from JWT)
|
|
18
|
+
setContextId,
|
|
19
|
+
getContextId,
|
|
20
|
+
setExecutorPublicKey,
|
|
21
|
+
getExecutorPublicKey,
|
|
22
|
+
setContextAndIdentityFromJWT, // extracts + stores contextId + executorPublicKey from JWT
|
|
23
|
+
// App ID
|
|
24
|
+
setApplicationId,
|
|
25
|
+
getApplicationId,
|
|
26
|
+
// Auth endpoint (separate from node URL)
|
|
27
|
+
setAuthEndpointURL,
|
|
28
|
+
getAuthEndpointURL,
|
|
29
|
+
// Decoded JWT payload
|
|
30
|
+
getJWTObject, // returns { context_id, context_identity, exp, permissions, ... }
|
|
31
|
+
// Full auth config (throws if required fields are missing)
|
|
32
|
+
getAuthConfig, // returns { appEndpointKey, contextId, executorPublicKey, jwtToken }
|
|
33
|
+
// Logout (clears all tokens + reloads)
|
|
34
|
+
clientLogout,
|
|
35
|
+
} from '@calimero-network/calimero-client';
|
|
26
36
|
```
|
|
27
37
|
|
|
28
|
-
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## SSO login (from Calimero Desktop — recommended path)
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
them automatically if you use the provided storage helpers:
|
|
42
|
+
When the app is opened by Desktop, tokens arrive in the URL hash. Store them:
|
|
32
43
|
|
|
33
44
|
```typescript
|
|
34
|
-
|
|
45
|
+
function initFromDesktopSSO(): boolean {
|
|
46
|
+
const hash = new URLSearchParams(window.location.hash.slice(1));
|
|
47
|
+
const accessToken = hash.get('access_token');
|
|
48
|
+
const refreshToken = hash.get('refresh_token');
|
|
49
|
+
const nodeUrl = hash.get('node_url');
|
|
50
|
+
const appId = hash.get('application_id');
|
|
35
51
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
//
|
|
52
|
+
if (!accessToken || !nodeUrl) return false;
|
|
53
|
+
|
|
54
|
+
// Store everything
|
|
55
|
+
setAppEndpointKey(nodeUrl);
|
|
56
|
+
setAccessToken(accessToken);
|
|
57
|
+
if (refreshToken) setRefreshToken(refreshToken);
|
|
58
|
+
if (appId) setApplicationId(appId);
|
|
59
|
+
|
|
60
|
+
// Extract contextId and executorPublicKey from JWT claims
|
|
61
|
+
setContextAndIdentityFromJWT(accessToken);
|
|
62
|
+
|
|
63
|
+
// Remove tokens from URL bar (don't let them sit in browser history)
|
|
64
|
+
history.replaceState(null, '', window.location.pathname + window.location.search);
|
|
65
|
+
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
39
68
|
```
|
|
40
69
|
|
|
41
|
-
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Manual login (no Desktop)
|
|
42
73
|
|
|
43
74
|
```typescript
|
|
44
|
-
import {
|
|
75
|
+
import { setAppEndpointKey, setAccessToken, setRefreshToken, setContextAndIdentityFromJWT } from '@calimero-network/calimero-client';
|
|
45
76
|
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
77
|
+
async function login(nodeUrl: string, accessToken: string, refreshToken: string) {
|
|
78
|
+
setAppEndpointKey(nodeUrl);
|
|
79
|
+
setAccessToken(accessToken);
|
|
80
|
+
setRefreshToken(refreshToken);
|
|
81
|
+
setContextAndIdentityFromJWT(accessToken);
|
|
49
82
|
}
|
|
50
83
|
```
|
|
51
84
|
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Checking if authenticated
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { getAuthConfig } from '@calimero-network/calimero-client';
|
|
91
|
+
|
|
92
|
+
function isAuthenticated(): boolean {
|
|
93
|
+
const config = getAuthConfig();
|
|
94
|
+
return config.error === null;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Reading the JWT payload
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { getJWTObject } from '@calimero-network/calimero-client';
|
|
104
|
+
|
|
105
|
+
const jwt = getJWTObject();
|
|
106
|
+
// jwt.context_id — the context this token is scoped to
|
|
107
|
+
// jwt.context_identity — the identity (executor public key)
|
|
108
|
+
// jwt.exp — expiry timestamp (seconds)
|
|
109
|
+
// jwt.permissions — array of permission strings
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
52
114
|
## Logout
|
|
53
115
|
|
|
54
116
|
```typescript
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
//
|
|
117
|
+
import { clientLogout } from '@calimero-network/calimero-client';
|
|
118
|
+
|
|
119
|
+
// Clears all tokens from localStorage and reloads the page
|
|
120
|
+
clientLogout();
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## App startup pattern (SSO + fallback)
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import {
|
|
129
|
+
setAppEndpointKey, setAccessToken, setRefreshToken,
|
|
130
|
+
setApplicationId, setContextAndIdentityFromJWT,
|
|
131
|
+
getAuthConfig,
|
|
132
|
+
} from '@calimero-network/calimero-client';
|
|
133
|
+
|
|
134
|
+
async function bootstrap() {
|
|
135
|
+
// Try SSO from Desktop hash
|
|
136
|
+
const hash = new URLSearchParams(window.location.hash.slice(1));
|
|
137
|
+
const accessToken = hash.get('access_token');
|
|
138
|
+
const nodeUrl = hash.get('node_url');
|
|
139
|
+
|
|
140
|
+
if (accessToken && nodeUrl) {
|
|
141
|
+
setAppEndpointKey(nodeUrl);
|
|
142
|
+
setAccessToken(accessToken);
|
|
143
|
+
const rt = hash.get('refresh_token');
|
|
144
|
+
if (rt) setRefreshToken(rt);
|
|
145
|
+
const appId = hash.get('application_id');
|
|
146
|
+
if (appId) setApplicationId(appId);
|
|
147
|
+
setContextAndIdentityFromJWT(accessToken);
|
|
148
|
+
history.replaceState(null, '', window.location.pathname);
|
|
149
|
+
renderApp();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check if already authenticated from a previous session
|
|
154
|
+
const config = getAuthConfig();
|
|
155
|
+
if (config.error === null) {
|
|
156
|
+
renderApp();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Not authenticated — show manual login
|
|
161
|
+
renderLogin();
|
|
162
|
+
}
|
|
58
163
|
```
|
|
@@ -2,74 +2,165 @@
|
|
|
2
2
|
|
|
3
3
|
Calling application methods on a Calimero node.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## The `rpcClient` singleton
|
|
6
|
+
|
|
7
|
+
`@calimero-network/calimero-client` exports a pre-configured `rpcClient` singleton.
|
|
8
|
+
Import it directly — do not construct `JsonRpcClient` manually.
|
|
6
9
|
|
|
7
10
|
```typescript
|
|
8
11
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
rpcClient,
|
|
13
|
+
getContextId,
|
|
14
|
+
getExecutorPublicKey,
|
|
12
15
|
} from '@calimero-network/calimero-client';
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Execute a method (mutation or view — same call)
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
const
|
|
22
|
+
```typescript
|
|
23
|
+
const response = await rpcClient.execute<ArgsType, OutputType>({
|
|
24
|
+
contextId: getContextId()!,
|
|
25
|
+
method: 'methodName',
|
|
26
|
+
argsJson: { /* your args */ },
|
|
27
|
+
executorPublicKey: getExecutorPublicKey()!,
|
|
28
|
+
});
|
|
16
29
|
|
|
17
|
-
|
|
30
|
+
if (response.error) {
|
|
31
|
+
console.error(response.error.error.cause.info?.message);
|
|
32
|
+
} else {
|
|
33
|
+
console.log(response.result?.output);
|
|
34
|
+
}
|
|
18
35
|
```
|
|
19
36
|
|
|
37
|
+
---
|
|
38
|
+
|
|
20
39
|
## Calling a mutation (changes state)
|
|
21
40
|
|
|
22
41
|
```typescript
|
|
23
|
-
const response = await
|
|
24
|
-
contextId:
|
|
42
|
+
const response = await rpcClient.execute<{ key: string; value: string }, void>({
|
|
43
|
+
contextId: getContextId()!,
|
|
25
44
|
method: 'set',
|
|
26
45
|
argsJson: { key: 'hello', value: 'world' },
|
|
27
|
-
executorPublicKey:
|
|
46
|
+
executorPublicKey: getExecutorPublicKey()!,
|
|
28
47
|
});
|
|
29
48
|
|
|
30
49
|
if (response.error) {
|
|
31
|
-
console.error(response.error);
|
|
50
|
+
console.error('set failed:', response.error.error.cause.info?.message);
|
|
32
51
|
}
|
|
33
52
|
```
|
|
34
53
|
|
|
35
54
|
## Calling a view (read-only)
|
|
36
55
|
|
|
37
56
|
```typescript
|
|
38
|
-
const response = await
|
|
39
|
-
contextId:
|
|
57
|
+
const response = await rpcClient.execute<{ key: string }, string | null>({
|
|
58
|
+
contextId: getContextId()!,
|
|
40
59
|
method: 'get',
|
|
41
60
|
argsJson: { key: 'hello' },
|
|
42
|
-
executorPublicKey:
|
|
61
|
+
executorPublicKey: getExecutorPublicKey()!,
|
|
43
62
|
});
|
|
44
63
|
|
|
45
|
-
|
|
64
|
+
if (!response.error) {
|
|
65
|
+
console.log(response.result?.output); // "world"
|
|
66
|
+
}
|
|
46
67
|
```
|
|
47
68
|
|
|
69
|
+
---
|
|
70
|
+
|
|
48
71
|
## Response shape
|
|
49
72
|
|
|
50
73
|
```typescript
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
// Success:
|
|
75
|
+
{ result: { output: T } }
|
|
76
|
+
|
|
77
|
+
// Error:
|
|
78
|
+
{
|
|
79
|
+
error: {
|
|
80
|
+
id: number;
|
|
81
|
+
jsonrpc: string;
|
|
82
|
+
code: number; // HTTP-like code (400, 401, 500)
|
|
83
|
+
error: {
|
|
84
|
+
name: string; // e.g. "FunctionCallError"
|
|
85
|
+
cause: {
|
|
86
|
+
name: string;
|
|
87
|
+
info?: { message: string };
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
headers?: Record<string, string>;
|
|
91
|
+
}
|
|
59
92
|
}
|
|
60
93
|
```
|
|
61
94
|
|
|
62
|
-
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Error names
|
|
98
|
+
|
|
99
|
+
| name | meaning |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `FunctionCallError` | App method returned an error |
|
|
102
|
+
| `RpcExecutionError` | Node couldn't execute the method |
|
|
103
|
+
| `InvalidRequestError` | Malformed request (wrong context-id, missing args) |
|
|
104
|
+
| `AuthenticationError` | JWT expired, revoked, or missing |
|
|
105
|
+
| `UnknownServerError` | Unexpected server error |
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Error handling with 401
|
|
110
|
+
|
|
111
|
+
The HTTP client inside `rpcClient` automatically refreshes tokens on `401 token_expired`.
|
|
112
|
+
For `token_revoked` or `invalid_token` you need to re-authenticate:
|
|
63
113
|
|
|
64
114
|
```typescript
|
|
65
|
-
const response = await
|
|
115
|
+
const response = await rpcClient.execute({ ... });
|
|
66
116
|
|
|
67
117
|
if (response.error) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
118
|
+
const { code, headers } = response.error;
|
|
119
|
+
const authError = headers?.['x-auth-error'];
|
|
120
|
+
|
|
121
|
+
if (code === 401 && (authError === 'token_revoked' || authError === 'invalid_token')) {
|
|
122
|
+
// Token cannot be refreshed — send user to login
|
|
123
|
+
clientLogout();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw new Error(response.error.error.cause.info?.message ?? 'Unknown error');
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Complete example: typed wrapper
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import {
|
|
137
|
+
rpcClient,
|
|
138
|
+
getContextId,
|
|
139
|
+
getExecutorPublicKey,
|
|
140
|
+
} from '@calimero-network/calimero-client';
|
|
141
|
+
|
|
142
|
+
async function setItem(key: string, value: string): Promise<void> {
|
|
143
|
+
const response = await rpcClient.execute<{ key: string; value: string }, void>({
|
|
144
|
+
contextId: getContextId()!,
|
|
145
|
+
method: 'set',
|
|
146
|
+
argsJson: { key, value },
|
|
147
|
+
executorPublicKey: getExecutorPublicKey()!,
|
|
148
|
+
});
|
|
149
|
+
if (response.error) {
|
|
150
|
+
throw new Error(response.error.error.cause.info?.message);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function getItem(key: string): Promise<string | null> {
|
|
155
|
+
const response = await rpcClient.execute<{ key: string }, string | null>({
|
|
156
|
+
contextId: getContextId()!,
|
|
157
|
+
method: 'get',
|
|
158
|
+
argsJson: { key },
|
|
159
|
+
executorPublicKey: getExecutorPublicKey()!,
|
|
160
|
+
});
|
|
161
|
+
if (response.error) {
|
|
162
|
+
throw new Error(response.error.error.cause.info?.message);
|
|
72
163
|
}
|
|
73
|
-
|
|
164
|
+
return response.result?.output ?? null;
|
|
74
165
|
}
|
|
75
166
|
```
|
|
@@ -34,18 +34,29 @@ function readSSOParams(): {
|
|
|
34
34
|
|
|
35
35
|
## Using SSO tokens on app startup
|
|
36
36
|
|
|
37
|
+
Use the storage helpers from `@calimero-network/calimero-client`:
|
|
38
|
+
|
|
37
39
|
```typescript
|
|
40
|
+
import {
|
|
41
|
+
setAppEndpointKey,
|
|
42
|
+
setAccessToken,
|
|
43
|
+
setRefreshToken,
|
|
44
|
+
setApplicationId,
|
|
45
|
+
setContextAndIdentityFromJWT,
|
|
46
|
+
} from '@calimero-network/calimero-client';
|
|
47
|
+
|
|
38
48
|
async function initApp() {
|
|
39
49
|
const sso = readSSOParams();
|
|
40
50
|
|
|
41
51
|
if (sso.accessToken && sso.nodeUrl) {
|
|
42
52
|
// Opened from Desktop — store tokens and skip login
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
setAppEndpointKey(sso.nodeUrl);
|
|
54
|
+
setAccessToken(sso.accessToken);
|
|
55
|
+
if (sso.refreshToken) setRefreshToken(sso.refreshToken);
|
|
56
|
+
if (sso.applicationId) setApplicationId(sso.applicationId);
|
|
57
|
+
// Extract contextId + executorPublicKey from the JWT claims
|
|
58
|
+
setContextAndIdentityFromJWT(sso.accessToken);
|
|
59
|
+
// Clear hash so tokens aren't in browser history
|
|
49
60
|
history.replaceState(null, '', window.location.pathname);
|
|
50
61
|
renderApp();
|
|
51
62
|
} else {
|
|
@@ -55,12 +66,6 @@ async function initApp() {
|
|
|
55
66
|
}
|
|
56
67
|
```
|
|
57
68
|
|
|
58
|
-
## calimero-client reads hash automatically
|
|
59
|
-
|
|
60
|
-
If you use `@calimero-network/calimero-client`'s built-in auth helpers, the library
|
|
61
|
-
reads `window.location.hash` automatically and stores the tokens. You don't need the
|
|
62
|
-
manual parsing above unless you're building a custom auth flow.
|
|
63
|
-
|
|
64
69
|
## Important
|
|
65
70
|
|
|
66
71
|
Always fall back to manual login if the hash params are absent — the app must work
|
|
@@ -2,65 +2,125 @@
|
|
|
2
2
|
|
|
3
3
|
Subscribe to real-time events emitted by the application running on the node.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Event types
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
The node sends two kinds of events to subscribers:
|
|
8
|
+
|
|
9
|
+
| type | when | payload |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `StateMutation` | Another member mutated shared state | `{ newRoot: string }` |
|
|
12
|
+
| `ExecutionEvent` | App emitted `app::emit!()` | `{ events: ExecutionEvent[] }` |
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
const jwt = getJWTObject();
|
|
14
|
+
An `ExecutionEvent` has: `{ kind: string; data: any }` where `kind` matches the Rust event variant name.
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Connect and subscribe
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
```typescript
|
|
21
|
+
import {
|
|
22
|
+
WsSubscriptionsClient,
|
|
23
|
+
getAppEndpointKey,
|
|
24
|
+
getContextId,
|
|
25
|
+
} from '@calimero-network/calimero-client';
|
|
26
|
+
|
|
27
|
+
const nodeUrl = getAppEndpointKey()!;
|
|
28
|
+
const ws = new WsSubscriptionsClient(nodeUrl, '/ws');
|
|
29
|
+
|
|
30
|
+
// Connect first, then subscribe and add callback
|
|
31
|
+
await ws.connect();
|
|
32
|
+
ws.subscribe([getContextId()!]);
|
|
33
|
+
ws.addCallback((event) => {
|
|
16
34
|
console.log('Event received:', event);
|
|
17
|
-
// event.data contains the serialized app event payload
|
|
18
35
|
});
|
|
19
36
|
```
|
|
20
37
|
|
|
21
|
-
|
|
38
|
+
---
|
|
22
39
|
|
|
23
|
-
|
|
24
|
-
interface AppEvent {
|
|
25
|
-
type: 'ItemAdded' | 'ItemRemoved' | 'MemberJoined';
|
|
26
|
-
key?: string;
|
|
27
|
-
value?: string;
|
|
28
|
-
identity?: string;
|
|
29
|
-
}
|
|
40
|
+
## Handling ExecutionEvents from app logic
|
|
30
41
|
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
```typescript
|
|
43
|
+
import type {
|
|
44
|
+
NodeEvent,
|
|
45
|
+
ExecutionEventPayload,
|
|
46
|
+
StateMutationPayload,
|
|
47
|
+
} from '@calimero-network/calimero-client';
|
|
48
|
+
|
|
49
|
+
ws.addCallback((event: NodeEvent) => {
|
|
50
|
+
if (event.type === 'ExecutionEvent') {
|
|
51
|
+
for (const e of event.data.events) {
|
|
52
|
+
// e.kind matches the Rust event variant name, e.g. 'ItemAdded'
|
|
53
|
+
// e.data contains the event payload
|
|
54
|
+
switch (e.kind) {
|
|
55
|
+
case 'ItemAdded':
|
|
56
|
+
console.log('Item added:', e.data);
|
|
57
|
+
addItemToUI(e.data.key, e.data.value);
|
|
58
|
+
break;
|
|
59
|
+
case 'ItemRemoved':
|
|
60
|
+
removeItemFromUI(e.data.key);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
33
65
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
case 'ItemRemoved':
|
|
39
|
-
removeItemFromUI(appEvent.key!);
|
|
40
|
-
break;
|
|
66
|
+
if (event.type === 'StateMutation') {
|
|
67
|
+
// Another member changed shared state — refresh data from node
|
|
68
|
+
console.log('State root changed:', event.data.newRoot);
|
|
69
|
+
refreshDataFromNode();
|
|
41
70
|
}
|
|
42
71
|
});
|
|
43
72
|
```
|
|
44
73
|
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Subscribe to multiple contexts
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
ws.subscribe([contextId1, contextId2]);
|
|
80
|
+
// Events include event.contextId to identify which context emitted them
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
45
85
|
## Cleanup
|
|
46
86
|
|
|
47
87
|
```typescript
|
|
48
|
-
//
|
|
88
|
+
// Remove a specific callback
|
|
89
|
+
ws.removeCallback(myCallback);
|
|
90
|
+
|
|
91
|
+
// Unsubscribe from specific contexts
|
|
49
92
|
ws.unsubscribe([contextId]);
|
|
50
93
|
|
|
51
|
-
//
|
|
52
|
-
ws.
|
|
94
|
+
// Close connection entirely
|
|
95
|
+
ws.disconnect();
|
|
53
96
|
```
|
|
54
97
|
|
|
98
|
+
---
|
|
99
|
+
|
|
55
100
|
## React hook pattern
|
|
56
101
|
|
|
57
102
|
```typescript
|
|
58
|
-
useEffect
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
103
|
+
import { useEffect } from 'react';
|
|
104
|
+
import { WsSubscriptionsClient, getAppEndpointKey, getContextId } from '@calimero-network/calimero-client';
|
|
105
|
+
|
|
106
|
+
function useNodeEvents(onEvent: (event: NodeEvent) => void) {
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const nodeUrl = getAppEndpointKey();
|
|
109
|
+
const contextId = getContextId();
|
|
110
|
+
if (!nodeUrl || !contextId) return;
|
|
111
|
+
|
|
112
|
+
const ws = new WsSubscriptionsClient(nodeUrl, '/ws');
|
|
113
|
+
|
|
114
|
+
ws.connect().then(() => {
|
|
115
|
+
ws.subscribe([contextId]);
|
|
116
|
+
ws.addCallback(onEvent);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return () => {
|
|
120
|
+
ws.removeCallback(onEvent);
|
|
121
|
+
ws.unsubscribe([contextId]);
|
|
122
|
+
ws.disconnect();
|
|
123
|
+
};
|
|
124
|
+
}, [onEvent]);
|
|
125
|
+
}
|
|
66
126
|
```
|
|
@@ -10,25 +10,63 @@ You are helping a developer manage a **Calimero node** using `merod` and `meroct
|
|
|
10
10
|
- **Application** — the WASM code; one app can power many contexts
|
|
11
11
|
- Installing an app and creating a context are two separate steps
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Node setup (first time)
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
#
|
|
16
|
+
# Initialize node configuration
|
|
17
|
+
merod --home ~/.calimero init
|
|
18
|
+
|
|
19
|
+
# Start the node
|
|
17
20
|
merod --home ~/.calimero run
|
|
21
|
+
# Node listens on http://localhost:2428 by default
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Complete workflow: app → context → call
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# 1. Install an app
|
|
28
|
+
meroctl --node-url http://localhost:2428 app install \
|
|
29
|
+
--path myapp.mpk
|
|
30
|
+
# → prints app-id
|
|
31
|
+
|
|
32
|
+
# 2. Create a context (instantiate the app — init() is called)
|
|
33
|
+
meroctl --node-url http://localhost:2428 context create \
|
|
34
|
+
--app-id <app-id>
|
|
35
|
+
# → prints context-id
|
|
36
|
+
|
|
37
|
+
# 3. Call a mutation (changes state)
|
|
38
|
+
meroctl --node-url http://localhost:2428 call <context-id> set \
|
|
39
|
+
--args '{"key":"hello","value":"world"}'
|
|
18
40
|
|
|
19
|
-
#
|
|
20
|
-
meroctl --node-url http://localhost:2428
|
|
41
|
+
# 4. Call a view (read-only)
|
|
42
|
+
meroctl --node-url http://localhost:2428 call <context-id> get \
|
|
43
|
+
--args '{"key":"hello"}' --view
|
|
21
44
|
|
|
22
|
-
#
|
|
23
|
-
meroctl --node-url http://localhost:2428
|
|
45
|
+
# 5. Check node is running
|
|
46
|
+
meroctl --node-url http://localhost:2428 node health
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Multi-node context (invite + join)
|
|
24
50
|
|
|
25
|
-
|
|
26
|
-
|
|
51
|
+
```bash
|
|
52
|
+
# On node A — invite a member
|
|
53
|
+
meroctl --node-url http://localhost:2428 context invite \
|
|
54
|
+
<context-id> --identity <identity-on-node-A>
|
|
55
|
+
# → prints invitation payload (JSON)
|
|
27
56
|
|
|
28
|
-
#
|
|
29
|
-
meroctl --node-url http://localhost:
|
|
57
|
+
# On node B — accept the invitation
|
|
58
|
+
meroctl --node-url http://localhost:2429 context join \
|
|
59
|
+
--invitation '<paste-invitation-payload>'
|
|
60
|
+
# → node B syncs state from node A
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Global flags
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
meroctl --node-url http://localhost:2428 <command> # connect to specific node
|
|
67
|
+
meroctl --home ~/.calimero <command> # use alternate config path
|
|
30
68
|
```
|
|
31
69
|
|
|
32
70
|
## References
|
|
33
71
|
|
|
34
|
-
See `references/` for
|
|
72
|
+
See `references/` for full meroctl command reference and context lifecycle.
|
|
@@ -5,76 +5,179 @@ Full CLI for managing a running Calimero node.
|
|
|
5
5
|
## Global flags
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
meroctl --node-url http://localhost:2428 <command>
|
|
9
|
-
meroctl --home ~/.calimero <command>
|
|
8
|
+
meroctl --node-url http://localhost:2428 <command> # connect to specific node
|
|
9
|
+
meroctl --home ~/.calimero <command> # alternate config path
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Node commands
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Check node health (returns "alive" when running)
|
|
18
|
+
meroctl --node-url http://localhost:2428 node health
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
12
23
|
## App commands
|
|
13
24
|
|
|
14
25
|
```bash
|
|
15
|
-
# Install app from bundle
|
|
16
|
-
meroctl app install --path myapp.mpk
|
|
26
|
+
# Install app from local bundle (.mpk or .wasm)
|
|
27
|
+
meroctl --node-url http://localhost:2428 app install --path myapp.mpk
|
|
28
|
+
meroctl --node-url http://localhost:2428 app install --path target/wasm32-unknown-unknown/release/myapp.wasm
|
|
17
29
|
|
|
18
|
-
# Install app from registry
|
|
19
|
-
meroctl app install
|
|
30
|
+
# Install app from registry URL
|
|
31
|
+
meroctl --node-url http://localhost:2428 app install \
|
|
32
|
+
--url https://registry.calimero.network/com.yourorg.myapp/1.0.0
|
|
20
33
|
|
|
21
34
|
# List installed apps
|
|
22
|
-
meroctl app ls
|
|
35
|
+
meroctl --node-url http://localhost:2428 app ls
|
|
23
36
|
|
|
24
|
-
# Get app
|
|
25
|
-
meroctl app get <app-id>
|
|
37
|
+
# Get details of a specific app
|
|
38
|
+
meroctl --node-url http://localhost:2428 app get <app-id>
|
|
26
39
|
|
|
27
|
-
# Remove app (only if no contexts
|
|
28
|
-
meroctl app remove <app-id>
|
|
40
|
+
# Remove an app (only works if no active contexts reference it)
|
|
41
|
+
meroctl --node-url http://localhost:2428 app remove <app-id>
|
|
29
42
|
```
|
|
30
43
|
|
|
44
|
+
---
|
|
45
|
+
|
|
31
46
|
## Context commands
|
|
32
47
|
|
|
33
48
|
```bash
|
|
34
|
-
# Create context (
|
|
35
|
-
meroctl context create --app-id <app-id>
|
|
49
|
+
# Create a context (instantiates the app — calls init())
|
|
50
|
+
meroctl --node-url http://localhost:2428 context create --app-id <app-id>
|
|
51
|
+
# Returns: context-id
|
|
52
|
+
|
|
53
|
+
# List all contexts on this node
|
|
54
|
+
meroctl --node-url http://localhost:2428 context ls
|
|
55
|
+
|
|
56
|
+
# Get details of a specific context
|
|
57
|
+
meroctl --node-url http://localhost:2428 context get <context-id>
|
|
58
|
+
|
|
59
|
+
# Delete a context (wipes all state and storage for this context)
|
|
60
|
+
meroctl --node-url http://localhost:2428 context delete <context-id>
|
|
61
|
+
|
|
62
|
+
# List members of a context
|
|
63
|
+
meroctl --node-url http://localhost:2428 context members <context-id>
|
|
64
|
+
|
|
65
|
+
# Invite a member to a context (generates an invitation payload)
|
|
66
|
+
meroctl --node-url http://localhost:2428 context invite \
|
|
67
|
+
<context-id> --identity <identity>
|
|
68
|
+
# Returns: invitation payload JSON — share this with the invitee
|
|
69
|
+
|
|
70
|
+
# Join a context using an invitation payload (run on the joining node)
|
|
71
|
+
meroctl --node-url http://localhost:2428 context join \
|
|
72
|
+
--invitation '<invitation-payload-json>'
|
|
73
|
+
# After this, the node syncs state from the inviting node
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Full invite + join example
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# ── Node A (inviter) ──
|
|
80
|
+
meroctl --node-url http://localhost:2428 context invite \
|
|
81
|
+
abc123ctx --identity ed25519:AAAA...
|
|
82
|
+
# Prints: {"payload":"..."}
|
|
83
|
+
|
|
84
|
+
# ── Node B (joiner) ──
|
|
85
|
+
meroctl --node-url http://localhost:2429 context join \
|
|
86
|
+
--invitation '{"payload":"..."}'
|
|
87
|
+
# Node B now participates in the context and syncs CRDT state
|
|
88
|
+
```
|
|
36
89
|
|
|
37
|
-
|
|
38
|
-
meroctl context ls
|
|
90
|
+
---
|
|
39
91
|
|
|
40
|
-
|
|
41
|
-
meroctl context get <context-id>
|
|
92
|
+
## Calling app methods
|
|
42
93
|
|
|
43
|
-
|
|
44
|
-
|
|
94
|
+
```bash
|
|
95
|
+
# Mutation — changes shared state (no --view flag)
|
|
96
|
+
meroctl --node-url http://localhost:2428 call <context-id> <method> \
|
|
97
|
+
--args '{"key":"hello","value":"world"}'
|
|
45
98
|
|
|
46
|
-
#
|
|
47
|
-
meroctl
|
|
99
|
+
# View — read-only, does NOT change state
|
|
100
|
+
meroctl --node-url http://localhost:2428 call <context-id> <method> \
|
|
101
|
+
--args '{"key":"hello"}' --view
|
|
48
102
|
|
|
49
|
-
#
|
|
50
|
-
meroctl
|
|
103
|
+
# Method with no arguments
|
|
104
|
+
meroctl --node-url http://localhost:2428 call <context-id> list_all \
|
|
105
|
+
--args '{}' --view
|
|
51
106
|
```
|
|
52
107
|
|
|
108
|
+
---
|
|
109
|
+
|
|
53
110
|
## Identity commands
|
|
54
111
|
|
|
55
112
|
```bash
|
|
56
|
-
# Create a new identity
|
|
57
|
-
meroctl identity create
|
|
113
|
+
# Create a new identity (root keypair for this node)
|
|
114
|
+
meroctl --node-url http://localhost:2428 identity create
|
|
58
115
|
|
|
59
|
-
# List identities
|
|
60
|
-
meroctl identity ls
|
|
116
|
+
# List all identities on this node
|
|
117
|
+
meroctl --node-url http://localhost:2428 identity ls
|
|
61
118
|
|
|
62
|
-
# Get identity
|
|
63
|
-
meroctl identity get <identity>
|
|
119
|
+
# Get details of a specific identity
|
|
120
|
+
meroctl --node-url http://localhost:2428 identity get <identity>
|
|
64
121
|
```
|
|
65
122
|
|
|
66
|
-
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Step-by-step: full local development flow
|
|
67
126
|
|
|
68
127
|
```bash
|
|
69
|
-
#
|
|
70
|
-
|
|
128
|
+
# 1. Build the WASM app
|
|
129
|
+
cargo build --target wasm32-unknown-unknown --release
|
|
130
|
+
|
|
131
|
+
# 2. Start the node (in a separate terminal)
|
|
132
|
+
merod --home ~/.calimero run
|
|
133
|
+
|
|
134
|
+
# 3. Install the app
|
|
135
|
+
meroctl --node-url http://localhost:2428 app install \
|
|
136
|
+
--path target/wasm32-unknown-unknown/release/myapp.wasm
|
|
137
|
+
# Copy the app-id from output
|
|
71
138
|
|
|
72
|
-
#
|
|
73
|
-
meroctl
|
|
139
|
+
# 4. Create a context
|
|
140
|
+
meroctl --node-url http://localhost:2428 context create --app-id <app-id>
|
|
141
|
+
# Copy the context-id from output
|
|
142
|
+
|
|
143
|
+
# 5. Interact with the app
|
|
144
|
+
meroctl --node-url http://localhost:2428 call <context-id> set \
|
|
145
|
+
--args '{"key":"foo","value":"bar"}'
|
|
146
|
+
|
|
147
|
+
meroctl --node-url http://localhost:2428 call <context-id> get \
|
|
148
|
+
--args '{"key":"foo"}' --view
|
|
74
149
|
```
|
|
75
150
|
|
|
76
|
-
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Step-by-step: multi-node context sharing
|
|
77
154
|
|
|
78
155
|
```bash
|
|
79
|
-
|
|
156
|
+
# ── Node A (port 2428) ──
|
|
157
|
+
# Install app and create context
|
|
158
|
+
meroctl --node-url http://localhost:2428 app install --path myapp.mpk
|
|
159
|
+
meroctl --node-url http://localhost:2428 context create --app-id <app-id>
|
|
160
|
+
# → <context-id>
|
|
161
|
+
|
|
162
|
+
# Create identity for node B to use
|
|
163
|
+
meroctl --node-url http://localhost:2428 identity create
|
|
164
|
+
# → <identity-b>
|
|
165
|
+
|
|
166
|
+
# Generate invitation
|
|
167
|
+
meroctl --node-url http://localhost:2428 context invite \
|
|
168
|
+
<context-id> --identity <identity-b>
|
|
169
|
+
# → <invitation-payload>
|
|
170
|
+
|
|
171
|
+
# ── Node B (port 2429) ──
|
|
172
|
+
# Accept invitation
|
|
173
|
+
meroctl --node-url http://localhost:2429 context join \
|
|
174
|
+
--invitation '<invitation-payload>'
|
|
175
|
+
# Node B syncs all existing state from node A
|
|
176
|
+
|
|
177
|
+
# Both nodes can now call methods and see each other's mutations:
|
|
178
|
+
meroctl --node-url http://localhost:2428 call <context-id> set \
|
|
179
|
+
--args '{"key":"shared","value":"data"}'
|
|
180
|
+
meroctl --node-url http://localhost:2429 call <context-id> get \
|
|
181
|
+
--args '{"key":"shared"}' --view
|
|
182
|
+
# → "data" (synced from node A)
|
|
80
183
|
```
|
|
@@ -12,19 +12,14 @@ You are helping a developer build a **Calimero WASM application** in Rust using
|
|
|
12
12
|
- Private storage is per-member and isolated; shared state syncs across all members
|
|
13
13
|
- There is no `main()` — the SDK provides the entry point
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Cargo.toml setup
|
|
16
16
|
|
|
17
|
-
```toml
|
|
18
|
-
[dependencies]
|
|
19
|
-
calimero-sdk = { path = "..." }
|
|
20
|
-
# or from crates.io once published:
|
|
21
|
-
calimero-sdk = "0.x"
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Also add to `Cargo.toml`:
|
|
25
17
|
```toml
|
|
26
18
|
[lib]
|
|
27
19
|
crate-type = ["cdylib"]
|
|
20
|
+
|
|
21
|
+
[dependencies]
|
|
22
|
+
calimero-sdk = "0.x"
|
|
28
23
|
```
|
|
29
24
|
|
|
30
25
|
## Minimal app skeleton
|
|
@@ -32,10 +27,11 @@ crate-type = ["cdylib"]
|
|
|
32
27
|
```rust
|
|
33
28
|
use calimero_sdk::app;
|
|
34
29
|
use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
|
|
30
|
+
use calimero_sdk::state::UnorderedMap;
|
|
35
31
|
|
|
36
32
|
#[derive(Default, BorshDeserialize, BorshSerialize)]
|
|
37
33
|
pub struct AppState {
|
|
38
|
-
|
|
34
|
+
items: UnorderedMap<String, String>,
|
|
39
35
|
}
|
|
40
36
|
|
|
41
37
|
#[app::state]
|
|
@@ -48,24 +44,66 @@ impl AppState {
|
|
|
48
44
|
AppState::default()
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
pub fn
|
|
52
|
-
|
|
47
|
+
pub fn set(&mut self, key: String, value: String) -> Result<(), String> {
|
|
48
|
+
self.items.insert(key, value);
|
|
53
49
|
Ok(())
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
pub fn
|
|
57
|
-
|
|
58
|
-
String::new()
|
|
52
|
+
pub fn get(&self, key: String) -> Option<String> {
|
|
53
|
+
self.items.get(&key).cloned()
|
|
59
54
|
}
|
|
60
55
|
}
|
|
61
56
|
```
|
|
62
57
|
|
|
58
|
+
## Building
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Add WASM target (one-time)
|
|
62
|
+
rustup target add wasm32-unknown-unknown
|
|
63
|
+
|
|
64
|
+
# Build
|
|
65
|
+
cargo build --target wasm32-unknown-unknown --release
|
|
66
|
+
|
|
67
|
+
# Output: target/wasm32-unknown-unknown/release/<crate_name>.wasm
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Installing and running on a node (dev workflow)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# 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
|
|
77
|
+
|
|
78
|
+
# 2. Create a context (instance of the app)
|
|
79
|
+
meroctl --node-url http://localhost:2428 context create --app-id <app-id>
|
|
80
|
+
# Returns: context-id
|
|
81
|
+
|
|
82
|
+
# 3. Call a method
|
|
83
|
+
meroctl --node-url http://localhost:2428 call <context-id> set \
|
|
84
|
+
--args '{"key":"hello","value":"world"}'
|
|
85
|
+
|
|
86
|
+
# 4. Call a view
|
|
87
|
+
meroctl --node-url http://localhost:2428 call <context-id> get \
|
|
88
|
+
--args '{"key":"hello"}' --view
|
|
89
|
+
```
|
|
90
|
+
|
|
63
91
|
## Key rules
|
|
64
92
|
|
|
65
93
|
- State struct **must** derive `Default`, `BorshDeserialize`, `BorshSerialize`
|
|
66
94
|
- Never use `HashMap`, `Vec`, `BTreeMap` directly for state — use CRDT collections
|
|
67
95
|
- No blocking I/O, no threads, no `async` in app logic
|
|
68
|
-
- Use `calimero_sdk::env::log()` not `println!`
|
|
96
|
+
- Use `calimero_sdk::env::log!()` not `println!`
|
|
97
|
+
- Mutations use `&mut self`, views use `&self`
|
|
98
|
+
|
|
99
|
+
## Logging
|
|
100
|
+
|
|
101
|
+
```rust
|
|
102
|
+
use calimero_sdk::env;
|
|
103
|
+
|
|
104
|
+
// Inside any app method:
|
|
105
|
+
env::log!("Processing key: {}", key);
|
|
106
|
+
```
|
|
69
107
|
|
|
70
108
|
## References
|
|
71
109
|
|