@agentlayer.tech/wallet 0.1.18 → 0.1.20

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 (27) hide show
  1. package/.openclaw/AGENTS.md +0 -7
  2. package/.openclaw/extensions/agent-wallet/README.md +3 -2
  3. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  4. package/README.md +111 -3
  5. package/RELEASING.md +5 -15
  6. package/agent-wallet/README.md +3 -0
  7. package/agent-wallet/agent_wallet/config.py +11 -0
  8. package/agent-wallet/agent_wallet/evm_user_wallets.py +310 -2
  9. package/agent-wallet/agent_wallet/openclaw_runtime.py +10 -41
  10. package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +52 -0
  11. package/agent-wallet/pyproject.toml +1 -1
  12. package/agent-wallet/scripts/build_release_bundle.py +1 -0
  13. package/agent-wallet/scripts/flash-sdk-bridge/bridge.mjs +21 -11
  14. package/agent-wallet/scripts/install_agent_wallet.py +250 -14
  15. package/agent-wallet/scripts/install_openclaw_local_config.py +20 -51
  16. package/agent-wallet/scripts/install_openclaw_sealed_keys.py +9 -1
  17. package/bin/openclaw-agent-wallet.mjs +282 -24
  18. package/package.json +1 -2
  19. package/.openclaw/extensions/pay-bridge/README.md +0 -38
  20. package/.openclaw/extensions/pay-bridge/core.mjs +0 -287
  21. package/.openclaw/extensions/pay-bridge/dist/core.mjs +0 -287
  22. package/.openclaw/extensions/pay-bridge/dist/index.js +0 -196
  23. package/.openclaw/extensions/pay-bridge/index.ts +0 -196
  24. package/.openclaw/extensions/pay-bridge/openclaw.plugin.json +0 -34
  25. package/.openclaw/extensions/pay-bridge/package.json +0 -49
  26. package/.openclaw/extensions/pay-bridge/skills/pay-operator/SKILL.md +0 -20
  27. package/.openclaw/extensions/pay-bridge/smoke_pay_bridge.mjs +0 -38
@@ -3,14 +3,28 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import os
7
+ import secrets
8
+ import subprocess
9
+ import time
6
10
  from pathlib import Path
7
11
  from typing import Any
8
-
9
- from agent_wallet.config import resolve_openclaw_home, settings
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 ensure_user_evm_wallet_binding, get_user_evm_wallet_binding
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
- if wallet_id:
181
- try:
182
- binding = get_user_evm_wallet_binding(user_id, network=effective_network)
183
- except WalletBackendError:
184
- binding = ensure_user_evm_wallet_binding(
185
- user_id,
186
- network=effective_network,
187
- service_url=service_url,
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((binding or {}).get("address") or "").strip()
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,
@@ -102,6 +102,35 @@ def _unwrap_payload(response: httpx.Response) -> dict[str, Any]:
102
102
  return data
103
103
 
104
104
 
105
+ def _unwrap_list_payload(response: httpx.Response) -> list[dict[str, Any]]:
106
+ try:
107
+ payload = response.json()
108
+ except Exception as exc: # pragma: no cover - defensive
109
+ raise WalletBackendError(
110
+ f"wdk-evm-wallet returned a non-JSON response ({response.status_code}).",
111
+ code="network_unavailable",
112
+ details={
113
+ "service": "wdk-evm-wallet",
114
+ "http_status": response.status_code,
115
+ },
116
+ ) from exc
117
+ if response.status_code >= 400 or payload.get("ok") is False:
118
+ detail = payload.get("error") or f"HTTP {response.status_code}"
119
+ raise WalletBackendError(
120
+ str(detail),
121
+ code=str(payload.get("error_code") or "").strip() or None,
122
+ details=_error_details_from_payload(payload),
123
+ )
124
+ data = payload.get("data")
125
+ if not isinstance(data, list):
126
+ raise WalletBackendError("wdk-evm-wallet returned an invalid list response payload.")
127
+ wallets: list[dict[str, Any]] = []
128
+ for item in data:
129
+ if isinstance(item, dict):
130
+ wallets.append(dict(item))
131
+ return wallets
132
+
133
+
105
134
  class WdkEvmLocalClient:
106
135
  """Small client for the local EVM wallet service."""
107
136
 
@@ -203,3 +232,26 @@ class WdkEvmLocalClient:
203
232
  details={"service": "wdk-evm-wallet", "path": path},
204
233
  ) from exc
