@agentlayer.tech/wallet 0.1.11 → 0.1.13

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 (29) hide show
  1. package/.openclaw/extensions/agent-wallet/index.ts +454 -18
  2. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +96 -0
  3. package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +2 -0
  4. package/CHANGELOG.md +25 -0
  5. package/README.md +43 -51
  6. package/agent-wallet/.env.example +11 -0
  7. package/agent-wallet/README.md +53 -0
  8. package/agent-wallet/agent_wallet/approval.py +4 -0
  9. package/agent-wallet/agent_wallet/config.py +6 -0
  10. package/agent-wallet/agent_wallet/exceptions.py +2 -1
  11. package/agent-wallet/agent_wallet/openclaw_adapter.py +361 -2
  12. package/agent-wallet/agent_wallet/openclaw_cli.py +13 -1
  13. package/agent-wallet/agent_wallet/openclaw_runtime.py +2 -5
  14. package/agent-wallet/agent_wallet/providers/houdini.py +539 -0
  15. package/agent-wallet/agent_wallet/transaction_policy.py +251 -0
  16. package/agent-wallet/agent_wallet/user_wallets.py +83 -0
  17. package/agent-wallet/agent_wallet/wallet_layer/base.py +40 -0
  18. package/agent-wallet/agent_wallet/wallet_layer/solana.py +885 -16
  19. package/agent-wallet/pyproject.toml +1 -1
  20. package/agent-wallet/scripts/bootstrap_openclaw_evm.py +291 -0
  21. package/agent-wallet/scripts/install_agent_wallet.py +54 -2
  22. package/agent-wallet/scripts/install_openclaw_local_config.py +84 -4
  23. package/agent-wallet/scripts/manage_openclaw_evm_wallet.py +343 -0
  24. package/agent-wallet/scripts/setup_evm_wallet.sh +151 -0
  25. package/hermes/plugins/agent_wallet/__init__.py +28 -2
  26. package/hermes/plugins/agent_wallet/plugin.yaml +2 -0
  27. package/hermes/plugins/agent_wallet/schemas.py +72 -0
  28. package/hermes/plugins/agent_wallet/tools.py +193 -9
  29. package/package.json +2 -2
@@ -15,6 +15,7 @@ from typing import Any
15
15
 
16
16
  SECRET_CONFIG_KEYS = {"privateKey", "masterKey", "approvalSecret"}
17
17
  BACKENDS = ("solana_local", "wdk_btc_local", "wdk_evm_local")
18
+ PREVIEW_BOUND_SWAP_TOOLS = {"swap_solana_tokens", "swap_solana_privately"}
18
19
 
19
20
 
20
21
  def _json(data: dict[str, Any]) -> str:
@@ -75,12 +76,12 @@ def _prune_preview_cache(cache: dict[str, Any]) -> dict[str, Any]:
75
76
 
76
77
 
77
78
  def _cache_swap_preview(tool_name: str, result: dict[str, Any], ttl_seconds: int = 900) -> None:
78
- if tool_name != "swap_solana_tokens" or result.get("ok") is not True:
79
+ if tool_name not in PREVIEW_BOUND_SWAP_TOOLS or result.get("ok") is not True:
79
80
  return
80
81
  preview = result.get("data")
81
82
  if not isinstance(preview, dict):
82
83
  return
83
- if preview.get("mode") != "preview" or preview.get("asset_type") != "swap":
84
+ if preview.get("mode") != "preview" or preview.get("asset_type") not in {"swap", "solana-private-swap"}:
84
85
  return
85
86
  summary = preview.get("confirmation_summary")
86
87
  if not isinstance(summary, dict):
@@ -174,6 +175,13 @@ def _python_bin(package_root: Path) -> str:
174
175
  return "python3"
175
176
 
176
177
 
178
+ def _script_path(package_root: Path, name: str) -> Path:
179
+ script = package_root / "scripts" / name
180
+ if not script.exists():
181
+ raise RuntimeError(f"Required host script is missing: {script}")
182
+ return script
183
+
184
+
177
185
  def _user_id(args: dict[str, Any]) -> str:
