@agentlayer.tech/wallet 0.1.53 → 0.1.58
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/extensions/agent-wallet/openclaw.plugin.json +1 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +63 -0
- package/README.md +14 -12
- package/VERSION +1 -1
- package/agent-wallet/.env.example +1 -0
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/config.py +9 -0
- package/agent-wallet/agent_wallet/openclaw_cli.py +95 -39
- package/agent-wallet/agent_wallet/openclaw_runtime.py +8 -3
- package/agent-wallet/agent_wallet/providers/jupiter.py +34 -0
- package/agent-wallet/agent_wallet/user_wallets.py +46 -3
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +34 -8
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/claude-code/plugins/agent-wallet/README.md +9 -3
- package/claude-code/plugins/agent-wallet/commands/agentlayer-autonomous-approve.md +17 -2
- package/claude-code/plugins/agent-wallet/commands/agentlayer-autonomous-revoke.md +1 -0
- package/claude-code/plugins/agent-wallet/commands/wallet-base.md +37 -0
- package/claude-code/plugins/agent-wallet/commands/wallet-ethereum.md +37 -0
- package/claude-code/plugins/agent-wallet/commands/wallet-evm.md +38 -0
- package/claude-code/plugins/agent-wallet/commands/wallet-sol.md +30 -0
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/README.md +9 -0
- package/codex/plugins/agent-wallet/server.py +427 -6
- package/codex/plugins/agent-wallet/skills/wallet-sol/SKILL.md +29 -0
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/package.json +1 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "agent-wallet",
|
|
3
3
|
"name": "Agent Wallet",
|
|
4
4
|
"description": "Official OpenClaw plugin bridge for the agent-wallet backends, including Solana, local BTC, and local EVM.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.58",
|
|
6
6
|
"contracts": {
|
|
7
7
|
"tools": [
|
|
8
8
|
"agentlayer_autonomous_approve",
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,69 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v0.1.58 - 2026-07-01
|
|
6
|
+
|
|
7
|
+
- Resolved SPL token symbol/name in `get_wallet_portfolio` via a batched,
|
|
8
|
+
concurrent Jupiter token-search lookup, so `/wallet-sol` shows tickers
|
|
9
|
+
(e.g. `USDC`) instead of raw mint addresses. The mint/token_address is
|
|
10
|
+
still returned in full and rendered shortened next to the label rather
|
|
11
|
+
than dropped. Falls back gracefully (symbol=None) when a mint isn't
|
|
12
|
+
indexed or the lookup provider is unavailable.
|
|
13
|
+
- `agent-wallet/agent_wallet/providers/jupiter.py`
|
|
14
|
+
- `agent-wallet/agent_wallet/wallet_layer/solana.py`
|
|
15
|
+
- `claude-code/plugins/agent-wallet/commands/wallet-sol.md`
|
|
16
|
+
- `codex/plugins/agent-wallet/skills/wallet-sol/SKILL.md`
|
|
17
|
+
- Dropped the `/wallet-sol` source-metadata footer line (`source`,
|
|
18
|
+
`token_discovery_source`, `pricing_source`, `pricing_errors`) from the
|
|
19
|
+
rendered chat output for a more compact report.
|
|
20
|
+
- `claude-code/plugins/agent-wallet/commands/wallet-sol.md`
|
|
21
|
+
- `codex/plugins/agent-wallet/skills/wallet-sol/SKILL.md`
|
|
22
|
+
|
|
23
|
+
## v0.1.57 - 2026-07-01
|
|
24
|
+
|
|
25
|
+
- Added a bundled Codex `wallet-sol` skill so Codex users can render the
|
|
26
|
+
connected Solana wallet portfolio in chat the same way Claude Code's
|
|
27
|
+
`/wallet-sol` command does.
|
|
28
|
+
- `codex/plugins/agent-wallet/skills/wallet-sol/SKILL.md`
|
|
29
|
+
- `codex/plugins/agent-wallet/README.md`
|
|
30
|
+
- Fixed the resident read worker (used by `get_wallet_balance` /
|
|
31
|
+
`get_wallet_portfolio`, e.g. `/wallet-sol`) decrypting the wallet's private
|
|
32
|
+
key on every read-only cold start even though it never needed it. Read-only
|
|
33
|
+
onboarding now resolves the address from the existing plaintext wallet pin
|
|
34
|
+
file once a wallet has been provisioned, instead of unsealing secrets and
|
|
35
|
+
deriving a signer just to discard it.
|
|
36
|
+
- `agent-wallet/agent_wallet/user_wallets.py`
|
|
37
|
+
- Reduced `/wallet-sol` context payload by dropping the unused raw Jupiter
|
|
38
|
+
price blob (`price_raw`) from portfolio token entries.
|
|
39
|
+
- `agent-wallet/agent_wallet/wallet_layer/solana.py`
|
|
40
|
+
- Parallelized Jupiter price batch fetches in `get_portfolio` instead of
|
|
41
|
+
awaiting each 20-mint batch sequentially, speeding up portfolio lookups for
|
|
42
|
+
wallets with many SPL token accounts.
|
|
43
|
+
- `agent-wallet/agent_wallet/wallet_layer/solana.py`
|
|
44
|
+
- Added a background prewarm of the resident read worker at MCP server
|
|
45
|
+
startup so interpreter boot and onboarding overlap with the user issuing
|
|
46
|
+
the first read-only command instead of blocking it. Opt out with
|
|
47
|
+
`AGENT_WALLET_PREWARM_READ_WORKER=0`.
|
|
48
|
+
- `codex/plugins/agent-wallet/server.py`
|
|
49
|
+
- Added idle eviction for resident read workers left over from a stale
|
|
50
|
+
config (e.g. after switching Solana network or wallet backend), bounded by
|
|
51
|
+
`AGENT_WALLET_READ_WORKER_IDLE_SECONDS` (default 10 minutes), and closed
|
|
52
|
+
resident read workers on SIGTERM in addition to the existing `atexit`
|
|
53
|
+
cleanup, since Python does not run `atexit` hooks on signal termination.
|
|
54
|
+
- `codex/plugins/agent-wallet/server.py`
|
|
55
|
+
|
|
56
|
+
## v0.1.54 - 2026-06-29
|
|
57
|
+
|
|
58
|
+
- Fixed Claude Code autonomous-mode slash command activation so
|
|
59
|
+
`/agentlayer-autonomous-approve` no longer relies on the model treating the
|
|
60
|
+
slash invocation itself as sufficient consent. The command now uses an
|
|
61
|
+
explicit in-command confirmation step before enabling the standing
|
|
62
|
+
permission, and both autonomous toggle commands are marked manual-only in the
|
|
63
|
+
Claude Code command metadata.
|
|
64
|
+
- `claude-code/plugins/agent-wallet/commands/agentlayer-autonomous-approve.md`
|
|
65
|
+
- `claude-code/plugins/agent-wallet/commands/agentlayer-autonomous-revoke.md`
|
|
66
|
+
- `claude-code/plugins/agent-wallet/README.md`
|
|
67
|
+
|
|
5
68
|
## v0.1.53 - 2026-06-26
|
|
6
69
|
|
|
7
70
|
- Fixed Morpho vault and market quote (preview) requests timing out under the
|
package/README.md
CHANGED
|
@@ -86,35 +86,37 @@ openclaw plugins install clawhub:@agentlayertech/agent-wallet-plugin
|
|
|
86
86
|
|
|
87
87
|
The ClawHub package does not replace the npm installer. `npx @agentlayer.tech/wallet install --yes` installs the local runtime, Python backend, and helper services. ClawHub only installs the OpenClaw plugin surface that points at that runtime.
|
|
88
88
|
|
|
89
|
-
Or install the CLI globally:
|
|
89
|
+
Or install the CLI globally for shorter commands:
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
92
|
npm install -g @agentlayer.tech/wallet
|
|
93
93
|
wallet install --yes
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
> **Why install globally?** Without the global install, `wallet` is not in your `PATH` and commands like `wallet update --yes` return `zsh: command not found: wallet`. The global install adds the `wallet` binary once; after that all `wallet <cmd>` shortcuts work. If you skip the global install, prefix every command with `npx @agentlayer.tech/wallet` instead (see the Updating section below).
|
|
97
|
+
|
|
96
98
|
The CLI uses a versioned runtime layout:
|
|
97
99
|
|
|
98
100
|
```bash
|
|
99
|
-
~/.openclaw/agent-wallet-runtime/releases/<version>
|
|
100
|
-
~/.openclaw/agent-wallet-runtime/current
|
|
101
|
+
~/.openclaw/agent-wallet-runtime/releases/<version> # immutable per-version snapshots
|
|
102
|
+
~/.openclaw/agent-wallet-runtime/current # symlink → active release
|
|
101
103
|
```
|
|
102
104
|
|
|
103
105
|
On first install, `--yes` generates local runtime secrets. The installer stores `master_key` and `approval_secret` in `~/.openclaw/sealed_keys.json`; only the boot key needed to unlock that sealed bundle is written to the runtime `.env`.
|
|
104
106
|
|
|
105
|
-
Useful
|
|
107
|
+
Useful CLI commands (require the global install above):
|
|
106
108
|
|
|
107
109
|
```bash
|
|
108
|
-
wallet status
|
|
109
|
-
wallet doctor
|
|
110
|
-
wallet
|
|
111
|
-
wallet
|
|
112
|
-
wallet
|
|
113
|
-
wallet
|
|
114
|
-
wallet
|
|
110
|
+
wallet status # show active runtime version and health
|
|
111
|
+
wallet doctor # diagnose common config problems
|
|
112
|
+
wallet update --yes # upgrade to the latest published version
|
|
113
|
+
wallet update --yes --dry-run # preview the upgrade without applying it
|
|
114
|
+
wallet rollback # revert to the previous release
|
|
115
|
+
wallet hermes install --yes # (re)connect Hermes to the current runtime
|
|
116
|
+
wallet codex install --yes # (re)connect Codex to the current runtime
|
|
115
117
|
```
|
|
116
118
|
|
|
117
|
-
`wallet update --yes`
|
|
119
|
+
`wallet update --yes` fetches the latest npm package, installs it under `releases/<version>`, and flips the `current` symlink. Python and Node dependency snapshots are reused when the dependency fingerprint has not changed, so subsequent updates are fast. All frameworks that read from `current/` (Claude Code, Codex, Hermes) pick up the new code automatically on their next session start; OpenClaw requires a gateway restart to reload its TypeScript extension.
|
|
118
120
|
|
|
119
121
|
## Native OpenClaw plugin installs
|
|
120
122
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.58
|
|
@@ -43,6 +43,7 @@ SOLANA_AGENT_KEYPAIR_PATH=
|
|
|
43
43
|
JUPITER_API_BASE_URL=https://lite-api.jup.ag/swap/v1
|
|
44
44
|
JUPITER_ULTRA_API_BASE_URL=https://lite-api.jup.ag/ultra/v1
|
|
45
45
|
JUPITER_PRICE_API_BASE_URL=https://lite-api.jup.ag/price/v3
|
|
46
|
+
JUPITER_TOKEN_SEARCH_API_BASE_URL=https://lite-api.jup.ag/tokens/v2
|
|
46
47
|
JUPITER_API_KEY=
|
|
47
48
|
|
|
48
49
|
# LI.FI cross-chain routing. API key is optional for basic read-only quote/status calls.
|
|
@@ -48,6 +48,7 @@ class Settings(BaseSettings):
|
|
|
48
48
|
jupiter_swap_v2_api_base_url: str = "https://api.jup.ag/swap/v2"
|
|
49
49
|
jupiter_ultra_api_base_url: str = "https://lite-api.jup.ag/ultra/v1"
|
|
50
50
|
jupiter_price_api_base_url: str = "https://lite-api.jup.ag/price/v3"
|
|
51
|
+
jupiter_token_search_api_base_url: str = "https://lite-api.jup.ag/tokens/v2"
|
|
51
52
|
jupiter_api_key: str = ""
|
|
52
53
|
lifi_api_base_url: str = "https://li.quest/v1"
|
|
53
54
|
lifi_api_key: str = ""
|
|
@@ -74,6 +75,14 @@ class Settings(BaseSettings):
|
|
|
74
75
|
settings = Settings()
|
|
75
76
|
|
|
76
77
|
|
|
78
|
+
def reload_settings() -> Settings:
|
|
79
|
+
"""Reload settings from the current environment without changing object identity."""
|
|
80
|
+
refreshed = Settings()
|
|
81
|
+
for field_name in Settings.model_fields:
|
|
82
|
+
setattr(settings, field_name, getattr(refreshed, field_name))
|
|
83
|
+
return settings
|
|
84
|
+
|
|
85
|
+
|
|
77
86
|
def normalize_solana_network(network: str | None) -> str:
|
|
78
87
|
"""Canonicalize supported Solana network names and reject test clusters."""
|
|
79
88
|
normalized = str(network or "").strip().lower() or "mainnet"
|
|
@@ -33,6 +33,12 @@ def _parse_csv(value: Any) -> str:
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
SECRET_CONFIG_KEYS = {"privateKey", "masterKey", "approvalSecret"}
|
|
36
|
+
READ_ONLY_WORKER_TOOLS = frozenset(
|
|
37
|
+
{
|
|
38
|
+
"get_wallet_balance",
|
|
39
|
+
"get_wallet_portfolio",
|
|
40
|
+
}
|
|
41
|
+
)
|
|
36
42
|
|
|
37
43
|
|
|
38
44
|
def _reject_secret_config_json(config: dict[str, Any]) -> None:
|
|
@@ -117,6 +123,9 @@ def _apply_config_overrides(config: dict[str, Any]) -> None:
|
|
|
117
123
|
if not allow_override and os.getenv(env_name, "").strip():
|
|
118
124
|
continue
|
|
119
125
|
os.environ[env_name] = text
|
|
126
|
+
from agent_wallet.config import reload_settings
|
|
127
|
+
|
|
128
|
+
reload_settings()
|
|
120
129
|
|
|
121
130
|
|
|
122
131
|
def _load_json(raw: str | None, default: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
@@ -138,10 +147,23 @@ def _read_stdin_secret(field_name: str) -> str:
|
|
|
138
147
|
async def _run_onboard(user_id: str, config: dict[str, Any]) -> dict[str, Any]:
|
|
139
148
|
from agent_wallet.openclaw_runtime import onboard_openclaw_user_wallet
|
|
140
149
|
|
|
141
|
-
context =
|
|
150
|
+
context = _build_runtime_context(user_id, config)
|
|
151
|
+
return context.serializable_bundle()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _build_runtime_context(
|
|
155
|
+
user_id: str,
|
|
156
|
+
config: dict[str, Any],
|
|
157
|
+
*,
|
|
158
|
+
read_only: bool = False,
|
|
159
|
+
) -> Any:
|
|
160
|
+
from agent_wallet.openclaw_runtime import onboard_openclaw_user_wallet
|
|
161
|
+
|
|
162
|
+
return onboard_openclaw_user_wallet(
|
|
142
163
|
user_id,
|
|
143
164
|
backend=config.get("backend"),
|
|
144
165
|
sign_only=config.get("signOnly"),
|
|
166
|
+
read_only=read_only,
|
|
145
167
|
network=config.get("network"),
|
|
146
168
|
rpc_url=config.get("rpcUrl"),
|
|
147
169
|
wdk_btc_service_url=config.get("wdkBtcServiceUrl"),
|
|
@@ -151,7 +173,6 @@ async def _run_onboard(user_id: str, config: dict[str, Any]) -> dict[str, Any]:
|
|
|
151
173
|
wdk_evm_wallet_id=config.get("wdkEvmWalletId"),
|
|
152
174
|
wdk_evm_account_index=config.get("wdkEvmAccountIndex"),
|
|
153
175
|
)
|
|
154
|
-
return context.serializable_bundle()
|
|
155
176
|
|
|
156
177
|
|
|
157
178
|
async def _run_invoke(
|
|
@@ -160,21 +181,7 @@ async def _run_invoke(
|
|
|
160
181
|
arguments: dict[str, Any],
|
|
161
182
|
config: dict[str, Any],
|
|
162
183
|
) -> dict[str, Any]:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
context = onboard_openclaw_user_wallet(
|
|
166
|
-
user_id,
|
|
167
|
-
backend=config.get("backend"),
|
|
168
|
-
sign_only=config.get("signOnly"),
|
|
169
|
-
network=config.get("network"),
|
|
170
|
-
rpc_url=config.get("rpcUrl"),
|
|
171
|
-
wdk_btc_service_url=config.get("wdkBtcServiceUrl"),
|
|
172
|
-
wdk_btc_wallet_id=config.get("wdkBtcWalletId"),
|
|
173
|
-
wdk_btc_account_index=config.get("wdkBtcAccountIndex"),
|
|
174
|
-
wdk_evm_service_url=config.get("wdkEvmServiceUrl"),
|
|
175
|
-
wdk_evm_wallet_id=config.get("wdkEvmWalletId"),
|
|
176
|
-
wdk_evm_account_index=config.get("wdkEvmAccountIndex"),
|
|
177
|
-
)
|
|
184
|
+
context = _build_runtime_context(user_id, config)
|
|
178
185
|
result = await context.adapter.invoke(tool_name, arguments)
|
|
179
186
|
return result.model_dump()
|
|
180
187
|
|
|
@@ -188,21 +195,7 @@ async def _run_issue_approval(
|
|
|
188
195
|
mainnet_confirmed: bool = False,
|
|
189
196
|
ttl_seconds: int | None = None,
|
|
190
197
|
) -> dict[str, Any]:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
context = onboard_openclaw_user_wallet(
|
|
194
|
-
user_id,
|
|
195
|
-
backend=config.get("backend"),
|
|
196
|
-
sign_only=config.get("signOnly"),
|
|
197
|
-
network=config.get("network"),
|
|
198
|
-
rpc_url=config.get("rpcUrl"),
|
|
199
|
-
wdk_btc_service_url=config.get("wdkBtcServiceUrl"),
|
|
200
|
-
wdk_btc_wallet_id=config.get("wdkBtcWalletId"),
|
|
201
|
-
wdk_btc_account_index=config.get("wdkBtcAccountIndex"),
|
|
202
|
-
wdk_evm_service_url=config.get("wdkEvmServiceUrl"),
|
|
203
|
-
wdk_evm_wallet_id=config.get("wdkEvmWalletId"),
|
|
204
|
-
wdk_evm_account_index=config.get("wdkEvmAccountIndex"),
|
|
205
|
-
)
|
|
198
|
+
context = _build_runtime_context(user_id, config)
|
|
206
199
|
token = context.issue_execute_approval(
|
|
207
200
|
tool_name=tool_name,
|
|
208
201
|
confirmation_summary=summary,
|
|
@@ -250,6 +243,69 @@ def _run_autonomous_permission(action: str, scope: str) -> dict[str, Any]:
|
|
|
250
243
|
raise WalletBackendError("action must be approve, revoke, or status.")
|
|
251
244
|
|
|
252
245
|
|
|
246
|
+
def _error_payload_for_exception(exc: Exception) -> dict[str, Any]:
|
|
247
|
+
payload: dict[str, Any] = {"ok": False, "error": str(exc)}
|
|
248
|
+
if isinstance(exc, WalletBackendError):
|
|
249
|
+
if exc.code:
|
|
250
|
+
payload["code"] = exc.code
|
|
251
|
+
if exc.details is not None:
|
|
252
|
+
payload["details"] = exc.details
|
|
253
|
+
return payload
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
async def _run_read_worker_tool(context: Any, request: dict[str, Any]) -> dict[str, Any]:
|
|
257
|
+
tool_name = str(request.get("tool") or "").strip()
|
|
258
|
+
if tool_name not in READ_ONLY_WORKER_TOOLS:
|
|
259
|
+
raise WalletBackendError(
|
|
260
|
+
f"read-worker only allows read-only tools: {', '.join(sorted(READ_ONLY_WORKER_TOOLS))}."
|
|
261
|
+
)
|
|
262
|
+
arguments = request.get("arguments") or {}
|
|
263
|
+
if not isinstance(arguments, dict):
|
|
264
|
+
raise WalletBackendError("read-worker request arguments must be a JSON object.")
|
|
265
|
+
result = await context.adapter.invoke(tool_name, arguments)
|
|
266
|
+
return result.model_dump()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
async def _serve_read_worker(user_id: str, config: dict[str, Any]) -> int:
|
|
270
|
+
context = _build_runtime_context(user_id, config, read_only=True)
|
|
271
|
+
while True:
|
|
272
|
+
line = sys.stdin.readline()
|
|
273
|
+
if not line:
|
|
274
|
+
break
|
|
275
|
+
text = line.strip()
|
|
276
|
+
if not text:
|
|
277
|
+
continue
|
|
278
|
+
request_id: str | None = None
|
|
279
|
+
try:
|
|
280
|
+
request = _load_json(text)
|
|
281
|
+
request_id = str(request.get("id") or "").strip() or None
|
|
282
|
+
if request.get("op") == "shutdown":
|
|
283
|
+
print(json.dumps({"id": request_id, "ok": True, "stopped": True}), flush=True)
|
|
284
|
+
break
|
|
285
|
+
payload = await _run_read_worker_tool(context, request)
|
|
286
|
+
print(
|
|
287
|
+
json.dumps(
|
|
288
|
+
{
|
|
289
|
+
"id": request_id,
|
|
290
|
+
"ok": True,
|
|
291
|
+
"payload": payload,
|
|
292
|
+
}
|
|
293
|
+
),
|
|
294
|
+
flush=True,
|
|
295
|
+
)
|
|
296
|
+
except Exception as exc:
|
|
297
|
+
print(
|
|
298
|
+
json.dumps(
|
|
299
|
+
{
|
|
300
|
+
"id": request_id,
|
|
301
|
+
**_error_payload_for_exception(exc),
|
|
302
|
+
}
|
|
303
|
+
),
|
|
304
|
+
flush=True,
|
|
305
|
+
)
|
|
306
|
+
return 0
|
|
307
|
+
|
|
308
|
+
|
|
253
309
|
async def _run_btc_wallet_get(user_id: str, config: dict[str, Any]) -> dict[str, Any]:
|
|
254
310
|
from agent_wallet.btc_user_wallets import get_user_btc_wallet_binding
|
|
255
311
|
|
|
@@ -522,6 +578,10 @@ def main() -> int:
|
|
|
522
578
|
evm_lock_parser.add_argument("--user-id", required=True)
|
|
523
579
|
evm_lock_parser.add_argument("--config-json", default="{}")
|
|
524
580
|
|
|
581
|
+
read_worker_parser = subparsers.add_parser("read-worker")
|
|
582
|
+
read_worker_parser.add_argument("--user-id", required=True)
|
|
583
|
+
read_worker_parser.add_argument("--config-json", default="{}")
|
|
584
|
+
|
|
525
585
|
args = parser.parse_args()
|
|
526
586
|
|
|
527
587
|
try:
|
|
@@ -643,6 +703,8 @@ def main() -> int:
|
|
|
643
703
|
)
|
|
644
704
|
elif args.command == "evm-wallet-lock":
|
|
645
705
|
payload = asyncio.run(_run_evm_wallet_lock(args.user_id, config))
|
|
706
|
+
elif args.command == "read-worker":
|
|
707
|
+
return asyncio.run(_serve_read_worker(args.user_id, config))
|
|
646
708
|
else:
|
|
647
709
|
payload = asyncio.run(
|
|
648
710
|
_run_invoke(
|
|
@@ -662,13 +724,7 @@ def main() -> int:
|
|
|
662
724
|
backend=str(locals().get("config", {}).get("backend", "") or ""),
|
|
663
725
|
ok=False,
|
|
664
726
|
)
|
|
665
|
-
|
|
666
|
-
if isinstance(exc, WalletBackendError):
|
|
667
|
-
if exc.code:
|
|
668
|
-
error_payload["code"] = exc.code
|
|
669
|
-
if exc.details is not None:
|
|
670
|
-
error_payload["details"] = exc.details
|
|
671
|
-
print(json.dumps(error_payload), file=sys.stderr)
|
|
727
|
+
print(json.dumps(_error_payload_for_exception(exc)), file=sys.stderr)
|
|
672
728
|
return 1
|
|
673
729
|
|
|
674
730
|
if getattr(args, "command", "") == "invoke":
|
|
@@ -87,6 +87,7 @@ def onboard_openclaw_user_wallet(
|
|
|
87
87
|
*,
|
|
88
88
|
backend: str | None = None,
|
|
89
89
|
sign_only: bool | None = None,
|
|
90
|
+
read_only: bool = False,
|
|
90
91
|
network: str | None = None,
|
|
91
92
|
rpc_url: str | None = None,
|
|
92
93
|
wdk_btc_service_url: str | None = None,
|
|
@@ -98,6 +99,9 @@ def onboard_openclaw_user_wallet(
|
|
|
98
99
|
) -> OpenClawWalletRuntimeContext:
|
|
99
100
|
"""Provision and assemble a runtime-ready wallet context for one OpenClaw user."""
|
|
100
101
|
backend_name = str(backend or settings.agent_wallet_backend).strip().lower()
|
|
102
|
+
effective_sign_only = True if read_only else (
|
|
103
|
+
settings.agent_wallet_sign_only if sign_only is None else sign_only
|
|
104
|
+
)
|
|
101
105
|
if backend_name in {"wdk_btc_local", "wdk-btc-local", "btc_local", "btc-local"}:
|
|
102
106
|
service_url = str(wdk_btc_service_url or settings.wdk_btc_service_url).strip()
|
|
103
107
|
account_index = (
|
|
@@ -133,7 +137,7 @@ def onboard_openclaw_user_wallet(
|
|
|
133
137
|
wallet_id=wallet_id,
|
|
134
138
|
network=effective_network,
|
|
135
139
|
account_index=account_index,
|
|
136
|
-
sign_only=
|
|
140
|
+
sign_only=effective_sign_only,
|
|
137
141
|
address=str(address_payload.get("address") or "").strip() or None,
|
|
138
142
|
)
|
|
139
143
|
wallet_info = {
|
|
@@ -188,7 +192,7 @@ def onboard_openclaw_user_wallet(
|
|
|
188
192
|
wallet_id=wallet_id,
|
|
189
193
|
network=effective_network,
|
|
190
194
|
account_index=account_index,
|
|
191
|
-
sign_only=
|
|
195
|
+
sign_only=effective_sign_only,
|
|
192
196
|
address=resolved_address or None,
|
|
193
197
|
)
|
|
194
198
|
wallet_info = {
|
|
@@ -213,7 +217,8 @@ def onboard_openclaw_user_wallet(
|
|
|
213
217
|
|
|
214
218
|
backend, wallet_info, created_now = create_openclaw_solana_backend(
|
|
215
219
|
user_id,
|
|
216
|
-
sign_only=
|
|
220
|
+
sign_only=effective_sign_only,
|
|
221
|
+
read_only=read_only,
|
|
217
222
|
network=network,
|
|
218
223
|
rpc_url=rpc_url,
|
|
219
224
|
)
|
|
@@ -483,3 +483,37 @@ async def fetch_prices(
|
|
|
483
483
|
if not isinstance(data, dict):
|
|
484
484
|
raise ProviderError("jupiter", "Unexpected price response from Jupiter.")
|
|
485
485
|
return data
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
async def fetch_token_metadata(*, mints: list[str]) -> dict[str, dict[str, Any]]:
|
|
489
|
+
"""Look up token symbol/name for a batch of mints via Jupiter's token
|
|
490
|
+
search API. Keyed by mint address; mints Jupiter doesn't index (very new
|
|
491
|
+
or illiquid tokens) are simply absent from the result rather than
|
|
492
|
+
raising, since this is display-only enrichment on top of the price data.
|
|
493
|
+
"""
|
|
494
|
+
if not mints:
|
|
495
|
+
return {}
|
|
496
|
+
client = get_client()
|
|
497
|
+
params = {"query": ",".join(mints)}
|
|
498
|
+
response = await client.get(
|
|
499
|
+
f"{settings.jupiter_token_search_api_base_url.rstrip('/')}/search",
|
|
500
|
+
params=params,
|
|
501
|
+
headers=_headers(),
|
|
502
|
+
)
|
|
503
|
+
if response.status_code != 200:
|
|
504
|
+
raise ProviderError("jupiter", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
505
|
+
data = response.json()
|
|
506
|
+
if not isinstance(data, list):
|
|
507
|
+
raise ProviderError("jupiter", "Unexpected token search response from Jupiter.")
|
|
508
|
+
result: dict[str, dict[str, Any]] = {}
|
|
509
|
+
for item in data:
|
|
510
|
+
if not isinstance(item, dict):
|
|
511
|
+
continue
|
|
512
|
+
mint = str(item.get("id") or "").strip()
|
|
513
|
+
if not mint:
|
|
514
|
+
continue
|
|
515
|
+
result[mint] = {
|
|
516
|
+
"symbol": item.get("symbol"),
|
|
517
|
+
"name": item.get("name"),
|
|
518
|
+
}
|
|
519
|
+
return result
|
|
@@ -147,10 +147,31 @@ def _load_user_wallet_secret_material(
|
|
|
147
147
|
)
|
|
148
148
|
|
|
149
149
|
|
|
150
|
-
def ensure_user_solana_wallet(
|
|
150
|
+
def ensure_user_solana_wallet(
|
|
151
|
+
user_id: str,
|
|
152
|
+
network: str | None = None,
|
|
153
|
+
*,
|
|
154
|
+
read_only: bool = False,
|
|
155
|
+
) -> dict[str, str]:
|
|
151
156
|
"""Provision a per-user Solana wallet if it does not exist yet."""
|
|
152
157
|
effective_network = _resolve_effective_network(network)
|
|
153
158
|
path = resolve_user_wallet_path(user_id, network=effective_network)
|
|
159
|
+
if read_only and path.exists():
|
|
160
|
+
# Read-only callers only need the address. The pin file already stores
|
|
161
|
+
# it in plaintext, so skip decrypting the wallet secret material and
|
|
162
|
+
# deriving the signer entirely rather than paying for a KDF unseal +
|
|
163
|
+
# keypair derivation just to read (and immediately discard) a private
|
|
164
|
+
# key. Falls through to the full decrypt path below if no pin exists
|
|
165
|
+
# yet (e.g. a wallet file predating pin files).
|
|
166
|
+
pin = load_wallet_pin(path)
|
|
167
|
+
if pin is not None and pin["network"] == effective_network:
|
|
168
|
+
return {
|
|
169
|
+
"user_id": user_id,
|
|
170
|
+
"address": pin["address"],
|
|
171
|
+
"path": str(path),
|
|
172
|
+
"storage_format": "encrypted",
|
|
173
|
+
"key_scope": "pinned-address-only",
|
|
174
|
+
}
|
|
154
175
|
if path.exists():
|
|
155
176
|
secret_material, storage_format, key_scope = _load_user_wallet_secret_material(
|
|
156
177
|
path,
|
|
@@ -362,6 +383,7 @@ def create_openclaw_solana_backend(
|
|
|
362
383
|
user_id: str,
|
|
363
384
|
*,
|
|
364
385
|
sign_only: bool | None = None,
|
|
386
|
+
read_only: bool = False,
|
|
365
387
|
network: str | None = None,
|
|
366
388
|
rpc_url: str | None = None,
|
|
367
389
|
) -> tuple[SolanaWalletBackend, dict[str, str], bool]:
|
|
@@ -396,6 +418,11 @@ def create_openclaw_solana_backend(
|
|
|
396
418
|
raise WalletBackendError(
|
|
397
419
|
"Configured Solana publicKey does not match the signer derived from keypairPath/runtime secret."
|
|
398
420
|
)
|
|
421
|
+
effective_sign_only = True if read_only else (
|
|
422
|
+
settings.agent_wallet_sign_only if sign_only is None else sign_only
|
|
423
|
+
)
|
|
424
|
+
if read_only:
|
|
425
|
+
signer = None
|
|
399
426
|
|
|
400
427
|
rpc_config = resolve_runtime_solana_rpc_config(
|
|
401
428
|
effective_network,
|
|
@@ -411,7 +438,7 @@ def create_openclaw_solana_backend(
|
|
|
411
438
|
network=effective_network,
|
|
412
439
|
signer=signer,
|
|
413
440
|
address=resolved_address or None,
|
|
414
|
-
sign_only=
|
|
441
|
+
sign_only=effective_sign_only,
|
|
415
442
|
rpc_provider_mode=str(rpc_config["mode"]),
|
|
416
443
|
rpc_provider=str(rpc_config["provider"]),
|
|
417
444
|
rpc_transport=str(rpc_config["transport"]),
|
|
@@ -429,11 +456,27 @@ def create_openclaw_solana_backend(
|
|
|
429
456
|
|
|
430
457
|
wallet_path = resolve_user_wallet_path(user_id, network=effective_network)
|
|
431
458
|
created_now = not wallet_path.exists()
|
|
459
|
+
wallet_info = ensure_user_solana_wallet(user_id, network=effective_network, read_only=read_only)
|
|
460
|
+
if read_only:
|
|
461
|
+
backend = SolanaWalletBackend(
|
|
462
|
+
rpc_url=rpc_config["rpc_urls"],
|
|
463
|
+
commitment=settings.solana_commitment,
|
|
464
|
+
network=effective_network,
|
|
465
|
+
signer=None,
|
|
466
|
+
address=wallet_info["address"] or None,
|
|
467
|
+
sign_only=effective_sign_only,
|
|
468
|
+
rpc_provider_mode=str(rpc_config["mode"]),
|
|
469
|
+
rpc_provider=str(rpc_config["provider"]),
|
|
470
|
+
rpc_transport=str(rpc_config["transport"]),
|
|
471
|
+
swap_provider=str(swap_config["provider"]),
|
|
472
|
+
swap_transport=str(swap_config["transport"]),
|
|
473
|
+
)
|
|
474
|
+
return backend, wallet_info, created_now
|
|
475
|
+
|
|
432
476
|
backend = create_wallet_backend_for_user(
|
|
433
477
|
user_id,
|
|
434
478
|
sign_only=sign_only,
|
|
435
479
|
network=effective_network,
|
|
436
480
|
rpc_url=rpc_url,
|
|
437
481
|
)
|
|
438
|
-
wallet_info = ensure_user_solana_wallet(user_id, network=effective_network)
|
|
439
482
|
return backend, wallet_info, created_now
|
|
@@ -339,17 +339,37 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
339
339
|
|
|
340
340
|
price_data_by_mint: dict[str, dict[str, Any]] = {}
|
|
341
341
|
price_errors: list[str] = []
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
342
|
+
token_metadata_by_mint: dict[str, dict[str, Any]] = {}
|
|
343
|
+
token_metadata_errors: list[str] = []
|
|
344
|
+
price_batches = [mints[index : index + 20] for index in range(0, len(mints), 20)]
|
|
345
|
+
# Independent HTTP calls against a shared async client, so fetch every
|
|
346
|
+
# price batch AND every token symbol/name lookup batch concurrently
|
|
347
|
+
# instead of paying sequential round trips for wallets with many SPL
|
|
348
|
+
# token accounts.
|
|
349
|
+
all_results = await asyncio.gather(
|
|
350
|
+
*(jupiter.fetch_prices(mints=batch) for batch in price_batches),
|
|
351
|
+
*(jupiter.fetch_token_metadata(mints=batch) for batch in price_batches),
|
|
352
|
+
return_exceptions=True,
|
|
353
|
+
)
|
|
354
|
+
batch_results = all_results[: len(price_batches)]
|
|
355
|
+
metadata_batch_results = all_results[len(price_batches) :]
|
|
356
|
+
for batch, result in zip(price_batches, batch_results):
|
|
357
|
+
if isinstance(result, ProviderError):
|
|
358
|
+
price_errors.append(str(result))
|
|
348
359
|
continue
|
|
360
|
+
if isinstance(result, BaseException):
|
|
361
|
+
raise result
|
|
349
362
|
for mint in batch:
|
|
350
|
-
entry = _jupiter_price_entry(
|
|
363
|
+
entry = _jupiter_price_entry(result, mint)
|
|
351
364
|
if entry is not None:
|
|
352
365
|
price_data_by_mint[mint] = entry
|
|
366
|
+
for result in metadata_batch_results:
|
|
367
|
+
if isinstance(result, ProviderError):
|
|
368
|
+
token_metadata_errors.append(str(result))
|
|
369
|
+
continue
|
|
370
|
+
if isinstance(result, BaseException):
|
|
371
|
+
raise result
|
|
372
|
+
token_metadata_by_mint.update(result)
|
|
353
373
|
|
|
354
374
|
native_price = _jupiter_usd_price(price_data_by_mint.get(NATIVE_SOL_MINT))
|
|
355
375
|
native_amount = _coerce_decimal(native_balance.get("balance_native"))
|
|
@@ -379,13 +399,15 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
379
399
|
if value is not None:
|
|
380
400
|
total_value += value
|
|
381
401
|
priced_asset_count += 1
|
|
402
|
+
metadata = token_metadata_by_mint.get(mint) or {}
|
|
382
403
|
enriched_tokens.append(
|
|
383
404
|
{
|
|
384
405
|
**token,
|
|
406
|
+
"symbol": metadata.get("symbol"),
|
|
407
|
+
"name": metadata.get("name"),
|
|
385
408
|
"price_usd": str(price) if price is not None else None,
|
|
386
409
|
"value_usd": _format_decimal(value),
|
|
387
410
|
"pricing_source": "jupiter-price" if price is not None else None,
|
|
388
|
-
"price_raw": price_data_by_mint.get(mint),
|
|
389
411
|
}
|
|
390
412
|
)
|
|
391
413
|
|
|
@@ -410,6 +432,8 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
410
432
|
{
|
|
411
433
|
"asset_type": "spl-token",
|
|
412
434
|
"mint": token.get("mint"),
|
|
435
|
+
"symbol": token.get("symbol"),
|
|
436
|
+
"name": token.get("name"),
|
|
413
437
|
"token_account": token.get("token_account"),
|
|
414
438
|
"amount_raw": token.get("amount_raw"),
|
|
415
439
|
"amount_ui": token.get("amount_ui"),
|
|
@@ -442,6 +466,8 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
442
466
|
"total_value_usd": formatted_total_value,
|
|
443
467
|
"pricing_source": "jupiter-price" if price_data_by_mint else None,
|
|
444
468
|
"pricing_errors": price_errors,
|
|
469
|
+
"token_metadata_source": "jupiter-token-search" if token_metadata_by_mint else None,
|
|
470
|
+
"token_metadata_errors": token_metadata_errors,
|
|
445
471
|
"token_discovery_source": "solana-rpc",
|
|
446
472
|
"source": "solana-rpc+jupiter-price",
|
|
447
473
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "agent-wallet",
|
|
3
3
|
"name": "Agent Wallet",
|
|
4
4
|
"description": "Plugin-friendly wallet backend for OpenClaw agents with safe wallet tools and runtime instructions across Solana, local BTC, and local EVM.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.58",
|
|
6
6
|
"skills": ["skills/wallet-operator"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|