205
234
  return _unwrap_payload(response)
235
+
236
+ def list_wallets_sync(self) -> list[dict[str, Any]]:
237
+ try:
238
+ with httpx.Client(
239
+ timeout=float(settings.http_timeout),
240
+ headers=self._headers,
241
+ follow_redirects=False,
242
+ trust_env=False,
243
+ ) as client:
244
+ response = client.get(f"{self.base_url}/v1/evm/wallets")
245
+ except httpx.TimeoutException as exc:
246
+ raise WalletBackendError(
247
+ "wdk-evm-wallet request timed out.",
248
+ code="network_unavailable",
249
+ details={"service": "wdk-evm-wallet", "path": "/v1/evm/wallets"},
250
+ ) from exc
251
+ except httpx.RequestError as exc:
252
+ raise WalletBackendError(
253
+ f"wdk-evm-wallet request failed: {exc}",
254
+ code="network_unavailable",
255
+ details={"service": "wdk-evm-wallet", "path": "/v1/evm/wallets"},
256
+ ) from exc
257
+ return _unwrap_list_payload(response)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.18"
7
+ version = "0.1.20"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -33,6 +33,7 @@ INCLUDED_TOP_LEVEL_DIRS = [
33
33
  EXCLUDED_EXACT_RELATIVE_PATHS = {
34
34
  ".openclaw/extensions-local",
35
35
  ".openclaw/openclaw.local.example.json",
36
+ ".openclaw/extensions/pay-bridge",
36
37
  }
37
38
  EXCLUDED_DIR_NAMES = {
38
39
  ".git",
@@ -350,14 +350,22 @@ function variantToSide(sideVariant) {
350
350
  return String(sideVariant ?? "");
351
351
  }
352
352
 
353
+ function safeTokenSymbol(poolConfig, mintPk) {
354
+ try {
355
+ return poolConfig.getTokenFromMintPk(mintPk)?.symbol ?? null;
356
+ } catch {
357
+ return null;
358
+ }
359
+ }
360
+
353
361
  function buildMarketSnapshot(poolConfig, marketConfig, deprecated = false) {
354
- const targetToken = poolConfig.getTokenFromMintPk(marketConfig.targetMint);
355
- const collateralToken = poolConfig.getTokenFromMintPk(marketConfig.collateralMint);
362
+ const targetSymbol = safeTokenSymbol(poolConfig, marketConfig.targetMint);
363
+ const collateralSymbol = safeTokenSymbol(poolConfig, marketConfig.collateralMint);
356
364
  return {
357
365
  pool_name: poolConfig.poolName,
358
- symbol: targetToken.symbol,
359
- market_symbol: targetToken.symbol,
360
- collateral_symbol: collateralToken.symbol,
366
+ symbol: targetSymbol,
367
+ market_symbol: targetSymbol,
368
+ collateral_symbol: collateralSymbol,
361
369
  side: variantToSide(marketConfig.side),
362
370
  market_id: marketConfig.marketId,
363
371
  market_address: marketConfig.marketAccount.toBase58(),
@@ -374,14 +382,16 @@ function buildMarketSnapshot(poolConfig, marketConfig, deprecated = false) {
374
382
 
375
383
  function buildPositionSnapshot(poolConfig, positionAccount) {
376
384
  const marketConfig = poolConfig.getMarketConfigByPk(positionAccount.market);
377
- const targetToken = poolConfig.getTokenFromMintPk(marketConfig.targetMint);
378
- const collateralToken = poolConfig.getTokenFromMintPk(marketConfig.collateralMint);
385
+ const targetSymbol = marketConfig ? safeTokenSymbol(poolConfig, marketConfig.targetMint) : null;
386
+ const collateralSymbol = marketConfig
387
+ ? safeTokenSymbol(poolConfig, marketConfig.collateralMint)
388
+ : null;
379
389
  return {
380
390
  pool_name: poolConfig.poolName,
381
- symbol: targetToken.symbol,
382
- market_symbol: targetToken.symbol,
383
- collateral_symbol: collateralToken.symbol,
384
- side: variantToSide(marketConfig.side),
391
+ symbol: targetSymbol,
392
+ market_symbol: targetSymbol,
393
+ collateral_symbol: collateralSymbol,
394
+ side: variantToSide(marketConfig?.side),
385
395
  is_active: Boolean(positionAccount.isActive),
386
396
  position_address: positionAccount.pubkey.toBase58(),
387
397
  market_address: positionAccount.market.toBase58(),