@unicitylabs/openclaw-unicity 0.1.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Unicity Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,285 @@
1
+ # Unicity wallet plugin for [OpenClaw](https://github.com/openclaw/openclaw) agents
2
+
3
+ <p align="center">
4
+ <img src="logo.png" alt="Unicity" width="300" />
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@unicitylabs/openclaw-unicity"><img src="https://img.shields.io/npm/v/@unicitylabs/openclaw-unicity" alt="npm version" /></a>
9
+ <a href="https://github.com/unicitynetwork/openclaw-unicity/blob/main/LICENSE"><img src="https://img.shields.io/github/license/unicitynetwork/openclaw-unicity" alt="license" /></a>
10
+ </p>
11
+
12
+ ---
13
+
14
+ **Unicity** is an [OpenClaw](https://github.com/openclaw/openclaw) plugin that gives your AI agent a Unicity wallet identity and the ability to send and receive encrypted direct messages over Unicity's private Nostr relay network, powered by the [Unicity Sphere SDK](https://github.com/unicitylabs/sphere-sdk).
15
+
16
+ ## Features
17
+
18
+ - **Wallet identity** — Auto-generates a Unicity wallet on first run (BIP-32 HD wallet with mnemonic backup)
19
+ - **Nametag minting** — Register a human-readable `@nametag` for your agent on the Unicity network
20
+ - **Encrypted DMs** — Send and receive direct messages over Unicity's private Nostr relays
21
+ - **Token management** — Send/receive tokens, check balances, view transaction history
22
+ - **Payment requests** — Request payments from other users, accept/reject/pay incoming requests
23
+ - **Faucet top-up** — Request test tokens on testnet via built-in faucet tool
24
+ - **Agent tools** — 9 tools for messaging, wallet operations, and payments (see [Agent Tools](#agent-tools))
25
+ - **OpenClaw channel** — Full channel plugin with inbound/outbound message handling, status reporting, and DM access control
26
+ - **Interactive setup** — `openclaw unicity setup` wizard and `openclaw onboard` integration
27
+ - **CLI commands** — `openclaw unicity init`, `status`, `send`, and `listen` for wallet management
28
+
29
+ ## Quick Start
30
+
31
+ ### 1. Install the plugin
32
+
33
+ ```bash
34
+ openclaw plugins install @unicitylabs/openclaw-unicity
35
+ ```
36
+
37
+ To update to the latest version later:
38
+
39
+ ```bash
40
+ openclaw plugins update openclaw-unicity
41
+ ```
42
+
43
+ ### 2. Run interactive setup
44
+
45
+ ```bash
46
+ openclaw unicity setup
47
+ ```
48
+
49
+ This walks you through choosing a nametag, owner, and network, then writes the config for you.
50
+
51
+ Alternatively, Unicity integrates with OpenClaw's onboarding wizard:
52
+
53
+ ```bash
54
+ openclaw onboard
55
+ ```
56
+
57
+ ### 3. Start the gateway
58
+
59
+ ```bash
60
+ openclaw gateway start
61
+ ```
62
+
63
+ On first start, Unicity auto-generates a wallet and mints your chosen nametag. The mnemonic backup is saved to `~/.openclaw/unicity/mnemonic.txt` (owner-only permissions).
64
+
65
+ That's it. Your agent can now send and receive encrypted DMs on the Unicity network.
66
+
67
+ ## Manual Configuration
68
+
69
+ If you prefer to edit config directly, add to `~/.openclaw/openclaw.json`:
70
+
71
+ ```json5
72
+ {
73
+ // Plugin settings (identity, owner, network)
74
+ "plugins": {
75
+ "entries": {
76
+ "unicity": {
77
+ "enabled": true,
78
+ "config": {
79
+ "nametag": "my-agent", // Optional: register a @nametag
80
+ "owner": "alice", // Nametag or pubkey of the trusted human owner
81
+ "network": "testnet", // testnet (default) | mainnet | dev
82
+ "additionalRelays": [ // Optional: extra Nostr relays
83
+ "wss://custom-relay.example.com"
84
+ ]
85
+ }
86
+ }
87
+ }
88
+ },
89
+
90
+ // Channel settings (DM access control)
91
+ "channels": {
92
+ "unicity": {
93
+ "enabled": true,
94
+ "dmPolicy": "open", // open | pairing | allowlist | disabled
95
+ "allowFrom": ["@trusted-user"] // Required when dmPolicy is "allowlist"
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ Config changes take effect on the next gateway restart — no need to reinstall the plugin.
102
+
103
+ ### Owner trust model
104
+
105
+ The `owner` field identifies the human who controls the agent. When set:
106
+
107
+ - **Only the owner** can give the agent commands, change its behavior, or instruct it to perform actions via DMs.
108
+ - **Anyone else** can chat with the agent — negotiate deals, discuss topics, ask questions — but the agent will not follow operational commands from non-owner senders.
109
+ - Owner matching works by nametag or public key (case-insensitive, `@` prefix optional).
110
+
111
+ ## CLI Commands
112
+
113
+ ### Interactive setup
114
+
115
+ ```bash
116
+ openclaw unicity setup
117
+ ```
118
+
119
+ Prompts for nametag, owner, and network, then writes the config file. Run this once to get started, or re-run to change settings.
120
+
121
+ ### Initialize wallet
122
+
123
+ ```bash
124
+ openclaw unicity init
125
+ ```
126
+
127
+ Creates a new wallet (if one doesn't exist), displays the public key and address, and mints the configured nametag. The mnemonic is automatically saved to `~/.openclaw/unicity/mnemonic.txt` (owner-only permissions).
128
+
129
+ ### Check status
130
+
131
+ ```bash
132
+ openclaw unicity status
133
+ ```
134
+
135
+ Shows network, public key, address, and nametag.
136
+
137
+ ## Agent Tools
138
+
139
+ Once the plugin is loaded, the agent has access to the following tools:
140
+
141
+ ### Messaging
142
+
143
+ | Tool | Description |
144
+ |------|-------------|
145
+ | `unicity_send_message` | Send an encrypted DM to a nametag or public key |
146
+
147
+ ### Wallet & Balances
148
+
149
+ | Tool | Description |
150
+ |------|-------------|
151
+ | `unicity_get_balance` | Check token balances (optionally filtered by coin) |
152
+ | `unicity_list_tokens` | List individual tokens with status and creation time |
153
+ | `unicity_get_transaction_history` | View recent transactions (sent/received) |
154
+
155
+ ### Transfers & Payments
156
+
157
+ | Tool | Description |
158
+ |------|-------------|
159
+ | `unicity_send_tokens` | Transfer tokens to a recipient (requires owner instruction) |
160
+ | `unicity_request_payment` | Send a payment request to another user |
161
+ | `unicity_list_payment_requests` | View incoming/outgoing payment requests |
162
+ | `unicity_respond_payment_request` | Pay, accept, or reject a payment request |
163
+ | `unicity_top_up` | Request test tokens from the faucet (testnet only) |
164
+
165
+ Recipients can be specified as a `@nametag` or a 64-character hex public key.
166
+
167
+ **Examples:**
168
+
169
+ > "Send a message to @alice saying hello"
170
+ >
171
+ > "What's my balance?"
172
+ >
173
+ > "Send 100 UCT to @bob for the pizza"
174
+ >
175
+ > "Top up 50 USDU from the faucet"
176
+
177
+ ### Receive messages
178
+
179
+ When the gateway is running, incoming DMs, token transfers, and payment requests are automatically routed to the agent's reply pipeline. The agent receives the event, processes it, and replies are delivered back as encrypted DMs.
180
+
181
+ ## Architecture
182
+
183
+ ```
184
+ ┌─────────────────────────────────────────────────┐
185
+ │ OpenClaw Gateway │
186
+ │ │
187
+ │ ┌────────────┐ ┌──────────┐ ┌───────────┐ │
188
+ │ │ Unicity │──▶│ Sphere │──▶│ Unicity │ │
189
+ │ │ Plugin │◀──│ SDK │◀──│ Relays │ │
190
+ │ └────────────┘ └──────────┘ └───────────┘ │
191
+ │ │ │
192
+ │ ▼ │
193
+ │ ┌───────────┐ │
194
+ │ │ Agent │ │
195
+ │ │ Pipeline │ │
196
+ │ └───────────┘ │
197
+ └─────────────────────────────────────────────────┘
198
+ ```
199
+
200
+ - **Plugin service** starts the Sphere SDK, creates/loads the wallet, and connects to Unicity relays
201
+ - **Gateway adapter** listens for inbound DMs, token transfers, and payment requests, dispatching them through OpenClaw's reply pipeline
202
+ - **Outbound adapter** delivers agent replies as encrypted DMs
203
+ - **Agent tools** (9 tools) allow the agent to send messages, manage tokens, and handle payments
204
+
205
+ ## Data Storage
206
+
207
+ | Path | Contents |
208
+ |------|----------|
209
+ | `~/.openclaw/unicity/` | Wallet data (keys, state) |
210
+ | `~/.openclaw/unicity/mnemonic.txt` | Mnemonic backup (mode 0600) |
211
+ | `~/.openclaw/unicity/tokens/` | Token storage |
212
+ | `~/.openclaw/unicity/trustbase.json` | Cached BFT trustbase (auto-downloaded) |
213
+
214
+ ## Environment Variables
215
+
216
+ | Variable | Description | Default |
217
+ |----------|-------------|---------|
218
+ | `UNICITY_TRUSTBASE_URL` | Override the BFT trustbase download URL | GitHub raw URL |
219
+ | `UNICITY_FAUCET_URL` | Override the faucet API endpoint | `https://faucet.unicity.network/api/v1/faucet/request` |
220
+
221
+ ## Development
222
+
223
+ ```bash
224
+ # Install dependencies
225
+ npm install
226
+
227
+ # Run tests
228
+ npm test
229
+
230
+ # Run tests in watch mode
231
+ npm run test:watch
232
+
233
+ # Run E2E tests (requires network, skipped in CI)
234
+ npm run test:e2e
235
+
236
+ # Lint
237
+ npm run lint
238
+ ```
239
+
240
+ ## Project Structure
241
+
242
+ ```
243
+ unicity/
244
+ ├── src/
245
+ │ ├── index.ts # Plugin entry point & registration
246
+ │ ├── config.ts # Configuration schema & validation
247
+ │ ├── validation.ts # Shared validation (nametag regex, recipient format)
248
+ │ ├── sphere.ts # Sphere SDK singleton lifecycle
249
+ │ ├── channel.ts # Channel plugin (7 adapters + onboarding)
250
+ │ ├── assets.ts # Asset registry & decimal conversion
251
+ │ ├── setup.ts # Interactive setup wizard
252
+ │ ├── cli-prompter.ts # WizardPrompter adapter for CLI
253
+ │ ├── resources/
254
+ │ │ └── unicity-ids.testnet.json # Fungible asset metadata
255
+ │ └── tools/
256
+ │ ├── send-message.ts # Send encrypted DMs
257
+ │ ├── get-balance.ts # Check wallet balances
258
+ │ ├── list-tokens.ts # List individual tokens
259
+ │ ├── get-transaction-history.ts # View transaction history
260
+ │ ├── send-tokens.ts # Transfer tokens
261
+ │ ├── request-payment.ts # Request payment from a user
262
+ │ ├── list-payment-requests.ts # View payment requests
263
+ │ ├── respond-payment-request.ts # Pay/accept/reject requests
264
+ │ └── top-up.ts # Testnet faucet
265
+ ├── test/
266
+ │ ├── config.test.ts
267
+ │ ├── assets.test.ts
268
+ │ ├── sphere.test.ts
269
+ │ ├── sphere.integration.test.ts
270
+ │ ├── channel.test.ts
271
+ │ ├── index.test.ts
272
+ │ ├── tools/ # One test file per tool
273
+ │ └── e2e/
274
+ │ └── wallet.test.ts # End-to-end wallet + DM + transfer tests
275
+ ├── openclaw.plugin.json # Plugin manifest
276
+ ├── package.json
277
+ ├── vitest.config.ts
278
+ ├── vitest.e2e.config.ts
279
+ ├── LICENSE
280
+ └── README.md
281
+ ```
282
+
283
+ ## License
284
+
285
+ [MIT](LICENSE)
@@ -0,0 +1,26 @@
1
+ {
2
+ "id": "unicity",
3
+ "channels": ["unicity"],
4
+ "configSchema": {
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "network": {
9
+ "type": "string",
10
+ "enum": ["testnet", "mainnet", "dev"],
11
+ "default": "testnet"
12
+ },
13
+ "nametag": {
14
+ "type": "string"
15
+ },
16
+ "owner": {
17
+ "type": "string",
18
+ "description": "Nametag or public key of the trusted human owner. Only the owner can give commands to the agent."
19
+ },
20
+ "additionalRelays": {
21
+ "type": "array",
22
+ "items": { "type": "string" }
23
+ }
24
+ }
25
+ }
26
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@unicitylabs/openclaw-unicity",
3
+ "version": "0.1.6",
4
+ "description": "Unicity wallet identity and encrypted DMs for OpenClaw agents — powered by Sphere SDK",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/unicitynetwork/openclaw-unicity.git"
11
+ },
12
+ "homepage": "https://github.com/unicitynetwork/openclaw-unicity#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/unicitynetwork/openclaw-unicity/issues"
15
+ },
16
+ "keywords": [
17
+ "unicity",
18
+ "openclaw",
19
+ "nostr",
20
+ "wallet",
21
+ "agent",
22
+ "dm",
23
+ "sphere-sdk"
24
+ ],
25
+ "files": [
26
+ "src/**/*",
27
+ "openclaw.plugin.json",
28
+ "LICENSE",
29
+ "README.md"
30
+ ],
31
+ "openclaw": {
32
+ "extensions": [
33
+ "./src/index.ts"
34
+ ]
35
+ },
36
+ "scripts": {
37
+ "test": "vitest run",
38
+ "test:watch": "vitest",
39
+ "test:coverage": "vitest run --coverage",
40
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
41
+ "lint": "oxlint src/ test/"
42
+ },
43
+ "dependencies": {
44
+ "@clack/prompts": "^0.10.0",
45
+ "@sinclair/typebox": "^0.34.48",
46
+ "@unicitylabs/sphere-sdk": "^0.1.9"
47
+ },
48
+ "peerDependencies": {
49
+ "openclaw": "*"
50
+ },
51
+ "devDependencies": {
52
+ "openclaw": "latest",
53
+ "oxlint": "^1.43.0",
54
+ "vitest": "^4.0.18"
55
+ },
56
+ "engines": {
57
+ "node": ">=22.0.0"
58
+ }
59
+ }
package/src/assets.ts ADDED
@@ -0,0 +1,164 @@
1
+ /** Asset registry — loads coin metadata from unicity-ids JSON. */
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import { dirname, join } from "node:path";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ interface AssetEntry {
11
+ network: string;
12
+ assetKind: string;
13
+ name: string;
14
+ symbol?: string;
15
+ decimals?: number;
16
+ description?: string;
17
+ id: string;
18
+ }
19
+
20
+ interface AssetRegistry {
21
+ /** Map from lowercase name, symbol, or coin id to coin name */
22
+ aliases: Map<string, string>;
23
+ /** Map from coin name to symbol */
24
+ symbols: Map<string, string>;
25
+ /** Map from coin name to decimals */
26
+ decimals: Map<string, number>;
27
+ /** Map from coin name to coin id */
28
+ coinIds: Map<string, string>;
29
+ /** List of all available symbols */
30
+ availableSymbols: string[];
31
+ }
32
+
33
+ let cachedRegistry: AssetRegistry | null = null;
34
+
35
+ function loadRegistry(): AssetRegistry {
36
+ if (cachedRegistry) return cachedRegistry;
37
+
38
+ const jsonPath = join(__dirname, "resources", "unicity-ids.testnet.json");
39
+ const raw = readFileSync(jsonPath, "utf-8");
40
+ const entries: AssetEntry[] = JSON.parse(raw);
41
+
42
+ const aliases = new Map<string, string>();
43
+ const symbols = new Map<string, string>();
44
+ const decimals = new Map<string, number>();
45
+ const coinIds = new Map<string, string>();
46
+ const availableSymbols: string[] = [];
47
+
48
+ for (const entry of entries) {
49
+ // Only include fungible assets
50
+ if (entry.assetKind !== "fungible") continue;
51
+ if (!entry.symbol) continue;
52
+
53
+ const name = entry.name;
54
+ const symbol = entry.symbol;
55
+
56
+ // Map name, symbol (lowercase), and coin id to the coin name
57
+ aliases.set(name.toLowerCase(), name);
58
+ aliases.set(symbol.toLowerCase(), name);
59
+ aliases.set(entry.id, name);
60
+
61
+ // Store symbol, decimals, and coin id
62
+ symbols.set(name, symbol);
63
+ coinIds.set(name, entry.id);
64
+ if (entry.decimals !== undefined) {
65
+ decimals.set(name, entry.decimals);
66
+ }
67
+
68
+ availableSymbols.push(symbol);
69
+ }
70
+
71
+ cachedRegistry = { aliases, symbols, decimals, coinIds, availableSymbols };
72
+ return cachedRegistry;
73
+ }
74
+
75
+ /** Resolve user input (name or symbol) to coin name, or null if not found */
76
+ export function resolveCoinId(input: string): string | null {
77
+ const registry = loadRegistry();
78
+ return registry.aliases.get(input.toLowerCase().trim()) ?? null;
79
+ }
80
+
81
+ /** Get symbol for a coin (accepts name, symbol, or coin id) */
82
+ export function getCoinSymbol(coin: string): string {
83
+ const registry = loadRegistry();
84
+ const name = registry.aliases.get(coin) ?? registry.aliases.get(coin.toLowerCase()) ?? coin;
85
+ return registry.symbols.get(name) ?? coin.toUpperCase();
86
+ }
87
+
88
+ /** Get decimals for a coin (accepts name, symbol, or coin id) */
89
+ export function getCoinDecimals(coin: string): number | undefined {
90
+ const registry = loadRegistry();
91
+ const name = registry.aliases.get(coin) ?? registry.aliases.get(coin.toLowerCase());
92
+ return name ? registry.decimals.get(name) : registry.decimals.get(coin);
93
+ }
94
+
95
+ /** Get coin id for a coin (accepts name, symbol, or coin id) */
96
+ export function getCoinId(coin: string): string | undefined {
97
+ const registry = loadRegistry();
98
+ const name = registry.aliases.get(coin) ?? registry.aliases.get(coin.toLowerCase());
99
+ return name ? registry.coinIds.get(name) : registry.coinIds.get(coin);
100
+ }
101
+
102
+ /** Get list of all available symbols for display */
103
+ export function getAvailableSymbols(): string[] {
104
+ const registry = loadRegistry();
105
+ return registry.availableSymbols;
106
+ }
107
+
108
+ // =============================================================================
109
+ // Currency Conversion Utilities
110
+ // =============================================================================
111
+
112
+ /**
113
+ * Convert human-readable amount to smallest unit string
114
+ * @example toSmallestUnit("1.5", 18) => "1500000000000000000"
115
+ */
116
+ export function toSmallestUnit(amount: number | string, decimals: number): string {
117
+ if (!amount) return "0";
118
+
119
+ const str = amount.toString();
120
+ const [integer, fraction = ""] = str.split(".");
121
+
122
+ // Pad fraction to exact decimal places, truncate if longer
123
+ const paddedFraction = fraction.padEnd(decimals, "0").slice(0, decimals);
124
+
125
+ // Remove leading zeros but keep at least one digit
126
+ const result = (integer + paddedFraction).replace(/^0+/, "") || "0";
127
+ return result;
128
+ }
129
+
130
+ /**
131
+ * Convert smallest unit string to human-readable amount
132
+ * @example toHumanReadable("1500000000000000000", 18) => "1.5"
133
+ */
134
+ export function toHumanReadable(amount: string, decimals: number): string {
135
+ if (!amount || amount === "0") return "0";
136
+
137
+ const str = amount.padStart(decimals + 1, "0");
138
+ const integer = str.slice(0, -decimals) || "0";
139
+ const fraction = str.slice(-decimals).replace(/0+$/, "");
140
+
141
+ return fraction ? `${integer}.${fraction}` : integer;
142
+ }
143
+
144
+ /**
145
+ * Format amount for display with symbol
146
+ * @param amount Amount in smallest units
147
+ * @param coinName Coin name (e.g., "unicity")
148
+ */
149
+ export function formatAmount(amount: string, coinName: string): string {
150
+ const decimals = getCoinDecimals(coinName) ?? 0;
151
+ const symbol = getCoinSymbol(coinName);
152
+ const readable = toHumanReadable(amount, decimals);
153
+ return `${readable} ${symbol}`;
154
+ }
155
+
156
+ /**
157
+ * Parse user input amount to smallest units for a given coin
158
+ * @param amount Human-readable amount (e.g., "100" or "1.5")
159
+ * @param coinName Coin name (e.g., "unicity")
160
+ */
161
+ export function parseAmount(amount: number | string, coinName: string): string {
162
+ const decimals = getCoinDecimals(coinName) ?? 0;
163
+ return toSmallestUnit(amount, decimals);
164
+ }