@agentlayer.tech/wallet 0.1.18 → 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/package.json +1 -1
- package/README.md +1 -3
- package/RELEASING.md +5 -15
- package/agent-wallet/README.md +3 -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_runtime.py +10 -41
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +52 -0
- package/agent-wallet/pyproject.toml +1 -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 +20 -51
- package/agent-wallet/scripts/install_openclaw_sealed_keys.py +9 -1
- package/package.json +1 -2
- 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
package/.openclaw/AGENTS.md
CHANGED
|
@@ -7,22 +7,16 @@ These instructions apply to the entire `.openclaw/` tree.
|
|
|
7
7
|
This tree contains local OpenClaw host-side workspace assets. In the current repo, its main responsibilities are:
|
|
8
8
|
|
|
9
9
|
- the `agent-wallet` extension that bridges OpenClaw to the authoritative Python `agent-wallet` backend
|
|
10
|
-
- the `pay-bridge` extension that bridges OpenClaw to the local `pay` CLI for paid API discovery and execution
|
|
11
10
|
|
|
12
11
|
## Current structure
|
|
13
12
|
- `.openclaw/extensions/agent-wallet/index.ts` — TypeScript extension entrypoint registered by OpenClaw.
|
|
14
13
|
- `.openclaw/extensions/agent-wallet/openclaw.plugin.json` — plugin manifest and config schema.
|
|
15
14
|
- `.openclaw/extensions/agent-wallet/package.json` — extension package metadata.
|
|
16
15
|
- `.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md` — user-facing operational wallet safety guidance.
|
|
17
|
-
- `.openclaw/extensions/pay-bridge/index.ts` — TypeScript entrypoint for the local `pay` CLI bridge.
|
|
18
|
-
- `.openclaw/extensions/pay-bridge/openclaw.plugin.json` — plugin manifest and config schema for pay tools.
|
|
19
|
-
- `.openclaw/extensions/pay-bridge/core.mjs` — local `pay` command execution and output shaping.
|
|
20
|
-
- `.openclaw/extensions/pay-bridge/skills/pay-operator/SKILL.md` — user-facing operational guidance for paid API usage.
|
|
21
16
|
|
|
22
17
|
## Design intent
|
|
23
18
|
- Keep the TypeScript extension thin and host-oriented.
|
|
24
19
|
- Let Python own wallet logic, policy, approvals, signing rules, and Solana implementation details.
|
|
25
|
-
- Let `pay` remain the source of truth for paid API wallet/account behavior.
|
|
26
20
|
- Let the extension focus on:
|
|
27
21
|
- resolving config
|
|
28
22
|
- locating the Python package
|
|
@@ -35,7 +29,6 @@ This tree contains local OpenClaw host-side workspace assets. In the current rep
|
|
|
35
29
|
|
|
36
30
|
### Keep bridge logic thin
|
|
37
31
|
- Do not duplicate business logic from Python unless OpenClaw requires it at registration time.
|
|
38
|
-
- Do not duplicate payment protocol logic from `pay`; prefer invoking the local CLI and shaping its output.
|
|
39
32
|
- Do not reimplement approval validation, transaction policy, wallet derivation, or Solana-specific rules in TypeScript.
|
|
40
33
|
- Prefer forwarding config into the CLI bridge and letting Python decide runtime behavior.
|
|
41
34
|
- Treat this layer as a transport and schema bridge, not an execution authority.
|
|
@@ -77,7 +77,7 @@ The ClawHub plugin package only installs the native OpenClaw plugin. It expects
|
|
|
77
77
|
|
|
78
78
|
If that runtime is not present, set `plugins.entries.agent-wallet.config.packageRoot` explicitly.
|
|
79
79
|
|
|
80
|
-
That installs the Python backend, Node dependencies for the local BTC/EVM runtimes, and patches the OpenClaw plugin config.
|
|
80
|
+
That installs the Python backend, Node dependencies for the local BTC/EVM runtimes, and patches the OpenClaw plugin config. Solana stays ready immediately; EVM readiness can now be auto-healed during normal wallet switching when the runtime has sealed local vault credentials.
|
|
81
81
|
|
|
82
82
|
For self-hosted installs, prefer `SOLANA_RPC_URL` / `SOLANA_RPC_URLS` in local env and treat the plugin `rpcUrl` / `rpcUrls` fields as fallback only. If the local runtime exposes `ALCHEMY_API_KEY` or `HELIUS_API_KEY`, the wallet can derive the Solana RPC URL automatically for `mainnet` or `devnet`. Local env always takes precedence over `openclaw.json`.
|
|
83
83
|
|
|
@@ -88,12 +88,13 @@ Important:
|
|
|
88
88
|
- For a local official OpenClaw install, `userId` should represent the wallet owner for that agent install.
|
|
89
89
|
- The public OpenClaw plugin docs do not document a per-request end-user identifier in `registerTool(...).execute(...)`, so dynamic multi-user wallet selection is intentionally kept in the Python/runtime layer, not inside the TypeScript plugin itself.
|
|
90
90
|
- Helper scripts in `agent-wallet/scripts/` are generic patch/finalize utilities and no longer assume a specific local username, path, or temporary master key.
|
|
91
|
-
- The OpenClaw plugin API in this repo exposes tool registration, not host password prompts
|
|
91
|
+
- The OpenClaw plugin API in this repo exposes tool registration, not host password prompts. EVM wallet create/unlock still is not a public agent tool, but the runtime can now auto-create or auto-unlock the local EVM wallet during `set_wallet_backend` or EVM tool calls when `sealed_keys.json` contains the local EVM vault password.
|
|
92
92
|
- For a one-command local BTC onboarding path, use `agent-wallet/scripts/bootstrap_openclaw_btc.py`, which both sets up the BTC wallet binding and patches local OpenClaw config for `backend=wdk_btc_local`.
|
|
93
93
|
- The BTC flow now only supports local service URLs (`127.0.0.1` / `localhost` / `::1`).
|
|
94
94
|
- The local BTC service is protected with a bearer token loaded from `~/.openclaw/wdk-btc-wallet/local-auth-token`, not from plugin config JSON.
|
|
95
95
|
- When the BTC service URL is local, that bootstrap script can also auto-start `wdk-btc-wallet` before patching OpenClaw config.
|
|
96
96
|
- The EVM flow also only supports local service URLs (`127.0.0.1` / `localhost` / `::1`) and uses a bearer token loaded from `~/.openclaw/wdk-evm-wallet/local-auth-token`.
|
|
97
|
+
- The installer now provisions a sealed local EVM vault password under `sealed_keys.json` by default, and host-side EVM setup helpers refresh that sealed value whenever the operator enters a new password.
|
|
97
98
|
- The EVM tool surface is intentionally narrow: Velora swap quote/execute, Aave V3 account/reserve/position flows, native transfers, ERC-20 transfers, fee quotes, and receipt lookup only. No arbitrary calldata, standalone approvals, or generic contract execution are exposed to the agent.
|
|
98
99
|
- Velora swap and Aave V3 support are currently limited to `ethereum` and `base`. Test carefully because the upstream WDK protocol packages are still beta.
|
|
99
100
|
- Agents can call `set_wallet_backend` to switch the active wallet for the current OpenClaw plugin session between Solana, EVM, and Bitcoin. This does not edit `openclaw.json`; plugin config remains the startup default.
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ AgentLayer is a beta local-first wallet and finance stack for agents.
|
|
|
18
18
|
The repository includes:
|
|
19
19
|
|
|
20
20
|
- `agent-wallet/` - the main wallet backend for AgentLayer
|
|
21
|
-
- `.openclaw/` - the local AgentLayer bridge layer
|
|
21
|
+
- `.openclaw/` - the local AgentLayer bridge layer for the OpenClaw wallet integration
|
|
22
22
|
- `hermes/` - optional Hermes Agent plugin bridge for the same wallet backend
|
|
23
23
|
- `wdk-btc-wallet/` - the local Bitcoin wallet service
|
|
24
24
|
- `wdk-evm-wallet/` - the local EVM wallet service
|
|
@@ -55,7 +55,6 @@ Install the native OpenClaw plugins from ClawHub:
|
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
57
|
openclaw plugins install clawhub:@agentlayertech/agent-wallet-plugin
|
|
58
|
-
openclaw plugins install clawhub:@agentlayertech/pay-bridge-plugin
|
|
59
58
|
```
|
|
60
59
|
|
|
61
60
|
Those ClawHub packages do not replace the npm installer. Keep `npx @agentlayer.tech/wallet install --yes` for laying down the local wallet runtime, Python backend, and helper services. The ClawHub packages only install the OpenClaw plugin surfaces that point at that runtime.
|
|
@@ -92,7 +91,6 @@ Use ClawHub when you want the plugin itself to be installed through OpenClaw:
|
|
|
92
91
|
|
|
93
92
|
```bash
|
|
94
93
|
openclaw plugins install clawhub:@agentlayertech/agent-wallet-plugin
|
|
95
|
-
openclaw plugins install clawhub:@agentlayertech/pay-bridge-plugin
|
|
96
94
|
```
|
|
97
95
|
|
|
98
96
|
Recommended order:
|
package/RELEASING.md
CHANGED
|
@@ -12,11 +12,10 @@ The public install command is:
|
|
|
12
12
|
npx @agentlayer.tech/wallet install --yes
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
The repo also ships
|
|
15
|
+
The repo also ships a native OpenClaw plugin package for ClawHub:
|
|
16
16
|
|
|
17
17
|
```text
|
|
18
18
|
@agentlayertech/agent-wallet-plugin
|
|
19
|
-
@agentlayertech/pay-bridge-plugin
|
|
20
19
|
```
|
|
21
20
|
|
|
22
21
|
## When npm Publishes
|
|
@@ -200,7 +199,6 @@ Dry-run the package contents from each plugin directory:
|
|
|
200
199
|
|
|
201
200
|
```bash
|
|
202
201
|
(cd .openclaw/extensions/agent-wallet && npm pack --dry-run)
|
|
203
|
-
(cd .openclaw/extensions/pay-bridge && npm pack --dry-run)
|
|
204
202
|
```
|
|
205
203
|
|
|
206
204
|
Publish to ClawHub with the package-specific command documented by OpenClaw:
|
|
@@ -208,16 +206,12 @@ Publish to ClawHub with the package-specific command documented by OpenClaw:
|
|
|
208
206
|
```bash
|
|
209
207
|
clawhub package publish .openclaw/extensions/agent-wallet --dry-run
|
|
210
208
|
clawhub package publish .openclaw/extensions/agent-wallet
|
|
211
|
-
|
|
212
|
-
clawhub package publish .openclaw/extensions/pay-bridge --dry-run
|
|
213
|
-
clawhub package publish .openclaw/extensions/pay-bridge
|
|
214
209
|
```
|
|
215
210
|
|
|
216
211
|
Users then install them natively through OpenClaw:
|
|
217
212
|
|
|
218
213
|
```bash
|
|
219
214
|
openclaw plugins install clawhub:@agentlayertech/agent-wallet-plugin
|
|
220
|
-
openclaw plugins install clawhub:@agentlayertech/pay-bridge-plugin
|
|
221
215
|
```
|
|
222
216
|
|
|
223
217
|
GitHub Actions can publish the same packages automatically from tags and manual
|
|
@@ -231,14 +225,13 @@ CLAWHUB_TOKEN
|
|
|
231
225
|
|
|
232
226
|
Workflow behavior:
|
|
233
227
|
|
|
234
|
-
- `pull_request`: packs
|
|
228
|
+
- `pull_request`: packs the plugin and runs ClawHub `--dry-run`
|
|
235
229
|
- `workflow_dispatch`: publishes or dry-runs based on the `dry_run` input
|
|
236
|
-
- `push` on `v*` tags: publishes
|
|
230
|
+
- `push` on `v*` tags: publishes the plugin automatically
|
|
237
231
|
|
|
238
232
|
The workflow currently publishes:
|
|
239
233
|
|
|
240
234
|
- `.openclaw/extensions/agent-wallet` as `bundle-plugin`
|
|
241
|
-
- `.openclaw/extensions/pay-bridge` as `code-plugin`
|
|
242
235
|
|
|
243
236
|
`agent-wallet` stays on `bundle-plugin` because that package name was first
|
|
244
237
|
published to ClawHub with that family, and ClawHub does not allow family
|
|
@@ -298,17 +291,16 @@ It publishes these ClawHub packages:
|
|
|
298
291
|
|
|
299
292
|
```text
|
|
300
293
|
@agentlayertech/agent-wallet-plugin
|
|
301
|
-
@agentlayertech/pay-bridge-plugin
|
|
302
294
|
```
|
|
303
295
|
|
|
304
296
|
### Triggers
|
|
305
297
|
|
|
306
298
|
- `pull_request`
|
|
307
|
-
- runs ClawHub publish in `--dry-run` mode for
|
|
299
|
+
- runs ClawHub publish in `--dry-run` mode for the plugin package
|
|
308
300
|
- `workflow_dispatch`
|
|
309
301
|
- supports manual runs with a `dry_run` boolean input
|
|
310
302
|
- `push` on git tags matching `v*`
|
|
311
|
-
- publishes
|
|
303
|
+
- publishes the plugin package to ClawHub
|
|
312
304
|
|
|
313
305
|
### Required secret
|
|
314
306
|
|
|
@@ -330,7 +322,6 @@ publisher access to:
|
|
|
330
322
|
The workflow currently publishes:
|
|
331
323
|
|
|
332
324
|
- `.openclaw/extensions/agent-wallet` as `bundle-plugin`
|
|
333
|
-
- `.openclaw/extensions/pay-bridge` as `code-plugin`
|
|
334
325
|
|
|
335
326
|
`agent-wallet` remains on `bundle-plugin` because the package
|
|
336
327
|
`@agentlayertech/agent-wallet-plugin` was first created in ClawHub with that
|
|
@@ -346,7 +337,6 @@ If you want one git tag to publish both npm and ClawHub surfaces together:
|
|
|
346
337
|
package.json
|
|
347
338
|
agent-wallet/pyproject.toml
|
|
348
339
|
.openclaw/extensions/agent-wallet/package.json
|
|
349
|
-
.openclaw/extensions/pay-bridge/package.json
|
|
350
340
|
```
|
|
351
341
|
|
|
352
342
|
2. Commit the release version bump.
|
package/agent-wallet/README.md
CHANGED
|
@@ -405,6 +405,7 @@ For the local EVM backend (`backend=wdk_evm_local`), the lifecycle mirrors the B
|
|
|
405
405
|
- the EVM service is localhost-only and no longer accepts remote service URLs through the OpenClaw EVM flow
|
|
406
406
|
- `agent-wallet` talks to it through a local bearer token loaded from `~/.openclaw/wdk-evm-wallet/local-auth-token`
|
|
407
407
|
- `agent-wallet` stores only a per-user EVM wallet binding under `~/.openclaw/users/<normalized-user-id>/wallets/evm-<network>-agent.json`
|
|
408
|
+
- the runtime can auto-create missing EVM bindings or auto-unlock the local vault during ordinary OpenClaw switching/tool calls when `sealed_keys.json` contains `wdk_evm_wallet_password`
|
|
408
409
|
- supported EVM networks are `ethereum`, `sepolia`, `base`, and `base-sepolia`
|
|
409
410
|
- OpenClaw-facing EVM tools accept an optional per-call `network` override for `ethereum` or `base`, so the agent can switch between the two mainnet EVM paths without editing host config
|
|
410
411
|
- EVM `get_wallet_balance` now returns an enriched portfolio-style payload with native balance, discovered ERC-20 balances, and USD values when token discovery and pricing are available
|
|
@@ -429,6 +430,7 @@ That wrapper:
|
|
|
429
430
|
- can auto-start `wdk-evm-wallet/run-local.sh` if the local service is not already healthy
|
|
430
431
|
- creates or unlocks the local EVM wallet binding
|
|
431
432
|
- also binds the paired EVM network by default: `ethereum <-> base`, `sepolia <-> base-sepolia`
|
|
433
|
+
- stores the entered EVM vault password into `sealed_keys.json` when `AGENT_WALLET_BOOT_KEY` is available, so later OpenClaw wallet switching can auto-raise the EVM backend without another password prompt
|
|
432
434
|
- patches OpenClaw config to `backend=wdk_evm_local`
|
|
433
435
|
|
|
434
436
|
Example host-side EVM wallet creation:
|
|
@@ -453,6 +455,7 @@ Per-user wallets are now encrypted at rest in one hardened mode:
|
|
|
453
455
|
|
|
454
456
|
Do not store `masterKey`, `privateKey`, or approval secrets in plugin config JSON or direct runtime environment variables.
|
|
455
457
|
`AGENT_WALLET_MASTER_KEY`, `AGENT_WALLET_APPROVAL_SECRET`, and `SOLANA_AGENT_PRIVATE_KEY` are now provisioning-only inputs for installer/admin scripts and are rejected by the runtime.
|
|
458
|
+
The installer also provisions a sealed `wdk_evm_wallet_password` by default so greenfield EVM wallet setup can happen automatically on first switch.
|
|
456
459
|
Create or update that sealed file with:
|
|
457
460
|
|
|
458
461
|
```bash
|
|
@@ -368,6 +368,17 @@ def resolve_solana_private_key() -> str:
|
|
|
368
368
|
)
|
|
369
369
|
|
|
370
370
|
|
|
371
|
+
def resolve_evm_wallet_password() -> str:
|
|
372
|
+
"""Resolve the local EVM vault password from env or the sealed secret store."""
|
|
373
|
+
direct = os.getenv("WDK_EVM_WALLET_PASSWORD", "").strip()
|
|
374
|
+
if direct:
|
|
375
|
+
return direct
|
|
376
|
+
return _resolve_sealed_secret(
|
|
377
|
+
"wdk_evm_wallet_password",
|
|
378
|
+
"evm_wallet_password",
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
371
382
|
def use_encrypted_user_wallets() -> bool:
|
|
372
383
|
"""Per-user wallet files are always encrypted in the hardened runtime."""
|
|
373
384
|
return True
|
|
@@ -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)),
|
|
@@ -8,7 +8,7 @@ from typing import Any
|
|
|
8
8
|
from agent_wallet.approval import issue_approval_token
|
|
9
9
|
from agent_wallet.btc_user_wallets import get_user_btc_wallet_binding
|
|
10
10
|
from agent_wallet.config import settings
|
|
11
|
-
from agent_wallet.evm_user_wallets import
|
|
11
|
+
from agent_wallet.evm_user_wallets import ensure_user_evm_wallet_ready
|
|
12
12
|
from agent_wallet.models import OpenClawWalletSessionMetadata
|
|
13
13
|
from agent_wallet.openclaw_adapter import OpenClawWalletAdapter
|
|
14
14
|
from agent_wallet.plugin_bundle import build_openclaw_plugin_bundle
|
|
@@ -173,38 +173,17 @@ def onboard_openclaw_user_wallet(
|
|
|
173
173
|
"base_sepolia": "base-sepolia",
|
|
174
174
|
}
|
|
175
175
|
effective_network = aliases.get(requested_network, requested_network)
|
|
176
|
-
binding: dict[str, Any] | None = None
|
|
177
176
|
wallet_id = str(wdk_evm_wallet_id or settings.wdk_evm_wallet_id).strip()
|
|
178
177
|
if not service_url:
|
|
179
178
|
raise WalletBackendError("wdk_evm_service_url is required for backend=wdk_evm_local.")
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
wallet_id=wallet_id,
|
|
189
|
-
account_index=account_index,
|
|
190
|
-
)
|
|
191
|
-
else:
|
|
192
|
-
if str(binding.get("wallet_id") or "").strip() != wallet_id:
|
|
193
|
-
binding = ensure_user_evm_wallet_binding(
|
|
194
|
-
user_id,
|
|
195
|
-
network=effective_network,
|
|
196
|
-
service_url=service_url,
|
|
197
|
-
wallet_id=wallet_id,
|
|
198
|
-
account_index=account_index,
|
|
199
|
-
)
|
|
200
|
-
else:
|
|
201
|
-
binding = ensure_user_evm_wallet_binding(
|
|
202
|
-
user_id,
|
|
203
|
-
network=effective_network,
|
|
204
|
-
service_url=service_url,
|
|
205
|
-
account_index=account_index,
|
|
206
|
-
)
|
|
207
|
-
wallet_id = str(binding.get("wallet_id") or "").strip()
|
|
179
|
+
binding = ensure_user_evm_wallet_ready(
|
|
180
|
+
user_id,
|
|
181
|
+
network=effective_network,
|
|
182
|
+
service_url=service_url,
|
|
183
|
+
wallet_id=wallet_id or None,
|
|
184
|
+
account_index=account_index,
|
|
185
|
+
)
|
|
186
|
+
wallet_id = str(binding.get("wallet_id") or wallet_id).strip()
|
|
208
187
|
if not wallet_id:
|
|
209
188
|
raise WalletBackendError(
|
|
210
189
|
"wdk_evm_wallet_id is required for backend=wdk_evm_local, or create a bound user EVM wallet first."
|
|
@@ -212,17 +191,7 @@ def onboard_openclaw_user_wallet(
|
|
|
212
191
|
|
|
213
192
|
client = WdkEvmLocalClient(service_url)
|
|
214
193
|
wallet_meta = client.post_sync("/v1/evm/wallets/get", {"walletId": wallet_id})
|
|
215
|
-
resolved_address = str(
|
|
216
|
-
if not resolved_address:
|
|
217
|
-
address_payload = client.post_sync(
|
|
218
|
-
"/v1/evm/address/resolve",
|
|
219
|
-
{
|
|
220
|
-
"walletId": wallet_id,
|
|
221
|
-
"accountIndex": account_index,
|
|
222
|
-
"network": effective_network,
|
|
223
|
-
},
|
|
224
|
-
)
|
|
225
|
-
resolved_address = str(address_payload.get("address") or "").strip()
|
|
194
|
+
resolved_address = str(binding.get("address") or "").strip()
|
|
226
195
|
backend = WdkEvmLocalWalletBackend(
|
|
227
196
|
service_url=service_url,
|
|
228
197
|
wallet_id=wallet_id,
|