@agentlayer.tech/wallet 0.1.33 → 0.1.35

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 (37) hide show
  1. package/.openclaw/extensions/agent-wallet/dist/index.js +2 -59
  2. package/.openclaw/extensions/agent-wallet/index.ts +2 -59
  3. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -4
  4. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  5. package/CHANGELOG.md +63 -0
  6. package/README.md +2 -5
  7. package/RELEASING.md +56 -29
  8. package/VERSION +1 -0
  9. package/agent-wallet/.env.example +0 -1
  10. package/agent-wallet/README.md +0 -8
  11. package/agent-wallet/agent_wallet/__init__.py +5 -0
  12. package/agent-wallet/agent_wallet/config.py +0 -1
  13. package/agent-wallet/agent_wallet/openclaw_adapter.py +25 -324
  14. package/agent-wallet/agent_wallet/openclaw_cli.py +0 -5
  15. package/agent-wallet/agent_wallet/providers/bags.py +1 -58
  16. package/agent-wallet/agent_wallet/providers/jupiter.py +1 -64
  17. package/agent-wallet/agent_wallet/update_check.py +191 -0
  18. package/agent-wallet/agent_wallet/wallet_layer/base.py +0 -44
  19. package/agent-wallet/agent_wallet/wallet_layer/solana.py +0 -236
  20. package/agent-wallet/openclaw.plugin.json +1 -5
  21. package/agent-wallet/pyproject.toml +1 -1
  22. package/agent-wallet/skills/wallet-operator/SKILL.md +2 -5
  23. package/bin/openclaw-agent-wallet.mjs +88 -1
  24. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
  25. package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +5 -3
  26. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
  27. package/codex/plugins/agent-wallet/scripts/run_mcp.sh +3 -1
  28. package/codex/plugins/agent-wallet/server.py +164 -46
  29. package/codex/plugins/agent-wallet/skills/wallet-operator/SKILL.md +1 -0
  30. package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
  31. package/package.json +5 -1
  32. package/scripts/check_release_version.mjs +50 -20
  33. package/scripts/version_targets.mjs +60 -0
  34. package/wdk-btc-wallet/package.json +1 -1
  35. package/wdk-evm-wallet/README.md +3 -3
  36. package/wdk-evm-wallet/package.json +1 -1
  37. package/wdk-evm-wallet/src/wdk_evm_wallet.js +17 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
3
  "displayName": "Agent Wallet",
4
- "version": "0.1.0",
4
+ "version": "0.1.35",
5
5
  "description": "Claude Code bridge for the existing AgentLayer wallet runtime. Connects to Solana, Bitcoin, and EVM wallets without creating a new one.",
