@agentlayer.tech/wallet 0.1.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.
Files changed (96) hide show
  1. package/.openclaw/AGENTS.md +98 -0
  2. package/.openclaw/extensions/agent-wallet/README.md +127 -0
  3. package/.openclaw/extensions/agent-wallet/index.ts +1520 -0
  4. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +184 -0
  5. package/.openclaw/extensions/agent-wallet/package.json +11 -0
  6. package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +20 -0
  7. package/CHANGELOG.md +42 -0
  8. package/LICENSE +104 -0
  9. package/README.md +332 -0
  10. package/RELEASING.md +204 -0
  11. package/agent-wallet/.env.example +62 -0
  12. package/agent-wallet/AGENTS.md +129 -0
  13. package/agent-wallet/README.md +527 -0
  14. package/agent-wallet/agent_wallet/__init__.py +11 -0
  15. package/agent-wallet/agent_wallet/approval.py +161 -0
  16. package/agent-wallet/agent_wallet/bootstrap.py +178 -0
  17. package/agent-wallet/agent_wallet/btc_user_wallets.py +217 -0
  18. package/agent-wallet/agent_wallet/config.py +382 -0
  19. package/agent-wallet/agent_wallet/encrypted_storage.py +161 -0
  20. package/agent-wallet/agent_wallet/evm_user_wallets.py +370 -0
  21. package/agent-wallet/agent_wallet/exceptions.py +9 -0
  22. package/agent-wallet/agent_wallet/file_ops.py +34 -0
  23. package/agent-wallet/agent_wallet/http_client.py +25 -0
  24. package/agent-wallet/agent_wallet/models.py +66 -0
  25. package/agent-wallet/agent_wallet/nonce_registry.py +59 -0
  26. package/agent-wallet/agent_wallet/openclaw_adapter.py +5128 -0
  27. package/agent-wallet/agent_wallet/openclaw_cli.py +626 -0
  28. package/agent-wallet/agent_wallet/openclaw_runtime.py +272 -0
  29. package/agent-wallet/agent_wallet/plugin_bundle.py +42 -0
  30. package/agent-wallet/agent_wallet/providers/__init__.py +1 -0
  31. package/agent-wallet/agent_wallet/providers/bags.py +259 -0
  32. package/agent-wallet/agent_wallet/providers/evm_portfolio.py +470 -0
  33. package/agent-wallet/agent_wallet/providers/jupiter.py +567 -0
  34. package/agent-wallet/agent_wallet/providers/kamino.py +215 -0
  35. package/agent-wallet/agent_wallet/providers/lifi.py +277 -0
  36. package/agent-wallet/agent_wallet/providers/solana_rpc.py +470 -0
  37. package/agent-wallet/agent_wallet/providers/wdk_btc_local.py +114 -0
  38. package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +205 -0
  39. package/agent-wallet/agent_wallet/sealed_keys.py +61 -0
  40. package/agent-wallet/agent_wallet/solana_stake.py +103 -0
  41. package/agent-wallet/agent_wallet/solana_tx.py +93 -0
  42. package/agent-wallet/agent_wallet/spending_limits.py +101 -0
  43. package/agent-wallet/agent_wallet/transaction_policy.py +518 -0
  44. package/agent-wallet/agent_wallet/user_wallets.py +355 -0
  45. package/agent-wallet/agent_wallet/validation.py +31 -0
  46. package/agent-wallet/agent_wallet/wallet_layer/__init__.py +1 -0
  47. package/agent-wallet/agent_wallet/wallet_layer/base.py +808 -0
  48. package/agent-wallet/agent_wallet/wallet_layer/base58.py +44 -0
  49. package/agent-wallet/agent_wallet/wallet_layer/factory.py +102 -0
  50. package/agent-wallet/agent_wallet/wallet_layer/solana.py +4252 -0
  51. package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +272 -0
  52. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +1628 -0
  53. package/agent-wallet/examples/bootstrap_wallet.py +21 -0
  54. package/agent-wallet/examples/openclaw_runtime_onboarding.py +28 -0
  55. package/agent-wallet/examples/openclaw_user_wallet_example.py +31 -0
  56. package/agent-wallet/examples/openclaw_wallet_adapter_example.py +33 -0
  57. package/agent-wallet/openclaw.plugin.json +138 -0
  58. package/agent-wallet/pyproject.toml +31 -0
  59. package/agent-wallet/scripts/bootstrap_openclaw_btc.py +278 -0
  60. package/agent-wallet/scripts/build_release_bundle.py +188 -0
  61. package/agent-wallet/scripts/finalize_openclaw_local_wallet_config.py +121 -0
  62. package/agent-wallet/scripts/install_agent_wallet.py +505 -0
  63. package/agent-wallet/scripts/install_openclaw_local_config.py +226 -0
  64. package/agent-wallet/scripts/install_openclaw_sealed_keys.py +105 -0
  65. package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +244 -0
  66. package/agent-wallet/scripts/reveal_btc_seed.sh +130 -0
  67. package/agent-wallet/scripts/security_utils.py +37 -0
  68. package/agent-wallet/scripts/setup_btc_wallet.sh +146 -0
  69. package/agent-wallet/scripts/switch_openclaw_wallet_network.py +106 -0
  70. package/agent-wallet/skills/wallet-operator/SKILL.md +128 -0
  71. package/bin/openclaw-agent-wallet.mjs +487 -0
  72. package/install-from-github.sh +134 -0
  73. package/package.json +61 -0
  74. package/setup.sh +40 -0
  75. package/wdk-btc-wallet/README.md +325 -0
  76. package/wdk-btc-wallet/bootstrap.sh +22 -0
  77. package/wdk-btc-wallet/package-lock.json +1839 -0
  78. package/wdk-btc-wallet/package.json +18 -0
  79. package/wdk-btc-wallet/run-local.sh +21 -0
  80. package/wdk-btc-wallet/src/config.js +160 -0
  81. package/wdk-btc-wallet/src/json.js +35 -0
  82. package/wdk-btc-wallet/src/local_vault.js +432 -0
  83. package/wdk-btc-wallet/src/network_state.js +84 -0
  84. package/wdk-btc-wallet/src/server.js +257 -0
  85. package/wdk-btc-wallet/src/wdk_btc_wallet.js +332 -0
  86. package/wdk-evm-wallet/README.md +183 -0
  87. package/wdk-evm-wallet/bootstrap.sh +8 -0
  88. package/wdk-evm-wallet/package-lock.json +2340 -0
  89. package/wdk-evm-wallet/package.json +23 -0
  90. package/wdk-evm-wallet/run-local.sh +12 -0
  91. package/wdk-evm-wallet/src/config.js +274 -0
  92. package/wdk-evm-wallet/src/json.js +35 -0
  93. package/wdk-evm-wallet/src/local_vault.js +430 -0
  94. package/wdk-evm-wallet/src/network_state.js +92 -0
  95. package/wdk-evm-wallet/src/server.js +575 -0
  96. package/wdk-evm-wallet/src/wdk_evm_wallet.js +4981 -0