178
186
  value = (
179
187
  args.get("user_id")
@@ -185,6 +193,85 @@ def _user_id(args: dict[str, Any]) -> str:
185
193
  return str(value).strip() or "hermes-local-user"
186
194
 
187
195
 
196
+ def _normalize_backend(value: Any) -> str:
197
+ normalized = str(value or "").strip().lower()
198
+ aliases = {
199
+ "sol": "solana_local",
200
+ "solana": "solana_local",
201
+ "solana_local": "solana_local",
202
+ "solana-local": "solana_local",
203
+ "evm": "wdk_evm_local",
204
+ "ethereum": "wdk_evm_local",
205
+ "eth": "wdk_evm_local",
206
+ "base": "wdk_evm_local",
207
+ "wdk_evm_local": "wdk_evm_local",
208
+ "wdk-evm-local": "wdk_evm_local",
209
+ "btc": "wdk_btc_local",
210
+ "bitcoin": "wdk_btc_local",
211
+ "wdk_btc_local": "wdk_btc_local",
212
+ "wdk-btc-local": "wdk_btc_local",
213
+ }
214
+ backend = aliases.get(normalized, normalized)
215
+ if backend not in BACKENDS:
216
+ raise RuntimeError(
217
+ "Wallet backend must be one of solana_local, wdk_btc_local, or wdk_evm_local."
218
+ )
219
+ return backend
220
+
221
+
222
+ def _infer_backend_for_tool(tool_name: str) -> str | None:
223
+ if (
224
+ tool_name.startswith("get_evm_")
225
+ or tool_name.startswith("manage_evm_")
226
+ or tool_name.startswith("swap_evm_")
227
+ or tool_name.startswith("transfer_evm_")
228
+ or tool_name == "agent_wallet_evm_status"
229
+ or tool_name == "agent_wallet_evm_setup"
230
+ ):
231
+ return "wdk_evm_local"
232
+ if tool_name.startswith("get_btc_") or tool_name == "transfer_btc":
233
+ return "wdk_btc_local"
234
+ if (
235
+ "solana" in tool_name
236
+ or "jupiter" in tool_name
237
+ or "kamino" in tool_name
238
+ or "bags" in tool_name
239
+ or tool_name in {"transfer_sol", "transfer_spl_token", "sign_wallet_message", "close_empty_token_accounts", "request_devnet_airdrop", "get_wallet_portfolio", "get_solana_token_prices"}
240
+ ):
241
+ return "solana_local"
242
+ return None
243
+
244
+
245
+ def _normalize_network_for_backend(backend: str, raw_network: Any) -> str:
246
+ network = str(raw_network or "").strip().lower()
247
+ if backend == "wdk_evm_local":
248
+ aliases = {
249
+ "mainnet": "ethereum",
250
+ "eth": "ethereum",
251
+ "eth-mainnet": "ethereum",
252
+ "base-mainnet": "base",
253
+ }
254
+ normalized = aliases.get(network, network)
255
+ return normalized if normalized in {"ethereum", "base"} else "ethereum"
256
+ if backend == "wdk_btc_local":
257
+ aliases = {
258
+ "btc": "bitcoin",
259
+ "bitcoin_mainnet": "bitcoin",
260
+ "bitcoin-mainnet": "bitcoin",
261
+ "mainnet": "bitcoin",
262
+ }
263
+ normalized = aliases.get(network, network)
264
+ return normalized if normalized in {"bitcoin", "testnet", "regtest"} else "bitcoin"
265
+ aliases = {
266
+ "solana": "mainnet",
267
+ "solana-mainnet": "mainnet",
268
+ "mainnet_beta": "mainnet",
269
+ "mainnet-beta": "mainnet",
270
+ }
271
+ normalized = aliases.get(network, network)
272
+ return normalized if normalized in {"mainnet", "devnet", "testnet"} else "mainnet"
273
+
274
+
188
275
  def _reject_secret_config(config: dict[str, Any]) -> None:
189
276
  present = sorted(key for key in SECRET_CONFIG_KEYS if str(config.get(key) or "").strip())
190
277
  if present:
@@ -195,16 +282,20 @@ def _reject_secret_config(config: dict[str, Any]) -> None:
195
282
  )
