@agentlayer.tech/wallet 0.1.12 → 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.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.12"
7
+ version = "0.1.13"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -44,6 +44,7 @@ EXCLUDED_RUNTIME_DIR_NAMES = {
44
44
  }
45
45
  EXCLUDED_RUNTIME_FILE_NAMES = {
46
46
  ".DS_Store",
47
+ ".env",
47
48
  }
48
49
  EXCLUDED_RUNTIME_SUFFIXES = {
49
50
  ".pyc",
@@ -270,6 +271,39 @@ def _ensure_env_file(env_path: Path, env_example_path: Path) -> bool:
270
271
  return True
271
272
 
272
273
 
274
+ def _upsert_env_value(env_path: Path, key: str, value: str) -> bool:
275
+ if not env_path.exists():
276
+ return False
277
+ lines = env_path.read_text(encoding="utf-8").splitlines()
278
+ updated = False
279
+ replaced = False
280
+ prefix = f"{key}="
281
+ new_line = f"{key}={value}"
282
+ for index, line in enumerate(lines):
283
+ if line.startswith(prefix):
284
+ replaced = True
285
+ if line != new_line:
286
+ lines[index] = new_line
287
+ updated = True
288
+ break
289
+ if not replaced:
290
+ if lines and lines[-1] != "":
291
+ lines.append("")
292
+ lines.append(new_line)
293
+ updated = True
294
+ if updated:
295
+ _atomic_write_text(env_path, "\n".join(lines) + "\n", mode=0o600)
296
+ _chmod_if_exists(env_path, 0o600)
297
+ return updated
298
+
299
+
300
+ def _ensure_runtime_boot_key_file_env(env_path: Path) -> bool:
301
+ boot_key_file = _resolve_openclaw_home() / "agent-wallet-runtime" / "boot-key"
302
+ if not boot_key_file.exists():
303
+ return False
304
+ return _upsert_env_value(env_path, "AGENT_WALLET_BOOT_KEY_FILE", str(boot_key_file))
305
+
306
+
273
307
  def _ensure_openclaw_config(config_path: Path) -> bool:
274
308
  if config_path.exists():
275
309
  return False
@@ -288,6 +322,22 @@ def _venv_python(venv_path: Path) -> Path:
288
322
  return venv_path / "bin" / "python"
289
323
 
290
324
 
325
+ def _venv_python_wrapper(venv_path: Path) -> Path:
326
+ if os.name == "nt":
327
+ return _venv_python(venv_path)
328
+ return venv_path / "bin" / "openclaw-agent-wallet-python"
329
+
330
+
331
+ def _ensure_python_wrapper(venv_path: Path) -> Path:
332
+ if os.name == "nt":
333
+ return _venv_python(venv_path)
334
+ wrapper = _venv_python_wrapper(venv_path)
335
+ wrapper.parent.mkdir(parents=True, exist_ok=True)
336
+ wrapper.write_text('#!/bin/sh\nexec "$(dirname "$0")/python" "$@"\n', encoding="utf-8")
337
+ wrapper.chmod(0o755)
338
+ return wrapper
339
+
340
+
291
341
  def _ensure_python_runtime(venv_path: Path, package_root: Path) -> tuple[Path, bool]:
292
342
  created = False
293
343
  python_bin = _venv_python(venv_path)
@@ -299,7 +349,7 @@ def _ensure_python_runtime(venv_path: Path, package_root: Path) -> tuple[Path, b
299
349
  [str(python_bin), "-m", "pip", "install", "-e", str(package_root)],
300
350
  check=True,
301
351
  )
302
- return python_bin, created
352
+ return _ensure_python_wrapper(venv_path), created
303
353
 
304
354
 
305
355
  def _ensure_node_runtime(npm_bin: str, project_root: Path) -> dict[str, object]:
@@ -441,6 +491,7 @@ def main() -> None:
441
491
  env_example_path = package_root / ".env.example"
442
492
 
443
493
  env_created = _ensure_env_file(env_path, env_example_path)
494
+ boot_key_file_env_updated = _ensure_runtime_boot_key_file_env(env_path)
444
495
  config_created = _ensure_openclaw_config(config_path)
445
496
 
446
497
  python_bin = Path(sys.executable)
@@ -449,7 +500,7 @@ def main() -> None:
449
500
  if not args.dry_run:
450
501
  python_bin, venv_created = _ensure_python_runtime(venv_path, package_root)
451
502
  else:
452
- python_bin = _venv_python(venv_path)
503
+ python_bin = _venv_python_wrapper(venv_path)
453
504
 
454
505
  node_runtime = {
455
506
  "skipped": bool(args.skip_node_setup),
@@ -500,6 +551,7 @@ def main() -> None:
500
551
  "ok": True,
501
552
  "env_path": str(env_path),
502
553
  "env_created": env_created,
554
+ "boot_key_file_env_updated": boot_key_file_env_updated,
503
555
  "config_path": str(config_path),
504
556
  "config_created": config_created,
505
557
  "package_root": str(package_root),
@@ -16,6 +16,25 @@ from agent_wallet.sealed_keys import resolve_sealed_keys_path, seal_keys, unseal
16
16
  from security_utils import write_redacted_backup
17
17
 
18
18
  OPTIONAL_TOOLS = [
19
+ "get_wallet_capabilities",
20
+ "get_wallet_address",
21
+ "get_wallet_balance",
22
+ "get_active_wallet_backend",
23
+ "set_wallet_backend",
24
+ "get_wallet_portfolio",
25
+ "get_solana_token_prices",
26
+ "swap_solana_privately",
27
+ "continue_solana_private_swap",
28
+ "list_pending_solana_private_swaps",
29
+ "get_solana_private_swap_status",
30
+ "get_kamino_lend_markets",
31
+ "get_kamino_lend_market_reserves",
32
+ "get_kamino_lend_user_obligations",
33
+ "get_kamino_lend_user_rewards",
34
+ "kamino_lend_deposit",
35
+ "kamino_lend_withdraw",
36
+ "kamino_lend_borrow",
37
+ "kamino_lend_repay",
19
38
  "sign_wallet_message",
20
39
  "transfer_sol",
21
40
  "transfer_btc",
@@ -30,20 +49,60 @@ def _default_config_path() -> Path:
30
49
  return Path(os.path.expanduser("~/.openclaw/openclaw.json"))
31
50
 
32
51
 
52
+ def _resolve_openclaw_home() -> Path:
53
+ return Path(os.path.expanduser(os.getenv("OPENCLAW_HOME", "~/.openclaw")))
54
+
55
+
56
+ def _default_runtime_root() -> Path:
57
+ explicit_target = os.getenv("OPENCLAW_INSTALL_TARGET", "").strip()
58
+ if explicit_target:
59
+ return Path(explicit_target).expanduser()
60
+ explicit_root = os.getenv("OPENCLAW_INSTALL_ROOT", "").strip()
61
+ if explicit_root:
62
+ return Path(explicit_root).expanduser() / "current"
63
+ return _resolve_openclaw_home() / "agent-wallet-runtime" / "current"
64
+
65
+
33
66
  def _repo_root() -> Path:
34
67
  return Path(__file__).resolve().parents[2]
35
68
 
36
69
 
70
+ def _trusted_runtime_root() -> Path | None:
71
+ runtime_root = _default_runtime_root().resolve()
72
+ plugin_manifest = runtime_root / ".openclaw" / "extensions" / "agent-wallet" / "openclaw.plugin.json"
73
+ package_root = runtime_root / "agent-wallet"
74
+ if plugin_manifest.exists() and package_root.exists():
75
+ return runtime_root
76
+ return None
77
+
78
+
37
79
  def _default_extension_path() -> Path:
80
+ runtime_root = _trusted_runtime_root()
81
+ if runtime_root is not None:
82
+ return runtime_root / ".openclaw" / "extensions" / "agent-wallet"
38
83
  return _repo_root() / ".openclaw" / "extensions" / "agent-wallet"
39
84
 
40
85
 
41
86
  def _default_package_root() -> Path:
87
+ runtime_root = _trusted_runtime_root()
88
+ if runtime_root is not None:
89
+ return runtime_root / "agent-wallet"
42
90
  return Path(__file__).resolve().parents[1]
43
91
 
44
92
 
45
93
  def _default_python_bin() -> str:
46
- return os.getenv("OPENCLAW_AGENT_WALLET_PYTHON", sys.executable)
94
+ explicit = os.getenv("OPENCLAW_AGENT_WALLET_PYTHON", "").strip()
95
+ if explicit:
96
+ return explicit
97
+ runtime_root = _trusted_runtime_root()
98
+ if runtime_root is not None:
99
+ wrapper = runtime_root / "agent-wallet" / ".runtime-venv" / "bin" / "openclaw-agent-wallet-python"
100
+ if wrapper.exists():
101
+ return str(wrapper)
102
+ runtime_python = runtime_root / "agent-wallet" / ".runtime-venv" / "bin" / "python"
103
+ if runtime_python.exists():
104
+ return str(runtime_python)
105
+ return sys.executable
47
106
 
48
107
 
49
108
  def _default_user_id() -> str:
@@ -64,7 +123,7 @@ def build_parser() -> argparse.ArgumentParser:
64
123
  parser = argparse.ArgumentParser(description=__doc__)
65
124
  parser.add_argument("--config-path", default=str(_default_config_path()))
66
125
  parser.add_argument("--plugin-id", default="agent-wallet")
67
- parser.add_argument("--user-id", default=_default_user_id())
126
+ parser.add_argument("--user-id", default="")
68
127
  parser.add_argument("--backend", default="solana_local")
69
128
  parser.add_argument("--network", default="devnet")
70
129
  parser.add_argument("--rpc-url", default="")
@@ -164,8 +223,20 @@ def main() -> None:
164
223
 
165
224
  entries = plugins.setdefault("entries", {})
166
225
  effective_network = _normalize_network(args.backend, args.network)
226
+ existing_entry = entries.get(args.plugin_id) if isinstance(entries.get(args.plugin_id), dict) else {}
227
+ existing_config = (
228
+ dict(existing_entry.get("config"))
229
+ if isinstance(existing_entry.get("config"), dict)
230
+ else {}
231
+ )
232
+ resolved_user_id = (
233
+ args.user_id.strip()
234
+ or str(existing_config.get("userId") or "").strip()
235
+ or _default_user_id()
236
+ )
167
237
  plugin_config = {
168
- "userId": args.user_id,
238
+ **existing_config,
239
+ "userId": resolved_user_id,
169
240
  "backend": args.backend,
170
241
  "network": effective_network,
171
242
  "signOnly": args.sign_only,
@@ -223,7 +294,7 @@ def main() -> None:
223
294
  "python_bin": args.python_bin,
224
295
  "package_root": plugin_config["packageRoot"],
225
296
  "plugin_id": args.plugin_id,
226
- "user_id": args.user_id,
297
+ "user_id": resolved_user_id,
227
298
  "sealed_keys_path": sealed_keys_path,
228
299
  },
229
300
  indent=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):
@@ -192,6 +193,85 @@ def _user_id(args: dict[str, Any]) -> str:
192
193
  return str(value).strip() or "hermes-local-user"
193
194
 
194
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
+
195
275
  def _reject_secret_config(config: dict[str, Any]) -> None:
196
276
  present = sorted(key for key in SECRET_CONFIG_KEYS if str(config.get(key) or "").strip())
197
277
  if present:
@@ -202,16 +282,20 @@ def _reject_secret_config(config: dict[str, Any]) -> None:
202
282
  )
203
283
 
204
284
 
205
- 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]:
206
286
  raw = args.get("config") or {}
