@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,355 @@
1
+ """User-scoped wallet provisioning and backend factory helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import re
7
+ from pathlib import Path
8
+
9
+ from agent_wallet.bootstrap import (
10
+ ensure_wallet_pin,
11
+ generate_solana_wallet_material,
12
+ load_wallet_pin,
13
+ refuse_recreation_if_pinned,
14
+ )
15
+ from agent_wallet.config import (
16
+ allow_plaintext_user_wallet_migration,
17
+ resolve_openclaw_home,
18
+ resolve_runtime_solana_rpc_config,
19
+ resolve_runtime_solana_swap_config,
20
+ resolve_wallet_master_key,
21
+ settings,
22
+ use_per_user_key_derivation,
23
+ )
24
+ from agent_wallet.encrypted_storage import (
25
+ _derive_user_scoped_key,
26
+ decrypt_secret_material,
27
+ encrypt_secret_material,
28
+ is_encrypted_wallet_payload,
29
+ write_encrypted_wallet_file,
30
+ )
31
+ from agent_wallet.wallet_layer.base import WalletBackendError
32
+ from agent_wallet.wallet_layer.solana import SolanaLocalKeypairSigner, SolanaWalletBackend
33
+
34
+
35
+ def normalize_user_id(user_id: str) -> str:
36
+ """Convert arbitrary user ids into stable filesystem-safe directory names."""
37
+ cleaned = re.sub(r"[^a-zA-Z0-9._-]+", "-", user_id.strip())
38
+ cleaned = cleaned.strip("-._")
39
+ if not cleaned:
40
+ cleaned = "user"
41
+ digest = hashlib.sha256(user_id.encode("utf-8")).hexdigest()[:12]
42
+ return f"{cleaned[:48]}-{digest}"
43
+
44
+
45
+ def resolve_user_wallet_path(user_id: str, network: str | None = None) -> Path:
46
+ """Resolve the wallet file path for a given OpenClaw user."""
47
+ effective_network = (network or settings.solana_network).strip().lower() or "mainnet"
48
+ user_dir = resolve_openclaw_home() / "users" / normalize_user_id(user_id) / "wallets"
49
+ return user_dir / f"solana-{effective_network}-agent.json"
50
+
51
+
52
+ def _user_wallet_metadata(user_id: str, address: str, network: str | None = None) -> dict[str, str]:
53
+ effective_network = (network or settings.solana_network).strip().lower() or "mainnet"
54
+ return {
55
+ "address": address,
56
+ "user_id": user_id,
57
+ "network": effective_network,
58
+ }
59
+
60
+
61
+ def _resolve_effective_network(network: str | None = None) -> str:
62
+ return (network or settings.solana_network).strip().lower() or "mainnet"
63
+
64
+
65
+ def _resolve_user_wallet_master_key(
66
+ user_id: str,
67
+ network: str,
68
+ *,
69
+ raw_master_key: str | None = None,
70
+ ) -> str:
71
+ effective_master_key = (
72
+ raw_master_key.strip() if raw_master_key is not None else resolve_wallet_master_key()
73
+ )
74
+ if not effective_master_key:
75
+ raise WalletBackendError(
76
+ "Encrypted per-user wallets require AGENT_WALLET_BOOT_KEY and a sealed master_key in sealed_keys.json."
77
+ )
78
+ if use_per_user_key_derivation():
79
+ return _derive_user_scoped_key(
80
+ effective_master_key,
81
+ user_id=user_id,
82
+ network=network,
83
+ )
84
+ return effective_master_key
85
+
86
+
87
+ def _candidate_user_wallet_master_keys(
88
+ user_id: str,
89
+ network: str,
90
+ *,
91
+ raw_master_key: str | None = None,
92
+ ) -> list[tuple[str, str]]:
93
+ effective_master_key = (
94
+ raw_master_key.strip() if raw_master_key is not None else resolve_wallet_master_key()
95
+ )
96
+ if not effective_master_key:
97
+ return []
98
+
99
+ candidates: list[tuple[str, str]] = []
100
+ if use_per_user_key_derivation():
101
+ candidates.append(
102
+ (
103
+ "per-user-derived",
104
+ _derive_user_scoped_key(
105
+ effective_master_key,
106
+ user_id=user_id,
107
+ network=network,
108
+ ),
109
+ )
110
+ )
111
+ candidates.append(("global-master", effective_master_key))
112
+ return candidates
113
+
114
+
115
+ def _load_user_wallet_secret_material(
116
+ path: Path,
117
+ *,
118
+ user_id: str,
119
+ network: str,
120
+ raw_master_key: str | None = None,
121
+ ) -> tuple[str, str, str | None]:
122
+ raw_text = path.read_text(encoding="utf-8").strip()
123
+ if not is_encrypted_wallet_payload(raw_text):
124
+ return raw_text, "plaintext", None
125
+
126
+ last_exc: WalletBackendError | None = None
127
+ for key_scope, candidate in _candidate_user_wallet_master_keys(
128
+ user_id,
129
+ network,
130
+ raw_master_key=raw_master_key,
131
+ ):
132
+ try:
133
+ return (
134
+ decrypt_secret_material(raw_text, master_key=candidate),
135
+ "encrypted",
136
+ key_scope,
137
+ )
138
+ except WalletBackendError as exc:
139
+ last_exc = exc
140
+ if last_exc is not None:
141
+ raise last_exc
142
+ raise WalletBackendError(
143
+ "Encrypted per-user wallets require AGENT_WALLET_BOOT_KEY and a sealed master_key in sealed_keys.json."
144
+ )
145
+
146
+
147
+ def ensure_user_solana_wallet(user_id: str, network: str | None = None) -> dict[str, str]:
148
+ """Provision a per-user Solana wallet if it does not exist yet."""
149
+ effective_network = _resolve_effective_network(network)
150
+ path = resolve_user_wallet_path(user_id, network=effective_network)
151
+ if path.exists():
152
+ secret_material, storage_format, key_scope = _load_user_wallet_secret_material(
153
+ path,
154
+ user_id=user_id,
155
+ network=effective_network,
156
+ )
157
+ signer = SolanaLocalKeypairSigner.from_secret_material(secret_material)
158
+ ensure_wallet_pin(path, address=signer.address, network=effective_network)
159
+ if storage_format == "plaintext":
160
+ if not allow_plaintext_user_wallet_migration():
161
+ raise WalletBackendError(
162
+ "Legacy plaintext user wallet files are no longer allowed. "
163
+ "Enable migration and provide a sealed master_key to upgrade them."
164
+ )
165
+ write_encrypted_wallet_file(
166
+ path,
167
+ secret_material,
168
+ master_key=_resolve_user_wallet_master_key(user_id, effective_network),
169
+ metadata=_user_wallet_metadata(user_id, signer.address, network=effective_network),
170
+ )
171
+ storage_format = "encrypted"
172
+ key_scope = "per-user-derived"
173
+ elif (
174
+ storage_format == "encrypted"
175
+ and key_scope == "global-master"
176
+ and allow_plaintext_user_wallet_migration()
177
+ ):
178
+ write_encrypted_wallet_file(
179
+ path,
180
+ secret_material,
181
+ master_key=_resolve_user_wallet_master_key(user_id, effective_network),
182
+ metadata=_user_wallet_metadata(user_id, signer.address, network=effective_network),
183
+ )
184
+ key_scope = "per-user-derived"
185
+ return {
186
+ "user_id": user_id,
187
+ "address": signer.address,
188
+ "path": str(path),
189
+ "storage_format": storage_format,
190
+ "key_scope": key_scope or "plaintext",
191
+ }
192
+
193
+ refuse_recreation_if_pinned(path, network=effective_network)
194
+
195
+ material = generate_solana_wallet_material()
196
+ write_encrypted_wallet_file(
197
+ path,
198
+ material["secret_material"],
199
+ master_key=_resolve_user_wallet_master_key(user_id, effective_network),
200
+ metadata=_user_wallet_metadata(user_id, material["address"], network=effective_network),
201
+ )
202
+ ensure_wallet_pin(path, address=material["address"], network=effective_network)
203
+ return {
204
+ "user_id": user_id,
205
+ "address": material["address"],
206
+ "path": str(path),
207
+ "storage_format": "encrypted",
208
+ "key_scope": "per-user-derived",
209
+ }
210
+
211
+
212
+ def get_user_wallet_storage_info(user_id: str, network: str | None = None) -> dict[str, str]:
213
+ """Describe the current storage state for a per-user wallet."""
214
+ path = resolve_user_wallet_path(user_id, network=network)
215
+ if not path.exists():
216
+ raise WalletBackendError(f"User wallet does not exist yet: {path}")
217
+ effective_network = _resolve_effective_network(network)
218
+ secret_material, storage_format, key_scope = _load_user_wallet_secret_material(
219
+ path,
220
+ user_id=user_id,
221
+ network=effective_network,
222
+ )
223
+ signer = SolanaLocalKeypairSigner.from_secret_material(secret_material)
224
+ pin = load_wallet_pin(path)
225
+ return {
226
+ "user_id": user_id,
227
+ "address": signer.address,
228
+ "path": str(path),
229
+ "storage_format": storage_format,
230
+ "key_scope": key_scope or "plaintext",
231
+ "network": effective_network,
232
+ "pinned_address": pin["address"] if pin else signer.address,
233
+ }
234
+
235
+
236
+ def rotate_user_wallet_encryption(
237
+ user_id: str,
238
+ *,
239
+ network: str | None = None,
240
+ new_master_key: str,
241
+ current_master_key: str | None = None,
242
+ ) -> dict[str, str]:
243
+ """Re-encrypt a per-user wallet with a new master key."""
244
+ if not new_master_key.strip():
245
+ raise WalletBackendError("new_master_key is required for wallet encryption rotation.")
246
+
247
+ path = resolve_user_wallet_path(user_id, network=network)
248
+ if not path.exists():
249
+ raise WalletBackendError(f"User wallet does not exist yet: {path}")
250
+
251
+ effective_network = _resolve_effective_network(network)
252
+ secret_material, storage_format, _ = _load_user_wallet_secret_material(
253
+ path,
254
+ user_id=user_id,
255
+ network=effective_network,
256
+ raw_master_key=current_master_key,
257
+ )
258
+ signer = SolanaLocalKeypairSigner.from_secret_material(secret_material)
259
+ write_encrypted_wallet_file(
260
+ path,
261
+ secret_material,
262
+ master_key=_resolve_user_wallet_master_key(
263
+ user_id,
264
+ effective_network,
265
+ raw_master_key=new_master_key,
266
+ ),
267
+ metadata=_user_wallet_metadata(user_id, signer.address, network=network),
268
+ )
269
+ return {
270
+ "user_id": user_id,
271
+ "address": signer.address,
272
+ "path": str(path),
273
+ "previous_storage_format": storage_format,
274
+ "storage_format": "encrypted",
275
+ "network": effective_network,
276
+ }
277
+
278
+
279
+ def export_user_wallet_backup(
280
+ user_id: str,
281
+ *,
282
+ network: str | None = None,
283
+ export_master_key: str,
284
+ current_master_key: str | None = None,
285
+ ) -> dict[str, str]:
286
+ """Export an encrypted backup payload for a per-user wallet."""
287
+ if not export_master_key.strip():
288
+ raise WalletBackendError("export_master_key is required for wallet backup export.")
289
+
290
+ path = resolve_user_wallet_path(user_id, network=network)
291
+ if not path.exists():
292
+ raise WalletBackendError(f"User wallet does not exist yet: {path}")
293
+
294
+ effective_network = _resolve_effective_network(network)
295
+ secret_material, storage_format, _ = _load_user_wallet_secret_material(
296
+ path,
297
+ user_id=user_id,
298
+ network=effective_network,
299
+ raw_master_key=current_master_key,
300
+ )
301
+ signer = SolanaLocalKeypairSigner.from_secret_material(secret_material)
302
+ exported_payload = encrypt_secret_material(
303
+ secret_material,
304
+ master_key=export_master_key,
305
+ metadata={
306
+ **_user_wallet_metadata(user_id, signer.address, network=network),
307
+ "export_kind": "backup",
308
+ },
309
+ )
310
+ return {
311
+ "user_id": user_id,
312
+ "address": signer.address,
313
+ "path": str(path),
314
+ "storage_format": storage_format,
315
+ "backup_format": "encrypted",
316
+ "backup_payload": exported_payload,
317
+ "network": effective_network,
318
+ }
319
+
320
+
321
+ def create_wallet_backend_for_user(
322
+ user_id: str,
323
+ *,
324
+ sign_only: bool | None = None,
325
+ network: str | None = None,
326
+ rpc_url: str | None = None,
327
+ ) -> SolanaWalletBackend:
328
+ """Create a user-scoped Solana backend for OpenClaw runtime integration."""
329
+ effective_network = _resolve_effective_network(network)
330
+ wallet_info = ensure_user_solana_wallet(user_id, network=effective_network)
331
+ secret_material, _, _ = _load_user_wallet_secret_material(
332
+ Path(wallet_info["path"]),
333
+ user_id=user_id,
334
+ network=effective_network,
335
+ )
336
+ signer = SolanaLocalKeypairSigner.from_secret_material(secret_material)
337
+ rpc_config = resolve_runtime_solana_rpc_config(
338
+ effective_network,
339
+ rpc_url or settings.solana_rpc_url,
340
+ settings.solana_rpc_urls,
341
+ )
342
+ swap_config = resolve_runtime_solana_swap_config(effective_network)
343
+ return SolanaWalletBackend(
344
+ rpc_url=rpc_config["rpc_urls"],
345
+ commitment=settings.solana_commitment,
346
+ network=effective_network,
347
+ signer=signer,
348
+ address=wallet_info["address"],
349
+ sign_only=settings.agent_wallet_sign_only if sign_only is None else sign_only,
350
+ rpc_provider_mode=str(rpc_config["mode"]),
351
+ rpc_provider=str(rpc_config["provider"]),
352
+ rpc_transport=str(rpc_config["transport"]),
353
+ swap_provider=str(swap_config["provider"]),
354
+ swap_transport=str(swap_config["transport"]),
355
+ )
@@ -0,0 +1,31 @@
1
+ """Validation helpers for wallet backends."""
2
+
3
+ from agent_wallet.wallet_layer.base58 import b58decode
4
+
5
+
6
+ def validate_solana_address(address: str) -> str:
7
+ """Validate a Solana wallet address via base58 decode + 32-byte length."""
8
+ value = address.strip()
9
+ if not value:
10
+ raise ValueError(
11
+ "Solana wallet address is required. "
12
+ "Example: 7vfCXTUXx5W9aSg6YpbbMHDLecxB2S7RMyCV2wAPYpQp"
13
+ )
14
+ try:
15
+ decoded = b58decode(value)
16
+ except ValueError as exc:
17
+ raise ValueError(
18
+ f"Invalid Solana address: '{address}'. "
19
+ "Expected a base58-encoded public key."
20
+ ) from exc
21
+ if len(decoded) != 32:
22
+ raise ValueError(
23
+ f"Invalid Solana address: '{address}'. "
24
+ "Expected a 32-byte base58 public key."
25
+ )
26
+ return value
27
+
28
+
29
+ def validate_solana_mint(address: str) -> str:
30
+ """Validate a Solana mint address. Same shape as a public key."""
31
+ return validate_solana_address(address)
@@ -0,0 +1 @@
1
+ """Plugin-friendly agent wallet backends."""