@agentlayer.tech/wallet 0.1.17 → 0.1.19

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.
Files changed (34) hide show
  1. package/.openclaw/AGENTS.md +0 -7
  2. package/.openclaw/extensions/agent-wallet/README.md +3 -2
  3. package/.openclaw/extensions/agent-wallet/dist/index.js +105 -7
  4. package/.openclaw/extensions/agent-wallet/index.ts +105 -7
  5. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +5 -1
  6. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  7. package/CHANGELOG.md +24 -0
  8. package/README.md +1 -3
  9. package/RELEASING.md +5 -15
  10. package/agent-wallet/README.md +7 -0
  11. package/agent-wallet/agent_wallet/config.py +11 -0
  12. package/agent-wallet/agent_wallet/evm_user_wallets.py +310 -2
  13. package/agent-wallet/agent_wallet/openclaw_adapter.py +303 -1
  14. package/agent-wallet/agent_wallet/openclaw_runtime.py +10 -41
  15. package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +52 -0
  16. package/agent-wallet/agent_wallet/providers/x402.py +1323 -0
  17. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +30 -0
  18. package/agent-wallet/pyproject.toml +2 -1
  19. package/agent-wallet/scripts/build_release_bundle.py +1 -0
  20. package/agent-wallet/scripts/install_agent_wallet.py +3 -0
  21. package/agent-wallet/scripts/install_openclaw_local_config.py +25 -49
  22. package/agent-wallet/scripts/install_openclaw_sealed_keys.py +9 -1
  23. package/package.json +1 -2
  24. package/wdk-evm-wallet/src/server.js +6 -0
  25. package/wdk-evm-wallet/src/wdk_evm_wallet.js +108 -0
  26. package/.openclaw/extensions/pay-bridge/README.md +0 -38
  27. package/.openclaw/extensions/pay-bridge/core.mjs +0 -287
  28. package/.openclaw/extensions/pay-bridge/dist/core.mjs +0 -287
  29. package/.openclaw/extensions/pay-bridge/dist/index.js +0 -196
  30. package/.openclaw/extensions/pay-bridge/index.ts +0 -196
  31. package/.openclaw/extensions/pay-bridge/openclaw.plugin.json +0 -34
  32. package/.openclaw/extensions/pay-bridge/package.json +0 -49
  33. package/.openclaw/extensions/pay-bridge/skills/pay-operator/SKILL.md +0 -20
  34. package/.openclaw/extensions/pay-bridge/smoke_pay_bridge.mjs +0 -38
@@ -3,14 +3,28 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import os
7
+ import secrets
8
+ import subprocess
9
+ import time
6
10
  from pathlib import Path
7
11
  from typing import Any
8
-
9
- from agent_wallet.config import resolve_openclaw_home, settings
12
+ from urllib.error import URLError
13
+ from urllib.parse import urlparse
14
+ from urllib.request import urlopen
15
+
16
+ from agent_wallet.config import (
17
+ resolve_boot_key,
18
+ resolve_evm_wallet_password,
19
+ resolve_openclaw_home,
20
+ settings,
21
+ )
10
22
  from agent_wallet.providers.wdk_evm_local import WdkEvmLocalClient
11
23
  from agent_wallet.user_wallets import normalize_user_id
12
24
  from agent_wallet.wallet_layer.base import WalletBackendError
13
25
 
26
+ LOCAL_WDK_EVM_HOSTS = {"127.0.0.1", "localhost", "::1"}
27
+
14
28
 
15
29
  def _normalize_evm_network(value: str | None) -> str:
16
30
  network = str(value or "").strip().lower()
@@ -34,6 +48,87 @@ def _resolve_service_url(service_url: str | None = None) -> str:
34
48
  return effective
35
49
 
36
50
 