207
287
  if not isinstance(raw, dict):
208
288
  raise RuntimeError("config must be a JSON object when provided.")
209
289
  config = dict(raw)
210
290
  backend = args.get("backend") or os.getenv("AGENT_WALLET_BACKEND")
211
- 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)
212
293
  if backend:
213
- config["backend"] = str(backend).strip()
214
- 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:
215
299
  config["network"] = str(network).strip()
216
300
  _reject_secret_config(config)
217
301
  return config
@@ -235,15 +319,15 @@ def _cli_env(package_root: Path) -> dict[str, str]:
235
319
 
236
320
  def _call_wallet_cli(args: dict[str, Any]) -> dict[str, Any]:
237
321
  package_root = _resolve_package_root()
238
- config = _base_config(args)
239
322
  tool_name = str(args.get("tool_name") or "").strip()
240
323
  if not tool_name:
241
324
  raise RuntimeError("tool_name is required.")
325
+ config = _base_config(args, tool_name=tool_name)
242
326
 
243
327
  tool_args = args.get("arguments") or {}
244
328
  if not isinstance(tool_args, dict):
245
329
  raise RuntimeError("arguments must be a JSON object when provided.")
246
- 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":
247
331
  approval_token = str(tool_args.get("approval_token") or "").strip()
248
332
  cached_preview = _lookup_preview_for_token(approval_token)
249
333
  if cached_preview is not None and "_approved_preview" not in tool_args:
@@ -321,10 +405,10 @@ def _call_issue_approval(args: dict[str, Any]) -> dict[str, Any]:
321
405
  "user_confirmed=true is required after explicit user approval of the exact confirmation_summary."
322
406
  )
323
407
  package_root = _resolve_package_root()
324
- config = _base_config(args)
325
408
  tool_name = str(args.get("tool_name") or "").strip()
326
409
  if not tool_name:
327
410
  raise RuntimeError("tool_name is required.")
411
+ config = _base_config(args, tool_name=tool_name)
328
412
 
329
413
  summary = args.get("confirmation_summary")
330
414
  if not isinstance(summary, dict) or not summary:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayer.tech/wallet",
3
- "version": "0.1.12",
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
+ }