196
283
 
197
284
 
198
- def _base_config(args: dict[str, Any]) -> dict[str, Any]:
285
+ def _base_config(args: dict[str, Any], *, tool_name: str | None = None) -> dict[str, Any]:
199
286
  raw = args.get("config") or {}
200
287
  if not isinstance(raw, dict):
201
288
  raise RuntimeError("config must be a JSON object when provided.")
202
289
  config = dict(raw)
203
290
  backend = args.get("backend") or os.getenv("AGENT_WALLET_BACKEND")
204
- network = args.get("network") or os.getenv("AGENT_WALLET_NETWORK")
291
+ if not backend and tool_name:
292
+ backend = _infer_backend_for_tool(tool_name)
205
293
  if backend:
206
- config["backend"] = str(backend).strip()
207
- if network:
294
+ config["backend"] = _normalize_backend(backend)
295
+ network = args.get("network") or os.getenv("AGENT_WALLET_NETWORK") or config.get("network")
296
+ if backend:
297
+ config["network"] = _normalize_network_for_backend(config["backend"], network)
298
+ elif network:
208
299
  config["network"] = str(network).strip()
209
300
  _reject_secret_config(config)
210
301
  return config
@@ -228,15 +319,15 @@ def _cli_env(package_root: Path) -> dict[str, str]:
228
319
 
229
320
  def _call_wallet_cli(args: dict[str, Any]) -> dict[str, Any]:
230
321
  package_root = _resolve_package_root()
231
- config = _base_config(args)
232
322
  tool_name = str(args.get("tool_name") or "").strip()
233
323
  if not tool_name:
234
324
  raise RuntimeError("tool_name is required.")
325
+ config = _base_config(args, tool_name=tool_name)
235
326
 
236
327
  tool_args = args.get("arguments") or {}
237
328
  if not isinstance(tool_args, dict):
238
329
  raise RuntimeError("arguments must be a JSON object when provided.")
239
- if tool_name == "swap_solana_tokens" and str(tool_args.get("mode") or "") == "execute":
330
+ if tool_name in PREVIEW_BOUND_SWAP_TOOLS and str(tool_args.get("mode") or "") == "execute":
240
331
  approval_token = str(tool_args.get("approval_token") or "").strip()
241
332
  cached_preview = _lookup_preview_for_token(approval_token)
242
333
  if cached_preview is not None and "_approved_preview" not in tool_args:
@@ -277,16 +368,47 @@ def _call_wallet_cli(args: dict[str, Any]) -> dict[str, Any]:
277
368
  return {"ok": False, "error": f"wallet CLI returned invalid JSON: {exc}"}
278
369
 
279
370
 
371
+ def _run_host_script(
372
+ package_root: Path,
373
+ script_name: str,
374
+ script_args: list[str],
375
+ *,
376
+ stdin_text: str | None = None,
377
+ ) -> dict[str, Any]:
378
+ command = [
379
+ _python_bin(package_root),
380
+ str(_script_path(package_root, script_name)),
381
+ *script_args,
382
+ ]
383
+ completed = subprocess.run(
384
+ command,
385
+ cwd=str(package_root),
386
+ env=_cli_env(package_root),
387
+ text=True,
388
+ input=stdin_text,
389
+ capture_output=True,
390
+ timeout=float(os.getenv("AGENT_WALLET_HERMES_TIMEOUT", "120")),
391
+ check=False,
392
+ )
393
+ if completed.returncode != 0:
394
+ detail = completed.stderr.strip() or completed.stdout.strip()
395
+ return {"ok": False, "error": detail or f"host script exited {completed.returncode}"}
396
+ try:
397
+ return json.loads(completed.stdout.strip() or "{}")
398
+ except json.JSONDecodeError as exc:
399
+ return {"ok": False, "error": f"host script returned invalid JSON: {exc}"}
400
+
401
+
280
402
  def _call_issue_approval(args: dict[str, Any]) -> dict[str, Any]:
