@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.
@@ -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())
@@ -26,6 +26,7 @@ INCLUDED_RUNTIME_TOP_LEVEL_DIRS = [
26
26
  ".openclaw",
27
27
  "agent-wallet",
28
28
  "agent-a2a-gateway",
29
+ "hermes",
29
30
  "wdk-btc-wallet",
30
31
  "wdk-evm-wallet",
31
32
  ]
@@ -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())