@agentlayer.tech/wallet 0.1.103 → 0.1.106
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/openclaw.plugin.json +1 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/VERSION +1 -1
- package/agent-wallet/README.md +7 -2
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/config.py +15 -1
- package/agent-wallet/agent_wallet/evm_user_wallets.py +101 -244
- package/agent-wallet/agent_wallet/openclaw_runtime.py +7 -2
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +40 -9
- package/agent-wallet/agent_wallet/wallet_layer/factory.py +25 -2
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/bootstrap_openclaw_evm.py +24 -14
- package/agent-wallet/scripts/install_agent_wallet.py +18 -6
- package/agent-wallet/scripts/manage_openclaw_evm_wallet.py +18 -15
- package/agent-wallet/scripts/setup_evm_wallet.sh +2 -3
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/server.py +74 -4
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/package.json +1 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/README.md +6 -2
- package/wdk-evm-wallet/package.json +3 -2
- package/wdk-evm-wallet/src/config.js +9 -0
- package/wdk-evm-wallet/src/server.js +768 -692
|
@@ -65,11 +65,36 @@ def _normalize_base_url(value: str) -> str:
|
|
|
65
65
|
if not text:
|
|
66
66
|
raise WalletBackendError("WDK EVM service URL is not configured.")
|
|
67
67
|
parsed = urlparse(text)
|
|
68
|
+
if parsed.scheme == "unix":
|
|
69
|
+
if not parsed.path:
|
|
70
|
+
raise WalletBackendError("WDK EVM unix socket URL must include a path.")
|
|
71
|
+
return text
|
|
68
72
|
if parsed.scheme not in {"http", "https"} or parsed.hostname not in LOCAL_WDK_EVM_HOSTS:
|
|
69
73
|
raise WalletBackendError("WDK EVM service URL must point to a localhost HTTP endpoint.")
|
|
70
74
|
return text.rstrip("/")
|
|
71
75
|
|
|
72
76
|
|
|
77
|
+
def _httpx_target(base_url: str, path: str) -> tuple[str, dict[str, Any]]:
|
|
78
|
+
"""Return (url, extra_httpx_client_kwargs) for either transport."""
|
|
79
|
+
parsed = urlparse(base_url)
|
|
80
|
+
if parsed.scheme == "unix":
|
|
81
|
+
return (
|
|
82
|
+
f"http://wdk-evm-wallet.local{path}",
|
|
83
|
+
{"transport": httpx.HTTPTransport(uds=parsed.path)},
|
|
84
|
+
)
|
|
85
|
+
return (f"{base_url}{path}", {})
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _async_httpx_target(base_url: str, path: str) -> tuple[str, dict[str, Any]]:
|
|
89
|
+
parsed = urlparse(base_url)
|
|
90
|
+
if parsed.scheme == "unix":
|
|
91
|
+
return (
|
|
92
|
+
f"http://wdk-evm-wallet.local{path}",
|
|
93
|
+
{"transport": httpx.AsyncHTTPTransport(uds=parsed.path)},
|
|
94
|
+
)
|
|
95
|
+
return (f"{base_url}{path}", {})
|
|
96
|
+
|
|
97
|
+
|
|
73
98
|
def _resolve_local_token_path() -> Path:
|
|
74
99
|
configured = os.getenv("WDK_EVM_LOCAL_TOKEN_PATH", "").strip()
|
|
75
100
|
if configured:
|
|
@@ -195,16 +220,16 @@ class WdkEvmLocalClient:
|
|
|
195
220
|
return {**payload, "password": password}
|
|
196
221
|
|
|
197
222
|
async def post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
223
|
+
url, transport_kwargs = _async_httpx_target(self.base_url, path)
|
|
198
224
|
try:
|
|
199
225
|
async with httpx.AsyncClient(
|
|
200
226
|
timeout=_timeout_for_path(path),
|
|
201
227
|
headers=self._headers,
|
|
202
228
|
follow_redirects=False,
|
|
203
229
|
trust_env=False,
|
|
230
|
+
**transport_kwargs,
|
|
204
231
|
) as client:
|
|
205
|
-
response = await client.post(
|
|
206
|
-
f"{self.base_url}{path}", json=self._with_credentials(payload)
|
|
207
|
-
)
|
|
232
|
+
response = await client.post(url, json=self._with_credentials(payload))
|
|
208
233
|
except httpx.TimeoutException as exc:
|
|
209
234
|
raise WalletBackendError(
|
|
210
235
|
"wdk-evm-wallet request timed out.",
|
|
@@ -220,14 +245,16 @@ class WdkEvmLocalClient:
|
|
|
220
245
|
return _unwrap_payload(response)
|
|
221
246
|
|
|
222
247
|
async def get(self, path: str) -> dict[str, Any]:
|
|
248
|
+
url, transport_kwargs = _async_httpx_target(self.base_url, path)
|
|
223
249
|
try:
|
|
224
250
|
async with httpx.AsyncClient(
|
|
225
251
|
timeout=_timeout_for_path(path),
|
|
226
252
|
headers=self._headers,
|
|
227
253
|
follow_redirects=False,
|
|
228
254
|
trust_env=False,
|
|
255
|
+
**transport_kwargs,
|
|
229
256
|
) as client:
|
|
230
|
-
response = await client.get(
|
|
257
|
+
response = await client.get(url)
|
|
231
258
|
except httpx.TimeoutException as exc:
|
|
232
259
|
raise WalletBackendError(
|
|
233
260
|
"wdk-evm-wallet request timed out.",
|
|
@@ -243,16 +270,16 @@ class WdkEvmLocalClient:
|
|
|
243
270
|
return _unwrap_payload(response)
|
|
244
271
|
|
|
245
272
|
def post_sync(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
273
|
+
url, transport_kwargs = _httpx_target(self.base_url, path)
|
|
246
274
|
try:
|
|
247
275
|
with httpx.Client(
|
|
248
276
|
timeout=_timeout_for_path(path),
|
|
249
277
|
headers=self._headers,
|
|
250
278
|
follow_redirects=False,
|
|
251
279
|
trust_env=False,
|
|
280
|
+
**transport_kwargs,
|
|
252
281
|
) as client:
|
|
253
|
-
response = client.post(
|
|
254
|
-
f"{self.base_url}{path}", json=self._with_credentials(payload)
|
|
255
|
-
)
|
|
282
|
+
response = client.post(url, json=self._with_credentials(payload))
|
|
256
283
|
except httpx.TimeoutException as exc:
|
|
257
284
|
raise WalletBackendError(
|
|
258
285
|
"wdk-evm-wallet request timed out.",
|
|
@@ -268,14 +295,16 @@ class WdkEvmLocalClient:
|
|
|
268
295
|
return _unwrap_payload(response)
|
|
269
296
|
|
|
270
297
|
def get_sync(self, path: str) -> dict[str, Any]:
|
|
298
|
+
url, transport_kwargs = _httpx_target(self.base_url, path)
|
|
271
299
|
try:
|
|
272
300
|
with httpx.Client(
|
|
273
301
|
timeout=_timeout_for_path(path),
|
|
274
302
|
headers=self._headers,
|
|
275
303
|
follow_redirects=False,
|
|
276
304
|
trust_env=False,
|
|
305
|
+
**transport_kwargs,
|
|
277
306
|
) as client:
|
|
278
|
-
response = client.get(
|
|
307
|
+
response = client.get(url)
|
|
279
308
|
except httpx.TimeoutException as exc:
|
|
280
309
|
raise WalletBackendError(
|
|
281
310
|
"wdk-evm-wallet request timed out.",
|
|
@@ -291,14 +320,16 @@ class WdkEvmLocalClient:
|
|
|
291
320
|
return _unwrap_payload(response)
|
|
292
321
|
|
|
293
322
|
def list_wallets_sync(self) -> list[dict[str, Any]]:
|
|
323
|
+
url, transport_kwargs = _httpx_target(self.base_url, "/v1/evm/wallets")
|
|
294
324
|
try:
|
|
295
325
|
with httpx.Client(
|
|
296
326
|
timeout=_timeout_for_path("/v1/evm/wallets"),
|
|
297
327
|
headers=self._headers,
|
|
298
328
|
follow_redirects=False,
|
|
299
329
|
trust_env=False,
|
|
330
|
+
**transport_kwargs,
|
|
300
331
|
) as client:
|
|
301
|
-
response = client.get(
|
|
332
|
+
response = client.get(url)
|
|
302
333
|
except httpx.TimeoutException as exc:
|
|
303
334
|
raise WalletBackendError(
|
|
304
335
|
"wdk-evm-wallet request timed out.",
|
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
from agent_wallet.bootstrap import ensure_solana_wallet_ready, ensure_wallet_pin
|
|
8
9
|
from agent_wallet.encrypted_storage import load_wallet_secret_material
|
|
9
10
|
from agent_wallet.config import (
|
|
10
11
|
normalize_btc_network,
|
|
12
|
+
normalize_evm_network,
|
|
11
13
|
normalize_solana_network,
|
|
12
14
|
resolve_runtime_solana_rpc_config,
|
|
13
15
|
resolve_runtime_solana_swap_config,
|
|
14
16
|
resolve_solana_private_key,
|
|
17
|
+
resolve_wdk_evm_service_url,
|
|
15
18
|
settings,
|
|
16
19
|
)
|
|
17
20
|
from agent_wallet.wallet_layer.base import AgentWalletBackend, WalletBackendError
|
|
@@ -20,6 +23,15 @@ from agent_wallet.wallet_layer.solana import SolanaLocalKeypairSigner, SolanaWal
|
|
|
20
23
|
from agent_wallet.wallet_layer.wdk_btc import WdkBtcLocalWalletBackend
|
|
21
24
|
|
|
22
25
|
|
|
26
|
+
def _evm_autostart_disabled() -> bool:
|
|
27
|
+
return os.getenv("AGENT_WALLET_EVM_DISABLE_AUTOSTART", "").strip().lower() in {
|
|
28
|
+
"1",
|
|
29
|
+
"true",
|
|
30
|
+
"yes",
|
|
31
|
+
"on",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
23
35
|
def _load_keypair_material() -> str | None:
|
|
24
36
|
secret = resolve_solana_private_key()
|
|
25
37
|
if secret:
|
|
@@ -91,10 +103,21 @@ def create_wallet_backend() -> AgentWalletBackend | None:
|
|
|
91
103
|
)
|
|
92
104
|
|
|
93
105
|
if backend in {"wdk_evm_local", "wdk-evm-local", "evm_local", "evm-local"}:
|
|
106
|
+
evm_network = normalize_evm_network(settings.solana_network)
|
|
107
|
+
service_url = resolve_wdk_evm_service_url()
|
|
108
|
+
if not _evm_autostart_disabled():
|
|
109
|
+
# Single-agent hosts (Claude Code/Codex) talked to the daemon over
|
|
110
|
+
# a bare HTTP client with no health check, so an unreachable or
|
|
111
|
+
# stale daemon surfaced as an opaque connection error instead of
|
|
112
|
+
# self-healing the way the multi-user OpenClaw path already does.
|
|
113
|
+
# Reuse that same recovery here instead of duplicating it.
|
|
114
|
+
from agent_wallet.evm_user_wallets import ensure_local_evm_service_ready
|
|
115
|
+
|
|
116
|
+
ensure_local_evm_service_ready(service_url, evm_network)
|
|
94
117
|
return WdkEvmLocalWalletBackend(
|
|
95
|
-
service_url=
|
|
118
|
+
service_url=service_url,
|
|
96
119
|
wallet_id=settings.wdk_evm_wallet_id,
|
|
97
|
-
network=
|
|
120
|
+
network=evm_network,
|
|
98
121
|
account_index=settings.wdk_evm_account_index,
|
|
99
122
|
sign_only=settings.agent_wallet_sign_only,
|
|
100
123
|
)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "agent-wallet",
|
|
3
3
|
"name": "Agent Wallet",
|
|
4
4
|
"description": "Plugin-friendly wallet backend for OpenClaw agents with safe wallet tools and runtime instructions across Solana, local BTC, and local EVM.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.106",
|
|
6
6
|
"skills": ["skills/wallet-operator"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
@@ -10,9 +10,7 @@ import subprocess
|
|
|
10
10
|
import sys
|
|
11
11
|
import time
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from urllib.error import URLError
|
|
14
13
|
from urllib.parse import urlparse
|
|
15
|
-
from urllib.request import urlopen
|
|
16
14
|
|
|
17
15
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
18
16
|
|
|
@@ -24,6 +22,12 @@ def _default_config_path() -> Path:
|
|
|
24
22
|
return Path(os.path.expanduser("~/.openclaw/openclaw.json"))
|
|
25
23
|
|
|
26
24
|
|
|
25
|
+
def _default_service_url() -> str:
|
|
26
|
+
from agent_wallet.config import resolve_wdk_evm_service_url
|
|
27
|
+
|
|
28
|
+
return resolve_wdk_evm_service_url()
|
|
29
|
+
|
|
30
|
+
|
|
27
31
|
def _default_user_id() -> str:
|
|
28
32
|
return f"{os.getenv('USER', 'openclaw-user')}-local"
|
|
29
33
|
|
|
@@ -54,7 +58,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
54
58
|
parser.add_argument("--plugin-id", default="agent-wallet")
|
|
55
59
|
parser.add_argument("--user-id", default=_default_user_id())
|
|
56
60
|
parser.add_argument("--network", default="base")
|
|
57
|
-
parser.add_argument("--service-url", default=
|
|
61
|
+
parser.add_argument("--service-url", default=_default_service_url())
|
|
58
62
|
parser.add_argument("--wdk-wallet-root", default=str(_repo_root() / "wdk-evm-wallet"))
|
|
59
63
|
parser.add_argument("--label", default="Agent EVM Wallet")
|
|
60
64
|
parser.add_argument("--account-index", type=int, default=0)
|
|
@@ -103,15 +107,19 @@ def _health_url(service_url: str) -> str:
|
|
|
103
107
|
|
|
104
108
|
|
|
105
109
|
def _service_is_healthy(service_url: str) -> bool:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
# Delegate to the shared probe: it dispatches on scheme, so a unix:// URL
|
|
111
|
+
# (the default since resolve_wdk_evm_service_url moved to a per-home socket)
|
|
112
|
+
# is fetched over AF_UNIX. urlopen alone raises "unknown url type: unix",
|
|
113
|
+
# which would make --no-auto-start-service reject a healthy daemon.
|
|
114
|
+
from agent_wallet.evm_user_wallets import _service_health
|
|
115
|
+
|
|
116
|
+
return _service_health(service_url) is not None
|
|
111
117
|
|
|
112
118
|
|
|
113
119
|
def _is_local_service_url(service_url: str) -> bool:
|
|
114
120
|
parsed = urlparse(service_url)
|
|
121
|
+
if parsed.scheme == "unix":
|
|
122
|
+
return bool(parsed.path)
|
|
115
123
|
return parsed.scheme in {"http", "https"} and parsed.hostname in {"127.0.0.1", "localhost", "::1"}
|
|
116
124
|
|
|
117
125
|
|
|
@@ -146,7 +154,6 @@ def _auto_start_local_service(
|
|
|
146
154
|
_service_health,
|
|
147
155
|
_should_restart_local_service,
|
|
148
156
|
_stop_local_service,
|
|
149
|
-
_write_service_owner,
|
|
150
157
|
)
|
|
151
158
|
|
|
152
159
|
restarted = False
|
|
@@ -167,15 +174,18 @@ def _auto_start_local_service(
|
|
|
167
174
|
raise SystemExit(f"Could not find wdk-evm-wallet launcher: {run_local}")
|
|
168
175
|
|
|
169
176
|
parsed = urlparse(service_url)
|
|
170
|
-
host = parsed.hostname or "127.0.0.1"
|
|
171
|
-
port = parsed.port or 8081
|
|
172
177
|
log_dir = _service_log_dir(config_path)
|
|
173
178
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
174
179
|
log_path = _service_log_path(config_path)
|
|
175
180
|
|
|
176
181
|
env = os.environ.copy()
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
if parsed.scheme == "unix":
|
|
183
|
+
env["WDK_EVM_TRANSPORT"] = "socket"
|
|
184
|
+
env["WDK_EVM_SOCKET_PATH"] = parsed.path
|
|
185
|
+
else:
|
|
186
|
+
env["WDK_EVM_TRANSPORT"] = "tcp"
|
|
187
|
+
env["HOST"] = parsed.hostname or "127.0.0.1"
|
|
188
|
+
env["PORT"] = str(parsed.port or 8081)
|
|
179
189
|
env["WDK_EVM_NETWORK"] = network
|
|
180
190
|
env["WDK_EVM_INSTANCE_ID"] = _expected_local_service_instance_id()
|
|
181
191
|
|
|
@@ -194,7 +204,7 @@ def _auto_start_local_service(
|
|
|
194
204
|
while time.time() < deadline:
|
|
195
205
|
health = _service_health(service_url)
|
|
196
206
|
if health is not None:
|
|
197
|
-
pid =
|
|
207
|
+
pid = int(health.get("pid") or 0)
|
|
198
208
|
return {
|
|
199
209
|
"started": True,
|
|
200
210
|
"already_healthy": False,
|
|
@@ -190,7 +190,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
190
190
|
parser.add_argument("--extension-path", default=str(_extension_path()))
|
|
191
191
|
parser.add_argument("--wdk-btc-root", default=str(_default_wdk_btc_root()))
|
|
192
192
|
parser.add_argument("--wdk-evm-root", default=str(_default_wdk_evm_root()))
|
|
193
|
-
parser.add_argument("--wdk-evm-service-url", default=
|
|
193
|
+
parser.add_argument("--wdk-evm-service-url", default=_default_evm_service_url())
|
|
194
194
|
parser.add_argument("--runtime-root", default=str(_default_runtime_root()))
|
|
195
195
|
parser.add_argument("--npm-bin", default=_default_npm_bin())
|
|
196
196
|
parser.add_argument("--plugin-id", default="agent-wallet")
|
|
@@ -735,8 +735,8 @@ def _build_next_steps(
|
|
|
735
735
|
command.extend(["--package-root", str(effective_package_root)])
|
|
736
736
|
command.extend(["--python-bin", str(python_bin)])
|
|
737
737
|
if _is_evm_backend(args.backend):
|
|
738
|
-
service_url = str(getattr(args, "wdk_evm_service_url", "") or
|
|
739
|
-
command.extend(["--wdk-evm-service-url", service_url or
|
|
738
|
+
service_url = str(getattr(args, "wdk_evm_service_url", "") or _default_evm_service_url()).strip()
|
|
739
|
+
command.extend(["--wdk-evm-service-url", service_url or _default_evm_service_url()])
|
|
740
740
|
return command
|
|
741
741
|
|
|
742
742
|
|
|
@@ -753,7 +753,19 @@ def _is_evm_backend(backend: str) -> bool:
|
|
|
753
753
|
}
|
|
754
754
|
|
|
755
755
|
|
|
756
|
-
|
|
756
|
+
def _default_evm_service_url() -> str:
|
|
757
|
+
# This installer bootstraps a brand-new install, so it runs before
|
|
758
|
+
# agent_wallet's *dependencies* (e.g. pydantic-settings) are guaranteed to
|
|
759
|
+
# be installed -- putting the package root on sys.path (as other scripts
|
|
760
|
+
# in this family do) isn't enough, since `import agent_wallet.config`
|
|
761
|
+
# itself pulls in pydantic_settings. Mirror
|
|
762
|
+
# agent_wallet.config.resolve_wdk_evm_service_url()'s stdlib-only logic
|
|
763
|
+
# directly instead of importing it.
|
|
764
|
+
explicit = os.environ.get("WDK_EVM_SERVICE_URL", "").strip()
|
|
765
|
+
if explicit:
|
|
766
|
+
return explicit
|
|
767
|
+
openclaw_home = Path(os.environ.get("OPENCLAW_HOME", "~/.openclaw")).expanduser()
|
|
768
|
+
return f"unix://{openclaw_home / 'wdk-evm-wallet' / 'daemon.sock'}"
|
|
757
769
|
|
|
758
770
|
|
|
759
771
|
def _build_evm_onboard_config(args: argparse.Namespace) -> dict[str, object]:
|
|
@@ -764,12 +776,12 @@ def _build_evm_onboard_config(args: argparse.Namespace) -> dict[str, object]:
|
|
|
764
776
|
network = args.network.strip().lower() if _is_evm_backend(args.backend) else "base"
|
|
765
777
|
if network not in {"base", "ethereum"}:
|
|
766
778
|
network = "base"
|
|
767
|
-
service_url = str(getattr(args, "wdk_evm_service_url", "") or
|
|
779
|
+
service_url = str(getattr(args, "wdk_evm_service_url", "") or _default_evm_service_url()).strip()
|
|
768
780
|
return {
|
|
769
781
|
"backend": "wdk_evm_local",
|
|
770
782
|
"network": network,
|
|
771
783
|
"signOnly": bool(args.sign_only),
|
|
772
|
-
"wdkEvmServiceUrl": service_url or
|
|
784
|
+
"wdkEvmServiceUrl": service_url or _default_evm_service_url(),
|
|
773
785
|
}
|
|
774
786
|
|
|
775
787
|
|
|
@@ -8,15 +8,17 @@ import json
|
|
|
8
8
|
import sys
|
|
9
9
|
from getpass import getpass
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from urllib.error import URLError
|
|
12
|
-
from urllib.request import urlopen
|
|
13
11
|
|
|
14
12
|
PACKAGE_ROOT = Path(__file__).resolve().parents[1]
|
|
15
13
|
if str(PACKAGE_ROOT) not in sys.path:
|
|
16
14
|
sys.path.insert(0, str(PACKAGE_ROOT))
|
|
17
15
|
|
|
18
|
-
from agent_wallet.config import
|
|
16
|
+
from agent_wallet.config import ( # noqa: E402
|
|
17
|
+
normalize_evm_network,
|
|
18
|
+
resolve_wdk_evm_service_url,
|
|
19
|
+
)
|
|
19
20
|
from agent_wallet.evm_user_wallets import ( # noqa: E402
|
|
21
|
+
_service_health as probe_service_health,
|
|
20
22
|
bind_user_evm_wallet,
|
|
21
23
|
create_user_evm_wallet,
|
|
22
24
|
get_user_evm_wallet_binding,
|
|
@@ -81,21 +83,22 @@ def _service_health(service_url: str | None) -> dict[str, object]:
|
|
|
81
83
|
target = str(service_url or "").strip()
|
|
82
84
|
if not target:
|
|
83
85
|
return {"service_url": None, "healthy": False, "error": "service_url is not configured"}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
86
|
+
# Delegate to the shared probe: it dispatches on scheme, so a unix:// URL
|
|
87
|
+
# (the default since resolve_wdk_evm_service_url moved to a per-home socket)
|
|
88
|
+
# is fetched over AF_UNIX. urlopen alone raises "unknown url type: unix" and
|
|
89
|
+
# would report a perfectly healthy socket daemon as down.
|
|
90
|
+
payload = probe_service_health(target)
|
|
91
|
+
if payload is None:
|
|
92
|
+
return {
|
|
93
|
+
"service_url": target,
|
|
94
|
+
"healthy": False,
|
|
95
|
+
"error": f"no healthy /health response from {target}",
|
|
96
|
+
}
|
|
97
|
+
return {"service_url": target, "healthy": True, "health": payload}
|
|
95
98
|
|
|
96
99
|
|
|
97
100
|
def _status_payload(user_id: str | None, network: str | None, service_url: str | None) -> dict[str, object]:
|
|
98
|
-
target_service_url = str(service_url or
|
|
101
|
+
target_service_url = str(service_url or resolve_wdk_evm_service_url()).strip() or None
|
|
99
102
|
payload: dict[str, object] = {
|
|
100
103
|
"ok": True,
|
|
101
104
|
"network": _normalize_network(network or "ethereum"),
|
|
@@ -106,7 +106,6 @@ prompt_network_choice() {
|
|
|
106
106
|
|
|
107
107
|
DEFAULT_USER_ID=${OPENCLAW_EVM_USER_ID:-${USER:-openclaw-user}-local}
|
|
108
108
|
DEFAULT_NETWORK=${OPENCLAW_EVM_NETWORK:-base}
|
|
109
|
-
DEFAULT_SERVICE_URL=${OPENCLAW_EVM_SERVICE_URL:-http://127.0.0.1:8081}
|
|
110
109
|
|
|
111
110
|
if ! has_flag --user-id "$@"; then
|
|
112
111
|
USER_ID=$(prompt_with_default "OpenClaw user id" "$DEFAULT_USER_ID")
|
|
@@ -118,8 +117,8 @@ if ! has_flag --network "$@"; then
|
|
|
118
117
|
set -- "$@" --network "$NETWORK"
|
|
119
118
|
fi
|
|
120
119
|
|
|
121
|
-
if ! has_flag --service-url "$@"; then
|
|
122
|
-
set -- "$@" --service-url "$
|
|
120
|
+
if ! has_flag --service-url "$@" && [ -n "${OPENCLAW_EVM_SERVICE_URL:-}" ]; then
|
|
121
|
+
set -- "$@" --service-url "$OPENCLAW_EVM_SERVICE_URL"
|
|
123
122
|
fi
|
|
124
123
|
|
|
125
124
|
if ! has_flag --config-path "$@" && [ -n "${OPENCLAW_EVM_CONFIG_PATH:-}" ]; then
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wallet",
|
|
3
3
|
"displayName": "Agent Wallet",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.106",
|
|
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"
|
|
@@ -13,8 +13,10 @@ import selectors
|
|
|
13
13
|
import signal
|
|
14
14
|
import subprocess
|
|
15
15
|
import sys
|
|
16
|
+
import tempfile
|
|
16
17
|
import threading
|
|
17
18
|
import time
|
|
19
|
+
from contextlib import suppress
|
|
18
20
|
from functools import lru_cache
|
|
19
21
|
from pathlib import Path
|
|
20
22
|
from typing import Any
|
|
@@ -122,6 +124,50 @@ def _openclaw_home() -> Path:
|
|
|
122
124
|
return Path(os.getenv("OPENCLAW_HOME", "~/.openclaw")).expanduser().resolve()
|
|
123
125
|
|
|
124
126
|
|
|
127
|
+
def _session_defaults_path() -> Path:
|
|
128
|
+
return _openclaw_home() / "agent-wallet" / "session-defaults.json"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _read_session_defaults() -> dict[str, Any]:
|
|
132
|
+
try:
|
|
133
|
+
payload = json.loads(_session_defaults_path().read_text(encoding="utf-8"))
|
|
134
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
135
|
+
return {}
|
|
136
|
+
return payload if isinstance(payload, dict) else {}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _write_session_default(key: str, value: str) -> None:
|
|
140
|
+
"""Remember a wallet/network switch so the next MCP session starts there.
|
|
141
|
+
|
|
142
|
+
set_wallet_backend/set_evm_network previously only mutated this process's
|
|
143
|
+
module globals ("session_override_active"/"config_file_changed: False" by
|
|
144
|
+
design) — every new session went back to the static openclaw.json/env
|
|
145
|
+
default. This is the one persisted side effect: a small, agent-wallet-
|
|
146
|
+
owned file, separate from the shared openclaw.json so a write here can
|
|
147
|
+
never clobber unrelated host config. Best-effort — a write failure must
|
|
148
|
+
never break the tool call that triggered it.
|
|
149
|
+
"""
|
|
150
|
+
path = _session_defaults_path()
|
|
151
|
+
try:
|
|
152
|
+
data = _read_session_defaults()
|
|
153
|
+
data[key] = value
|
|
154
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
fd, temp_path = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
|
|
156
|
+
try:
|
|
157
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
158
|
+
handle.write(json.dumps(data, indent=2, sort_keys=True) + "\n")
|
|
159
|
+
handle.flush()
|
|
160
|
+
os.fsync(handle.fileno())
|
|
161
|
+
os.chmod(temp_path, 0o600)
|
|
162
|
+
os.replace(temp_path, path)
|
|
163
|
+
except Exception:
|
|
164
|
+
with suppress(FileNotFoundError):
|
|
165
|
+
os.unlink(temp_path)
|
|
166
|
+
raise
|
|
167
|
+
except OSError:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
|
|
125
171
|
@lru_cache(maxsize=1)
|
|
126
172
|
def _openclaw_plugin_config() -> dict[str, Any]:
|
|
127
173
|
# Cached for the process lifetime: openclaw.json is read once per MCP server
|
|
@@ -469,9 +515,18 @@ def _normalize_btc_network(value: Any) -> str | None:
|
|
|
469
515
|
|
|
470
516
|
|
|
471
517
|
def _default_backend() -> str:
|
|
518
|
+
# Precedence: explicit env override > last backend the user actually
|
|
519
|
+
# picked via set_wallet_backend/set_evm_network in some previous session
|
|
520
|
+
# > the host's static openclaw.json config > the hardcoded fallback.
|
|
521
|
+
session_default = _read_session_defaults().get("backend")
|
|
522
|
+
try:
|
|
523
|
+
normalized_session_default = _normalize_wallet_backend(session_default) if session_default else None
|
|
524
|
+
except RuntimeError:
|
|
525
|
+
normalized_session_default = None
|
|
472
526
|
return _normalize_wallet_backend(
|
|
473
527
|
os.getenv("AGENT_WALLET_BACKEND")
|
|
474
528
|
or os.getenv("OPENCLAW_AGENT_WALLET_BACKEND")
|
|
529
|
+
or normalized_session_default
|
|
475
530
|
or _configured_backend()
|
|
476
531
|
or "solana_local"
|
|
477
532
|
)
|
|
@@ -481,6 +536,12 @@ def _default_evm_network() -> str | None:
|
|
|
481
536
|
configured = _normalize_evm_network(os.getenv("WDK_EVM_NETWORK"))
|
|
482
537
|
if configured in {"ethereum", "base", "robinhood", "goat"}:
|
|
483
538
|
return configured
|
|
539
|
+
session_default = _read_session_defaults().get("evm_network")
|
|
540
|
+
if session_default:
|
|
541
|
+
try:
|
|
542
|
+
return _normalize_selectable_evm_network(session_default)
|
|
543
|
+
except RuntimeError:
|
|
544
|
+
pass
|
|
484
545
|
return _configured_network_for_backend("wdk_evm_local")
|
|
485
546
|
|
|
486
547
|
|
|
@@ -1381,6 +1442,9 @@ async def _handle_set_wallet_backend(params: dict[str, Any]) -> dict[str, Any]:
|
|
|
1381
1442
|
else:
|
|
1382
1443
|
selected_solana_network = resolved_network
|
|
1383
1444
|
selected_wallet_backend = backend
|
|
1445
|
+
_write_session_default("backend", backend)
|
|
1446
|
+
if backend == "wdk_evm_local":
|
|
1447
|
+
_write_session_default("evm_network", resolved_network)
|
|
1384
1448
|
return {
|
|
1385
1449
|
"selected_backend": backend,
|
|
1386
1450
|
"selected_wallet": _backend_label(backend),
|
|
@@ -1388,9 +1452,11 @@ async def _handle_set_wallet_backend(params: dict[str, Any]) -> dict[str, Any]:
|
|
|
1388
1452
|
"configured_backend": _default_backend(),
|
|
1389
1453
|
"session_override_active": True,
|
|
1390
1454
|
"config_file_changed": False,
|
|
1455
|
+
"remembered_as_default": True,
|
|
1391
1456
|
"usage": (
|
|
1392
|
-
"
|
|
1393
|
-
"
|
|
1457
|
+
"Wallet calls in this session use this backend by default, and the next MCP "
|
|
1458
|
+
"session starts here too. openclaw.json/env config is unchanged; use those for "
|
|
1459
|
+
"a fixed deployment-wide default instead."
|
|
1394
1460
|
),
|
|
1395
1461
|
"data": payload.get("data", {}),
|
|
1396
1462
|
}
|
|
@@ -1407,15 +1473,19 @@ async def _handle_set_evm_network(params: dict[str, Any]) -> dict[str, Any]:
|
|
|
1407
1473
|
raise RuntimeError(str(payload.get("error") or "set_evm_network failed"))
|
|
1408
1474
|
selected_wallet_backend = "wdk_evm_local"
|
|
1409
1475
|
selected_evm_network = network
|
|
1476
|
+
_write_session_default("backend", "wdk_evm_local")
|
|
1477
|
+
_write_session_default("evm_network", network)
|
|
1410
1478
|
return {
|
|
1411
1479
|
"selected_backend": "wdk_evm_local",
|
|
1412
1480
|
"selected_wallet": "evm",
|
|
1413
1481
|
"selected_network": network,
|
|
1414
1482
|
"session_active_network": network,
|
|
1415
1483
|
"session_override_active": True,
|
|
1484
|
+
"remembered_as_default": True,
|
|
1416
1485
|
"usage": (
|
|
1417
|
-
"
|
|
1418
|
-
"You can still override a single
|
|
1486
|
+
"EVM wallet calls in this session use this network by default, and the next MCP "
|
|
1487
|
+
"session starts here too. You can still override a single call with its network "
|
|
1488
|
+
"parameter."
|
|
1419
1489
|
),
|
|
1420
1490
|
"data": payload.get("data", {}),
|
|
1421
1491
|
}
|
package/package.json
CHANGED
package/wdk-evm-wallet/README.md
CHANGED
|
@@ -181,8 +181,12 @@ cd wdk-evm-wallet && sh run-local.sh
|
|
|
181
181
|
|
|
182
182
|
Environment variables:
|
|
183
183
|
|
|
184
|
-
- `
|
|
185
|
-
|
|
184
|
+
- `WDK_EVM_TRANSPORT` — `socket` (default) or `tcp`. `socket` serves over a unix
|
|
185
|
+
domain socket at `WDK_EVM_SOCKET_PATH`; `tcp` serves over `HOST`/`PORT`.
|
|
186
|
+
- `WDK_EVM_SOCKET_PATH` — unix socket path, used only when `WDK_EVM_TRANSPORT=socket`.
|
|
187
|
+
Defaults to `<dataDir>/daemon.sock`.
|
|
188
|
+
- `HOST` — used only when `WDK_EVM_TRANSPORT=tcp`.
|
|
189
|
+
- `PORT` — used only when `WDK_EVM_TRANSPORT=tcp`.
|
|
186
190
|
- `WDK_EVM_NETWORK`
|
|
187
191
|
- `WDK_EVM_DATA_DIR`
|
|
188
192
|
- `WDK_EVM_LOCAL_TOKEN`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wdk-evm-wallet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.106",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Separate EVM wallet service built on Tether WDK.",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"test:shutdown": "node --test tests/shutdown.test.mjs",
|
|
20
20
|
"test:network-config": "node --test tests/unit_network_config.mjs",
|
|
21
21
|
"test:network-state": "node --test tests/unit_network_state.mjs",
|
|
22
|
-
"test:wallet-network": "node --test tests/unit_wdk_wallet_network.mjs"
|
|
22
|
+
"test:wallet-network": "node --test tests/unit_wdk_wallet_network.mjs",
|
|
23
|
+
"test:socket-transport": "node --test tests/unit_socket_transport.mjs"
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
25
26
|
"@morpho-org/morpho-sdk": "^3.2.0",
|