51
+ def _paired_network(network: str) -> str | None:
52
+ mapping = {
53
+ "ethereum": "base",
54
+ "base": "ethereum",
55
+ "sepolia": "base-sepolia",
56
+ "base-sepolia": "sepolia",
57
+ }
58
+ return mapping.get(_normalize_evm_network(network))
59
+
60
+
61
+ def _health_url(service_url: str) -> str:
62
+ return f"{service_url.rstrip('/')}/health"
63
+
64
+
65
+ def _service_is_healthy(service_url: str) -> bool:
66
+ try:
67
+ with urlopen(_health_url(service_url), timeout=1.5) as response:
68
+ return int(getattr(response, "status", 0) or 0) == 200
69
+ except (URLError, TimeoutError, OSError):
70
+ return False
71
+
72
+
73
+ def _is_local_service_url(service_url: str) -> bool:
74
+ parsed = urlparse(service_url)
75
+ return parsed.scheme in {"http", "https"} and parsed.hostname in LOCAL_WDK_EVM_HOSTS
76
+
77
+
78
+ def _resolve_local_wdk_evm_root() -> Path | None:
79
+ configured = os.getenv("OPENCLAW_EVM_WDK_WALLET_ROOT", "").strip()
80
+ candidates = [configured] if configured else []
81
+ candidates.extend(
82
+ [
83
+ str(Path(__file__).resolve().parents[2] / "wdk-evm-wallet"),
84
+ str(resolve_openclaw_home() / "agent-wallet-runtime" / "current" / "wdk-evm-wallet"),
85
+ ]
86
+ )
87
+ for candidate in candidates:
88
+ root = Path(candidate).expanduser()
89
+ if (root / "run-local.sh").exists():
90
+ return root
91
+ return None
92
+
93
+
94
+ def _auto_start_local_service(service_url: str, network: str) -> None:
95
+ if _service_is_healthy(service_url):
96
+ return
97
+ if not _is_local_service_url(service_url):
98
+ raise WalletBackendError(
99
+ f"wdk-evm-wallet is unreachable at {_health_url(service_url)} and auto-start only supports localhost URLs."
100
+ )
101
+ wallet_root = _resolve_local_wdk_evm_root()
102
+ if wallet_root is None:
103
+ raise WalletBackendError(
104
+ "wdk-evm-wallet is not healthy and the local launcher could not be found."
105
+ )
106
+ parsed = urlparse(service_url)
107
+ env = os.environ.copy()
108
+ env["HOST"] = parsed.hostname or "127.0.0.1"
109
+ env["PORT"] = str(parsed.port or 8081)
110
+ env["WDK_EVM_NETWORK"] = _normalize_evm_network(network)
111
+ process = subprocess.Popen( # noqa: S603
112
+ ["sh", str(wallet_root / "run-local.sh")],
113
+ cwd=str(wallet_root),
114
+ env=env,
115
+ stdin=subprocess.DEVNULL,
116
+ stdout=subprocess.DEVNULL,
117
+ stderr=subprocess.DEVNULL,
118
+ start_new_session=True,
119
+ )
120
+ deadline = time.time() + 30.0
121
+ while time.time() < deadline:
122
+ if _service_is_healthy(service_url):
123
+ return
124
+ if process.poll() is not None:
125
+ raise WalletBackendError("wdk-evm-wallet exited before becoming healthy.")
126
+ time.sleep(0.5)
127
+ raise WalletBackendError(
128
+ f"Timed out waiting for wdk-evm-wallet health at {_health_url(service_url)}."
129
+ )
130
+
131
+
37
132
  def _resolve_user_evm_wallet_dir(user_id: str) -> Path:
38
133
  return resolve_openclaw_home() / "users" / normalize_user_id(user_id) / "wallets"
39
134
 
@@ -98,6 +193,58 @@ def list_user_evm_wallet_bindings(user_id: str) -> list[dict[str, Any]]:
98
193
  return bindings
99
194
 
100
195
 
196
+ def _maybe_store_evm_wallet_password(password: str) -> bool:
197
+ value = str(password or "").strip()
198
+ if not value:
199
+ return False
200
+ boot_key = resolve_boot_key()
201
+ if not boot_key:
202
+ return False
203
+ from agent_wallet.sealed_keys import resolve_sealed_keys_path, seal_keys, unseal_keys
204
+
205
+ sealed_path = resolve_sealed_keys_path()
206
+ existing = unseal_keys(boot_key) if sealed_path.exists() else {}
207
+ if existing.get("wdk_evm_wallet_password") == value:
208
+ return False
209
+ seal_keys(boot_key, {**existing, "wdk_evm_wallet_password": value})
210
+ return True
211
+
212
+
213
+ def _ensure_evm_wallet_password() -> str:
214
+ existing = resolve_evm_wallet_password()
215
+ if existing:
216
+ return existing
217
+ boot_key = resolve_boot_key()
218
+ if not boot_key:
219
+ return ""
220
+ generated = secrets.token_urlsafe(24)
221
+ _maybe_store_evm_wallet_password(generated)
222
+ return generated
223
+
224
+
225
+ def _bind_network_pair(
226
+ user_id: str,
227
+ *,
228
+ wallet_id: str,
229
+ network: str,
230
+ service_url: str,
231
+ account_index: int,
232
+ address: str | None,
233
+ ) -> None:
234
+ paired = _paired_network(network)
235
+ if not paired:
236
+ return
237
+ bind_user_evm_wallet(
238
+ user_id,
239
+ wallet_id=wallet_id,
240
+ network=paired,
241
+ service_url=service_url,
242
+ account_index=account_index,
243
+ tolerate_locked=True,
244
+ fallback_address=address,
245
+ )
246
+
247
+
101
248
  def bind_user_evm_wallet(
102
249
  user_id: str,
103
250
  *,
@@ -208,6 +355,164 @@ def ensure_user_evm_wallet_binding(
208
355
  )
209
356
 
210
357
 
358
+ def ensure_user_evm_wallet_ready(
359
+ user_id: str,
360
+ *,
361
+ network: str | None = None,
362
+ service_url: str | None = None,
363
+ wallet_id: str | None = None,
364
+ account_index: int | None = None,
365
+ auto_start_service: bool = True,
366
+ ) -> dict[str, Any]:
367
+ effective_network = _normalize_evm_network(network or settings.solana_network)
368
+ effective_service_url = _resolve_service_url(service_url)
369
+ effective_account_index = settings.wdk_evm_account_index if account_index is None else int(account_index)
370
+ if auto_start_service:
371
+ _auto_start_local_service(effective_service_url, effective_network)
372
+ elif not _service_is_healthy(effective_service_url):
373
+ raise WalletBackendError(
374
+ f"wdk-evm-wallet is not healthy at {_health_url(effective_service_url)}."
375
+ )
376
+
377
+ client = WdkEvmLocalClient(effective_service_url)
378
+ explicit_wallet_id = str(wallet_id or "").strip()
379
+ binding: dict[str, Any] | None = None
380
+ if explicit_wallet_id:
381
+ binding = ensure_user_evm_wallet_binding(
382
+ user_id,
383
+ network=effective_network,
384
+ service_url=effective_service_url,
385
+ wallet_id=explicit_wallet_id,
386
+ account_index=effective_account_index,
387
+ )
388
+ else:
389
+ try:
390
+ binding = get_user_evm_wallet_binding(user_id, network=effective_network)
391
+ except WalletBackendError:
392
+ binding = None
393
+
394
+ if binding is None:
395
+ existing_bindings = list_user_evm_wallet_bindings(user_id)
396
+ wallet_ids = {
397
+ str(item.get("wallet_id") or "").strip()
398
+ for item in existing_bindings
399
+ if str(item.get("wallet_id") or "").strip()
400
+ }
401
+ if len(wallet_ids) > 1:
402
+ raise WalletBackendError(
403
+ "Multiple EVM wallet bindings exist for this user. Set wdk_evm_wallet_id explicitly to auto-bind a new network."
404
+ )
405
+ if wallet_ids:
406
+ binding = bind_user_evm_wallet(
407
+ user_id,
408
+ wallet_id=next(iter(wallet_ids)),
409
+ network=effective_network,
410
+ service_url=effective_service_url,
411
+ account_index=effective_account_index,
412
+ tolerate_locked=True,
413
+ fallback_address=str(existing_bindings[0].get("address") or "").strip() or None,
414
+ )
415
+ else:
416
+ service_wallets = client.list_wallets_sync()
417
+ service_wallet_ids = {
418
+ str(item.get("walletId") or "").strip()
419
+ for item in service_wallets
420
+ if str(item.get("walletId") or "").strip()
421
+ }
422
+ if len(service_wallet_ids) > 1:
423
+ raise WalletBackendError(
424
+ "Multiple local EVM vault wallets exist. Set wdk_evm_wallet_id explicitly before automatic switching."
425
+ )
426
+ if service_wallet_ids:
427
+ binding = bind_user_evm_wallet(
428
+ user_id,
429
+ wallet_id=next(iter(service_wallet_ids)),
430
+ network=effective_network,
431
+ service_url=effective_service_url,
432
+ account_index=effective_account_index,
433
+ tolerate_locked=True,
434
+ )
435
+ else:
436
+ password = _ensure_evm_wallet_password()
437
+ if not password:
438
+ raise WalletBackendError(
439
+ "EVM wallet is not set up yet and no sealed local EVM wallet password is available for automatic creation."
440
+ )
441
+ created = create_user_evm_wallet(
442
+ user_id,
443
+ password=password,
444
+ network=effective_network,
445
+ service_url=effective_service_url,
446
+ account_index=effective_account_index,
447
+ )
448
+ binding = get_user_evm_wallet_binding(user_id, network=effective_network)
449
+ _bind_network_pair(
450
+ user_id,
451
+ wallet_id=str(created.get("wallet_id") or ""),
452
+ network=effective_network,
453
+ service_url=effective_service_url,
454
+ account_index=effective_account_index,
455
+ address=str(created.get("address") or "").strip() or None,
456
+ )
457
+
458
+ resolved_wallet_id = str(binding.get("wallet_id") or explicit_wallet_id).strip()
459
+ if not resolved_wallet_id:
460
+ raise WalletBackendError("EVM wallet binding is missing wallet_id.")
461
+
462
+ def _resolve_address() -> str:
463
+ payload = client.post_sync(
464
+ "/v1/evm/address/resolve",
465
+ {
466
+ "walletId": resolved_wallet_id,
467
+ "accountIndex": effective_account_index,
468
+ "network": effective_network,
469
+ },
470
+ )
471
+ address = str(payload.get("address") or "").strip()
472
+ if not address:
473
+ raise WalletBackendError("wdk-evm-wallet did not return an address.")
474
+ return address
475
+
476
+ try:
477
+ resolved_address = _resolve_address()
478
+ except WalletBackendError as exc:
479
+ is_locked = exc.code == "wallet_locked" or "wallet is locked" in str(exc).strip().lower()
480
+ if not is_locked:
481
+ raise
482
+ password = resolve_evm_wallet_password()
483
+ if not password:
484
+ raise WalletBackendError(
485
+ "EVM wallet exists but cannot be unlocked automatically because no sealed local EVM wallet password is available."
486
+ ) from exc
487
+ unlock_user_evm_wallet(
488
+ user_id,
489
+ password=password,
490
+ network=effective_network,
491
+ service_url=effective_service_url,
492
+ wallet_id=resolved_wallet_id,
493
+ account_index=effective_account_index,
494
+ )
495
+ resolved_address = _resolve_address()
496
+
497
+ binding = bind_user_evm_wallet(
498
+ user_id,
499
+ wallet_id=resolved_wallet_id,
500
+ network=effective_network,
501
+ service_url=effective_service_url,
502
+ account_index=effective_account_index,
503
+ fallback_address=resolved_address,
504
+ )
505
+ _bind_network_pair(
506
+ user_id,
507
+ wallet_id=resolved_wallet_id,
508
+ network=effective_network,
509
+ service_url=effective_service_url,
510
+ account_index=effective_account_index,
511
+ address=resolved_address,
512
+ )
513
+ return binding
514
+
515
+
211
516
  def create_user_evm_wallet(
212
517
  user_id: str,
213
518
  *,
@@ -251,6 +556,7 @@ def create_user_evm_wallet(
251
556
  "updated_at": created.get("updatedAt"),
252
557
  }
253
558
  _write_wallet_binding(resolve_user_evm_wallet_path(user_id, effective_network), binding)
559
+ _maybe_store_evm_wallet_password(password)
254
560
  return {
255
561
  **binding,
256
562
  "unlocked": bool(created.get("unlocked", True)),
@@ -302,6 +608,7 @@ def import_user_evm_wallet(
302
608
  "updated_at": created.get("updatedAt"),
303
609
  }
304
610
  _write_wallet_binding(resolve_user_evm_wallet_path(user_id, effective_network), binding)
611
+ _maybe_store_evm_wallet_password(password)
305
612
  return {
306
613
  **binding,
307
614
  "unlocked": bool(created.get("unlocked", True)),
@@ -334,6 +641,7 @@ def unlock_user_evm_wallet(
334
641
  "timeoutSeconds": 0,
335
642
  },
336
643
  )
644
+ _maybe_store_evm_wallet_password(password)
337
645
  return {
338
646
  **binding,
339
647
  "unlocked": bool(payload.get("unlocked", True)),