281
403
  if args.get("user_confirmed") is not True:
282
404
  raise RuntimeError(
283
405
  "user_confirmed=true is required after explicit user approval of the exact confirmation_summary."
284
406
  )
285
407
  package_root = _resolve_package_root()
286
- config = _base_config(args)
287
408
  tool_name = str(args.get("tool_name") or "").strip()
288
409
  if not tool_name:
289
410
  raise RuntimeError("tool_name is required.")
411
+ config = _base_config(args, tool_name=tool_name)
290
412
 
291
413
  summary = args.get("confirmation_summary")
292
414
  if not isinstance(summary, dict) or not summary:
@@ -337,6 +459,54 @@ def _call_issue_approval(args: dict[str, Any]) -> dict[str, Any]:
337
459
  return {"ok": False, "error": f"wallet CLI returned invalid JSON: {exc}"}
338
460
 
339
461
 
462
+ def _call_evm_status(args: dict[str, Any]) -> dict[str, Any]:
463
+ package_root = _resolve_package_root()
464
+ command = ["status"]
465
+ user_id = str(args.get("user_id") or "").strip()
466
+ network = str(args.get("network") or "").strip()
467
+ service_url = str(args.get("service_url") or "").strip()
468
+ if user_id:
469
+ command.extend(["--user-id", user_id])
470
+ if network:
471
+ command.extend(["--network", network])
472
+ if service_url:
473
+ command.extend(["--service-url", service_url])
474
+ return _run_host_script(package_root, "manage_openclaw_evm_wallet.py", command)
475
+
476
+
477
+ def _call_evm_setup(args: dict[str, Any]) -> dict[str, Any]:
478
+ package_root = _resolve_package_root()
479
+ password = str(args.get("password") or "").strip()
480
+ if not password:
481
+ raise RuntimeError("password is required.")
482
+ command = [
483
+ "--user-id",
484
+ _user_id(args),
485
+ "--password-stdin",
486
+ ]
487
+ network = str(args.get("network") or "").strip()
488
+ label = str(args.get("label") or "").strip()
489
+ service_url = str(args.get("service_url") or "").strip()
490
+ auto_start_service = args.get("auto_start_service")
491
+ bind_network_pair = args.get("bind_network_pair")
492
+ if network:
493
+ command.extend(["--network", network])
494
+ if label:
495
+ command.extend(["--label", label])
496
+ if service_url:
497
+ command.extend(["--service-url", service_url])
498
+ if auto_start_service is False:
499
+ command.append("--no-auto-start-service")
500
+ if bind_network_pair is False:
501
+ command.append("--no-bind-network-pair")
502
+ return _run_host_script(
503
+ package_root,
504
+ "bootstrap_openclaw_evm.py",
505
+ command,
506
+ stdin_text=f"{password}\n",
507
+ )
508
+
509
+
340
510
  class _SchemaOnlyBackend:
341
511
  def __init__(self, *, name: str, chain: str, network: str):
342
512
  self.name = name
@@ -431,3 +601,17 @@ def agent_wallet_approve(args: dict, **kwargs) -> str:
431
601
  return _json(_call_issue_approval(args or {}))
432
602
  except Exception as exc:
433
603
  return _json({"ok": False, "error": str(exc)})
604
+
605
+
606
+ def agent_wallet_evm_status(args: dict, **kwargs) -> str:
607
+ try:
608
+ return _json(_call_evm_status(args or {}))
609
+ except Exception as exc:
610
+ return _json({"ok": False, "error": str(exc)})
611
+
612
+
613
+ def agent_wallet_evm_setup(args: dict, **kwargs) -> str:
614
+ try:
615
+ return _json(_call_evm_setup(args or {}))
616
+ except Exception as exc:
617
+ return _json({"ok": False, "error": str(exc)})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayer.tech/wallet",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -69,4 +69,4 @@
69
69
  "evm"
70
70
  ],
71
71
  "license": "SEE LICENSE IN LICENSE"
72
- }
72
+ }