@agentlayer.tech/wallet 0.1.11 → 0.1.12
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/CHANGELOG.md +25 -0
- package/README.md +28 -50
- package/agent-wallet/README.md +24 -0
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/bootstrap_openclaw_evm.py +291 -0
- package/agent-wallet/scripts/install_openclaw_local_config.py +9 -0
- package/agent-wallet/scripts/manage_openclaw_evm_wallet.py +343 -0
- package/agent-wallet/scripts/setup_evm_wallet.sh +151 -0
- package/hermes/plugins/agent_wallet/__init__.py +28 -2
- package/hermes/plugins/agent_wallet/plugin.yaml +2 -0
- package/hermes/plugins/agent_wallet/schemas.py +72 -0
- package/hermes/plugins/agent_wallet/tools.py +100 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
OpenClaw wallet tools or policy.
|
|
8
8
|
- Added `wallet hermes install --yes` and `AGENT_WALLET_BOOT_KEY_FILE` support
|
|
9
9
|
for smoother Hermes onboarding without manual `.env` editing.
|
|
10
|
+
- Added Hermes EVM onboarding helpers:
|
|
11
|
+
`agent_wallet_evm_status` and `agent_wallet_evm_setup`.
|
|
12
|
+
- Added host-side EVM bootstrap scripts for packaged/runtime installs:
|
|
13
|
+
`manage_openclaw_evm_wallet.py`, `bootstrap_openclaw_evm.py`, and
|
|
14
|
+
`setup_evm_wallet.sh`.
|
|
15
|
+
- Kept Hermes EVM routing on the existing `wdk-evm-wallet` and
|
|
16
|
+
`provider-gateway` path for `ethereum` and `base`, without copying wallet
|
|
17
|
+
policy or duplicating tool implementations.
|
|
10
18
|
- Replaced the repo license with `PolyForm Small Business 1.0.0`.
|
|
11
19
|
- Clarified in `README.md` that individuals can audit, fork, run, and modify
|
|
12
20
|
the code for themselves, and that company use follows the PolyForm small
|
|
@@ -15,6 +23,23 @@
|
|
|
15
23
|
`wdk-evm-wallet`; cross-chain swaps now stay on LI.FI/Jupiter-backed paths
|
|
16
24
|
with Mayan denied as a LI.FI bridge.
|
|
17
25
|
|
|
26
|
+
## v0.1.12 - 2026-05-06
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Hermes EVM runtime helpers for packaged installs:
|
|
31
|
+
`agent_wallet_evm_status` and `agent_wallet_evm_setup`.
|
|
32
|
+
- Host-side EVM lifecycle scripts in the runtime bundle:
|
|
33
|
+
`manage_openclaw_evm_wallet.py`, `bootstrap_openclaw_evm.py`, and
|
|
34
|
+
`setup_evm_wallet.sh`.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Hermes installs now support the same local EVM onboarding shape as BTC:
|
|
39
|
+
inspect runtime health, auto-start the local service, create or unlock the
|
|
40
|
+
vault wallet, and bind paired `ethereum/base` networks through the thin
|
|
41
|
+
bridge.
|
|
42
|
+
|
|
18
43
|
## v0.1.0-beta.2 - 2026-03-31
|
|
19
44
|
|
|
20
45
|
Second public beta release that expands the stack beyond the initial MCP, wallet, and AgentLayer bridge scope.
|
package/README.md
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
```bash
|
|
5
5
|
npx @agentlayer.tech/wallet install --yes
|
|
6
6
|
```
|
|
7
|
+
For install to Hermes use
|
|
8
|
+
```bash
|
|
9
|
+
npx @agentlayer.tech/wallet install --yes && npx @agentlayer.tech/wallet hermes install --yes
|
|
10
|
+
```
|
|
11
|
+
|
|
7
12
|
AgentLayer is a beta local-first wallet and finance stack for agents.
|
|
8
13
|
|
|
9
14
|
The repository includes:
|
|
@@ -92,28 +97,10 @@ sh agent-wallet/scripts/setup_btc_wallet.sh
|
|
|
92
97
|
EVM:
|
|
93
98
|
|
|
94
99
|
```bash
|
|
95
|
-
|
|
100
|
+
sh agent-wallet/scripts/setup_evm_wallet.sh
|
|
96
101
|
```
|
|
97
102
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
printf '%s\n' 'your-local-evm-password' | \
|
|
102
|
-
agent-wallet/.venv/bin/python -m agent_wallet.openclaw_cli evm-wallet-create \
|
|
103
|
-
--user-id your-user-id \
|
|
104
|
-
--password-stdin \
|
|
105
|
-
--config-json '{"backend":"wdk_evm_local","network":"base","wdkEvmServiceUrl":"http://127.0.0.1:8081"}'
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Unlock an existing EVM wallet binding:
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
printf '%s\n' 'your-local-evm-password' | \
|
|
112
|
-
agent-wallet/.venv/bin/python -m agent_wallet.openclaw_cli evm-wallet-unlock \
|
|
113
|
-
--user-id your-user-id \
|
|
114
|
-
--password-stdin \
|
|
115
|
-
--config-json '{"backend":"wdk_evm_local","network":"base","wdkEvmServiceUrl":"http://127.0.0.1:8081"}'
|
|
116
|
-
```
|
|
103
|
+
That host-side bootstrap can auto-start the local `wdk-evm-wallet` service, create or unlock the vault wallet, bind both `base` and `ethereum` for the same local user, and patch OpenClaw config to `backend=wdk_evm_local`.
|
|
117
104
|
|
|
118
105
|
That generates three fresh local secrets in the current shell session. If you prefer Python instead of `openssl`:
|
|
119
106
|
|
|
@@ -149,7 +136,15 @@ OpenClaw remains the primary local environment, but the repo also ships an optio
|
|
|
149
136
|
hermes/plugins/agent_wallet
|
|
150
137
|
```
|
|
151
138
|
|
|
152
|
-
It exposes
|
|
139
|
+
It exposes a thin bridge, not a port of wallet logic:
|
|
140
|
+
|
|
141
|
+
- `agent_wallet_tools`
|
|
142
|
+
- `agent_wallet_invoke`
|
|
143
|
+
- `agent_wallet_approve`
|
|
144
|
+
- `agent_wallet_evm_status`
|
|
145
|
+
- `agent_wallet_evm_setup`
|
|
146
|
+
|
|
147
|
+
Install it by symlinking the plugin directory into Hermes:
|
|
153
148
|
|
|
154
149
|
```bash
|
|
155
150
|
npx @agentlayer.tech/wallet hermes install --yes
|
|
@@ -229,49 +224,32 @@ sh agent-wallet/scripts/reveal_btc_seed.sh
|
|
|
229
224
|
|
|
230
225
|
## EVM setup
|
|
231
226
|
|
|
232
|
-
The EVM runtime is installed by `setup.sh`,
|
|
233
|
-
|
|
234
|
-
Start the local EVM service:
|
|
227
|
+
The EVM runtime is installed by `setup.sh`, and the host-side onboarding now has the same one-command shape as BTC:
|
|
235
228
|
|
|
236
229
|
```bash
|
|
237
|
-
|
|
230
|
+
sh agent-wallet/scripts/setup_evm_wallet.sh
|
|
238
231
|
```
|
|
239
232
|
|
|
240
|
-
|
|
233
|
+
That flow:
|
|
241
234
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
235
|
+
- prompts for `user-id`
|
|
236
|
+
- prompts for `ethereum`, `base`, `sepolia`, or `base-sepolia`
|
|
237
|
+
- defaults to `http://127.0.0.1:8081`
|
|
238
|
+
- can auto-start `wdk-evm-wallet/run-local.sh` if the local service is not already healthy
|
|
239
|
+
- creates or unlocks the local EVM wallet binding
|
|
240
|
+
- also binds the paired EVM network by default: `ethereum <-> base`, `sepolia <-> base-sepolia`
|
|
241
|
+
- patches OpenClaw config to `backend=wdk_evm_local`
|
|
249
242
|
|
|
250
|
-
|
|
243
|
+
You can still use the lower-level CLI if needed:
|
|
251
244
|
|
|
252
245
|
```bash
|
|
253
246
|
printf '%s\n' 'your-local-evm-password' | \
|
|
254
|
-
agent-wallet/.venv/bin/python -m agent_wallet.openclaw_cli evm-wallet-
|
|
247
|
+
agent-wallet/.venv/bin/python -m agent_wallet.openclaw_cli evm-wallet-create \
|
|
255
248
|
--user-id your-user-id \
|
|
256
249
|
--password-stdin \
|
|
257
250
|
--config-json '{"backend":"wdk_evm_local","network":"base","wdkEvmServiceUrl":"http://127.0.0.1:8081"}'
|
|
258
251
|
```
|
|
259
252
|
|
|
260
|
-
Then switch the OpenClaw plugin config to the EVM backend:
|
|
261
|
-
|
|
262
|
-
```bash
|
|
263
|
-
AGENT_WALLET_BOOT_KEY='...' \
|
|
264
|
-
agent-wallet/.venv/bin/python agent-wallet/scripts/install_openclaw_local_config.py \
|
|
265
|
-
--backend wdk_evm_local \
|
|
266
|
-
--network base \
|
|
267
|
-
--user-id your-user-id \
|
|
268
|
-
--package-root agent-wallet \
|
|
269
|
-
--extension-path .openclaw/extensions/agent-wallet \
|
|
270
|
-
--python-bin agent-wallet/.venv/bin/python
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
That final config step assumes `~/.openclaw/sealed_keys.json` already exists. The normal path is to let the main installer create it by running install with `AGENT_WALLET_BOOT_KEY`, `AGENT_WALLET_MASTER_KEY`, and `AGENT_WALLET_APPROVAL_SECRET` available.
|
|
274
|
-
|
|
275
253
|
Important EVM notes:
|
|
276
254
|
|
|
277
255
|
- only localhost service URLs are supported for the OpenClaw EVM flow
|
package/agent-wallet/README.md
CHANGED
|
@@ -34,6 +34,9 @@ The optional Hermes plugin is intentionally a bridge, not a port of the OpenClaw
|
|
|
34
34
|
|
|
35
35
|
- `agent_wallet_tools` - read-only discovery for the underlying Python adapter tool specs.
|
|
36
36
|
- `agent_wallet_invoke` - a dispatcher that calls `python -m agent_wallet.openclaw_cli invoke`.
|
|
37
|
+
- `agent_wallet_approve` - a host approval-token issuer for exact execute operations.
|
|
38
|
+
- `agent_wallet_evm_status` - a read-only EVM runtime and binding inspector.
|
|
39
|
+
- `agent_wallet_evm_setup` - a host-side EVM bootstrap helper for Hermes.
|
|
37
40
|
|
|
38
41
|
Install it with:
|
|
39
42
|
|
|
@@ -45,6 +48,12 @@ That command symlinks `hermes/plugins/agent_wallet` into `~/.hermes/plugins/agen
|
|
|
45
48
|
|
|
46
49
|
Hermes tool config must not contain wallet secrets. Use the existing sealed runtime path and host-issued approval tokens for execute flows. `AGENT_WALLET_BOOT_KEY_FILE` lets OpenClaw and Hermes reference one local boot-key file instead of duplicating the boot key across multiple env files.
|
|
47
50
|
|
|
51
|
+
For EVM on Hermes, the intended host flow is:
|
|
52
|
+
|
|
53
|
+
- call `agent_wallet_evm_status` to inspect `wdk-evm-wallet` health and current bindings
|
|
54
|
+
- call `agent_wallet_evm_setup` once to auto-start the local service when needed, create or unlock the local EVM wallet, and patch local OpenClaw config to `backend=wdk_evm_local`
|
|
55
|
+
- then use ordinary `agent_wallet_invoke` calls for EVM reads, transfers, swaps, and Aave flows
|
|
56
|
+
|
|
48
57
|
Current safe tools:
|
|
49
58
|
|
|
50
59
|
- `get_wallet_capabilities`
|
|
@@ -375,6 +384,21 @@ For the local EVM backend (`backend=wdk_evm_local`), the lifecycle mirrors the B
|
|
|
375
384
|
- `evm-wallet-unlock`
|
|
376
385
|
- `evm-wallet-lock`
|
|
377
386
|
|
|
387
|
+
For a simpler host-side bootstrap, use:
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
sh agent-wallet/scripts/setup_evm_wallet.sh
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
That wrapper:
|
|
394
|
+
|
|
395
|
+
- prompts for `user-id` and EVM network when run interactively
|
|
396
|
+
- defaults to `http://127.0.0.1:8081`
|
|
397
|
+
- can auto-start `wdk-evm-wallet/run-local.sh` if the local service is not already healthy
|
|
398
|
+
- creates or unlocks the local EVM wallet binding
|
|
399
|
+
- also binds the paired EVM network by default: `ethereum <-> base`, `sepolia <-> base-sepolia`
|
|
400
|
+
- patches OpenClaw config to `backend=wdk_evm_local`
|
|
401
|
+
|
|
378
402
|
Example host-side EVM wallet creation:
|
|
379
403
|
|
|
380
404
|
```bash
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""One-command host bootstrap for the local OpenClaw EVM wallet flow."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from urllib.error import URLError
|
|
14
|
+
from urllib.parse import urlparse
|
|
15
|
+
from urllib.request import urlopen
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
18
|
+
|
|
19
|
+
from agent_wallet.file_ops import atomic_write_text, chmod_if_exists
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _default_config_path() -> Path:
|
|
23
|
+
return Path(os.path.expanduser("~/.openclaw/openclaw.json"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _default_user_id() -> str:
|
|
27
|
+
return f"{os.getenv('USER', 'openclaw-user')}-local"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _default_python_bin() -> str:
|
|
31
|
+
return os.getenv("OPENCLAW_AGENT_WALLET_PYTHON", sys.executable)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _package_root() -> Path:
|
|
35
|
+
return Path(__file__).resolve().parents[1]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _repo_root() -> Path:
|
|
39
|
+
return Path(__file__).resolve().parents[2]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _script_path(name: str) -> Path:
|
|
43
|
+
return _package_root() / "scripts" / name
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _normalize_network(value: str) -> str:
|
|
47
|
+
network = str(value or "").strip().lower()
|
|
48
|
+
aliases = {
|
|
49
|
+
"mainnet": "ethereum",
|
|
50
|
+
"eth": "ethereum",
|
|
51
|
+
"eth-mainnet": "ethereum",
|
|
52
|
+
"base-mainnet": "base",
|
|
53
|
+
"base_sepolia": "base-sepolia",
|
|
54
|
+
}
|
|
55
|
+
network = aliases.get(network, network)
|
|
56
|
+
if network not in {"ethereum", "sepolia", "base", "base-sepolia"}:
|
|
57
|
+
return "ethereum"
|
|
58
|
+
return network
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
62
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
63
|
+
parser.add_argument("--config-path", default=str(_default_config_path()))
|
|
64
|
+
parser.add_argument("--plugin-id", default="agent-wallet")
|
|
65
|
+
parser.add_argument("--user-id", default=_default_user_id())
|
|
66
|
+
parser.add_argument("--network", default="base")
|
|
67
|
+
parser.add_argument("--service-url", default="http://127.0.0.1:8081")
|
|
68
|
+
parser.add_argument("--wdk-wallet-root", default=str(_repo_root() / "wdk-evm-wallet"))
|
|
69
|
+
parser.add_argument("--label", default="Agent EVM Wallet")
|
|
70
|
+
parser.add_argument("--account-index", type=int, default=0)
|
|
71
|
+
parser.add_argument("--python-bin", default=_default_python_bin())
|
|
72
|
+
parser.add_argument("--package-root", default=str(_package_root()))
|
|
73
|
+
parser.add_argument("--password-stdin", action=argparse.BooleanOptionalAction, default=False)
|
|
74
|
+
parser.add_argument("--sign-only", action=argparse.BooleanOptionalAction, default=False)
|
|
75
|
+
parser.add_argument("--auto-start-service", action=argparse.BooleanOptionalAction, default=True)
|
|
76
|
+
parser.add_argument("--bind-network-pair", action=argparse.BooleanOptionalAction, default=True)
|
|
77
|
+
return parser
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _ensure_openclaw_config(config_path: Path) -> bool:
|
|
81
|
+
if config_path.exists():
|
|
82
|
+
return False
|
|
83
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
atomic_write_text(
|
|
85
|
+
config_path,
|
|
86
|
+
json.dumps({"plugins": {"entries": {}}, "tools": {"alsoAllow": []}}, indent=2) + "\n",
|
|
87
|
+
mode=0o600,
|
|
88
|
+
)
|
|
89
|
+
chmod_if_exists(config_path, 0o600)
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _run_script(
|
|
94
|
+
python_bin: str,
|
|
95
|
+
script_name: str,
|
|
96
|
+
args: list[str],
|
|
97
|
+
*,
|
|
98
|
+
stdin_text: str | None = None,
|
|
99
|
+
) -> dict:
|
|
100
|
+
completed = subprocess.run(
|
|
101
|
+
[python_bin, str(_script_path(script_name)), *args],
|
|
102
|
+
check=True,
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
input=stdin_text,
|
|
106
|
+
env=os.environ.copy(),
|
|
107
|
+
)
|
|
108
|
+
return json.loads(completed.stdout)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _health_url(service_url: str) -> str:
|
|
112
|
+
return f"{service_url.rstrip('/')}/health"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _service_is_healthy(service_url: str) -> bool:
|
|
116
|
+
try:
|
|
117
|
+
with urlopen(_health_url(service_url), timeout=1.5) as response:
|
|
118
|
+
return int(getattr(response, "status", 0) or 0) == 200
|
|
119
|
+
except (URLError, TimeoutError, OSError):
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _is_local_service_url(service_url: str) -> bool:
|
|
124
|
+
parsed = urlparse(service_url)
|
|
125
|
+
return parsed.scheme in {"http", "https"} and parsed.hostname in {"127.0.0.1", "localhost", "::1"}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _require_local_service_url(service_url: str) -> None:
|
|
129
|
+
if not _is_local_service_url(service_url):
|
|
130
|
+
raise SystemExit(
|
|
131
|
+
f"EVM bootstrap only supports a localhost service URL. Refusing non-local endpoint: {service_url}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _service_log_dir(config_path: Path) -> Path:
|
|
136
|
+
return config_path.expanduser().parent / "logs"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _service_log_path(config_path: Path) -> Path:
|
|
140
|
+
return _service_log_dir(config_path) / "wdk-evm-wallet.log"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _auto_start_local_service(
|
|
144
|
+
*,
|
|
145
|
+
service_url: str,
|
|
146
|
+
network: str,
|
|
147
|
+
wdk_wallet_root: Path,
|
|
148
|
+
config_path: Path,
|
|
149
|
+
) -> dict[str, object]:
|
|
150
|
+
if _service_is_healthy(service_url):
|
|
151
|
+
return {"started": False, "already_healthy": True}
|
|
152
|
+
|
|
153
|
+
if not _is_local_service_url(service_url):
|
|
154
|
+
raise SystemExit(
|
|
155
|
+
f"EVM service at {service_url} is unreachable and auto-start is only supported for localhost URLs."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
run_local = wdk_wallet_root / "run-local.sh"
|
|
159
|
+
if not run_local.exists():
|
|
160
|
+
raise SystemExit(f"Could not find wdk-evm-wallet launcher: {run_local}")
|
|
161
|
+
|
|
162
|
+
parsed = urlparse(service_url)
|
|
163
|
+
host = parsed.hostname or "127.0.0.1"
|
|
164
|
+
port = parsed.port or 8081
|
|
165
|
+
log_dir = _service_log_dir(config_path)
|
|
166
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
log_path = _service_log_path(config_path)
|
|
168
|
+
|
|
169
|
+
env = os.environ.copy()
|
|
170
|
+
env["HOST"] = host
|
|
171
|
+
env["PORT"] = str(port)
|
|
172
|
+
env["WDK_EVM_NETWORK"] = network
|
|
173
|
+
|
|
174
|
+
with log_path.open("a", encoding="utf-8") as log_file:
|
|
175
|
+
process = subprocess.Popen( # noqa: S603
|
|
176
|
+
["sh", str(run_local)],
|
|
177
|
+
cwd=str(wdk_wallet_root),
|
|
178
|
+
env=env,
|
|
179
|
+
stdin=subprocess.DEVNULL,
|
|
180
|
+
stdout=log_file,
|
|
181
|
+
stderr=log_file,
|
|
182
|
+
start_new_session=True,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
deadline = time.time() + 30.0
|
|
186
|
+
while time.time() < deadline:
|
|
187
|
+
if _service_is_healthy(service_url):
|
|
188
|
+
return {
|
|
189
|
+
"started": True,
|
|
190
|
+
"already_healthy": False,
|
|
191
|
+
"pid": process.pid,
|
|
192
|
+
"log_path": str(log_path),
|
|
193
|
+
}
|
|
194
|
+
if process.poll() is not None:
|
|
195
|
+
raise SystemExit(
|
|
196
|
+
f"wdk-evm-wallet exited before becoming healthy. Check log: {log_path}"
|
|
197
|
+
)
|
|
198
|
+
time.sleep(0.5)
|
|
199
|
+
|
|
200
|
+
raise SystemExit(
|
|
201
|
+
f"Timed out waiting for wdk-evm-wallet health at {_health_url(service_url)}. Check log: {log_path}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def main() -> int:
|
|
206
|
+
args = build_parser().parse_args()
|
|
207
|
+
effective_network = _normalize_network(args.network)
|
|
208
|
+
_require_local_service_url(args.service_url)
|
|
209
|
+
config_path = Path(args.config_path).expanduser()
|
|
210
|
+
config_created = _ensure_openclaw_config(config_path)
|
|
211
|
+
service_bootstrap: dict[str, object] | None = None
|
|
212
|
+
if args.auto_start_service:
|
|
213
|
+
service_bootstrap = _auto_start_local_service(
|
|
214
|
+
service_url=args.service_url,
|
|
215
|
+
network=effective_network,
|
|
216
|
+
wdk_wallet_root=Path(args.wdk_wallet_root).expanduser(),
|
|
217
|
+
config_path=config_path,
|
|
218
|
+
)
|
|
219
|
+
elif not _service_is_healthy(args.service_url):
|
|
220
|
+
raise SystemExit(
|
|
221
|
+
f"EVM service is not healthy at {_health_url(args.service_url)} and --no-auto-start-service was set."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
stdin_text = sys.stdin.read() if args.password_stdin else None
|
|
225
|
+
setup_payload = _run_script(
|
|
226
|
+
args.python_bin,
|
|
227
|
+
"manage_openclaw_evm_wallet.py",
|
|
228
|
+
[
|
|
229
|
+
"setup",
|
|
230
|
+
"--user-id",
|
|
231
|
+
args.user_id,
|
|
232
|
+
"--network",
|
|
233
|
+
effective_network,
|
|
234
|
+
"--service-url",
|
|
235
|
+
args.service_url,
|
|
236
|
+
"--label",
|
|
237
|
+
args.label,
|
|
238
|
+
"--account-index",
|
|
239
|
+
str(args.account_index),
|
|
240
|
+
*([] if not args.password_stdin else ["--password-stdin"]),
|
|
241
|
+
*(["--bind-network-pair"] if args.bind_network_pair else ["--no-bind-network-pair"]),
|
|
242
|
+
],
|
|
243
|
+
stdin_text=stdin_text,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
wallet = dict(setup_payload.get("wallet") or {})
|
|
247
|
+
install_args = [
|
|
248
|
+
"--config-path",
|
|
249
|
+
str(config_path),
|
|
250
|
+
"--plugin-id",
|
|
251
|
+
args.plugin_id,
|
|
252
|
+
"--user-id",
|
|
253
|
+
args.user_id,
|
|
254
|
+
"--backend",
|
|
255
|
+
"wdk_evm_local",
|
|
256
|
+
"--network",
|
|
257
|
+
effective_network,
|
|
258
|
+
"--wdk-evm-service-url",
|
|
259
|
+
args.service_url,
|
|
260
|
+
"--wdk-evm-wallet-id",
|
|
261
|
+
str(wallet.get("wallet_id") or ""),
|
|
262
|
+
"--wdk-evm-account-index",
|
|
263
|
+
str(wallet.get("account_index") or args.account_index),
|
|
264
|
+
"--package-root",
|
|
265
|
+
args.package_root,
|
|
266
|
+
"--python-bin",
|
|
267
|
+
args.python_bin,
|
|
268
|
+
"--sign-only" if args.sign_only else "--no-sign-only",
|
|
269
|
+
]
|
|
270
|
+
install_payload = _run_script(
|
|
271
|
+
args.python_bin,
|
|
272
|
+
"install_openclaw_local_config.py",
|
|
273
|
+
install_args,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
print(
|
|
277
|
+
json.dumps(
|
|
278
|
+
{
|
|
279
|
+
"ok": True,
|
|
280
|
+
"config_created": config_created,
|
|
281
|
+
"service_bootstrap": service_bootstrap,
|
|
282
|
+
"evm_setup": setup_payload,
|
|
283
|
+
"openclaw_install": install_payload,
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
return 0
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
if __name__ == "__main__":
|
|
291
|
+
raise SystemExit(main())
|
|
@@ -72,6 +72,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
72
72
|
parser.add_argument("--wdk-btc-service-url", default="")
|
|
73
73
|
parser.add_argument("--wdk-btc-wallet-id", default="")
|
|
74
74
|
parser.add_argument("--wdk-btc-account-index", type=int, default=0)
|
|
75
|
+
parser.add_argument("--wdk-evm-service-url", default="")
|
|
76
|
+
parser.add_argument("--wdk-evm-wallet-id", default="")
|
|
77
|
+
parser.add_argument("--wdk-evm-account-index", type=int, default=0)
|
|
75
78
|
parser.add_argument("--sign-only", action=argparse.BooleanOptionalAction, default=False)
|
|
76
79
|
parser.add_argument("--encrypt-user-wallets", action=argparse.BooleanOptionalAction, default=True)
|
|
77
80
|
parser.add_argument(
|
|
@@ -183,6 +186,12 @@ def main() -> None:
|
|
|
183
186
|
plugin_config["wdkBtcWalletId"] = args.wdk_btc_wallet_id.strip()
|
|
184
187
|
if args.wdk_btc_account_index is not None:
|
|
185
188
|
plugin_config["wdkBtcAccountIndex"] = int(args.wdk_btc_account_index)
|
|
189
|
+
if args.wdk_evm_service_url.strip():
|
|
190
|
+
plugin_config["wdkEvmServiceUrl"] = args.wdk_evm_service_url.strip()
|
|
191
|
+
if args.wdk_evm_wallet_id.strip():
|
|
192
|
+
plugin_config["wdkEvmWalletId"] = args.wdk_evm_wallet_id.strip()
|
|
193
|
+
if args.wdk_evm_account_index is not None:
|
|
194
|
+
plugin_config["wdkEvmAccountIndex"] = int(args.wdk_evm_account_index)
|
|
186
195
|
if args.write_master_key:
|
|
187
196
|
raise SystemExit(
|
|
188
197
|
"Refusing to write masterKey into config. Runtime secrets must live in sealed_keys.json."
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Host-side helper for managing a local OpenClaw EVM wallet binding."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from getpass import getpass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from urllib.error import URLError
|
|
12
|
+
from urllib.request import urlopen
|
|
13
|
+
|
|
14
|
+
PACKAGE_ROOT = Path(__file__).resolve().parents[1]
|
|
15
|
+
if str(PACKAGE_ROOT) not in sys.path:
|
|
16
|
+
sys.path.insert(0, str(PACKAGE_ROOT))
|
|
17
|
+
|
|
18
|
+
from agent_wallet.config import settings # noqa: E402
|
|
19
|
+
from agent_wallet.evm_user_wallets import ( # noqa: E402
|
|
20
|
+
bind_user_evm_wallet,
|
|
21
|
+
create_user_evm_wallet,
|
|
22
|
+
get_user_evm_wallet_binding,
|
|
23
|
+
import_user_evm_wallet,
|
|
24
|
+
list_user_evm_wallet_bindings,
|
|
25
|
+
lock_user_evm_wallet,
|
|
26
|
+
unlock_user_evm_wallet,
|
|
27
|
+
)
|
|
28
|
+
from agent_wallet.providers.wdk_evm_local import WdkEvmLocalClient # noqa: E402
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _normalize_network(value: str) -> str:
|
|
32
|
+
network = str(value or "").strip().lower()
|
|
33
|
+
aliases = {
|
|
34
|
+
"mainnet": "ethereum",
|
|
35
|
+
"eth": "ethereum",
|
|
36
|
+
"eth-mainnet": "ethereum",
|
|
37
|
+
"base-mainnet": "base",
|
|
38
|
+
"base_sepolia": "base-sepolia",
|
|
39
|
+
}
|
|
40
|
+
network = aliases.get(network, network)
|
|
41
|
+
if network not in {"ethereum", "sepolia", "base", "base-sepolia"}:
|
|
42
|
+
return "ethereum"
|
|
43
|
+
return network
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _paired_network(network: str) -> str | None:
|
|
47
|
+
mapping = {
|
|
48
|
+
"ethereum": "base",
|
|
49
|
+
"base": "ethereum",
|
|
50
|
+
"sepolia": "base-sepolia",
|
|
51
|
+
"base-sepolia": "sepolia",
|
|
52
|
+
}
|
|
53
|
+
return mapping.get(_normalize_network(network))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _read_secret(
|
|
57
|
+
*,
|
|
58
|
+
prompt: str,
|
|
59
|
+
confirm_prompt: str | None = None,
|
|
60
|
+
stdin_mode: bool = False,
|
|
61
|
+
) -> str:
|
|
62
|
+
if stdin_mode:
|
|
63
|
+
value = sys.stdin.read().strip()
|
|
64
|
+
if not value:
|
|
65
|
+
raise SystemExit(f"{prompt.rstrip(':')} is required on stdin.")
|
|
66
|
+
return value
|
|
67
|
+
value = getpass(prompt)
|
|
68
|
+
if confirm_prompt is not None:
|
|
69
|
+
confirmed = getpass(confirm_prompt)
|
|
70
|
+
if value != confirmed:
|
|
71
|
+
raise SystemExit("Secrets did not match.")
|
|
72
|
+
if not value.strip():
|
|
73
|
+
raise SystemExit(f"{prompt.rstrip(':')} is required.")
|
|
74
|
+
return value.strip()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _read_password_and_seed_from_stdin() -> tuple[str, str]:
|
|
78
|
+
raw = sys.stdin.read().strip()
|
|
79
|
+
if not raw:
|
|
80
|
+
raise SystemExit("Password and seed phrase payload is required on stdin.")
|
|
81
|
+
lines = raw.splitlines()
|
|
82
|
+
if len(lines) < 2:
|
|
83
|
+
raise SystemExit(
|
|
84
|
+
"For import via stdin, provide password on the first line and the seed phrase on the remaining lines."
|
|
85
|
+
)
|
|
86
|
+
password = lines[0].strip()
|
|
87
|
+
seed_phrase = " ".join(line.strip() for line in lines[1:] if line.strip())
|
|
88
|
+
if not password or not seed_phrase:
|
|
89
|
+
raise SystemExit("Both password and seed phrase are required.")
|
|
90
|
+
return password, seed_phrase
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _service_health(service_url: str | None) -> dict[str, object]:
|
|
94
|
+
target = str(service_url or "").strip()
|
|
95
|
+
if not target:
|
|
96
|
+
return {"service_url": None, "healthy": False, "error": "service_url is not configured"}
|
|
97
|
+
health_url = f"{target.rstrip('/')}/health"
|
|
98
|
+
try:
|
|
99
|
+
with urlopen(health_url, timeout=1.5) as response:
|
|
100
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
101
|
+
return {
|
|
102
|
+
"service_url": target,
|
|
103
|
+
"healthy": int(getattr(response, "status", 0) or 0) == 200,
|
|
104
|
+
"health": payload,
|
|
105
|
+
}
|
|
106
|
+
except (URLError, TimeoutError, OSError, ValueError) as exc:
|
|
107
|
+
return {"service_url": target, "healthy": False, "error": str(exc)}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _status_payload(user_id: str | None, network: str | None, service_url: str | None) -> dict[str, object]:
|
|
111
|
+
target_service_url = str(service_url or settings.wdk_evm_service_url).strip() or None
|
|
112
|
+
payload: dict[str, object] = {
|
|
113
|
+
"ok": True,
|
|
114
|
+
"network": _normalize_network(network or "ethereum"),
|
|
115
|
+
"service": _service_health(target_service_url),
|
|
116
|
+
}
|
|
117
|
+
target_network = _normalize_network(network or "ethereum")
|
|
118
|
+
if target_service_url:
|
|
119
|
+
try:
|
|
120
|
+
payload["network_info"] = WdkEvmLocalClient(target_service_url).get_sync("/v1/evm/network")
|
|
121
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
122
|
+
payload["network_info_error"] = str(exc)
|
|
123
|
+
if user_id:
|
|
124
|
+
payload["bindings"] = list_user_evm_wallet_bindings(user_id)
|
|
125
|
+
try:
|
|
126
|
+
payload["binding"] = get_user_evm_wallet_binding(user_id, network=target_network)
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
payload["binding_error"] = str(exc)
|
|
129
|
+
return payload
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main() -> int:
|
|
133
|
+
parser = argparse.ArgumentParser(description="Manage a local OpenClaw EVM wallet binding")
|
|
134
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
135
|
+
|
|
136
|
+
common_parent = argparse.ArgumentParser(add_help=False)
|
|
137
|
+
common_parent.add_argument("--network", default="ethereum")
|
|
138
|
+
common_parent.add_argument("--service-url")
|
|
139
|
+
|
|
140
|
+
get_parser = subparsers.add_parser("get", parents=[common_parent])
|
|
141
|
+
get_parser.add_argument("--user-id", required=True)
|
|
142
|
+
|
|
143
|
+
list_parser = subparsers.add_parser("list", parents=[common_parent])
|
|
144
|
+
list_parser.add_argument("--user-id", required=True)
|
|
145
|
+
|
|
146
|
+
status_parser = subparsers.add_parser("status", parents=[common_parent])
|
|
147
|
+
status_parser.add_argument("--user-id", default="")
|
|
148
|
+
|
|
149
|
+
setup_parser = subparsers.add_parser("setup", parents=[common_parent])
|
|
150
|
+
setup_parser.add_argument("--user-id", required=True)
|
|
151
|
+
setup_parser.add_argument("--label")
|
|
152
|
+
setup_parser.add_argument("--account-index", type=int)
|
|
153
|
+
setup_parser.add_argument("--password-stdin", action="store_true")
|
|
154
|
+
setup_parser.add_argument("--bind-network-pair", action=argparse.BooleanOptionalAction, default=True)
|
|
155
|
+
|
|
156
|
+
create_parser = subparsers.add_parser("create", parents=[common_parent])
|
|
157
|
+
create_parser.add_argument("--user-id", required=True)
|
|
158
|
+
create_parser.add_argument("--label")
|
|
159
|
+
create_parser.add_argument("--account-index", type=int)
|
|
160
|
+
create_parser.add_argument("--reveal-seed", action="store_true")
|
|
161
|
+
create_parser.add_argument("--password-stdin", action="store_true")
|
|
162
|
+
create_parser.add_argument("--bind-network-pair", action=argparse.BooleanOptionalAction, default=True)
|
|
163
|
+
|
|
164
|
+
import_parser = subparsers.add_parser("import", parents=[common_parent])
|
|
165
|
+
import_parser.add_argument("--user-id", required=True)
|
|
166
|
+
import_parser.add_argument("--label")
|
|
167
|
+
import_parser.add_argument("--account-index", type=int)
|
|
168
|
+
import_parser.add_argument("--password-stdin", action="store_true")
|
|
169
|
+
import_parser.add_argument("--seed-stdin", action="store_true")
|
|
170
|
+
import_parser.add_argument("--bind-network-pair", action=argparse.BooleanOptionalAction, default=True)
|
|
171
|
+
|
|
172
|
+
unlock_parser = subparsers.add_parser("unlock", parents=[common_parent])
|
|
173
|
+
unlock_parser.add_argument("--user-id", required=True)
|
|
174
|
+
unlock_parser.add_argument("--password-stdin", action="store_true")
|
|
175
|
+
unlock_parser.add_argument("--wallet-id", default="")
|
|
176
|
+
unlock_parser.add_argument("--account-index", type=int)
|
|
177
|
+
unlock_parser.add_argument("--bind-network-pair", action=argparse.BooleanOptionalAction, default=True)
|
|
178
|
+
|
|
179
|
+
lock_parser = subparsers.add_parser("lock", parents=[common_parent])
|
|
180
|
+
lock_parser.add_argument("--user-id", required=True)
|
|
181
|
+
lock_parser.add_argument("--wallet-id", default="")
|
|
182
|
+
lock_parser.add_argument("--account-index", type=int)
|
|
183
|
+
|
|
184
|
+
args = parser.parse_args()
|
|
185
|
+
effective_network = _normalize_network(args.network)
|
|
186
|
+
|
|
187
|
+
def _config_hint(wallet: dict[str, object]) -> dict[str, object]:
|
|
188
|
+
return {
|
|
189
|
+
"backend": "wdk_evm_local",
|
|
190
|
+
"network": effective_network,
|
|
191
|
+
"wdkEvmServiceUrl": args.service_url,
|
|
192
|
+
"wdkEvmWalletId": wallet.get("wallet_id"),
|
|
193
|
+
"wdkEvmAccountIndex": wallet.get("account_index"),
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
def _bind_pair(wallet: dict[str, object]) -> dict[str, object] | None:
|
|
197
|
+
if not getattr(args, "bind_network_pair", False):
|
|
198
|
+
return None
|
|
199
|
+
paired = _paired_network(effective_network)
|
|
200
|
+
if not paired:
|
|
201
|
+
return None
|
|
202
|
+
return bind_user_evm_wallet(
|
|
203
|
+
args.user_id,
|
|
204
|
+
wallet_id=str(wallet.get("wallet_id") or ""),
|
|
205
|
+
network=paired,
|
|
206
|
+
service_url=args.service_url,
|
|
207
|
+
account_index=wallet.get("account_index"),
|
|
208
|
+
tolerate_locked=True,
|
|
209
|
+
fallback_address=str(wallet.get("address") or "").strip() or None,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if args.command == "status":
|
|
213
|
+
payload = _status_payload(args.user_id or None, effective_network, args.service_url)
|
|
214
|
+
elif args.command == "list":
|
|
215
|
+
payload = {"ok": True, "wallets": list_user_evm_wallet_bindings(args.user_id)}
|
|
216
|
+
elif args.command == "get":
|
|
217
|
+
wallet = get_user_evm_wallet_binding(args.user_id, network=effective_network)
|
|
218
|
+
payload = {"ok": True, "wallet": wallet, "openclaw_config_hint": _config_hint(wallet)}
|
|
219
|
+
elif args.command == "setup":
|
|
220
|
+
password = _read_secret(
|
|
221
|
+
prompt="EVM wallet password: ",
|
|
222
|
+
confirm_prompt=None,
|
|
223
|
+
stdin_mode=bool(args.password_stdin),
|
|
224
|
+
)
|
|
225
|
+
try:
|
|
226
|
+
existing = get_user_evm_wallet_binding(args.user_id, network=effective_network)
|
|
227
|
+
except Exception:
|
|
228
|
+
existing = None
|
|
229
|
+
|
|
230
|
+
if existing is None:
|
|
231
|
+
wallet = create_user_evm_wallet(
|
|
232
|
+
args.user_id,
|
|
233
|
+
password=password,
|
|
234
|
+
label=args.label,
|
|
235
|
+
network=effective_network,
|
|
236
|
+
service_url=args.service_url,
|
|
237
|
+
account_index=args.account_index,
|
|
238
|
+
)
|
|
239
|
+
paired_binding = _bind_pair(wallet)
|
|
240
|
+
payload = {
|
|
241
|
+
"ok": True,
|
|
242
|
+
"action": "created",
|
|
243
|
+
"wallet": wallet,
|
|
244
|
+
"paired_binding": paired_binding,
|
|
245
|
+
"openclaw_config_hint": _config_hint(wallet),
|
|
246
|
+
}
|
|
247
|
+
else:
|
|
248
|
+
wallet = unlock_user_evm_wallet(
|
|
249
|
+
args.user_id,
|
|
250
|
+
password=password,
|
|
251
|
+
network=effective_network,
|
|
252
|
+
service_url=args.service_url,
|
|
253
|
+
account_index=args.account_index,
|
|
254
|
+
)
|
|
255
|
+
paired_binding = _bind_pair(wallet)
|
|
256
|
+
payload = {
|
|
257
|
+
"ok": True,
|
|
258
|
+
"action": "unlocked",
|
|
259
|
+
"wallet": wallet,
|
|
260
|
+
"paired_binding": paired_binding,
|
|
261
|
+
"openclaw_config_hint": _config_hint(wallet),
|
|
262
|
+
}
|
|
263
|
+
elif args.command == "create":
|
|
264
|
+
wallet = create_user_evm_wallet(
|
|
265
|
+
args.user_id,
|
|
266
|
+
password=_read_secret(
|
|
267
|
+
prompt="EVM wallet password: ",
|
|
268
|
+
confirm_prompt="Confirm EVM wallet password: ",
|
|
269
|
+
stdin_mode=bool(args.password_stdin),
|
|
270
|
+
),
|
|
271
|
+
label=args.label,
|
|
272
|
+
network=effective_network,
|
|
273
|
+
service_url=args.service_url,
|
|
274
|
+
reveal_seed_phrase=bool(args.reveal_seed),
|
|
275
|
+
account_index=args.account_index,
|
|
276
|
+
)
|
|
277
|
+
payload = {
|
|
278
|
+
"ok": True,
|
|
279
|
+
"wallet": wallet,
|
|
280
|
+
"paired_binding": _bind_pair(wallet),
|
|
281
|
+
}
|
|
282
|
+
elif args.command == "import":
|
|
283
|
+
if args.password_stdin and args.seed_stdin:
|
|
284
|
+
password, seed_phrase = _read_password_and_seed_from_stdin()
|
|
285
|
+
else:
|
|
286
|
+
password = _read_secret(
|
|
287
|
+
prompt="EVM wallet password: ",
|
|
288
|
+
confirm_prompt="Confirm EVM wallet password: ",
|
|
289
|
+
stdin_mode=bool(args.password_stdin),
|
|
290
|
+
)
|
|
291
|
+
seed_phrase = _read_secret(
|
|
292
|
+
prompt="EVM seed phrase: ",
|
|
293
|
+
stdin_mode=bool(args.seed_stdin),
|
|
294
|
+
)
|
|
295
|
+
wallet = import_user_evm_wallet(
|
|
296
|
+
args.user_id,
|
|
297
|
+
password=password,
|
|
298
|
+
seed_phrase=seed_phrase,
|
|
299
|
+
label=args.label,
|
|
300
|
+
network=effective_network,
|
|
301
|
+
service_url=args.service_url,
|
|
302
|
+
account_index=args.account_index,
|
|
303
|
+
)
|
|
304
|
+
payload = {
|
|
305
|
+
"ok": True,
|
|
306
|
+
"wallet": wallet,
|
|
307
|
+
"paired_binding": _bind_pair(wallet),
|
|
308
|
+
}
|
|
309
|
+
elif args.command == "unlock":
|
|
310
|
+
wallet = unlock_user_evm_wallet(
|
|
311
|
+
args.user_id,
|
|
312
|
+
password=_read_secret(
|
|
313
|
+
prompt="EVM wallet password: ",
|
|
314
|
+
stdin_mode=bool(args.password_stdin),
|
|
315
|
+
),
|
|
316
|
+
network=effective_network,
|
|
317
|
+
service_url=args.service_url,
|
|
318
|
+
wallet_id=args.wallet_id or None,
|
|
319
|
+
account_index=args.account_index,
|
|
320
|
+
)
|
|
321
|
+
payload = {
|
|
322
|
+
"ok": True,
|
|
323
|
+
"wallet": wallet,
|
|
324
|
+
"paired_binding": _bind_pair(wallet),
|
|
325
|
+
}
|
|
326
|
+
else:
|
|
327
|
+
payload = {
|
|
328
|
+
"ok": True,
|
|
329
|
+
"wallet": lock_user_evm_wallet(
|
|
330
|
+
args.user_id,
|
|
331
|
+
network=effective_network,
|
|
332
|
+
service_url=args.service_url,
|
|
333
|
+
wallet_id=args.wallet_id or None,
|
|
334
|
+
account_index=args.account_index,
|
|
335
|
+
),
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
print(json.dumps(payload))
|
|
339
|
+
return 0
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
if __name__ == "__main__":
|
|
343
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
5
|
+
PACKAGE_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
|
|
6
|
+
|
|
7
|
+
resolve_python_bin() {
|
|
8
|
+
if [ -n "${OPENCLAW_AGENT_WALLET_PYTHON:-}" ] && [ -x "${OPENCLAW_AGENT_WALLET_PYTHON}" ]; then
|
|
9
|
+
printf "%s" "${OPENCLAW_AGENT_WALLET_PYTHON}"
|
|
10
|
+
return 0
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
if [ -x "/tmp/agent-wallet-venv/bin/python" ]; then
|
|
14
|
+
printf "%s" "/tmp/agent-wallet-venv/bin/python"
|
|
15
|
+
return 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
if [ -x "$PACKAGE_ROOT/.venv/bin/python" ]; then
|
|
19
|
+
printf "%s" "$PACKAGE_ROOT/.venv/bin/python"
|
|
20
|
+
return 0
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
24
|
+
command -v python3
|
|
25
|
+
return 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
command -v python
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
PYTHON_BIN=$(resolve_python_bin)
|
|
32
|
+
export OPENCLAW_AGENT_WALLET_PYTHON="$PYTHON_BIN"
|
|
33
|
+
|
|
34
|
+
has_flag() {
|
|
35
|
+
flag=$1
|
|
36
|
+
shift
|
|
37
|
+
for arg in "$@"; do
|
|
38
|
+
case "$arg" in
|
|
39
|
+
"$flag"|"$flag"=*)
|
|
40
|
+
return 0
|
|
41
|
+
;;
|
|
42
|
+
esac
|
|
43
|
+
done
|
|
44
|
+
return 1
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
prompt_with_default() {
|
|
48
|
+
label=$1
|
|
49
|
+
default_value=$2
|
|
50
|
+
if [ -t 0 ]; then
|
|
51
|
+
printf "%s [%s]: " "$label" "$default_value" >&2
|
|
52
|
+
read -r value
|
|
53
|
+
if [ -z "${value:-}" ]; then
|
|
54
|
+
printf "%s" "$default_value"
|
|
55
|
+
else
|
|
56
|
+
printf "%s" "$value"
|
|
57
|
+
fi
|
|
58
|
+
return 0
|
|
59
|
+
fi
|
|
60
|
+
printf "%s" "$default_value"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
normalize_network_value() {
|
|
64
|
+
case $(printf "%s" "$1" | tr '[:upper:]' '[:lower:]') in
|
|
65
|
+
1|ethereum|eth|mainnet)
|
|
66
|
+
printf "ethereum"
|
|
67
|
+
;;
|
|
68
|
+
2|base)
|
|
69
|
+
printf "base"
|
|
70
|
+
;;
|
|
71
|
+
3|sepolia)
|
|
72
|
+
printf "sepolia"
|
|
73
|
+
;;
|
|
74
|
+
4|base-sepolia|base_sepolia)
|
|
75
|
+
printf "base-sepolia"
|
|
76
|
+
;;
|
|
77
|
+
*)
|
|
78
|
+
return 1
|
|
79
|
+
;;
|
|
80
|
+
esac
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
prompt_network_choice() {
|
|
84
|
+
default_value=$1
|
|
85
|
+
if ! [ -t 0 ]; then
|
|
86
|
+
printf "%s" "$default_value"
|
|
87
|
+
return 0
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
case "$default_value" in
|
|
91
|
+
ethereum) default_hint="1" ;;
|
|
92
|
+
base) default_hint="2" ;;
|
|
93
|
+
sepolia) default_hint="3" ;;
|
|
94
|
+
base-sepolia) default_hint="4" ;;
|
|
95
|
+
*) default_hint="2" ;;
|
|
96
|
+
esac
|
|
97
|
+
|
|
98
|
+
while true; do
|
|
99
|
+
printf "EVM network:\n" >&2
|
|
100
|
+
printf " 1) ethereum\n" >&2
|
|
101
|
+
printf " 2) base\n" >&2
|
|
102
|
+
printf " 3) sepolia\n" >&2
|
|
103
|
+
printf " 4) base-sepolia\n" >&2
|
|
104
|
+
printf "Choose network [%s]: " "$default_hint" >&2
|
|
105
|
+
read -r choice
|
|
106
|
+
if [ -z "${choice:-}" ]; then
|
|
107
|
+
choice=$default_hint
|
|
108
|
+
fi
|
|
109
|
+
if network=$(normalize_network_value "$choice"); then
|
|
110
|
+
printf "%s" "$network"
|
|
111
|
+
return 0
|
|
112
|
+
fi
|
|
113
|
+
printf "Invalid choice. Enter 1, 2, 3, 4, ethereum, base, sepolia, or base-sepolia.\n" >&2
|
|
114
|
+
done
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
DEFAULT_USER_ID=${OPENCLAW_EVM_USER_ID:-${USER:-openclaw-user}-local}
|
|
118
|
+
DEFAULT_NETWORK=${OPENCLAW_EVM_NETWORK:-base}
|
|
119
|
+
DEFAULT_SERVICE_URL=${OPENCLAW_EVM_SERVICE_URL:-http://127.0.0.1:8081}
|
|
120
|
+
|
|
121
|
+
if ! has_flag --user-id "$@"; then
|
|
122
|
+
USER_ID=$(prompt_with_default "OpenClaw user id" "$DEFAULT_USER_ID")
|
|
123
|
+
set -- "$@" --user-id "$USER_ID"
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
if ! has_flag --network "$@"; then
|
|
127
|
+
NETWORK=$(prompt_network_choice "$DEFAULT_NETWORK")
|
|
128
|
+
set -- "$@" --network "$NETWORK"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
if ! has_flag --service-url "$@"; then
|
|
132
|
+
set -- "$@" --service-url "$DEFAULT_SERVICE_URL"
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
if ! has_flag --config-path "$@" && [ -n "${OPENCLAW_EVM_CONFIG_PATH:-}" ]; then
|
|
136
|
+
set -- "$@" --config-path "$OPENCLAW_EVM_CONFIG_PATH"
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
if ! has_flag --wdk-wallet-root "$@" && [ -n "${OPENCLAW_EVM_WDK_WALLET_ROOT:-}" ]; then
|
|
140
|
+
set -- "$@" --wdk-wallet-root "$OPENCLAW_EVM_WDK_WALLET_ROOT"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
if ! has_flag --python-bin "$@"; then
|
|
144
|
+
set -- "$@" --python-bin "$PYTHON_BIN"
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
if ! has_flag --package-root "$@"; then
|
|
148
|
+
set -- "$@" --package-root "$PACKAGE_ROOT"
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
exec "$PYTHON_BIN" "$SCRIPT_DIR/bootstrap_openclaw_evm.py" "$@"
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
"""Hermes Agent plugin bridge for AgentLayer wallet tools."""
|
|
2
2
|
|
|
3
|
-
from .schemas import
|
|
4
|
-
|
|
3
|
+
from .schemas import (
|
|
4
|
+
AGENT_WALLET_APPROVE,
|
|
5
|
+
AGENT_WALLET_EVM_SETUP,
|
|
6
|
+
AGENT_WALLET_EVM_STATUS,
|
|
7
|
+
AGENT_WALLET_INVOKE,
|
|
8
|
+
AGENT_WALLET_TOOLS,
|
|
9
|
+
)
|
|
10
|
+
from .tools import (
|
|
11
|
+
agent_wallet_approve,
|
|
12
|
+
agent_wallet_evm_setup,
|
|
13
|
+
agent_wallet_evm_status,
|
|
14
|
+
agent_wallet_invoke,
|
|
15
|
+
agent_wallet_tools,
|
|
16
|
+
)
|
|
5
17
|
|
|
6
18
|
|
|
7
19
|
def register(ctx):
|
|
@@ -27,3 +39,17 @@ def register(ctx):
|
|
|
27
39
|
handler=agent_wallet_approve,
|
|
28
40
|
description=AGENT_WALLET_APPROVE["description"],
|
|
29
41
|
)
|
|
42
|
+
ctx.register_tool(
|
|
43
|
+
name=AGENT_WALLET_EVM_STATUS["name"],
|
|
44
|
+
toolset="agent_wallet",
|
|
45
|
+
schema=AGENT_WALLET_EVM_STATUS,
|
|
46
|
+
handler=agent_wallet_evm_status,
|
|
47
|
+
description=AGENT_WALLET_EVM_STATUS["description"],
|
|
48
|
+
)
|
|
49
|
+
ctx.register_tool(
|
|
50
|
+
name=AGENT_WALLET_EVM_SETUP["name"],
|
|
51
|
+
toolset="agent_wallet",
|
|
52
|
+
schema=AGENT_WALLET_EVM_SETUP,
|
|
53
|
+
handler=agent_wallet_evm_setup,
|
|
54
|
+
description=AGENT_WALLET_EVM_SETUP["description"],
|
|
55
|
+
)
|
|
@@ -132,3 +132,75 @@ AGENT_WALLET_APPROVE = {
|
|
|
132
132
|
"additionalProperties": False,
|
|
133
133
|
},
|
|
134
134
|
}
|
|
135
|
+
|
|
136
|
+
AGENT_WALLET_EVM_STATUS = {
|
|
137
|
+
"name": "agent_wallet_evm_status",
|
|
138
|
+
"description": (
|
|
139
|
+
"Inspect the local EVM wallet runtime used by AgentLayer/OpenClaw. "
|
|
140
|
+
"Returns wdk-evm-wallet health, network info, and existing user wallet "
|
|
141
|
+
"bindings without changing wallet state."
|
|
142
|
+
),
|
|
143
|
+
"parameters": {
|
|
144
|
+
"type": "object",
|
|
145
|
+
"properties": {
|
|
146
|
+
"user_id": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"description": "Optional local wallet owner id to inspect.",
|
|
149
|
+
},
|
|
150
|
+
"network": {
|
|
151
|
+
"type": "string",
|
|
152
|
+
"description": "Optional EVM network hint, such as ethereum, base, sepolia, or base-sepolia.",
|
|
153
|
+
},
|
|
154
|
+
"service_url": {
|
|
155
|
+
"type": "string",
|
|
156
|
+
"description": "Optional localhost override for the wdk-evm-wallet service.",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
"additionalProperties": False,
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
AGENT_WALLET_EVM_SETUP = {
|
|
164
|
+
"name": "agent_wallet_evm_setup",
|
|
165
|
+
"description": (
|
|
166
|
+
"Create or unlock the local EVM wallet binding used by AgentLayer/OpenClaw "
|
|
167
|
+
"for Hermes. This can auto-start the localhost-only wdk-evm-wallet service, "
|
|
168
|
+
"set up the selected network, and bind the same wallet to the paired EVM network "
|
|
169
|
+
"such as ethereum/base."
|
|
170
|
+
),
|
|
171
|
+
"parameters": {
|
|
172
|
+
"type": "object",
|
|
173
|
+
"properties": {
|
|
174
|
+
"password": {
|
|
175
|
+
"type": "string",
|
|
176
|
+
"description": "Local EVM wallet password used to create or unlock the vault wallet.",
|
|
177
|
+
},
|
|
178
|
+
"user_id": {
|
|
179
|
+
"type": "string",
|
|
180
|
+
"description": "Optional local wallet owner id. Defaults to AGENT_WALLET_USER_ID, USER, or hermes-local-user.",
|
|
181
|
+
},
|
|
182
|
+
"network": {
|
|
183
|
+
"type": "string",
|
|
184
|
+
"description": "Selected EVM network, typically ethereum or base.",
|
|
185
|
+
},
|
|
186
|
+
"label": {
|
|
187
|
+
"type": "string",
|
|
188
|
+
"description": "Optional wallet label used when creating a new local EVM wallet.",
|
|
189
|
+
},
|
|
190
|
+
"service_url": {
|
|
191
|
+
"type": "string",
|
|
192
|
+
"description": "Optional localhost override for the wdk-evm-wallet service.",
|
|
193
|
+
},
|
|
194
|
+
"auto_start_service": {
|
|
195
|
+
"type": "boolean",
|
|
196
|
+
"description": "Whether to auto-start the local wdk-evm-wallet service when it is not healthy. Defaults to true.",
|
|
197
|
+
},
|
|
198
|
+
"bind_network_pair": {
|
|
199
|
+
"type": "boolean",
|
|
200
|
+
"description": "Whether to also bind the paired EVM network such as ethereum/base. Defaults to true.",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
"required": ["password"],
|
|
204
|
+
"additionalProperties": False,
|
|
205
|
+
},
|
|
206
|
+
}
|
|
@@ -174,6 +174,13 @@ def _python_bin(package_root: Path) -> str:
|
|
|
174
174
|
return "python3"
|
|
175
175
|
|
|
176
176
|
|
|
177
|
+
def _script_path(package_root: Path, name: str) -> Path:
|
|
178
|
+
script = package_root / "scripts" / name
|
|
179
|
+
if not script.exists():
|
|
180
|
+
raise RuntimeError(f"Required host script is missing: {script}")
|
|
181
|
+
return script
|
|
182
|
+
|
|
183
|
+
|
|
177
184
|
def _user_id(args: dict[str, Any]) -> str:
|
|
178
185
|
value = (
|
|
179
186
|
args.get("user_id")
|
|
@@ -277,6 +284,37 @@ def _call_wallet_cli(args: dict[str, Any]) -> dict[str, Any]:
|
|
|
277
284
|
return {"ok": False, "error": f"wallet CLI returned invalid JSON: {exc}"}
|
|
278
285
|
|
|
279
286
|
|
|
287
|
+
def _run_host_script(
|
|
288
|
+
package_root: Path,
|
|
289
|
+
script_name: str,
|
|
290
|
+
script_args: list[str],
|
|
291
|
+
*,
|
|
292
|
+
stdin_text: str | None = None,
|
|
293
|
+
) -> dict[str, Any]:
|
|
294
|
+
command = [
|
|
295
|
+
_python_bin(package_root),
|
|
296
|
+
str(_script_path(package_root, script_name)),
|
|
297
|
+
*script_args,
|
|
298
|
+
]
|
|
299
|
+
completed = subprocess.run(
|
|
300
|
+
command,
|
|
301
|
+
cwd=str(package_root),
|
|
302
|
+
env=_cli_env(package_root),
|
|
303
|
+
text=True,
|
|
304
|
+
input=stdin_text,
|
|
305
|
+
capture_output=True,
|
|
306
|
+
timeout=float(os.getenv("AGENT_WALLET_HERMES_TIMEOUT", "120")),
|
|
307
|
+
check=False,
|
|
308
|
+
)
|
|
309
|
+
if completed.returncode != 0:
|
|
310
|
+
detail = completed.stderr.strip() or completed.stdout.strip()
|
|
311
|
+
return {"ok": False, "error": detail or f"host script exited {completed.returncode}"}
|
|
312
|
+
try:
|
|
313
|
+
return json.loads(completed.stdout.strip() or "{}")
|
|
314
|
+
except json.JSONDecodeError as exc:
|
|
315
|
+
return {"ok": False, "error": f"host script returned invalid JSON: {exc}"}
|
|
316
|
+
|
|
317
|
+
|
|
280
318
|
def _call_issue_approval(args: dict[str, Any]) -> dict[str, Any]:
|
|
281
319
|
if args.get("user_confirmed") is not True:
|
|
282
320
|
raise RuntimeError(
|
|
@@ -337,6 +375,54 @@ def _call_issue_approval(args: dict[str, Any]) -> dict[str, Any]:
|
|
|
337
375
|
return {"ok": False, "error": f"wallet CLI returned invalid JSON: {exc}"}
|
|
338
376
|
|
|
339
377
|
|
|
378
|
+
def _call_evm_status(args: dict[str, Any]) -> dict[str, Any]:
|
|
379
|
+
package_root = _resolve_package_root()
|
|
380
|
+
command = ["status"]
|
|
381
|
+
user_id = str(args.get("user_id") or "").strip()
|
|
382
|
+
network = str(args.get("network") or "").strip()
|
|
383
|
+
service_url = str(args.get("service_url") or "").strip()
|
|
384
|
+
if user_id:
|
|
385
|
+
command.extend(["--user-id", user_id])
|
|
386
|
+
if network:
|
|
387
|
+
command.extend(["--network", network])
|
|
388
|
+
if service_url:
|
|
389
|
+
command.extend(["--service-url", service_url])
|
|
390
|
+
return _run_host_script(package_root, "manage_openclaw_evm_wallet.py", command)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _call_evm_setup(args: dict[str, Any]) -> dict[str, Any]:
|
|
394
|
+
package_root = _resolve_package_root()
|
|
395
|
+
password = str(args.get("password") or "").strip()
|
|
396
|
+
if not password:
|
|
397
|
+
raise RuntimeError("password is required.")
|
|
398
|
+
command = [
|
|
399
|
+
"--user-id",
|
|
400
|
+
_user_id(args),
|
|
401
|
+
"--password-stdin",
|
|
402
|
+
]
|
|
403
|
+
network = str(args.get("network") or "").strip()
|
|
404
|
+
label = str(args.get("label") or "").strip()
|
|
405
|
+
service_url = str(args.get("service_url") or "").strip()
|
|
406
|
+
auto_start_service = args.get("auto_start_service")
|
|
407
|
+
bind_network_pair = args.get("bind_network_pair")
|
|
408
|
+
if network:
|
|
409
|
+
command.extend(["--network", network])
|
|
410
|
+
if label:
|
|
411
|
+
command.extend(["--label", label])
|
|
412
|
+
if service_url:
|
|
413
|
+
command.extend(["--service-url", service_url])
|
|
414
|
+
if auto_start_service is False:
|
|
415
|
+
command.append("--no-auto-start-service")
|
|
416
|
+
if bind_network_pair is False:
|
|
417
|
+
command.append("--no-bind-network-pair")
|
|
418
|
+
return _run_host_script(
|
|
419
|
+
package_root,
|
|
420
|
+
"bootstrap_openclaw_evm.py",
|
|
421
|
+
command,
|
|
422
|
+
stdin_text=f"{password}\n",
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
340
426
|
class _SchemaOnlyBackend:
|
|
341
427
|
def __init__(self, *, name: str, chain: str, network: str):
|
|
342
428
|
self.name = name
|
|
@@ -431,3 +517,17 @@ def agent_wallet_approve(args: dict, **kwargs) -> str:
|
|
|
431
517
|
return _json(_call_issue_approval(args or {}))
|
|
432
518
|
except Exception as exc:
|
|
433
519
|
return _json({"ok": False, "error": str(exc)})
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def agent_wallet_evm_status(args: dict, **kwargs) -> str:
|
|
523
|
+
try:
|
|
524
|
+
return _json(_call_evm_status(args or {}))
|
|
525
|
+
except Exception as exc:
|
|
526
|
+
return _json({"ok": False, "error": str(exc)})
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def agent_wallet_evm_setup(args: dict, **kwargs) -> str:
|
|
530
|
+
try:
|
|
531
|
+
return _json(_call_evm_setup(args or {}))
|
|
532
|
+
except Exception as exc:
|
|
533
|
+
return _json({"ok": False, "error": str(exc)})
|