@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.
- package/.openclaw/extensions/agent-wallet/dist/index.js +2 -59
- package/.openclaw/extensions/agent-wallet/index.ts +2 -59
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -4
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +63 -0
- package/README.md +2 -5
- package/RELEASING.md +56 -29
- package/VERSION +1 -0
- package/agent-wallet/.env.example +0 -1
- package/agent-wallet/README.md +0 -8
- package/agent-wallet/agent_wallet/__init__.py +5 -0
- package/agent-wallet/agent_wallet/config.py +0 -1
- package/agent-wallet/agent_wallet/openclaw_adapter.py +25 -324
- package/agent-wallet/agent_wallet/openclaw_cli.py +0 -5
- package/agent-wallet/agent_wallet/providers/bags.py +1 -58
- package/agent-wallet/agent_wallet/providers/jupiter.py +1 -64
- package/agent-wallet/agent_wallet/update_check.py +191 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +0 -44
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +0 -236
- package/agent-wallet/openclaw.plugin.json +1 -5
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/skills/wallet-operator/SKILL.md +2 -5
- package/bin/openclaw-agent-wallet.mjs +88 -1
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +5 -3
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/scripts/run_mcp.sh +3 -1
- package/codex/plugins/agent-wallet/server.py +164 -46
- package/codex/plugins/agent-wallet/skills/wallet-operator/SKILL.md +1 -0
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/package.json +5 -1
- package/scripts/check_release_version.mjs +50 -20
- package/scripts/version_targets.mjs +60 -0
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/README.md +3 -3
- package/wdk-evm-wallet/package.json +1 -1
- 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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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({
|
|
@@ -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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
approval_preview_cache.
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
245
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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(
|
|
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
|
-
|
|
884
|
+
resolved_network = _normalize_selectable_evm_network(implied)
|
|
830
885
|
elif backend == "wdk_btc_local":
|
|
831
|
-
|
|
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
|
-
|
|
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 =
|
|
840
|
-
|
|
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
|
|
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
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentlayer.tech/wallet",
|
|
3
|
-
"version": "0.1.
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
|
|
23
|
+
let canonical = "";
|
|
24
|
+
try {
|
|
25
|
+
canonical = readCanonicalVersion(root);
|
|
26
|
+
} catch {
|
|
27
|
+
errors.push(`${VERSION_FILE} is missing`);
|
|
15
28
|
}
|
|
16
|
-
if (
|
|
17
|
-
errors.push(
|
|
29
|
+
if (canonical === "") {
|
|
30
|
+
errors.push(`${VERSION_FILE} is empty`);
|
|
18
31
|
}
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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 =
|
|
34
|
-
console.log(`release_version=${
|
|
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
|
+
}
|
package/wdk-evm-wallet/README.md
CHANGED
|
@@ -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
|
|
@@ -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:
|
|
384
|
-
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");
|