@@ -0,0 +1,370 @@
1
+ """Host-side helpers for binding local EVM wallets to OpenClaw users."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from agent_wallet.config import resolve_openclaw_home, settings
10
+ from agent_wallet.providers.wdk_evm_local import WdkEvmLocalClient
11
+ from agent_wallet.user_wallets import normalize_user_id
12
+ from agent_wallet.wallet_layer.base import WalletBackendError
13
+
14
+
15
+ def _normalize_evm_network(value: str | None) -> str:
16
+ network = str(value or "").strip().lower()
17
+ aliases = {
18
+ "mainnet": "ethereum",
19
+ "eth": "ethereum",
20
+ "eth-mainnet": "ethereum",
21
+ "base-mainnet": "base",
22
+ "base_sepolia": "base-sepolia",
23
+ }
24
+ network = aliases.get(network, network)
25
+ if network not in {"ethereum", "sepolia", "base", "base-sepolia"}:
26
+ return "ethereum"
27
+ return network
28
+
29
+
30
+ def _resolve_service_url(service_url: str | None = None) -> str:
31
+ effective = (service_url or settings.wdk_evm_service_url).strip()
32
+ if not effective:
33
+ raise WalletBackendError("wdk_evm_service_url is required for EVM wallet host operations.")
34
+ return effective
35
+
36
+
37
+ def _resolve_user_evm_wallet_dir(user_id: str) -> Path:
38
+ return resolve_openclaw_home() / "users" / normalize_user_id(user_id) / "wallets"
39
+
40
+
41
+ def resolve_user_evm_wallet_path(user_id: str, network: str | None = None) -> Path:
42
+ effective_network = _normalize_evm_network(network or settings.solana_network)
43
+ user_dir = _resolve_user_evm_wallet_dir(user_id)
44
+ return user_dir / f"evm-{effective_network}-agent.json"
45
+
46
+
47
+ def _write_wallet_binding(path: Path, payload: dict[str, Any]) -> None:
48
+ path.parent.mkdir(parents=True, exist_ok=True)
49
+ path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
50
+
51
+
52
+ def get_user_evm_wallet_binding(user_id: str, network: str | None = None) -> dict[str, Any]:
53
+ path = resolve_user_evm_wallet_path(user_id, network=network)
54
+ if not path.exists():
55
+ raise WalletBackendError(f"EVM wallet binding does not exist yet: {path}")
56
+ payload = json.loads(path.read_text(encoding="utf-8"))
57
+ if not isinstance(payload, dict) or not str(payload.get("wallet_id") or "").strip():
58
+ raise WalletBackendError(f"EVM wallet binding is invalid: {path}")
59
+ return payload
60
+
61
+
62
+ def resolve_user_evm_wallet_binding(
63
+ user_id: str,
64
+ *,
65
+ network: str | None = None,
66
+ service_url: str | None = None,
67
+ wallet_id: str | None = None,
68
+ account_index: int | None = None,
69
+ ) -> dict[str, Any]:
70
+ effective_network = _normalize_evm_network(network or settings.solana_network)
71
+ explicit_wallet_id = str(wallet_id or "").strip()
72
+ if explicit_wallet_id:
73
+ return ensure_user_evm_wallet_binding(
74
+ user_id,
75
+ network=effective_network,
76
+ service_url=service_url,
77
+ wallet_id=explicit_wallet_id,
78
+ account_index=account_index,
79
+ )
80
+ return get_user_evm_wallet_binding(user_id, network=effective_network)
81
+
82
+
83
+ def list_user_evm_wallet_bindings(user_id: str) -> list[dict[str, Any]]:
84
+ user_dir = _resolve_user_evm_wallet_dir(user_id)
85
+ if not user_dir.exists():
86
+ return []
87
+
88
+ bindings: list[dict[str, Any]] = []
89
+ for path in sorted(user_dir.glob("evm-*-agent.json")):
90
+ try:
91
+ payload = json.loads(path.read_text(encoding="utf-8"))
92
+ except Exception:
93
+ continue
94
+ wallet_id = str(payload.get("wallet_id") or "").strip()
95
+ if not wallet_id:
96
+ continue
97
+ bindings.append(payload)
98
+ return bindings
99
+
100
+
101
+ def bind_user_evm_wallet(
102
+ user_id: str,
103
+ *,
104
+ wallet_id: str,
105
+ network: str | None = None,
106
+ service_url: str | None = None,
107
+ account_index: int | None = None,
108
+ tolerate_locked: bool = False,
109
+ fallback_address: str | None = None,
110
+ ) -> dict[str, Any]:
111
+ effective_network = _normalize_evm_network(network or settings.solana_network)
112
+ effective_account_index = settings.wdk_evm_account_index if account_index is None else int(account_index)
113
+ effective_wallet_id = str(wallet_id or "").strip()
114
+ if not effective_wallet_id:
115
+ raise WalletBackendError("wallet_id is required for EVM wallet binding.")
116
+
117
+ client = WdkEvmLocalClient(_resolve_service_url(service_url))
118
+ wallet_meta = client.post_sync("/v1/evm/wallets/get", {"walletId": effective_wallet_id})
119
+ resolved_address = str(fallback_address or "").strip()
120
+ try:
121
+ address = client.post_sync(
122
+ "/v1/evm/address/resolve",
123
+ {
124
+ "walletId": effective_wallet_id,
125
+ "accountIndex": effective_account_index,
126
+ "network": effective_network,
127
+ },
128
+ )
129
+ except WalletBackendError as exc:
130
+ is_locked = exc.code == "wallet_locked" or "wallet is locked" in str(exc).strip().lower()
131
+ if not (tolerate_locked and is_locked):
132
+ raise
133
+ else:
134
+ resolved_address = str(address.get("address") or "").strip()
135
+ binding = {
136
+ "user_id": user_id,
137
+ "wallet_id": effective_wallet_id,
138
+ "label": str(wallet_meta.get("label") or "Agent EVM Wallet"),
139
+ "network": effective_network,
140
+ "account_index": effective_account_index,
141
+ "address": resolved_address,
142
+ "storage_format": "local_vault",
143
+ "service_kind": "wdk-evm-wallet",
144
+ "created_at": wallet_meta.get("createdAt"),
145
+ "updated_at": wallet_meta.get("updatedAt"),
146
+ }
147
+ _write_wallet_binding(resolve_user_evm_wallet_path(user_id, effective_network), binding)
148
+ return binding
149
+
150
+
151
+ def ensure_user_evm_wallet_binding(
152
+ user_id: str,
153
+ *,
154
+ network: str | None = None,
155
+ service_url: str | None = None,
156
+ wallet_id: str | None = None,
157
+ account_index: int | None = None,
158
+ ) -> dict[str, Any]:
159
+ effective_network = _normalize_evm_network(network or settings.solana_network)
160
+ path = resolve_user_evm_wallet_path(user_id, network=effective_network)
161
+ explicit_wallet_id = str(wallet_id or "").strip()
162
+ if path.exists():
163
+ existing = get_user_evm_wallet_binding(user_id, network=effective_network)
164
+ if explicit_wallet_id and str(existing.get("wallet_id") or "").strip() != explicit_wallet_id:
165
+ return bind_user_evm_wallet(
166
+ user_id,
167
+ wallet_id=explicit_wallet_id,
168
+ network=effective_network,
169
+ service_url=service_url,
170
+ account_index=account_index,
171
+ tolerate_locked=True,
172
+ fallback_address=str(existing.get("address") or "").strip() or None,
173
+ )
174
+ return existing
175
+
176
+ if explicit_wallet_id:
177
+ return bind_user_evm_wallet(
178
+ user_id,
179
+ wallet_id=explicit_wallet_id,
180
+ network=effective_network,
181
+ service_url=service_url,
182
+ account_index=account_index,
183
+ tolerate_locked=True,
184
+ )
185
+
186
+ bindings = list_user_evm_wallet_bindings(user_id)
187
+ if not bindings:
188
+ raise WalletBackendError(f"EVM wallet binding does not exist yet: {path}")
189
+
190
+ wallet_ids = {
191
+ str(binding.get("wallet_id") or "").strip()
192
+ for binding in bindings
193
+ if str(binding.get("wallet_id") or "").strip()
194
+ }
195
+ if not wallet_ids:
196
+ raise WalletBackendError(f"EVM wallet binding does not exist yet: {path}")
197
+ if len(wallet_ids) > 1:
198
+ raise WalletBackendError(
199
+ "Multiple EVM wallet bindings exist for this user. Set wdk_evm_wallet_id explicitly to auto-bind a new network."
200
+ )
201
+
202
+ return bind_user_evm_wallet(
203
+ user_id,
204
+ wallet_id=next(iter(wallet_ids)),
205
+ network=effective_network,
206
+ service_url=service_url,
207
+ account_index=account_index,
208
+ )
209
+
210
+
211
+ def create_user_evm_wallet(
212
+ user_id: str,
213
+ *,
214
+ password: str,
215
+ label: str | None = None,
216
+ network: str | None = None,
217
+ service_url: str | None = None,
218
+ reveal_seed_phrase: bool = False,
219
+ account_index: int | None = None,
220
+ ) -> dict[str, Any]:
221
+ effective_network = _normalize_evm_network(network or settings.solana_network)
222
+ effective_account_index = settings.wdk_evm_account_index if account_index is None else int(account_index)
223
+ client = WdkEvmLocalClient(_resolve_service_url(service_url))
224
+ created = client.post_sync(
225
+ "/v1/evm/wallets/create",
226
+ {
227
+ "label": (label or "").strip() or "Agent EVM Wallet",
228
+ "password": password,
229
+ "network": effective_network,
230
+ "revealSeedPhrase": bool(reveal_seed_phrase),
231
+ },
232
+ )
233
+ address = client.post_sync(
234
+ "/v1/evm/address/resolve",
235
+ {
236
+ "walletId": created["walletId"],
237
+ "accountIndex": effective_account_index,
238
+ "network": effective_network,
239
+ },
240
+ )
241
+ binding = {
242
+ "user_id": user_id,
243
+ "wallet_id": str(created["walletId"]),
244
+ "label": str(created.get("label") or "Agent EVM Wallet"),
245
+ "network": effective_network,
246
+ "account_index": effective_account_index,
247
+ "address": str(address.get("address") or ""),
248
+ "storage_format": "local_vault",
249
+ "service_kind": "wdk-evm-wallet",
250
+ "created_at": created.get("createdAt"),
251
+ "updated_at": created.get("updatedAt"),
252
+ }
253
+ _write_wallet_binding(resolve_user_evm_wallet_path(user_id, effective_network), binding)
254
+ return {
255
+ **binding,
256
+ "unlocked": bool(created.get("unlocked", True)),
257
+ "unlock_expires_at": created.get("unlockExpiresAt"),
258
+ **({"seed_phrase": created["seedPhrase"]} if created.get("seedPhrase") else {}),
259
+ }
260
+
261
+
262
+ def import_user_evm_wallet(
263
+ user_id: str,
264
+ *,
265
+ password: str,
266
+ seed_phrase: str,
267
+ label: str | None = None,
268
+ network: str | None = None,
269
+ service_url: str | None = None,
270
+ account_index: int | None = None,
271
+ ) -> dict[str, Any]:
272
+ effective_network = _normalize_evm_network(network or settings.solana_network)
273
+ effective_account_index = settings.wdk_evm_account_index if account_index is None else int(account_index)
274
+ client = WdkEvmLocalClient(_resolve_service_url(service_url))
275
+ created = client.post_sync(
276
+ "/v1/evm/wallets/import",
277
+ {
278
+ "label": (label or "").strip() or "Agent EVM Wallet",
279
+ "password": password,
280
+ "seedPhrase": seed_phrase,
281
+ "network": effective_network,
282
+ },
283
+ )
284
+ address = client.post_sync(
285
+ "/v1/evm/address/resolve",
286
+ {
287
+ "walletId": created["walletId"],
288
+ "accountIndex": effective_account_index,
289
+ "network": effective_network,
290
+ },
291
+ )
292
+ binding = {
293
+ "user_id": user_id,
294
+ "wallet_id": str(created["walletId"]),
295
+ "label": str(created.get("label") or "Agent EVM Wallet"),
296
+ "network": effective_network,
297
+ "account_index": effective_account_index,
298
+ "address": str(address.get("address") or ""),
299
+ "storage_format": "local_vault",
300
+ "service_kind": "wdk-evm-wallet",
301
+ "created_at": created.get("createdAt"),
302
+ "updated_at": created.get("updatedAt"),
303
+ }
304
+ _write_wallet_binding(resolve_user_evm_wallet_path(user_id, effective_network), binding)
305
+ return {
306
+ **binding,
307
+ "unlocked": bool(created.get("unlocked", True)),
308
+ "unlock_expires_at": created.get("unlockExpiresAt"),
309
+ }
310
+
311
+
312
+ def unlock_user_evm_wallet(
313
+ user_id: str,
314
+ *,
315
+ password: str,
316
+ network: str | None = None,
317
+ service_url: str | None = None,
318
+ wallet_id: str | None = None,
319
+ account_index: int | None = None,
320
+ ) -> dict[str, Any]:
321
+ binding = resolve_user_evm_wallet_binding(
322
+ user_id,
323
+ network=network,
324
+ service_url=service_url,
325
+ wallet_id=wallet_id,
326
+ account_index=account_index,
327
+ )
328
+ client = WdkEvmLocalClient(_resolve_service_url(service_url))
329
+ payload = client.post_sync(
330
+ "/v1/evm/wallets/unlock",
331
+ {
332
+ "walletId": binding["wallet_id"],
333
+ "password": password,
334
+ "timeoutSeconds": 0,
335
+ },
336
+ )
337
+ return {
338
+ **binding,
339
+ "unlocked": bool(payload.get("unlocked", True)),
340
+ "unlock_expires_at": payload.get("unlockExpiresAt"),
341
+ }
342
+
343
+
344
+ def lock_user_evm_wallet(
345
+ user_id: str,
346
+ *,
347
+ network: str | None = None,
348
+ service_url: str | None = None,
349
+ wallet_id: str | None = None,
350
+ account_index: int | None = None,
351
+ ) -> dict[str, Any]:
352
+ binding = resolve_user_evm_wallet_binding(
353
+ user_id,
354
+ network=network,
355
+ service_url=service_url,
356
+ wallet_id=wallet_id,
357
+ account_index=account_index,
358
+ )
359
+ client = WdkEvmLocalClient(_resolve_service_url(service_url))
360
+ payload = client.post_sync(
361
+ "/v1/evm/wallets/lock",
362
+ {
363
+ "walletId": binding["wallet_id"],
364
+ },
365
+ )
366
+ return {
367
+ **binding,
368
+ "unlocked": bool(payload.get("unlocked", False)),
369
+ "unlock_expires_at": None,
370
+ }
@@ -0,0 +1,9 @@
1
+ """Wallet package exceptions."""
2
+
3
+
4
+ class ProviderError(Exception):
5
+ """A provider failed to return data."""
6
+
7
+ def __init__(self, provider: str, message: str):
8
+ self.provider = provider
9
+ super().__init__(f"[{provider}] {message}")
@@ -0,0 +1,34 @@
1
+ """Filesystem helpers for sensitive wallet/config state."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+
10
+ def atomic_write_text(path: Path, content: str, *, mode: int = 0o600) -> None:
11
+ """Atomically write text to a path with restrictive permissions."""
12
+ path.parent.mkdir(parents=True, exist_ok=True)
13
+ fd, temp_path = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
14
+ try:
15
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
16
+ handle.write(content)
17
+ handle.flush()
18
+ os.fsync(handle.fileno())
19
+ os.chmod(temp_path, mode)
20
+ os.replace(temp_path, path)
21
+ except Exception:
22
+ try:
23
+ os.unlink(temp_path)
24
+ except FileNotFoundError:
25
+ pass
26
+ raise
27
+
28
+
29
+ def chmod_if_exists(path: Path, mode: int = 0o600) -> None:
30
+ """Best-effort chmod for sensitive files."""
31
+ try:
32
+ path.chmod(mode)
33
+ except FileNotFoundError:
34
+ return
@@ -0,0 +1,25 @@
1
+ """Shared httpx async client for wallet providers."""
2
+
3
+ import httpx
4
+
5
+ from agent_wallet.config import settings
6
+
7
+ _client: httpx.AsyncClient | None = None
8
+
9
+
10
+ def get_client() -> httpx.AsyncClient:
11
+ global _client
12
+ if _client is None or _client.is_closed:
13
+ _client = httpx.AsyncClient(
14
+ timeout=httpx.Timeout(settings.http_timeout),
15
+ headers={"Accept": "application/json"},
16
+ follow_redirects=True,
17
+ )
18
+ return _client
19
+
20
+
21
+ async def close_client() -> None:
22
+ global _client
23
+ if _client and not _client.is_closed:
24
+ await _client.aclose()
25
+ _client = None
@@ -0,0 +1,66 @@
1
+ """Pydantic models for wallet backend state."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class AgentWalletCapabilities(BaseModel):
9
+ backend: str
10
+ chain: str
11
+ custody_model: str
12
+ sign_only: bool
13
+ has_signer: bool
14
+ can_get_address: bool = True
15
+ can_get_balance: bool = True
16
+ can_sign_message: bool = False
17
+ can_sign_transaction: bool = False
18
+ can_send_transaction: bool = False
19
+ external_dependencies: list[str] = []
20
+
21
+
22
+ class SolanaWalletState(BaseModel):
23
+ chain: str = "solana"
24
+ backend: str
25
+ address: str | None = None
26
+ balance_native: float | None = None
27
+ sign_only: bool = True
28
+ has_signer: bool = False
29
+ source: str = "solana-rpc"
30
+
31
+
32
+ class AgentToolSpec(BaseModel):
33
+ name: str
34
+ description: str
35
+ input_schema: dict[str, Any]
36
+ read_only: bool = True
37
+ requires_explicit_user_intent: bool = False
38
+ risk_level: str = "low"
39
+
40
+
41
+ class AgentToolResult(BaseModel):
42
+ tool: str
43
+ ok: bool
44
+ data: dict[str, Any] | None = None
45
+ error: str | None = None
46
+ error_code: str | None = None
47
+ error_details: dict[str, Any] | None = None
48
+
49
+
50
+ class OpenClawWalletSessionMetadata(BaseModel):
51
+ user_id: str
52
+ chain: str = "solana"
53
+ network: str
54
+ backend: str
55
+ address: str
56
+ wallet_path: str
57
+ storage_format: str
58
+ created_now: bool
59
+ sign_only: bool
60
+ rpc_provider_mode: str | None = None
61
+ rpc_provider: str | None = None
62
+ rpc_transport: str | None = None
63
+ swap_provider: str | None = None
64
+ swap_transport: str | None = None
65
+ tool_names: list[str]
66
+ approval_token_required_for_execute: bool = True
@@ -0,0 +1,59 @@
1
+ """In-memory nonce registry to prevent approval token replay.
2
+
3
+ Each approval token can be used exactly once. After successful verification,
4
+ the token fingerprint is recorded and any subsequent attempt to reuse it is
5
+ rejected with a clear error.
6
+
7
+ For single-process deployments the in-memory registry is sufficient. For
8
+ multi-instance deployments, swap ``_registry`` for a shared store (Redis,
9
+ SQLite, etc.) behind the same interface.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import threading
16
+ import time
17
+
18
+ from agent_wallet.wallet_layer.base import WalletBackendError
19
+
20
+
21
+ class NonceRegistry:
22
+ """Thread-safe, self-cleaning registry of consumed approval-token fingerprints."""
23
+
24
+ def __init__(self, max_age_seconds: int = 900) -> None:
25
+ self._used: dict[str, float] = {}
26
+ self._lock = threading.Lock()
27
+ self._max_age = max_age_seconds
28
+
29
+ def mark_used(self, token_fingerprint: str) -> None:
30
+ """Record a token as consumed. Raises on duplicate."""
31
+ with self._lock:
32
+ self._cleanup()
33
+ if token_fingerprint in self._used:
34
+ raise WalletBackendError(
35
+ "Approval token has already been used. "
36
+ "Each approval token is single-use — request a new one."
37
+ )
38
+ self._used[token_fingerprint] = time.monotonic()
39
+
40
+ def _cleanup(self) -> None:
41
+ cutoff = time.monotonic() - self._max_age
42
+ expired = [k for k, t in self._used.items() if t < cutoff]
43
+ for k in expired:
44
+ del self._used[k]
45
+
46
+
47
+ # Module-level singleton — shared across the process.
48
+ _registry = NonceRegistry()
49
+
50
+
51
+ def require_single_use(token: str) -> None:
52
+ """Reject reused approval tokens.
53
+
54
+ Computes a SHA-256 fingerprint of the raw token string and checks it
55
+ against the registry. Safe to call from async or sync contexts because
56
+ the registry uses an ordinary ``threading.Lock``.
57
+ """
58
+ fp = hashlib.sha256(token.encode("utf-8")).hexdigest()[:16]
59
+ _registry.mark_used(fp)