6
6
  "author": {
7
7
  "name": "AgentLayer"
@@ -10,13 +10,13 @@ PACKAGE_ROOT=${AGENT_WALLET_PACKAGE_ROOT:-${OPENCLAW_AGENT_WALLET_PACKAGE_ROOT:-
10
10
  # relative sibling paths below no longer resolve, so fall back to the codex
11
11
  # plugin copy inside the installed runtime package, which is always present.
12
12
  LOCAL_SERVER="$PLUGIN_ROOT/server.py"
13
- CODEX_SERVER="$PLUGIN_ROOT/../../codex/plugins/agent-wallet/server.py"
13
+ CODEX_SERVER="$PLUGIN_ROOT/../../../codex/plugins/agent-wallet/server.py"
14
14
  RUNTIME_CODEX_DIR="$OPENCLAW_HOME/agent-wallet-runtime/current/codex/plugins/agent-wallet"
15
15
 
16
16
  if [ -f "$LOCAL_SERVER" ]; then
17
17
  SERVER_PY="$LOCAL_SERVER"
18
18
  elif [ -f "$CODEX_SERVER" ]; then
19
- SERVER_PY=$(CDPATH= cd -- "$PLUGIN_ROOT/../../codex/plugins/agent-wallet" && pwd)/server.py
19
+ SERVER_PY=$(CDPATH= cd -- "$PLUGIN_ROOT/../../../codex/plugins/agent-wallet" && pwd)/server.py
20
20
  elif [ -f "$RUNTIME_CODEX_DIR/server.py" ]; then
21
21
  SERVER_PY=$(CDPATH= cd -- "$RUNTIME_CODEX_DIR" && pwd)/server.py
22
22
  else
@@ -37,7 +37,9 @@ else
37
37
  fi
38
38
 
39
39
  # Fail loudly (not -32000) if the resolved server cannot even be parsed.
40
- if ! "$PYTHON_BIN" -m py_compile "$SERVER_PY" 2>/dev/null; then
40
+ # Use ast.parse (no bytecode written) so a read-only install dir cannot trigger
41
+ # a false "runtime broken" error from py_compile failing to write __pycache__.
42
+ if ! "$PYTHON_BIN" -c 'import sys, ast; ast.parse(open(sys.argv[1], encoding="utf-8").read())' "$SERVER_PY" 2>/dev/null; then
41
43
  "$PYTHON_BIN" - "$SERVER_PY" >&2 <<'PY'
42
44
  import json, sys
43
45
  print(json.dumps({
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
- "version": "0.1.0",
3
+ "version": "0.1.35",
4
4
  "description": "Codex plugin bridge for the AgentLayer wallet runtime.",
5
5
  "author": {
6
6
  "name": "AgentLayer"
@@ -24,7 +24,9 @@ if [ ! -f "$PLUGIN_ROOT/server.py" ]; then
24
24
  fi
25
25
 
26
26
  # Fail loudly (not -32000) if the resolved server cannot even be parsed.
27
- if ! "$PYTHON_BIN" -m py_compile "$PLUGIN_ROOT/server.py" 2>/dev/null; then
27
+ # Use ast.parse (no bytecode written) so a read-only install dir cannot trigger
28
+ # a false "runtime broken" error from py_compile failing to write __pycache__.
29
+ if ! "$PYTHON_BIN" -c 'import sys, ast; ast.parse(open(sys.argv[1], encoding="utf-8").read())' "$PLUGIN_ROOT/server.py" 2>/dev/null; then
28
30
  "$PYTHON_BIN" - "$PLUGIN_ROOT/server.py" >&2 <<'PY'
29
31
  import json, sys
30
32
  print(json.dumps({
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  import copy
6
7
  import base64
7
8
  import hashlib
@@ -9,6 +10,7 @@ import json
9
10
  import os
10
11
  import subprocess
11
12
  import sys
13
+ import threading
12
14
  import time
13
15
  from functools import lru_cache
14
16
  from pathlib import Path
@@ -71,6 +73,9 @@ selected_solana_network: str | None = None
71
73
  selected_evm_network: str | None = None
72
74
  selected_btc_network: str | None = None
73
75
  approval_preview_cache: dict[str, dict[str, Any]] = {}
76
+ # Guards approval_preview_cache against races once wallet calls run concurrently
77
+ # via asyncio.to_thread. Reentrant so prune helpers can be nested under writers.
78
+ _approval_cache_lock = threading.RLock()
74
79
 
75
80
 
76
81
  class WalletCliError(RuntimeError):
@@ -94,6 +99,8 @@ def _openclaw_home() -> Path:
94
99
 
95
100
  @lru_cache(maxsize=1)
96
101
  def _openclaw_plugin_config() -> dict[str, Any]:
102
+ # Cached for the process lifetime: openclaw.json is read once per MCP server
103
+ # start. Edits to plugin config require restarting the bridge to take effect.
97
104
  config_path = _openclaw_home() / "openclaw.json"
98
105
  try:
99
106
  payload = json.loads(config_path.read_text(encoding="utf-8"))
@@ -212,9 +219,10 @@ def _approval_preview_tool_name(tool_name: str) -> str:
212
219
 
213
220
  def _prune_approval_preview_cache() -> None:
214
221
  now = time.time()
215
- for key in list(approval_preview_cache):
216
- if float(approval_preview_cache[key].get("expires_at") or 0) <= now:
217
- approval_preview_cache.pop(key, None)
222
+ with _approval_cache_lock:
223
+ for key in list(approval_preview_cache):
224
+ if float(approval_preview_cache[key].get("expires_at") or 0) <= now:
225
+ approval_preview_cache.pop(key, None)
218
226
 
219
227
 
220
228
  def _cache_preview_for_approval(user_id: str, tool_name: str, payload: dict[str, Any]) -> None:
@@ -231,18 +239,35 @@ def _cache_preview_for_approval(user_id: str, tool_name: str, payload: dict[str,
231
239
  summary = data.get("confirmation_summary")
232
240
  if not isinstance(summary, dict):
233
241
  return
234
- _prune_approval_preview_cache()
235
- approval_preview_cache[_approval_cache_key(user_id, cache_tool_name)] = {
236
- "digest": _preview_digest(data),
237
- "expires_at": time.time() + PREVIEW_CACHE_TTL_SECONDS,
238
- "preview": data,
239
- "summary": summary,
240
- }
242
+ with _approval_cache_lock:
243
+ _prune_approval_preview_cache()
244
+ approval_preview_cache[_approval_cache_key(user_id, cache_tool_name)] = {
245
+ "digest": _preview_digest(data),
246
+ "expires_at": time.time() + PREVIEW_CACHE_TTL_SECONDS,
247
+ "preview": data,
248
+ "summary": summary,
249
+ }
241
250
 
242
251
 
243
252
  def _latest_cached_preview(user_id: str, tool_name: str) -> dict[str, Any] | None:
244
- _prune_approval_preview_cache()
245
- return approval_preview_cache.get(_approval_cache_key(user_id, _approval_preview_tool_name(tool_name)))
253
+ with _approval_cache_lock:
254
+ _prune_approval_preview_cache()
255
+ return approval_preview_cache.get(_approval_cache_key(user_id, _approval_preview_tool_name(tool_name)))
256
+
257
+
258
+ def _consume_cached_preview(user_id: str, tool_name: str) -> None:
259
+ """Drop the cached preview once a successful execute has consumed it.
260
+
261
+ Without this, a preview lingers for the full TTL and a duplicate execute
262
+ call could re-run the operation from stale approval context — the runtime's
263
+ single-use nonce registry cannot stop it, because the bridge runs each
264
+ invoke in a fresh subprocess (empty registry) and mints a new token per
265
+ execute. Requiring a fresh preview before the next execute is the safe rule.
266
+ """
267
+ with _approval_cache_lock:
268
+ approval_preview_cache.pop(
269
+ _approval_cache_key(user_id, _approval_preview_tool_name(tool_name)), None
270
+ )
246
271
 
247
272
 
248
273
  def _approval_token_preview_digest(token: str) -> str:
@@ -509,28 +534,55 @@ def _parse_cli_error(text: str) -> WalletCliError:
509
534
  )
510
535
 
511
536
 
537
+ def _cli_timeout_seconds() -> float:
538
+ """Parse the CLI timeout from env, falling back to 180s on bad values."""
539
+ raw = os.getenv("AGENT_WALLET_CODEX_TIMEOUT", "180")
540
+ try:
541
+ value = float(raw)
542
+ except (TypeError, ValueError):
543
+ return 180.0
544
+ return value if value > 0 else 180.0
545
+
546
+
512
547
  def _call_wallet_cli(command: str, extra_args: list[str]) -> dict[str, Any]:
513
548
  package_root = _resolve_package_root()
514
- completed = subprocess.run(
515
- [
516
- _python_bin(package_root),
517
- "-m",
518
- "agent_wallet.openclaw_cli",
519
- command,
520
- *extra_args,
521
- ],
522
- cwd=str(package_root),
523
- env=_cli_env(package_root),
524
- text=True,
525
- capture_output=True,
526
- timeout=float(os.getenv("AGENT_WALLET_CODEX_TIMEOUT", "180")),
527
- check=False,
528
- )
549
+ timeout_seconds = _cli_timeout_seconds()
550
+ try:
551
+ completed = subprocess.run(
552
+ [
553
+ _python_bin(package_root),
554
+ "-m",
555
+ "agent_wallet.openclaw_cli",
556
+ command,
557
+ *extra_args,
558
+ ],
559
+ cwd=str(package_root),
560
+ env=_cli_env(package_root),
561
+ text=True,
562
+ capture_output=True,
563
+ timeout=timeout_seconds,
564
+ check=False,
565
+ )
566
+ except subprocess.TimeoutExpired as exc:
567
+ # Surface a clean, actionable message instead of the raw TimeoutExpired
568
+ # repr, which would echo the full argv (approval_token, config JSON).
569
+ raise WalletCliError(
570
+ f"agent-wallet CLI '{command}' timed out after {timeout_seconds:g}s. "
571
+ "Retry, or raise AGENT_WALLET_CODEX_TIMEOUT if the network is slow.",
572
+ code="timeout",
573
+ ) from exc
529
574
  if completed.returncode != 0:
530
575
  detail = completed.stderr.strip() or completed.stdout.strip()
531
576
  raise _parse_cli_error(detail)
577
+ # The CLI contract is a single JSON line on stdout. Parse the last non-empty
578
+ # line so a stray print/warning ahead of it from the runtime cannot break an
579
+ # otherwise-successful call.
580
+ stdout = completed.stdout.strip()
581
+ if not stdout:
582
+ return {}
583
+ last_line = stdout.splitlines()[-1].strip()
532
584
  try:
533
- return json.loads(completed.stdout.strip() or "{}")
585
+ return json.loads(last_line)
534
586
  except json.JSONDecodeError as exc:
535
587
  raise WalletCliError(f"agent-wallet CLI returned invalid JSON: {exc}") from exc
536
588
 
@@ -824,26 +876,38 @@ async def _handle_set_wallet_backend(params: dict[str, Any]) -> dict[str, Any]:
824
876
 
825
877
  requested = params.get("backend", params.get("wallet"))
826
878
  backend = _normalize_wallet_backend(requested)
879
+ # Resolve the target network into a local first; only commit to the session
880
+ # globals after the validating wallet call succeeds, so a failed switch never
881
+ # leaves a stale backend paired with a freshly mutated network selection.
827
882
  if backend == "wdk_evm_local":
828
883
  implied = params.get("network") or selected_evm_network or _default_evm_network() or "ethereum"
829
- selected_evm_network = _normalize_selectable_evm_network(implied)
884
+ resolved_network = _normalize_selectable_evm_network(implied)
830
885
  elif backend == "wdk_btc_local":
831
- selected_btc_network = _normalize_btc_network(
886
+ resolved_network = _normalize_btc_network(
832
887
  params.get("network") or selected_btc_network or _default_btc_network()
833
888
  )
834
889
  else:
835
- selected_solana_network = _normalize_solana_network(
890
+ resolved_network = _normalize_solana_network(
836
891
  params.get("network") or selected_solana_network or _default_solana_network()
837
892
  )
838
893
 
839
- config = _effective_config_for_backend(backend)
840
- payload = _invoke_tool(
894
+ config = _host_default_config()
895
+ config["backend"] = backend
896
+ config["network"] = resolved_network
897
+ payload = await asyncio.to_thread(
898
+ _invoke_tool,
841
899
  "get_evm_network" if backend == "wdk_evm_local" else "get_wallet_capabilities",
842
900
  {} if backend != "wdk_evm_local" else {"network": config["network"]},
843
901
  config,
844
902
  )
845
903
  if payload.get("ok") is False:
846
904
  raise RuntimeError(str(payload.get("error") or "set_wallet_backend failed"))
905
+ if backend == "wdk_evm_local":
906
+ selected_evm_network = resolved_network
907
+ elif backend == "wdk_btc_local":
908
+ selected_btc_network = resolved_network
909
+ else:
910
+ selected_solana_network = resolved_network
847
911
  selected_wallet_backend = backend
848
912
  return {
849
913
  "selected_backend": backend,
@@ -866,7 +930,7 @@ async def _handle_set_evm_network(params: dict[str, Any]) -> dict[str, Any]:
866
930
  network = _normalize_selectable_evm_network(params.get("network"))
867
931
  config = _effective_config_for_backend("wdk_evm_local")
868
932
  config["network"] = network
869
- payload = _invoke_tool("get_evm_network", {"network": network}, config)
933
+ payload = await asyncio.to_thread(_invoke_tool, "get_evm_network", {"network": network}, config)
870
934
  if payload.get("ok") is False:
871
935
  raise RuntimeError(str(payload.get("error") or "set_evm_network failed"))
872
936
  selected_wallet_backend = "wdk_evm_local"
@@ -885,6 +949,30 @@ async def _handle_set_evm_network(params: dict[str, Any]) -> dict[str, Any]:
885
949
  }
886
950
 
887
951
 
952
+ def _invoke_wallet_tool_blocking(
953
+ tool_name: str,
954
+ config: dict[str, Any],
955
+ effective_params: dict[str, Any],
956
+ ) -> dict[str, Any]:
957
+ """Synchronous wallet invocation: approval attach + CLI subprocess + cache.
958
+
959
+ Runs off the event loop via ``asyncio.to_thread`` so a slow or hung wallet
960
+ call never freezes the MCP server (tools/list, read-only calls, and
961
+ cancellation stay responsive).
962
+ """
963
+ used_cache = _attach_approval_for_execute(tool_name, config, effective_params)
964
+ try:
965
+ payload = _invoke_tool(tool_name, effective_params, config)
966
+ except Exception as exc:
967
+ raise _normalize_approval_context_error(exc) from exc
968
+ _cache_preview_for_approval(_user_id(), tool_name, payload)
969
+ # A bridge-managed preview that was just executed successfully is single-use:
970
+ # drop it so a duplicate execute cannot silently re-run the operation.
971
+ if used_cache is not None and payload.get("ok") is not False:
972
+ _consume_cached_preview(_user_id(), tool_name)
973
+ return payload
974
+
975
+
888
976
  async def _handle_wallet_tool(tool_name: str, params: dict[str, Any]) -> dict[str, Any]:
889
977
  config = _base_config(params, tool_name=tool_name)
890
978
  backend = _normalize_wallet_backend(config.get("backend"))
@@ -893,30 +981,60 @@ async def _handle_wallet_tool(tool_name: str, params: dict[str, Any]) -> dict[st
893
981
  config["network"] = selected_evm_network
894
982
 
895
983
  effective_params = dict(params)
896
- _attach_approval_for_execute(tool_name, config, effective_params)
897
-
898
- try:
899
- payload = _invoke_tool(tool_name, effective_params, config)
900
- except Exception as exc:
901
- raise _normalize_approval_context_error(exc) from exc
984
+ payload = await asyncio.to_thread(
985
+ _invoke_wallet_tool_blocking, tool_name, config, effective_params
986
+ )
902
987
 
903
- _cache_preview_for_approval(_user_id(), tool_name, payload)
904
988
  if payload.get("ok") is False:
905
989
  raise RuntimeError(str(payload.get("error") or f"{tool_name} failed"))
906
990
  return payload.get("data", {})
907
991
 
908
992
 
993
+ BASE_INSTRUCTIONS = (
994
+ "Use the local AgentLayer wallet runtime through explicit wallet tools. Keep wallet "
995
+ "secrets local. Preview writes first when supported, and execute only after explicit "
996
+ "user confirmation."
997
+ )
998
+
999
+
1000
+ def _update_notice_instructions(base: str) -> str:
1001
+ """Append a one-time update notice to ``base`` when a newer version exists.
1002
+
1003
+ Fully fail-open: any error (package not importable, malformed cache, etc.)
1004
+ returns ``base`` unchanged so the server always starts. The network refresh
1005
+ runs in a background daemon thread and only affects the *next* start; the
1006
+ notice itself is decided synchronously from the cache.
1007
+ """
1008
+ try:
1009
+ package_root_text = str(_resolve_package_root())
1010
+ inserted = package_root_text not in sys.path
1011
+ if inserted:
1012
+ sys.path.insert(0, package_root_text)
1013
+ try:
1014
+ from agent_wallet import update_check
1015
+
1016
+ update_check.maybe_refresh_in_background()
1017
+ notice = update_check.pending_notice(mark_shown=True)
1018
+ finally:
1019
+ if inserted:
1020
+ try:
1021
+ sys.path.remove(package_root_text)
1022
+ except ValueError:
1023
+ pass
1024
+ if notice:
1025
+ return f"{base}\n\n⚠️ UPDATE AVAILABLE: {notice}"
1026
+ except Exception:
1027
+ pass
1028
+ return base
1029
+
1030
+
909
1031
  def build_server():
910
1032
  from fastmcp import FastMCP
911
1033
  from fastmcp.tools import FunctionTool
912
1034
 
913
1035
  mcp = FastMCP(
914
1036
  "Agent Wallet",
915
- instructions=(
916
- "Use the local AgentLayer wallet runtime through explicit wallet tools. Keep wallet "
917
- "secrets local. Preview writes first when supported, and execute only after explicit "
918
- "user confirmation."
919
- ),
1037
+ instructions=_update_notice_instructions(BASE_INSTRUCTIONS),
920
1038
  )
921
1039
 
922
1040
  async def _dispatch(tool_name: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
@@ -16,3 +16,4 @@ Rules:
16
16
  - On mainnet, restate the network, asset, amount, and destination before execute.
17
17
  - Do not ask the user for `approval_token`. The bridge manages approval binding internally.
18
18
  - If approval context is missing or stale, repeat preview instead of improvising.
19
+ - Use `set_wallet_backend` to switch between Solana, EVM, and Bitcoin wallets within a session, and `set_evm_network` to pick ethereum or base.
@@ -1,5 +1,5 @@
1
1
  name: agent-wallet
2
- version: 0.1.0
2
+ version: 0.1.35
3
3
  description: Thin Hermes Agent bridge to the existing AgentLayer/OpenClaw wallet backend
4
4
  provides_tools:
5
5
  - agent_wallet_tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayer.tech/wallet",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -19,12 +19,16 @@
19
19
  "build:openclaw-plugins": "node scripts/manage_openclaw_plugin_packages.mjs build",
20
20
  "check:openclaw-plugins": "node scripts/manage_openclaw_plugin_packages.mjs check",
21
21
  "check:release-version": "node scripts/check_release_version.mjs",
22
+ "version:sync": "node scripts/sync_version.mjs",
23
+ "release:local": "node scripts/release_local.mjs",
22
24
  "test:npm-installer": "python3 agent-wallet/tests/smoke_npm_installer.py",
23
25
  "pack:dry-run": "npm pack --dry-run"
24
26
  },
25
27
  "files": [
26
28
  "bin/",
29
+ "VERSION",
27
30
  "scripts/check_release_version.mjs",
31
+ "scripts/version_targets.mjs",
28
32
  "setup.sh",
29
33
  "install-from-github.sh",
30
34
  "README.md",
@@ -1,26 +1,59 @@
1
1
  #!/usr/bin/env node
2
+ // Verify the project version is consistent across every framework manifest and,
3
+ // when run on a release tag, that the tag matches the canonical VERSION.
4
+ //
5
+ // Canonical source: the root VERSION file. All derived manifests (see
6
+ // scripts/version_targets.mjs) must equal it — stamp them with
7
+ // scripts/sync_version.mjs if this fails. Emits the resolved version and npm
8
+ // dist-tag for the publish workflow.
2
9
 
3
10
  import fs from "node:fs";
4
11
 
5
- const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"));
6
- const pyproject = fs.readFileSync("agent-wallet/pyproject.toml", "utf8");
7
- const pyVersion = pyproject.match(/^version\s*=\s*"([^"]+)"/m)?.[1] || "";
8
- const packageVersion = packageJson.version;
9
- const refName = process.env.GITHUB_REF_NAME || process.argv[2] || "";
10
- const expectedFromTag = refName.startsWith("v") ? refName.slice(1) : "";
12
+ import {
13
+ TARGETS,
14
+ VERSION_FILE,
15
+ npmTagFor,
16
+ readCanonicalVersion,
17
+ readTargetVersion,
18
+ } from "./version_targets.mjs";
19
+
20
+ const root = process.cwd();
11
21
  const errors = [];
12
22
 
13
- if (!packageVersion) {
14
- errors.push("package.json version is missing");
23
+ let canonical = "";
24
+ try {
25
+ canonical = readCanonicalVersion(root);
26
+ } catch {
27
+ errors.push(`${VERSION_FILE} is missing`);
15
28
  }
16
- if (!pyVersion) {
17
- errors.push("agent-wallet/pyproject.toml version is missing");
29
+ if (canonical === "") {
30
+ errors.push(`${VERSION_FILE} is empty`);
18
31
  }
19
- if (packageVersion && pyVersion && packageVersion !== pyVersion) {
20
- errors.push(`version mismatch: package.json=${packageVersion}, pyproject=${pyVersion}`);
32
+
33
+ if (canonical) {
34
+ for (const target of TARGETS) {
35
+ let actual = null;
36
+ try {
37
+ actual = readTargetVersion(root, target);
38
+ } catch {
39
+ errors.push(`${target.file} is missing`);
40
+ continue;
41
+ }
42
+ if (!actual) {
43
+ errors.push(`${target.file}: version field not found`);
44
+ } else if (actual !== canonical) {
45
+ errors.push(
46
+ `version mismatch: ${target.file}=${actual}, ${VERSION_FILE}=${canonical} ` +
47
+ `(run: npm run version:sync)`,
48
+ );
49
+ }
50
+ }
21
51
  }
22
- if (expectedFromTag && packageVersion !== expectedFromTag) {
23
- errors.push(`tag/version mismatch: tag=${refName}, package.json=${packageVersion}`);
52
+
53
+ const refName = process.env.GITHUB_REF_NAME || process.argv[2] || "";
54
+ const expectedFromTag = refName.startsWith("v") ? refName.slice(1) : "";
55
+ if (expectedFromTag && canonical && canonical !== expectedFromTag) {
56
+ errors.push(`tag/version mismatch: tag=${refName}, ${VERSION_FILE}=${canonical}`);
24
57
  }
25
58
 
26
59
  if (errors.length > 0) {
@@ -30,13 +63,10 @@ if (errors.length > 0) {
30
63
  process.exit(1);
31
64
  }
32
65
 
33
- const npmTag = packageVersion.includes("-") ? "beta" : "latest";
34
- console.log(`release_version=${packageVersion}`);
66
+ const npmTag = npmTagFor(canonical);
67
+ console.log(`release_version=${canonical}`);
35
68
  console.log(`npm_tag=${npmTag}`);
36
69
 
37
70
  if (process.env.GITHUB_OUTPUT) {
38
- fs.appendFileSync(
39
- process.env.GITHUB_OUTPUT,
40
- `release_version=${packageVersion}\nnpm_tag=${npmTag}\n`,
41
- );
71
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `release_version=${canonical}\nnpm_tag=${npmTag}\n`);
42
72
  }
@@ -0,0 +1,60 @@
1
+ // Single source of truth for the project version.
2
+ //
3
+ // The root VERSION file is canonical. Every other manifest below is a *derived*
4
+ // target that must carry the exact same version. These targets are stamped by
5
+ // scripts/sync_version.mjs and verified by scripts/check_release_version.mjs, so
6
+ // they should never be edited by hand.
7
+
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+
11
+ export const VERSION_FILE = "VERSION";
12
+
13
+ // kind -> regex capturing (prefix)(version)(suffix). Non-global on purpose: we
14
+ // only ever replace the first (top-level) occurrence in each file.
15
+ const PATTERNS = {
16
+ json: /("version"\s*:\s*")([^"]*)(")/,
17
+ toml: /^(version\s*=\s*")([^"]*)(")/m,
18
+ pyinit: /^(__version__\s*=\s*")([^"]*)(")/m,
19
+ yaml: /^(version:\s*)(\S+)(.*)$/m,
20
+ };
21
+
22
+ // Every place the version lives, across all agent frameworks and packages.
23
+ export const TARGETS = [
24
+ { file: "package.json", kind: "json" },
25
+ { file: "agent-wallet/pyproject.toml", kind: "toml" },
26
+ { file: "agent-wallet/agent_wallet/__init__.py", kind: "pyinit" },
27
+ { file: ".openclaw/extensions/agent-wallet/package.json", kind: "json" },
28
+ { file: "agent-wallet/openclaw.plugin.json", kind: "json" },
29
+ { file: ".openclaw/extensions/agent-wallet/openclaw.plugin.json", kind: "json" },
30
+ { file: "codex/plugins/agent-wallet/.codex-plugin/plugin.json", kind: "json" },
31
+ { file: "claude-code/plugins/agent-wallet/.claude-plugin/plugin.json", kind: "json" },
32
+ { file: "hermes/plugins/agent_wallet/plugin.yaml", kind: "yaml" },
33
+ { file: "wdk-btc-wallet/package.json", kind: "json" },
34
+ { file: "wdk-evm-wallet/package.json", kind: "json" },
35
+ ];
36
+
37
+ export function readCanonicalVersion(root) {
38
+ return fs.readFileSync(path.join(root, VERSION_FILE), "utf8").trim();
39
+ }
40
+
41
+ export function readTargetVersion(root, target) {
42
+ const content = fs.readFileSync(path.join(root, target.file), "utf8");
43
+ const match = content.match(PATTERNS[target.kind]);
44
+ return match ? match[2] : null;
45
+ }
46
+
47
+ export function stampTarget(root, target, version) {
48
+ const filePath = path.join(root, target.file);
49
+ const content = fs.readFileSync(filePath, "utf8");
50
+ const pattern = PATTERNS[target.kind];
51
+ if (!pattern.test(content)) {
52
+ throw new Error(`No version field found in ${target.file}`);
53
+ }
54
+ const updated = content.replace(pattern, (_m, prefix, _old, suffix) => `${prefix}${version}${suffix}`);
55
+ fs.writeFileSync(filePath, updated);
56
+ }
57
+
58
+ export function npmTagFor(version) {
59
+ return version.includes("-") ? "beta" : "latest";
60
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdk-btc-wallet",
3
- "version": "0.1.0",
3
+ "version": "0.1.35",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Separate BTC-only wallet service built on Tether WDK.",
@@ -18,8 +18,8 @@ Current scope:
18
18
  - fetch ERC-20 balances
19
19
  - fetch ERC-20 token metadata (`name`, `symbol`, `decimals`)
20
20
  - fetch fee-rate suggestions
21
- - fetch read-only Velora swap quotes for supported mainnet ERC-20 pairs
22
- - execute Velora ERC-20 swaps on supported mainnet networks through the local wallet account
21
+ - fetch read-only Velora swap quotes for supported mainnet ERC-20 and native ETH pairs
22
+ - execute Velora ERC-20 and native ETH swaps on supported mainnet networks through the local wallet account
23
23
  - fetch Aave V3 account data on supported mainnet networks
24
24
  - fetch Aave V3 reserve catalog on supported mainnet networks
25
25
  - fetch Aave V3 per-reserve user positions on supported mainnet networks
@@ -177,7 +177,7 @@ Local security note:
177
177
  - unlocked seed phrases live only in memory
178
178
  - explicit `lock` or process restart clears the in-memory unlocked state
179
179
  - seed reveal is password-gated and separate from normal agent operations
180
- - Velora swap support is currently limited to `ethereum` and `base` ERC-20 pairs
180
+ - Velora swap support is currently limited to `ethereum` and `base` ERC-20 and native ETH pairs
181
181
  - the underlying WDK Velora package is still beta; test swap execution carefully before relying on it
182
182
  - Aave V3 support is currently limited to `ethereum` and `base`
183
183
  - Aave `supply` and `repay` may perform pool-scoped ERC-20 approvals; if a send fails after approval, the service attempts to restore the original allowance
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdk-evm-wallet",
3
- "version": "0.1.0",
3
+ "version": "0.1.35",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Separate EVM wallet service built on Tether WDK.",
@@ -246,6 +246,21 @@ function normalizeEvmTokenAddressAllowingNative(value, fieldName) {
246
246
  return normalizeAddress(address, fieldName).toLowerCase();
247
247
  }
248
248
 
249
+ function normalizeVeloraTokenAddress(value, fieldName) {
250
+ const raw = assertNonEmptyString(value, fieldName);
251
+ const alias = raw.toLowerCase();
252
+ if (
253
+ alias === "native" ||
254
+ alias === "eth" ||
255
+ alias === "ethereum" ||
256
+ isZeroAddress(raw) ||
257
+ isVeloraNativeTokenAddress(raw)
258
+ ) {
259
+ return VELORA_NATIVE_TOKEN_ADDRESS;
260
+ }
261
+ return normalizeAddress(raw, fieldName);
262
+ }
263
+
249
264
  function normalizeLifiOutputTokenAddress(value, destinationChainId, fieldName) {
250
265
  const raw = assertNonEmptyString(value, fieldName);
251
266
  const alias = raw.toLowerCase();
@@ -380,8 +395,8 @@ function normalizeX402ExactTypedData({ domain, types, primaryType, message }, ru
380
395
 
381
396
  function buildSwapRequest({ tokenIn, tokenOut, tokenInAmount }) {
382
397
  const swapRequest = {
383
- tokenIn: normalizeAddress(tokenIn, "tokenIn"),
384
- tokenOut: normalizeAddress(tokenOut, "tokenOut"),
398
+ tokenIn: normalizeVeloraTokenAddress(tokenIn, "tokenIn"),
399
+ tokenOut: normalizeVeloraTokenAddress(tokenOut, "tokenOut"),
385
400
  tokenInAmount: assertPositiveBigIntString(tokenInAmount, "tokenInAmount"),
386
401
  };
387
402
  assertDistinctAddresses(swapRequest.tokenIn, "tokenIn", swapRequest.tokenOut, "tokenOut");