@agentlayer.tech/wallet 0.1.55 → 0.1.64

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 (29) hide show
  1. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
  2. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  3. package/CHANGELOG.md +81 -0
  4. package/README.md +1 -1
  5. package/VERSION +1 -1
  6. package/agent-wallet/.env.example +1 -0
  7. package/agent-wallet/agent_wallet/__init__.py +1 -1
  8. package/agent-wallet/agent_wallet/boot_key_migration.py +158 -0
  9. package/agent-wallet/agent_wallet/boot_key_recovery.py +46 -0
  10. package/agent-wallet/agent_wallet/config.py +30 -1
  11. package/agent-wallet/agent_wallet/keystore.py +303 -0
  12. package/agent-wallet/agent_wallet/openclaw_cli.py +115 -39
  13. package/agent-wallet/agent_wallet/openclaw_runtime.py +28 -3
  14. package/agent-wallet/agent_wallet/providers/jupiter.py +34 -0
  15. package/agent-wallet/agent_wallet/user_wallets.py +46 -3
  16. package/agent-wallet/agent_wallet/wallet_layer/solana.py +34 -8
  17. package/agent-wallet/openclaw.plugin.json +1 -1
  18. package/agent-wallet/pyproject.toml +1 -1
  19. package/bin/openclaw-agent-wallet.mjs +73 -4
  20. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
  21. package/claude-code/plugins/agent-wallet/commands/wallet-sol.md +9 -15
  22. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
  23. package/codex/plugins/agent-wallet/README.md +9 -0
  24. package/codex/plugins/agent-wallet/server.py +353 -4
  25. package/codex/plugins/agent-wallet/skills/wallet-sol/SKILL.md +29 -0
  26. package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
  27. package/package.json +1 -1
  28. package/wdk-btc-wallet/package.json +1 -1
  29. package/wdk-evm-wallet/package.json +1 -1
@@ -147,10 +147,31 @@ def _load_user_wallet_secret_material(
147
147
  )
148
148
 
149
149
 
150
- def ensure_user_solana_wallet(user_id: str, network: str | None = None) -> dict[str, str]:
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=settings.agent_wallet_sign_only if sign_only is None else 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
- for index in range(0, len(mints), 20):
343
- batch = mints[index : index + 20]
344
- try:
345
- price_data = await jupiter.fetch_prices(mints=batch)
346
- except ProviderError as exc:
347
- price_errors.append(str(exc))
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(price_data, mint)
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.55",
5
+ "version": "0.1.64",
6
6
  "skills": ["skills/wallet-operator"],
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.55"
7
+ version = "0.1.64"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -23,6 +23,7 @@ const TELEMETRY_FLUSH_THROTTLE_SECONDS = 20;
23
23
  const TELEMETRY_FORCE_LINES = 25;
24
24
  const TELEMETRY_MAX_EVENTS_PER_FLUSH = 100;
25
25
  const TELEMETRY_HTTP_TIMEOUT_MS = 1500;
26
+ const KEYSTORE_BRIDGE_TIMEOUT_MS = 30000;
26
27
 
