@agentlayer.tech/wallet 0.1.10 → 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 +30 -0
- package/README.md +50 -57
- package/agent-wallet/README.md +42 -0
- package/agent-wallet/agent_wallet/config.py +11 -1
- package/agent-wallet/agent_wallet/openclaw_adapter.py +74 -9
- package/agent-wallet/agent_wallet/providers/jupiter.py +171 -2
- package/agent-wallet/pyproject.toml +2 -1
- package/agent-wallet/scripts/bootstrap_openclaw_evm.py +291 -0
- package/agent-wallet/scripts/install_agent_wallet.py +1 -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/bin/openclaw-agent-wallet.mjs +229 -5
- package/hermes/plugins/agent_wallet/README.md +54 -0
- package/hermes/plugins/agent_wallet/__init__.py +55 -0
- package/hermes/plugins/agent_wallet/plugin.yaml +9 -0
- package/hermes/plugins/agent_wallet/schemas.py +206 -0
- package/hermes/plugins/agent_wallet/tools.py +533 -0
- package/package.json +4 -1
|
@@ -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())
|