@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.
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +81 -0
- package/README.md +1 -1
- 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/boot_key_migration.py +158 -0
- package/agent-wallet/agent_wallet/boot_key_recovery.py +46 -0
- package/agent-wallet/agent_wallet/config.py +30 -1
- package/agent-wallet/agent_wallet/keystore.py +303 -0
- package/agent-wallet/agent_wallet/openclaw_cli.py +115 -39
- package/agent-wallet/agent_wallet/openclaw_runtime.py +28 -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/bin/openclaw-agent-wallet.mjs +73 -4
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/claude-code/plugins/agent-wallet/commands/wallet-sol.md +9 -15
- 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 +353 -4
- 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
|
@@ -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.64",
|
|
6
6
|
"skills": ["skills/wallet-operator"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
@@ -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
|
-
|
|
1322
|
-
|
|
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.
|
|
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
|
|
3
|
-
allowed-tools:
|
|
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
|
|
7
|
+
Show the connected Solana wallet portfolio directly in chat.
|
|
8
8
|
|
|
9
|
-
1. Call `
|
|
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.
|
|
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.
|
|
@@ -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:
|