@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.
- package/.openclaw/AGENTS.md +0 -7
- package/.openclaw/extensions/agent-wallet/README.md +3 -2
- package/.openclaw/extensions/agent-wallet/dist/index.js +105 -7
- package/.openclaw/extensions/agent-wallet/index.ts +105 -7
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +5 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +24 -0
- package/README.md +1 -3
- package/RELEASING.md +5 -15
- package/agent-wallet/README.md +7 -0
- package/agent-wallet/agent_wallet/config.py +11 -0
- package/agent-wallet/agent_wallet/evm_user_wallets.py +310 -2
- package/agent-wallet/agent_wallet/openclaw_adapter.py +303 -1
- package/agent-wallet/agent_wallet/openclaw_runtime.py +10 -41
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +52 -0
- package/agent-wallet/agent_wallet/providers/x402.py +1323 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +30 -0
- package/agent-wallet/pyproject.toml +2 -1
- package/agent-wallet/scripts/build_release_bundle.py +1 -0
- package/agent-wallet/scripts/install_agent_wallet.py +3 -0
- package/agent-wallet/scripts/install_openclaw_local_config.py +25 -49
- package/agent-wallet/scripts/install_openclaw_sealed_keys.py +9 -1
- package/package.json +1 -2
- package/wdk-evm-wallet/src/server.js +6 -0
- package/wdk-evm-wallet/src/wdk_evm_wallet.js +108 -0
- package/.openclaw/extensions/pay-bridge/README.md +0 -38
- package/.openclaw/extensions/pay-bridge/core.mjs +0 -287
- package/.openclaw/extensions/pay-bridge/dist/core.mjs +0 -287
- package/.openclaw/extensions/pay-bridge/dist/index.js +0 -196
- package/.openclaw/extensions/pay-bridge/index.ts +0 -196
- package/.openclaw/extensions/pay-bridge/openclaw.plugin.json +0 -34
- package/.openclaw/extensions/pay-bridge/package.json +0 -49
- package/.openclaw/extensions/pay-bridge/skills/pay-operator/SKILL.md +0 -20
- 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
|
|
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)),
|