27
28
  function printHelp() {
28
29
  console.log(`openclaw-agent-wallet
@@ -148,6 +149,11 @@ function telemetrySource(env = process.env) {
148
149
  : "global_cli";
149
150
  }
150
151
 
152
+ function positiveIntEnv(name, fallback, env = process.env) {
153
+ const parsed = Number.parseInt(String(env[name] || ""), 10);
154
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
155
+ }
156
+
151
157
  function telemetryGatewayUrl(env = process.env) {
152
158
  return String(env.PROVIDER_GATEWAY_URL || DEFAULT_PROVIDER_GATEWAY_URL).trim().replace(/\/+$/, "");
153
159
  }
@@ -900,6 +906,60 @@ function ensureBootKeyFile(env = process.env) {
900
906
  return { path: keyFile, status: "created" };
901
907
  }
902
908
 
909
+ // Read the boot key from the OS keystore via the current runtime's Python
910
+ // (best-effort, "" on any failure). Lets a re-install after the runtime migration
911
+ // — which moves the key into the keystore and deletes every plaintext copy — still
912
+ // resolve the existing boot key instead of refusing to touch sealed secrets.
913
+ function readBootKeyFromKeystore(env = process.env) {
914
+ const runtimeRoot = resolvedCurrentRuntimeRoot(env);
915
+ if (!runtimeRoot) return "";
916
+ const py = resolveVenvPython(runtimeRoot);
917
+ if (!py) return "";
918
+ try {
919
+ const res = spawnSync(
920
+ py,
921
+ ["-c", "from agent_wallet.config import read_boot_key_from_keystore as r; print(r())"],
922
+ {
923
+ cwd: path.join(runtimeRoot, "agent-wallet"),
924
+ encoding: "utf8",
925
+ timeout: positiveIntEnv("AGENT_WALLET_KEYSTORE_BRIDGE_TIMEOUT_MS", KEYSTORE_BRIDGE_TIMEOUT_MS, env),
926
+ env: { ...env, OPENCLAW_HOME: resolveOpenclawHome(env) },
927
+ },
928
+ );
929
+ if ((res.status ?? 1) !== 0) return "";
930
+ return String(res.stdout || "").trim();
931
+ } catch {
932
+ return "";
933
+ }
934
+ }
935
+
936
+ // Provision the boot key into the OS keystore via the freshly installed runtime's
937
+ // Python. Returns true only when the import stored AND verified the key, so the
938
+ // caller may safely drop the plaintext .env copy. Best-effort: false on any failure
939
+ // (e.g. no usable keystore), in which case the caller keeps the legacy .env write.
940
+ function provisionBootKeyToKeystore(releaseRoot, env, bootKey) {
941
+ const key = String(bootKey || "").trim();
942
+ if (!key) return false;
943
+ const py = resolveVenvPython(releaseRoot);
944
+ if (!py) return false;
945
+ try {
946
+ const res = spawnSync(
947
+ py,
948
+ ["-m", "agent_wallet.openclaw_cli", "boot-key-import", "--key-stdin"],
949
+ {
950
+ cwd: path.join(releaseRoot, "agent-wallet"),
951
+ input: key,
952
+ encoding: "utf8",
953
+ timeout: positiveIntEnv("AGENT_WALLET_KEYSTORE_BRIDGE_TIMEOUT_MS", KEYSTORE_BRIDGE_TIMEOUT_MS, env),
954
+ env: { ...env, OPENCLAW_HOME: resolveOpenclawHome(env) },
955
+ },
956
+ );
957
+ return (res.status ?? 1) === 0;
958
+ } catch {
959
+ return false;
960
+ }
961
+ }
962
+
903
963
  function resolveVenvPython(releaseRoot) {
904
964
  const candidates = [
905
965
  path.join(releaseRoot, "agent-wallet", ".venv", "bin", "python"),
@@ -1169,7 +1229,8 @@ function buildInstallerEnv(args) {
1169
1229
  const existingBootKey =
1170
1230
  resolveBootKeyFromFile(env) ||
1171
1231
  readTextIfExists(defaultBootKeyFile(env)).trim() ||
1172
- currentBootKey(env);
1232
+ currentBootKey(env) ||
1233
+ readBootKeyFromKeystore(env);
1173
1234
  if (existingBootKey) {
1174
1235
  env.AGENT_WALLET_BOOT_KEY = existingBootKey;
1175
1236
  }
@@ -1318,9 +1379,17 @@ function runInstall(args, { commandName = "install" } = {}) {
1318
1379
  }
1319
1380
 
1320
1381
  if (env.AGENT_WALLET_BOOT_KEY) {
1321
- envFileSet(path.join(releaseRoot, "agent-wallet", ".env"), {
1322
- AGENT_WALLET_BOOT_KEY: env.AGENT_WALLET_BOOT_KEY,
1323
- });
1382
+ const envPath = path.join(releaseRoot, "agent-wallet", ".env");
1383
+ // Prefer the OS keystore: provision the boot key there and keep the plaintext
1384
+ // out of the release .env entirely. Only fall back to the legacy .env write when
1385
+ // no keystore round-trip is possible, so the runtime can still resolve the key.
1386
+ if (provisionBootKeyToKeystore(releaseRoot, env, env.AGENT_WALLET_BOOT_KEY)) {
1387
+ envFileUnset(envPath, ["AGENT_WALLET_BOOT_KEY"]);
1388
+ } else {
1389
+ envFileSet(envPath, {
1390
+ AGENT_WALLET_BOOT_KEY: env.AGENT_WALLET_BOOT_KEY,
1391
+ });
1392
+ }
1324
1393
  }
1325
1394
 
1326
1395
  const pythonInfo = activePythonRuntimeInfo(env);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
3
  "displayName": "Agent Wallet",
4
- "version": "0.1.55",
4
+ "version": "0.1.64",
5
5
  "description": "Claude Code bridge for the existing AgentLayer wallet runtime. Connects to Solana, Bitcoin, and EVM wallets without creating a new one.",
6
6
  "author": {
7
7
  "name": "AgentLayer"
@@ -1,17 +1,15 @@
1
1
  ---
2
- description: Show the connected Solana wallet overview directly in chat.
3
- allowed-tools: mcp__agent_wallet__get_wallet_overview
2
+ description: Show the connected Solana wallet portfolio directly in chat.
3
+ allowed-tools: mcp__agent_wallet__get_wallet_portfolio
4
4
  disable-model-invocation: true
5
5
  ---
6
6
 
7
- Show the connected Solana wallet overview directly in chat.
7
+ Show the connected Solana wallet portfolio directly in chat.
8
8
 
9
- 1. Call `get_wallet_overview` with:
9
+ 1. Call `get_wallet_portfolio` with:
10
10
 
11
11
  ```json
12
- {
13
- "backend": "solana"
14
- }
12
+ {}
15
13
  ```
16
14
 
17
15
  2. Format the response as a compact wallet report:
@@ -21,16 +19,12 @@ Show the connected Solana wallet overview directly in chat.
21
19
  - render a Markdown table with columns: `Asset | Type | Amount | USD Value`
22
20
  - use `assets` when present
23
21
  - for each asset row, prefer:
24
- - asset label: `symbol`, then `mint`, then `token_address`, then `asset_type`
22
+ - asset label: `symbol`, then `name`, then `mint`, then `token_address`, then `asset_type`
23
+ - when the label came from `symbol`/`name` (not the mint/token_address itself) and a `mint` or `token_address` is present, append it shortened in parentheses next to the label as `first6…last4` (e.g. `USDC (EPjFWd…TDt1v)`) — keep the contract visible but compact, never show it twice
25
24
  - amount: `amount_ui`, then `balance_ui`, then `balance_native`, then `amount_raw`
26
25
  - usd value: `value_usd`, then `balance_usd`
27
26
  - omit zero-value rows only when both the amount and USD value are clearly zero
28
27
  - if no asset rows are available, still report the native balance summary in prose
28
+ - do not include source metadata lines or footer fields such as `source`, `token_discovery_source`, `pricing_source`, or `pricing_errors`
29
29
 
30
- 3. After the table, add one short metadata line with any available sources:
31
-
32
- - `source`
33
- - `token_discovery_source`
34
- - `pricing_source`
35
-
36
- 4. Do not suggest transfers, swaps, or other write actions unless the user explicitly asks for them.
30
+ 3. Do not suggest transfers, swaps, or other write actions unless the user explicitly asks for them.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
- "version": "0.1.55",
3
+ "version": "0.1.64",
4
4
  "description": "Codex plugin bridge for the AgentLayer wallet runtime.",
5
5
  "author": {
6
6
  "name": "AgentLayer"
@@ -21,6 +21,8 @@ Primary design rules:
21
21
  - session wallet selection with `set_wallet_backend`
22
22
  - EVM network selection with `set_evm_network`
23
23
  - auto-managed approval binding for `preview -> execute` write flows
24
+ - bundled Codex skills, including `wallet-sol` for showing the Solana wallet
25
+ portfolio directly in chat
24
26
 
25
27
  ## Runtime requirements
26
28
 
@@ -28,6 +30,13 @@ Primary design rules:
28
30
  - keep the local wallet files and `~/.openclaw/sealed_keys.json` in place
29
31
  - use `wallet codex install --yes` to install this plugin into Codex
30
32
 
33
+ ## Bundled skill
34
+
35
+ After `wallet codex install --yes` and a Codex restart, the plugin ships a
36
+ bundled `wallet-sol` skill. In Codex you can invoke it from the slash menu or
37
+ explicitly as `$wallet-sol` to render the connected Solana wallet portfolio as a
38
+ compact chat table.
39
+
31
40
  ## Path resolution
32
41
 
33
42
  The bridge resolves the wallet runtime from: