@pollar/core 0.8.2 → 0.9.0-rc.0

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/dist/index.rn.js CHANGED
@@ -267,11 +267,14 @@ async function computeJwkThumbprint(jwk) {
267
267
  const digest = await sha256(new TextEncoder().encode(canonical));
268
268
  return base64urlEncode(digest);
269
269
  }
270
+ function toBase64url(value) {
271
+ return value.replace(/\+/g, "-").replace(/\//g, "_").replace(/[^A-Za-z0-9_-]/g, "");
272
+ }
270
273
  function canonicalEcJwk(jwk) {
271
274
  if (jwk.kty !== "EC" || jwk.crv !== "P-256" || typeof jwk.x !== "string" || typeof jwk.y !== "string") {
272
275
  throw new Error("[PollarClient:thumbprint] Source JWK is not an EC P-256 public key");
273
276
  }
274
- return { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y };
277
+ return { kty: "EC", crv: "P-256", x: toBase64url(jwk.x), y: toBase64url(jwk.y) };
275
278
  }
276
279
 
277
280
  // src/keys/noble.ts
@@ -377,167 +380,6 @@ var NobleKeyManager = class {
377
380
  }
378
381
  };
379
382
 
380
- // src/keys/web-crypto.ts
381
- var DB_NAME = "pollar-keys";
382
- var DB_VERSION = 1;
383
- var STORE_NAME = "keys";
384
- function openDb() {
385
- return new Promise((resolve, reject) => {
386
- if (typeof indexedDB === "undefined") {
387
- reject(new Error("[PollarClient:keys] IndexedDB not available"));
388
- return;
389
- }
390
- const req = indexedDB.open(DB_NAME, DB_VERSION);
391
- req.onerror = () => reject(req.error ?? new Error("IDB open failed"));
392
- req.onsuccess = () => resolve(req.result);
393
- req.onupgradeneeded = () => {
394
- const db = req.result;
395
- if (!db.objectStoreNames.contains(STORE_NAME)) {
396
- db.createObjectStore(STORE_NAME);
397
- }
398
- };
399
- });
400
- }
401
- function awaitTx(req) {
402
- return new Promise((resolve, reject) => {
403
- req.onsuccess = () => resolve(req.result);
404
- req.onerror = () => reject(req.error ?? new Error("IDB request failed"));
405
- });
406
- }
407
- async function dbGet(key) {
408
- const db = await openDb();
409
- try {
410
- const tx = db.transaction(STORE_NAME, "readonly");
411
- const result = await awaitTx(tx.objectStore(STORE_NAME).get(key));
412
- return result;
413
- } finally {
414
- db.close();
415
- }
416
- }
417
- async function dbPut(key, value) {
418
- const db = await openDb();
419
- try {
420
- const tx = db.transaction(STORE_NAME, "readwrite");
421
- await awaitTx(tx.objectStore(STORE_NAME).put(value, key));
422
- } finally {
423
- db.close();
424
- }
425
- }
426
- async function dbDelete(key) {
427
- const db = await openDb();
428
- try {
429
- const tx = db.transaction(STORE_NAME, "readwrite");
430
- await awaitTx(tx.objectStore(STORE_NAME).delete(key));
431
- } finally {
432
- db.close();
433
- }
434
- }
435
- function isCryptoKeyPair(v) {
436
- if (typeof v !== "object" || v === null) return false;
437
- const obj = v;
438
- return obj.privateKey !== void 0 && obj.publicKey !== void 0;
439
- }
440
- var WebCryptoKeyManager = class {
441
- constructor(apiKey) {
442
- this.apiKeyHash = null;
443
- this.keyPair = null;
444
- this.publicJwk = null;
445
- this.thumbprint = null;
446
- /**
447
- * Cached in-flight init. Lets `init()` be called concurrently (or implicitly
448
- * from `getPublicJwk` / `sign`) without doing the work twice. Cleared on
449
- * failure so callers can retry, and cleared on `reset()`.
450
- */
451
- this._initPromise = null;
452
- if (typeof globalThis.crypto === "undefined" || !globalThis.crypto.subtle) {
453
- throw new Error("[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost).");
454
- }
455
- this.apiKey = apiKey;
456
- }
457
- /**
458
- * Idempotent and safe under concurrency. The first call kicks off the real
459
- * init; subsequent (and concurrent) calls return the same in-flight promise.
460
- * Other methods (`getPublicJwk`, `getThumbprint`, `sign`) auto-await this so
461
- * the manager is self-healing if `init()` was never explicitly invoked.
462
- */
463
- async init() {
464
- if (this.keyPair) return;
465
- if (!this._initPromise) {
466
- this._initPromise = this._doInit().catch((err) => {
467
- console.error("[PollarClient:keys] WebCryptoKeyManager init failed", err);
468
- this._initPromise = null;
469
- throw err;
470
- });
471
- }
472
- return this._initPromise;
473
- }
474
- async _doInit() {
475
- if (!this.apiKeyHash) {
476
- this.apiKeyHash = await hashApiKey(this.apiKey);
477
- }
478
- let pair;
479
- try {
480
- pair = await dbGet(this.apiKeyHash);
481
- if (pair && !isCryptoKeyPair(pair)) pair = void 0;
482
- } catch {
483
- pair = void 0;
484
- }
485
- if (!pair) {
486
- pair = await globalThis.crypto.subtle.generateKey(
487
- { name: "ECDSA", namedCurve: "P-256" },
488
- // false → private key non-extractable; per W3C ECDSA spec the public
489
- // key is always extractable regardless of this flag.
490
- false,
491
- ["sign", "verify"]
492
- );
493
- try {
494
- await dbPut(this.apiKeyHash, pair);
495
- } catch {
496
- }
497
- }
498
- this.keyPair = pair;
499
- const exported = await globalThis.crypto.subtle.exportKey("jwk", pair.publicKey);
500
- this.publicJwk = canonicalEcJwk(exported);
501
- this.thumbprint = await computeJwkThumbprint(this.publicJwk);
502
- }
503
- async reset() {
504
- try {
505
- if (this.apiKeyHash) await dbDelete(this.apiKeyHash);
506
- } catch {
507
- }
508
- this.keyPair = null;
509
- this.publicJwk = null;
510
- this.thumbprint = null;
511
- this._initPromise = null;
512
- }
513
- async getPublicJwk() {
514
- if (!this.publicJwk) await this.init();
515
- if (!this.publicJwk) {
516
- throw new Error("[PollarClient:keys] Keypair initialization failed; getPublicJwk unavailable");
517
- }
518
- return { kty: this.publicJwk.kty, crv: this.publicJwk.crv, x: this.publicJwk.x, y: this.publicJwk.y };
519
- }
520
- async getThumbprint() {
521
- if (!this.thumbprint) await this.init();
522
- if (!this.thumbprint) {
523
- throw new Error("[PollarClient:keys] Keypair initialization failed; getThumbprint unavailable");
524
- }
525
- return this.thumbprint;
526
- }
527
- async sign(payload) {
528
- if (!this.keyPair) await this.init();
529
- if (!this.keyPair) {
530
- throw new Error("[PollarClient:keys] Keypair initialization failed; sign unavailable");
531
- }
532
- const sig = await globalThis.crypto.subtle.sign(
533
- { name: "ECDSA", hash: "SHA-256" },
534
- this.keyPair.privateKey,
535
- payload
536
- );
537
- return new Uint8Array(sig);
538
- }
539
- };
540
-
541
383
  // ../../node_modules/openapi-fetch/dist/index.mjs
542
384
  var PATH_PARAM_RE = /\{[^{}]+\}/g;
543
385
  var supportsRequestInitExt = () => {
@@ -1257,6 +1099,9 @@ function defaultStorage(options = {}) {
1257
1099
  return createLocalStorageAdapter(options);
1258
1100
  }
1259
1101
 
1102
+ // src/version.ts
1103
+ var POLLAR_CORE_VERSION = "0.9.0-rc.0" ;
1104
+
1260
1105
  // src/visibility/noop.ts
1261
1106
  function createNoopVisibilityProvider() {
1262
1107
  return {
@@ -1321,6 +1166,7 @@ var AUTH_ERROR_CODES = {
1321
1166
  WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
1322
1167
  WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
1323
1168
  WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
1169
+ PASSKEY_FAILED: "PASSKEY_FAILED",
1324
1170
  UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
1325
1171
  };
1326
1172
  var PollarFlowError = class extends Error {
@@ -1366,7 +1212,7 @@ var FreighterAdapter = class {
1366
1212
  if (!userInfo?.publicKey) {
1367
1213
  throw new Error("Failed to get user information from Freighter");
1368
1214
  }
1369
- return { address: userInfo.publicKey, publicKey: userInfo.publicKey };
1215
+ return { address: userInfo.publicKey };
1370
1216
  }
1371
1217
  async disconnect() {
1372
1218
  }
@@ -1404,6 +1250,19 @@ var FreighterAdapter = class {
1404
1250
  };
1405
1251
 
1406
1252
  // src/wallets/AlbedoAdapter.ts
1253
+ var PUBLIC_PASSPHRASE = "Public Global Stellar Network ; September 2015";
1254
+ var TESTNET_PASSPHRASE = "Test SDF Network ; September 2015";
1255
+ function albedoNetwork(options, fallback) {
1256
+ switch (options?.networkPassphrase) {
1257
+ case PUBLIC_PASSPHRASE:
1258
+ return "public";
1259
+ case TESTNET_PASSPHRASE:
1260
+ return "testnet";
1261
+ }
1262
+ if (options?.network === "public" || options?.network === "mainnet") return "public";
1263
+ if (options?.network === "testnet") return "testnet";
1264
+ return fallback;
1265
+ }
1407
1266
  function openAlbedoPopup(url) {
1408
1267
  const popup = window.open(url, "albedo", "width=420,height=720,resizable=yes,scrollbars=yes");
1409
1268
  if (!popup) {
@@ -1442,7 +1301,13 @@ function waitForAlbedoResult() {
1442
1301
  });
1443
1302
  }
1444
1303
  var AlbedoAdapter = class {
1445
- constructor() {
1304
+ /**
1305
+ * Network used for `connect` and `signAuthEntry` (which carry no per-call
1306
+ * network) and as the fallback for `signTransaction`. Defaults to `'testnet'`
1307
+ * to preserve the previous behavior when constructed with no argument.
1308
+ */
1309
+ constructor(network = "testnet") {
1310
+ this.network = network;
1446
1311
  this.type = "albedo" /* ALBEDO */;
1447
1312
  }
1448
1313
  async isAvailable() {
@@ -1452,7 +1317,7 @@ var AlbedoAdapter = class {
1452
1317
  const url = new URL("https://albedo.link");
1453
1318
  url.searchParams.set("intent", "public-key");
1454
1319
  url.searchParams.set("app_name", "Pollar");
1455
- url.searchParams.set("network", "testnet");
1320
+ url.searchParams.set("network", this.network);
1456
1321
  url.searchParams.set("callback", `${window.location.origin}/albedo-callback`);
1457
1322
  url.searchParams.set("origin", window.location.origin);
1458
1323
  openAlbedoPopup(url.toString());
@@ -1460,7 +1325,7 @@ var AlbedoAdapter = class {
1460
1325
  if (!result.pubkey) {
1461
1326
  throw new Error("Albedo connection rejected");
1462
1327
  }
1463
- return { address: result.pubkey, publicKey: result.pubkey };
1328
+ return { address: result.pubkey };
1464
1329
  }
1465
1330
  async disconnect() {
1466
1331
  }
@@ -1470,12 +1335,12 @@ var AlbedoAdapter = class {
1470
1335
  async getNetwork() {
1471
1336
  throw new Error("Albedo does not expose network");
1472
1337
  }
1473
- async signTransaction(xdr, _options) {
1338
+ async signTransaction(xdr, options) {
1474
1339
  const url = new URL("https://albedo.link");
1475
1340
  url.searchParams.set("intent", "tx");
1476
1341
  url.searchParams.set("xdr", xdr);
1477
1342
  url.searchParams.set("app_name", "Pollar");
1478
- url.searchParams.set("network", "testnet");
1343
+ url.searchParams.set("network", albedoNetwork(options, this.network));
1479
1344
  url.searchParams.set("callback", window.location.href);
1480
1345
  url.searchParams.set("origin", window.location.origin);
1481
1346
  window.location.href = url.toString();
@@ -1488,7 +1353,7 @@ var AlbedoAdapter = class {
1488
1353
  url.searchParams.set("intent", "sign-auth-entry");
1489
1354
  url.searchParams.set("xdr", entryXdr);
1490
1355
  url.searchParams.set("app_name", "Pollar");
1491
- url.searchParams.set("network", "testnet");
1356
+ url.searchParams.set("network", this.network);
1492
1357
  url.searchParams.set("callback", window.location.href);
1493
1358
  url.searchParams.set("origin", window.location.origin);
1494
1359
  window.location.href = url.toString();
@@ -1575,8 +1440,12 @@ function isValidSession(value) {
1575
1440
  return false;
1576
1441
  }
1577
1442
  const w = wallet;
1578
- if (w["publicKey"] !== null && !isBoundedString(w["publicKey"], MAX_WALLET_PUBLIC_KEY)) {
1579
- console.warn("[PollarClient:session] Invalid session \u2014 wallet.publicKey must be string|null");
1443
+ if (w["type"] !== "custodial" && w["type"] !== "smart" && w["type"] !== "external") {
1444
+ console.warn("[PollarClient:session] Invalid session \u2014 wallet.type must be custodial|smart|external");
1445
+ return false;
1446
+ }
1447
+ if (w["address"] !== null && !isBoundedString(w["address"], MAX_WALLET_PUBLIC_KEY)) {
1448
+ console.warn("[PollarClient:session] Invalid session \u2014 wallet.address must be string|null");
1580
1449
  return false;
1581
1450
  }
1582
1451
  if (w["existsOnStellar"] !== void 0 && typeof w["existsOnStellar"] !== "boolean") {
@@ -1587,6 +1456,10 @@ function isValidSession(value) {
1587
1456
  console.warn("[PollarClient:session] Invalid session \u2014 wallet.createdAt must be a finite number if present");
1588
1457
  return false;
1589
1458
  }
1459
+ if (w["linkedAt"] !== void 0 && (typeof w["linkedAt"] !== "number" || !Number.isFinite(w["linkedAt"]))) {
1460
+ console.warn("[PollarClient:session] Invalid session \u2014 wallet.linkedAt must be a finite number if present");
1461
+ return false;
1462
+ }
1590
1463
  return true;
1591
1464
  }
1592
1465
  async function readStorage(storage, apiKeyHash) {
@@ -1594,6 +1467,12 @@ async function readStorage(storage, apiKeyHash) {
1594
1467
  if (!raw) return null;
1595
1468
  try {
1596
1469
  const session = JSON.parse(raw);
1470
+ if (typeof session === "object" && session !== null) {
1471
+ const w = session.wallet;
1472
+ if (w && w["address"] == null && typeof w["publicKey"] === "string") {
1473
+ w["address"] = w["publicKey"];
1474
+ }
1475
+ }
1597
1476
  if (!isValidSession(session)) {
1598
1477
  await storage.remove(sessionStorageKey(apiKeyHash));
1599
1478
  console.warn("[PollarClient:session] Stored session is invalid \u2014 clearing storage");
@@ -1684,7 +1563,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1684
1563
  }));
1685
1564
  } catch (e) {
1686
1565
  if (e instanceof Error && e.name === "AbortError") throw e;
1687
- console.warn(e);
1566
+ console.warn("[PollarClient:stream] session-status request failed; will retry", e);
1688
1567
  }
1689
1568
  if (error || !data) {
1690
1569
  await sleep(backoff);
@@ -1724,7 +1603,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1724
1603
  } catch (e) {
1725
1604
  if (e instanceof Error && e.name === "AbortError") throw e;
1726
1605
  if (e instanceof SessionStatusError) throw e;
1727
- console.warn(e);
1606
+ console.warn("[PollarClient:stream] session-status stream read failed; will retry", e);
1728
1607
  } finally {
1729
1608
  reader.releaseLock();
1730
1609
  }
@@ -1752,7 +1631,7 @@ async function pollUntilFound(baseUrl, clientSessionId, check, intervalMs = 500,
1752
1631
  envelope = await response.json().catch(() => null);
1753
1632
  } catch (e) {
1754
1633
  if (e instanceof Error && e.name === "AbortError") throw e;
1755
- console.warn(e);
1634
+ console.warn("[PollarClient:stream] session-status poll failed; will retry", e);
1756
1635
  }
1757
1636
  if (httpStatus === 404 || envelope?.code === "INVALID_CLIENT_SESSION_ID") {
1758
1637
  throw new SessionStatusError("INVALID_CLIENT_SESSION_ID");
@@ -1956,6 +1835,55 @@ async function loginOAuth(provider, deps) {
1956
1835
  await authenticate(clientSessionId, deps);
1957
1836
  }
1958
1837
 
1838
+ // src/client/auth/passkeyFlow.ts
1839
+ async function loginSmartWallet(deps) {
1840
+ const { api, signal, setAuthState, passkey } = deps;
1841
+ if (!passkey) {
1842
+ setAuthState({
1843
+ step: "error",
1844
+ previousStep: "creating_session",
1845
+ message: "Passkey support is not configured",
1846
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1847
+ });
1848
+ return;
1849
+ }
1850
+ const clientSessionId = await createAuthSession(deps);
1851
+ if (!clientSessionId) return;
1852
+ try {
1853
+ const { data: challengeData } = await api.POST("/auth/passkey/challenge", {
1854
+ body: { clientSessionId },
1855
+ signal
1856
+ });
1857
+ const challenge = challengeData?.content?.challenge;
1858
+ if (!challengeData?.success || !challenge) {
1859
+ return failPasskey(setAuthState, "Failed to start passkey");
1860
+ }
1861
+ setAuthState({ step: "creating_passkey" });
1862
+ const ceremony = await passkey({ challenge });
1863
+ const response = ceremony.response;
1864
+ if (ceremony.kind === "register") {
1865
+ setAuthState({ step: "deploying_smart_account" });
1866
+ const { data } = await api.POST("/auth/passkey/register", {
1867
+ body: { clientSessionId, response },
1868
+ signal
1869
+ });
1870
+ if (!data?.success) return failPasskey(setAuthState, "Passkey registration failed");
1871
+ } else {
1872
+ const { data } = await api.POST("/auth/passkey/login", {
1873
+ body: { clientSessionId, response },
1874
+ signal
1875
+ });
1876
+ if (!data?.success) return failPasskey(setAuthState, "Passkey authentication failed");
1877
+ }
1878
+ } catch {
1879
+ return failPasskey(setAuthState, "Passkey login failed");
1880
+ }
1881
+ await authenticate(clientSessionId, deps);
1882
+ }
1883
+ function failPasskey(setAuthState, message) {
1884
+ setAuthState({ step: "error", previousStep: "creating_passkey", message, errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED });
1885
+ }
1886
+
1959
1887
  // src/client/auth/walletFlow.ts
1960
1888
  function withSignal(promise, signal) {
1961
1889
  return Promise.race([
@@ -1982,12 +1910,12 @@ async function loginWallet(type, deps) {
1982
1910
  setAuthState({ step: "wallet_not_installed", walletType: type });
1983
1911
  return;
1984
1912
  }
1985
- const { publicKey } = await withSignal(adapter.connect(), signal);
1986
- connectedWallet = publicKey;
1913
+ const { address } = await withSignal(adapter.connect(), signal);
1914
+ connectedWallet = address;
1987
1915
  deps.storeWalletAdapter(adapter, type);
1988
1916
  setAuthState({ step: "authenticating_wallet" });
1989
1917
  const { data: walletData, error: walletError } = await api.POST("/auth/wallet", {
1990
- body: { clientSessionId, walletAddress: publicKey },
1918
+ body: { clientSessionId, walletAddress: address },
1991
1919
  signal
1992
1920
  });
1993
1921
  if (walletError || !walletData?.success) {
@@ -2052,8 +1980,12 @@ var PollarClient = class {
2052
1980
  this._transactionStateListeners = /* @__PURE__ */ new Set();
2053
1981
  this._txHistoryState = { step: "idle" };
2054
1982
  this._txHistoryStateListeners = /* @__PURE__ */ new Set();
1983
+ this._sessionsState = { step: "idle" };
1984
+ this._sessionsStateListeners = /* @__PURE__ */ new Set();
2055
1985
  this._walletBalanceState = { step: "idle" };
2056
1986
  this._walletBalanceStateListeners = /* @__PURE__ */ new Set();
1987
+ this._enabledAssetsState = { step: "idle" };
1988
+ this._enabledAssetsStateListeners = /* @__PURE__ */ new Set();
2057
1989
  this._authState = { step: "idle" };
2058
1990
  this._authStateListeners = /* @__PURE__ */ new Set();
2059
1991
  this._networkState = { step: "idle" };
@@ -2069,6 +2001,8 @@ var PollarClient = class {
2069
2001
  this._storageDegradeListeners = /* @__PURE__ */ new Set();
2070
2002
  this._walletAdapter = null;
2071
2003
  this._loginController = null;
2004
+ /** Aborts an in-flight `/auth/session/resume` on destroy() or re-trigger. */
2005
+ this._resumeController = null;
2072
2006
  this.apiKey = config.apiKey;
2073
2007
  this.id = randomUUID();
2074
2008
  this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
@@ -2081,6 +2015,8 @@ var PollarClient = class {
2081
2015
  this._keyManager = config.keyManager ?? defaultKeyManager(this._storage, config.apiKey);
2082
2016
  this._walletAdapterResolver = config.walletAdapter ?? null;
2083
2017
  this._walletResolverTimeoutMs = config.walletResolverTimeoutMs ?? 5e3;
2018
+ this._passkey = config.passkey ?? null;
2019
+ this._passkeySign = config.passkeySign ?? null;
2084
2020
  this._deviceLabel = config.deviceLabel;
2085
2021
  this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
2086
2022
  this._maxIdleMs = config.maxIdleMs;
@@ -2094,7 +2030,9 @@ var PollarClient = class {
2094
2030
  this._initialized = Promise.resolve();
2095
2031
  return;
2096
2032
  }
2097
- console.info(`[PollarClient] Initialized \u2014 endpoint: ${this.basePath}, network: ${this._networkState.network}`);
2033
+ console.info(
2034
+ `[PollarClient] Initialized v${POLLAR_CORE_VERSION} \u2014 endpoint: ${this.basePath}, network: ${this._networkState.network}`
2035
+ );
2098
2036
  this._initialized = this._initialize();
2099
2037
  }
2100
2038
  /**
@@ -2132,7 +2070,11 @@ var PollarClient = class {
2132
2070
  }
2133
2071
  await this._restoreSession();
2134
2072
  this._visibilityUnsubscribe = this._visibilityProvider.onChange((visible) => {
2135
- if (visible) void this._maybeProactiveRefresh();
2073
+ if (!visible) return;
2074
+ void this._maybeProactiveRefresh();
2075
+ if (this._authState.step === "authenticated" && !this._authState.verified) {
2076
+ void this._resume();
2077
+ }
2136
2078
  });
2137
2079
  }
2138
2080
  /** Detach the cross-tab storage listener and abort any in-flight login. */
@@ -2143,6 +2085,8 @@ var PollarClient = class {
2143
2085
  }
2144
2086
  this._loginController?.abort();
2145
2087
  this._loginController = null;
2088
+ this._resumeController?.abort();
2089
+ this._resumeController = null;
2146
2090
  this._clearRefreshTimer();
2147
2091
  if (this._visibilityUnsubscribe) {
2148
2092
  this._visibilityUnsubscribe();
@@ -2157,7 +2101,9 @@ var PollarClient = class {
2157
2101
  request.headers.set("x-pollar-api-key", self.apiKey);
2158
2102
  self._lastRequestAt = Date.now();
2159
2103
  await self._initialized;
2160
- if (request.body !== null) {
2104
+ const cacheMethod = request.method.toUpperCase();
2105
+ const cacheBodyAllowed = cacheMethod !== "GET" && cacheMethod !== "HEAD";
2106
+ if (cacheBodyAllowed && request.body != null) {
2161
2107
  try {
2162
2108
  self._requestBodyCache.set(request, await request.clone().arrayBuffer());
2163
2109
  } catch (err) {
@@ -2244,7 +2190,9 @@ var PollarClient = class {
2244
2190
  }
2245
2191
  }
2246
2192
  }
2247
- const cachedBody = this._requestBodyCache.get(originalRequest);
2193
+ const retryMethod = originalRequest.method.toUpperCase();
2194
+ const retryBodyAllowed = retryMethod !== "GET" && retryMethod !== "HEAD";
2195
+ const cachedBody = retryBodyAllowed ? this._requestBodyCache.get(originalRequest) : void 0;
2248
2196
  const retried = new Request(originalRequest.url, {
2249
2197
  method: originalRequest.method,
2250
2198
  headers,
@@ -2288,19 +2236,26 @@ var PollarClient = class {
2288
2236
  throw err;
2289
2237
  }
2290
2238
  if (error || !data) {
2291
- console.warn("[PollarClient] /auth/refresh returned error", { error });
2239
+ console.error("[PollarClient] /auth/refresh returned error", { error });
2292
2240
  await this._clearSession();
2293
2241
  throw new Error("Refresh failed");
2294
2242
  }
2295
2243
  const successData = data;
2296
2244
  if (!successData.success || !successData.content?.token) {
2297
- console.warn("[PollarClient] /auth/refresh response malformed", successData);
2245
+ console.error("[PollarClient] /auth/refresh response malformed", {
2246
+ success: successData.success,
2247
+ hasToken: !!successData.content?.token
2248
+ });
2298
2249
  await this._clearSession();
2299
2250
  throw new Error("Refresh response malformed");
2300
2251
  }
2301
2252
  const newToken = successData.content.token;
2302
2253
  if (typeof newToken.accessToken !== "string" || typeof newToken.refreshToken !== "string" || typeof newToken.expiresAt !== "number") {
2303
- console.warn("[PollarClient] /auth/refresh token shape invalid", newToken);
2254
+ console.error("[PollarClient] /auth/refresh token shape invalid", {
2255
+ accessToken: typeof newToken.accessToken,
2256
+ refreshToken: typeof newToken.refreshToken,
2257
+ expiresAt: typeof newToken.expiresAt
2258
+ });
2304
2259
  await this._clearSession();
2305
2260
  throw new Error("Refresh response token shape invalid");
2306
2261
  }
@@ -2494,6 +2449,19 @@ var PollarClient = class {
2494
2449
  const controller = this._newController();
2495
2450
  loginWallet(type, this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
2496
2451
  }
2452
+ /**
2453
+ * "Smart Wallet" login: runs the passkey (WebAuthn) ceremony and, for a new
2454
+ * user, creates a sponsored smart-account C-address. Requires the `passkey`
2455
+ * ceremony to be configured (e.g. via `@pollar/react`).
2456
+ */
2457
+ loginSmartWallet() {
2458
+ if (!isClientRuntime) {
2459
+ warnServerSide("loginSmartWallet");
2460
+ return;
2461
+ }
2462
+ const controller = this._newController();
2463
+ loginSmartWallet(this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
2464
+ }
2497
2465
  // ─── Cancel ───────────────────────────────────────────────────────────────
2498
2466
  cancelLogin() {
2499
2467
  this._loginController?.abort();
@@ -2556,6 +2524,29 @@ var PollarClient = class {
2556
2524
  }
2557
2525
  return data.content.sessions;
2558
2526
  }
2527
+ getSessionsState() {
2528
+ return this._sessionsState;
2529
+ }
2530
+ onSessionsStateChange(cb) {
2531
+ this._sessionsStateListeners.add(cb);
2532
+ cb(this._sessionsState);
2533
+ return () => this._sessionsStateListeners.delete(cb);
2534
+ }
2535
+ /**
2536
+ * Fire-and-forget variant of {@link listSessions} that drives the observable
2537
+ * `SessionsState` store instead of returning the array. UI layers subscribe
2538
+ * via `onSessionsStateChange` and stay pure readers — mirrors `fetchTxHistory`.
2539
+ */
2540
+ async fetchSessions() {
2541
+ this._setSessionsState({ step: "loading" });
2542
+ try {
2543
+ const sessions = await this.listSessions();
2544
+ this._setSessionsState({ step: "loaded", sessions });
2545
+ } catch (err) {
2546
+ const message = err instanceof Error ? err.message : "Failed to load sessions";
2547
+ this._setSessionsState({ step: "error", message });
2548
+ }
2549
+ }
2559
2550
  /**
2560
2551
  * Revoke a specific refresh-token family (a single device session). Use
2561
2552
  * `listSessions` to enumerate the familyIds. Revoking the current session
@@ -2632,16 +2623,19 @@ var PollarClient = class {
2632
2623
  cb(this._walletBalanceState);
2633
2624
  return () => this._walletBalanceStateListeners.delete(cb);
2634
2625
  }
2635
- async refreshBalance(publicKey) {
2636
- const pk = publicKey ?? this._session?.wallet?.publicKey;
2637
- if (!pk) {
2626
+ /**
2627
+ * Refreshes the balances of the authenticated user's OWN wallet. The wallet
2628
+ * and network are resolved server-side from the session — no arguments. Drives
2629
+ * `walletBalanceState`. For an arbitrary wallet, use {@link getWalletBalance}.
2630
+ */
2631
+ async refreshBalance() {
2632
+ if (!this._session?.wallet?.address) {
2638
2633
  this._setWalletBalanceState({ step: "error", message: "No wallet connected" });
2639
2634
  return;
2640
2635
  }
2641
2636
  this._setWalletBalanceState({ step: "loading" });
2642
2637
  try {
2643
- const network = this.getNetwork();
2644
- const { data, error } = await this._api.GET("/wallet/balance", { params: { query: { publicKey: pk, network } } });
2638
+ const { data, error } = await this._api.GET("/wallet/balance");
2645
2639
  if (!error && data?.success && data.content) {
2646
2640
  this._setWalletBalanceState({ step: "loaded", data: data.content });
2647
2641
  } else {
@@ -2651,6 +2645,53 @@ var PollarClient = class {
2651
2645
  this._setWalletBalanceState({ step: "error", message: "Failed to load balance" });
2652
2646
  }
2653
2647
  }
2648
+ /**
2649
+ * General-purpose balance lookup for ANY wallet on ANY network — not scoped
2650
+ * to this application. Enumerates the account's real on-chain holdings via
2651
+ * Horizon (server-side) and returns the data directly (no reactive state).
2652
+ * `network` defaults to the client's current network.
2653
+ */
2654
+ async getWalletBalance(publicKey, network) {
2655
+ const { data, error } = await this._api.GET("/wallet/{publicKey}/balance", {
2656
+ params: { path: { publicKey }, query: { network: network ?? this.getNetwork() } }
2657
+ });
2658
+ if (error || !data?.success || !data.content) {
2659
+ throw new Error("[PollarClient] Failed to load wallet balance");
2660
+ }
2661
+ return data.content;
2662
+ }
2663
+ // ─── Enabled assets ───────────────────────────────────────────────────────
2664
+ getEnabledAssetsState() {
2665
+ return this._enabledAssetsState;
2666
+ }
2667
+ onEnabledAssetsStateChange(cb) {
2668
+ this._enabledAssetsStateListeners.add(cb);
2669
+ cb(this._enabledAssetsState);
2670
+ return () => this._enabledAssetsStateListeners.delete(cb);
2671
+ }
2672
+ /**
2673
+ * Loads the application's enabled assets paired with the authenticated
2674
+ * wallet's on-chain trustline state — so the SDK knows which trustlines still
2675
+ * need to be added. Wallet and network are resolved server-side from the
2676
+ * session. Drives `enabledAssetsState`; mirrors {@link refreshBalance}.
2677
+ */
2678
+ async refreshAssets() {
2679
+ if (!this._session?.wallet?.address) {
2680
+ this._setEnabledAssetsState({ step: "error", message: "No wallet connected" });
2681
+ return;
2682
+ }
2683
+ this._setEnabledAssetsState({ step: "loading" });
2684
+ try {
2685
+ const { data, error } = await this._api.GET("/wallet/assets");
2686
+ if (!error && data?.success && data.content) {
2687
+ this._setEnabledAssetsState({ step: "loaded", data: data.content });
2688
+ } else {
2689
+ this._setEnabledAssetsState({ step: "error", message: "Failed to load assets" });
2690
+ }
2691
+ } catch {
2692
+ this._setEnabledAssetsState({ step: "error", message: "Failed to load assets" });
2693
+ }
2694
+ }
2654
2695
  // ─── Transactions ─────────────────────────────────────────────────────────
2655
2696
  /**
2656
2697
  * Builds an unsigned XDR. Drives `_setTransactionState` for modal-style UIs
@@ -2658,14 +2699,14 @@ var PollarClient = class {
2658
2699
  * inspect the result without subscribing to state changes.
2659
2700
  */
2660
2701
  async buildTx(operation, params, options) {
2661
- if (!this._session?.wallet?.publicKey) {
2702
+ if (!this._session?.wallet?.address) {
2662
2703
  const details = "No wallet connected";
2663
2704
  this._setTransactionState({ step: "error", phase: "building", details });
2664
2705
  return { status: "error", details };
2665
2706
  }
2666
2707
  const body = {
2667
2708
  network: this.getNetwork(),
2668
- publicKey: this._session.wallet.publicKey,
2709
+ address: this._session.wallet.address,
2669
2710
  operation,
2670
2711
  params,
2671
2712
  options: options ?? {}
@@ -2706,7 +2747,7 @@ var PollarClient = class {
2706
2747
  const buildData = this._currentBuildData();
2707
2748
  this._setTransactionState({ step: "signing", ...buildData && { buildData } });
2708
2749
  if (this._walletAdapter) {
2709
- const accountToSign = this._session?.wallet?.publicKey;
2750
+ const accountToSign = this._session?.wallet?.address;
2710
2751
  const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
2711
2752
  try {
2712
2753
  const { signedTxXdr } = await this._walletAdapter.signTransaction(unsignedXdr, signOpts);
@@ -2727,10 +2768,10 @@ var PollarClient = class {
2727
2768
  return { status: "error", ...details && { details } };
2728
2769
  }
2729
2770
  }
2730
- const publicKey = this._session?.wallet?.publicKey ?? "";
2771
+ const address = this._session?.wallet?.address ?? "";
2731
2772
  try {
2732
2773
  const { data, error } = await this._api.POST("/tx/sign", {
2733
- body: { network: this.getNetwork(), publicKey, unsignedXdr }
2774
+ body: { network: this.getNetwork(), address, unsignedXdr }
2734
2775
  });
2735
2776
  if (!error && data?.success && data.content?.signedXdr) {
2736
2777
  const { signedXdr, idempotencyKey } = data.content;
@@ -2783,12 +2824,12 @@ var PollarClient = class {
2783
2824
  const buildData = this._currentBuildData();
2784
2825
  const outcomeExtra = buildData ? { buildData } : {};
2785
2826
  this._setTransactionState({ step: "submitting", signedXdr, ...buildData && { buildData } });
2786
- const publicKey = this._session?.wallet?.publicKey ?? "";
2827
+ const address = this._session?.wallet?.address ?? "";
2787
2828
  try {
2788
2829
  const { data, error } = await this._api.POST("/tx/submit", {
2789
2830
  body: {
2790
2831
  network: this.getNetwork(),
2791
- publicKey,
2832
+ address,
2792
2833
  signedXdr,
2793
2834
  ...opts?.submissionToken && { idempotencyKey: opts.submissionToken }
2794
2835
  }
@@ -2848,6 +2889,19 @@ var PollarClient = class {
2848
2889
  * `success` (ledger-confirmed), or `error[phase: 'signing-submitting']`.
2849
2890
  */
2850
2891
  async signAndSubmitTx(unsignedXdr) {
2892
+ if (this._session?.wallet?.type === "smart") {
2893
+ const buildData2 = this._currentBuildData();
2894
+ if (!buildData2?.smart) {
2895
+ const details = "no prepared smart transaction; call buildTx first";
2896
+ this._setTransactionState({ step: "error", phase: "signing", details });
2897
+ return { status: "error", details };
2898
+ }
2899
+ return this._signSubmitSmart(buildData2);
2900
+ }
2901
+ if (!unsignedXdr) {
2902
+ this._setTransactionState({ step: "error", phase: "signing", details: "missing unsigned transaction" });
2903
+ return { status: "error", details: "missing unsigned transaction" };
2904
+ }
2851
2905
  if (this._walletAdapter) {
2852
2906
  const signed = await this.signTx(unsignedXdr);
2853
2907
  if (signed.status === "error") {
@@ -2865,7 +2919,7 @@ var PollarClient = class {
2865
2919
  this._setTransactionState({ step: "signing-submitting", ...buildData && { buildData } });
2866
2920
  const body = {
2867
2921
  network: this.getNetwork(),
2868
- publicKey: this._session?.wallet?.publicKey ?? "",
2922
+ address: this._session?.wallet?.address ?? "",
2869
2923
  unsignedXdr
2870
2924
  };
2871
2925
  try {
@@ -2934,14 +2988,20 @@ var PollarClient = class {
2934
2988
  * `signTx`, and `submitTx` separately instead.
2935
2989
  */
2936
2990
  async buildAndSignAndSubmitTx(operation, params, options) {
2991
+ if (this._session?.wallet?.type === "smart") {
2992
+ return this._runSmartTx(operation, params, options);
2993
+ }
2937
2994
  if (this._walletAdapter) {
2938
2995
  const built = await this.buildTx(operation, params, options);
2939
2996
  if (built.status === "error") {
2940
2997
  return { status: "error", ...built.details && { details: built.details } };
2941
2998
  }
2999
+ if (!built.buildData.unsignedXdr) {
3000
+ return { status: "error", details: "build returned no unsigned transaction" };
3001
+ }
2942
3002
  return this.signAndSubmitTx(built.buildData.unsignedXdr);
2943
3003
  }
2944
- if (!this._session?.wallet?.publicKey) {
3004
+ if (!this._session?.wallet?.address) {
2945
3005
  this._setTransactionState({ step: "error", phase: "building-signing-submitting", details: "No wallet connected" });
2946
3006
  return { status: "error", details: "No wallet connected" };
2947
3007
  }
@@ -2950,7 +3010,7 @@ var PollarClient = class {
2950
3010
  const { data, error } = await this._api.POST("/tx/build-sign-submit", {
2951
3011
  body: {
2952
3012
  network: this.getNetwork(),
2953
- publicKey: this._session.wallet.publicKey,
3013
+ address: this._session.wallet.address,
2954
3014
  operation,
2955
3015
  params,
2956
3016
  options: options ?? {}
@@ -2994,6 +3054,113 @@ var PollarClient = class {
2994
3054
  async runTx(operation, params, options) {
2995
3055
  return this.buildAndSignAndSubmitTx(operation, params, options);
2996
3056
  }
3057
+ /**
3058
+ * Smart-wallet (passkey / C-address) transaction: build (server prepares the
3059
+ * SAC transfer + returns the auth digest) → sign the digest with the passkey
3060
+ * → submit (server assembles the signed auth entry and broadcasts; the
3061
+ * sponsor pays the fee). State machine: building → built → signing →
3062
+ * submitting → success.
3063
+ */
3064
+ async _runSmartTx(operation, params, options) {
3065
+ const address = this._session?.wallet?.address;
3066
+ if (!address) {
3067
+ this._setTransactionState({ step: "error", phase: "building", details: "No wallet connected" });
3068
+ return { status: "error", details: "No wallet connected" };
3069
+ }
3070
+ if (!this._passkeySign) {
3071
+ const details = "Passkey signer not configured";
3072
+ this._setTransactionState({ step: "error", phase: "signing", details });
3073
+ return { status: "error", details };
3074
+ }
3075
+ this._setTransactionState({ step: "building" });
3076
+ let buildData;
3077
+ try {
3078
+ const body = {
3079
+ network: this.getNetwork(),
3080
+ address,
3081
+ operation,
3082
+ params,
3083
+ options: options ?? {}
3084
+ };
3085
+ const { data, error } = await this._api.POST("/tx/build", { body });
3086
+ if (error || !data?.success || !data.content?.smart) {
3087
+ const details = error?.details ?? "Failed to build transaction";
3088
+ this._setTransactionState({ step: "error", phase: "building", details });
3089
+ return { status: "error", details };
3090
+ }
3091
+ buildData = data.content;
3092
+ } catch (err) {
3093
+ const details = err instanceof Error ? err.message : void 0;
3094
+ this._setTransactionState({ step: "error", phase: "building", ...details && { details } });
3095
+ return { status: "error", ...details && { details } };
3096
+ }
3097
+ this._setTransactionState({ step: "built", buildData });
3098
+ return this._signSubmitSmart(buildData);
3099
+ }
3100
+ /**
3101
+ * Steps 2–3 of the smart-wallet flow: sign the prepared auth digest with the
3102
+ * passkey, then submit. Shared by `_runSmartTx` (atomic) and `signAndSubmitTx`
3103
+ * (split flow, when a smart build is already on the state machine).
3104
+ */
3105
+ async _signSubmitSmart(buildData) {
3106
+ const address = this._session?.wallet?.address;
3107
+ const smart = buildData.smart;
3108
+ if (!address || !smart) {
3109
+ const details = "no prepared smart transaction";
3110
+ this._setTransactionState({ step: "error", phase: "signing", buildData, details });
3111
+ return { status: "error", buildData, details };
3112
+ }
3113
+ if (!this._passkeySign) {
3114
+ const details = "Passkey signer not configured";
3115
+ this._setTransactionState({ step: "error", phase: "signing", buildData, details });
3116
+ return { status: "error", buildData, details };
3117
+ }
3118
+ this._setTransactionState({ step: "signing", buildData });
3119
+ let assertion;
3120
+ try {
3121
+ assertion = await this._passkeySign({ credentialId: smart.credentialId, challenge: smart.digest });
3122
+ } catch (err) {
3123
+ const details = err instanceof Error ? err.message : void 0;
3124
+ this._setTransactionState({ step: "error", phase: "signing", buildData, ...details && { details } });
3125
+ return { status: "error", buildData, ...details && { details } };
3126
+ }
3127
+ this._setTransactionState({ step: "submitting", buildData });
3128
+ const outcomeExtra = { buildData };
3129
+ try {
3130
+ const { data, error } = await this._api.POST("/tx/submit", {
3131
+ body: {
3132
+ network: this.getNetwork(),
3133
+ address,
3134
+ smart: { entryXdr: smart.entryXdr, funcXdr: smart.funcXdr, assertion }
3135
+ }
3136
+ });
3137
+ if (!error && data?.success && data.content) {
3138
+ const { hash, status: backendStatus, resultCode } = data.content;
3139
+ if (backendStatus === "SUCCESS") {
3140
+ this._setTransactionState({ step: "success", hash, buildData });
3141
+ return { status: "success", hash, ...outcomeExtra };
3142
+ }
3143
+ if (backendStatus === "PENDING") {
3144
+ this._setTransactionState({ step: "submitted", hash, buildData });
3145
+ return { status: "pending", hash, ...outcomeExtra };
3146
+ }
3147
+ this._setTransactionState({
3148
+ step: "error",
3149
+ phase: "submitting",
3150
+ buildData,
3151
+ ...resultCode && { details: resultCode }
3152
+ });
3153
+ return { status: "error", hash, ...outcomeExtra, ...resultCode && { details: resultCode, resultCode } };
3154
+ }
3155
+ const details = error?.details;
3156
+ this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
3157
+ return { status: "error", ...outcomeExtra, ...details && { details } };
3158
+ } catch (err) {
3159
+ const details = err instanceof Error ? err.message : void 0;
3160
+ this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
3161
+ return { status: "error", ...outcomeExtra, ...details && { details } };
3162
+ }
3163
+ }
2997
3164
  // ─── App config ───────────────────────────────────────────────────────────
2998
3165
  async getAppConfig() {
2999
3166
  try {
@@ -3047,10 +3214,18 @@ var PollarClient = class {
3047
3214
  this._txHistoryState = next;
3048
3215
  for (const cb of this._txHistoryStateListeners) cb(next);
3049
3216
  }
3217
+ _setSessionsState(next) {
3218
+ this._sessionsState = next;
3219
+ for (const cb of this._sessionsStateListeners) cb(next);
3220
+ }
3050
3221
  _setWalletBalanceState(next) {
3051
3222
  this._walletBalanceState = next;
3052
3223
  for (const cb of this._walletBalanceStateListeners) cb(next);
3053
3224
  }
3225
+ _setEnabledAssetsState(next) {
3226
+ this._enabledAssetsState = next;
3227
+ for (const cb of this._enabledAssetsStateListeners) cb(next);
3228
+ }
3054
3229
  // ─── Private ──────────────────────────────────────────────────────────────
3055
3230
  _newController() {
3056
3231
  this._loginController?.abort();
@@ -3075,6 +3250,7 @@ var PollarClient = class {
3075
3250
  this._walletAdapter = adapter;
3076
3251
  await writeWalletType(this._storage, this.apiKeyHash, id);
3077
3252
  },
3253
+ ...this._passkey ? { passkey: this._passkey } : {},
3078
3254
  ...this._deviceLabel ? { deviceLabel: this._deviceLabel } : {}
3079
3255
  };
3080
3256
  }
@@ -3104,7 +3280,7 @@ var PollarClient = class {
3104
3280
  }
3105
3281
  }
3106
3282
  if (id === "freighter" /* FREIGHTER */) return new FreighterAdapter();
3107
- if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter();
3283
+ if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter(this.getNetwork() === "mainnet" ? "public" : "testnet");
3108
3284
  throw new Error(
3109
3285
  `[PollarClient] No wallet adapter configured for "${id}". Pass a walletAdapter resolver in PollarClientConfig.`
3110
3286
  );
@@ -3145,21 +3321,66 @@ var PollarClient = class {
3145
3321
  }
3146
3322
  }
3147
3323
  console.info("[PollarClient] Session restored from storage");
3148
- this._setAuthState({ step: "authenticated", session: this._session });
3324
+ this._setAuthState({ step: "authenticated", session: this._session, verified: false });
3149
3325
  this._scheduleNextRefresh();
3326
+ void this._resume();
3150
3327
  } else {
3151
3328
  console.info("[PollarClient] No session in storage");
3152
3329
  }
3153
3330
  }
3331
+ /**
3332
+ * Validate the restored session against the server and repopulate the
3333
+ * in-memory profile (PII is never persisted, so it's null after a cold
3334
+ * reload). Goes through the normal authed client, so it coalesces with any
3335
+ * in-flight refresh (onRequest awaits `_refreshPromise`) and, being a GET,
3336
+ * is auto-retried after a 401-triggered refresh.
3337
+ *
3338
+ * - 200 → store profile, mark the session `verified`.
3339
+ * - 401 → the refresh-on-401 path already ran; if the family was
3340
+ * revoked, refresh failed and `_clearSession()` took us to
3341
+ * idle. Nothing to do here — don't double-handle.
3342
+ * - network error → stay optimistic (do NOT log out); revalidated later on
3343
+ * `visibilitychange` or first use.
3344
+ */
3345
+ async _resume() {
3346
+ if (!this._session) return;
3347
+ this._resumeController?.abort();
3348
+ const controller = new AbortController();
3349
+ this._resumeController = controller;
3350
+ try {
3351
+ const { data, error } = await this._api.GET("/auth/session/resume", { signal: controller.signal });
3352
+ if (error || !data) return;
3353
+ const content = data.content;
3354
+ if (!content || !this._session) return;
3355
+ this._profile = { ...content };
3356
+ this._setAuthState({ step: "authenticated", session: this._session, verified: true });
3357
+ } catch (err) {
3358
+ if (err?.name === "AbortError") return;
3359
+ console.warn("[PollarClient] resume failed (network); will retry", err);
3360
+ } finally {
3361
+ if (this._resumeController === controller) this._resumeController = null;
3362
+ }
3363
+ }
3154
3364
  async _storeSession(session) {
3155
3365
  console.info("[PollarClient] Session stored");
3366
+ const w = session.wallet;
3156
3367
  const persisted = {
3157
3368
  clientSessionId: session.clientSessionId,
3158
3369
  userId: session.userId ?? null,
3159
3370
  status: session.status,
3160
3371
  token: session.token,
3161
3372
  user: session.user,
3162
- wallet: session.wallet
3373
+ // The wire response still carries the legacy `publicKey` alias (kept for
3374
+ // older SDKs); the persisted session standardizes on `address` only.
3375
+ wallet: {
3376
+ type: w.type,
3377
+ address: w.address ?? w.publicKey ?? null,
3378
+ ...w.existsOnStellar !== void 0 ? { existsOnStellar: w.existsOnStellar } : {},
3379
+ ...w.createdAt !== void 0 ? { createdAt: w.createdAt } : {},
3380
+ ...w.linkedAt !== void 0 ? { linkedAt: w.linkedAt } : {},
3381
+ ...w.network !== void 0 ? { network: w.network } : {},
3382
+ ...w.deployTxHash !== void 0 ? { deployTxHash: w.deployTxHash } : {}
3383
+ }
3163
3384
  };
3164
3385
  this._session = persisted;
3165
3386
  if (session.data) {
@@ -3172,7 +3393,7 @@ var PollarClient = class {
3172
3393
  };
3173
3394
  }
3174
3395
  await writeStorage(this._storage, this.apiKeyHash, persisted);
3175
- this._setAuthState({ step: "authenticated", session: persisted });
3396
+ this._setAuthState({ step: "authenticated", session: persisted, verified: true });
3176
3397
  this._scheduleNextRefresh();
3177
3398
  }
3178
3399
  async _clearSession() {
@@ -3224,6 +3445,196 @@ var PollarClient = class {
3224
3445
  }
3225
3446
  };
3226
3447
 
3448
+ // src/keys/web-crypto.ts
3449
+ var DB_NAME = "pollar-keys";
3450
+ var DB_VERSION = 1;
3451
+ var STORE_NAME = "keys";
3452
+ function openDb() {
3453
+ return new Promise((resolve, reject) => {
3454
+ if (typeof indexedDB === "undefined") {
3455
+ reject(new Error("[PollarClient:keys] IndexedDB not available"));
3456
+ return;
3457
+ }
3458
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
3459
+ req.onerror = () => reject(req.error ?? new Error("[PollarClient:keys] IDB open failed"));
3460
+ req.onsuccess = () => resolve(req.result);
3461
+ req.onupgradeneeded = () => {
3462
+ const db = req.result;
3463
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
3464
+ db.createObjectStore(STORE_NAME);
3465
+ }
3466
+ };
3467
+ });
3468
+ }
3469
+ function awaitTx(req) {
3470
+ return new Promise((resolve, reject) => {
3471
+ req.onsuccess = () => resolve(req.result);
3472
+ req.onerror = () => reject(req.error ?? new Error("[PollarClient:keys] IDB request failed"));
3473
+ });
3474
+ }
3475
+ async function dbGet(key) {
3476
+ const db = await openDb();
3477
+ try {
3478
+ const tx = db.transaction(STORE_NAME, "readonly");
3479
+ const result = await awaitTx(tx.objectStore(STORE_NAME).get(key));
3480
+ return result;
3481
+ } finally {
3482
+ db.close();
3483
+ }
3484
+ }
3485
+ async function dbPut(key, value) {
3486
+ const db = await openDb();
3487
+ try {
3488
+ const tx = db.transaction(STORE_NAME, "readwrite");
3489
+ await awaitTx(tx.objectStore(STORE_NAME).put(value, key));
3490
+ } finally {
3491
+ db.close();
3492
+ }
3493
+ }
3494
+ async function dbDelete(key) {
3495
+ const db = await openDb();
3496
+ try {
3497
+ const tx = db.transaction(STORE_NAME, "readwrite");
3498
+ await awaitTx(tx.objectStore(STORE_NAME).delete(key));
3499
+ } finally {
3500
+ db.close();
3501
+ }
3502
+ }
3503
+ function isCryptoKeyPair(v) {
3504
+ if (typeof v !== "object" || v === null) return false;
3505
+ const obj = v;
3506
+ return obj.privateKey !== void 0 && obj.publicKey !== void 0;
3507
+ }
3508
+ var WebCryptoKeyManager = class {
3509
+ constructor(apiKey) {
3510
+ this.apiKeyHash = null;
3511
+ this.keyPair = null;
3512
+ this.publicJwk = null;
3513
+ this.thumbprint = null;
3514
+ /**
3515
+ * Cached in-flight init. Lets `init()` be called concurrently (or implicitly
3516
+ * from `getPublicJwk` / `sign`) without doing the work twice. Cleared on
3517
+ * failure so callers can retry, and cleared on `reset()`.
3518
+ */
3519
+ this._initPromise = null;
3520
+ if (typeof globalThis.crypto === "undefined" || !globalThis.crypto.subtle) {
3521
+ throw new Error("[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost).");
3522
+ }
3523
+ this.apiKey = apiKey;
3524
+ }
3525
+ /**
3526
+ * Idempotent and safe under concurrency. The first call kicks off the real
3527
+ * init; subsequent (and concurrent) calls return the same in-flight promise.
3528
+ * Other methods (`getPublicJwk`, `getThumbprint`, `sign`) auto-await this so
3529
+ * the manager is self-healing if `init()` was never explicitly invoked.
3530
+ */
3531
+ async init() {
3532
+ if (this.keyPair) return;
3533
+ if (!this._initPromise) {
3534
+ this._initPromise = this._doInit().catch((err) => {
3535
+ console.error("[PollarClient:keys] WebCryptoKeyManager init failed", err);
3536
+ this._initPromise = null;
3537
+ throw err;
3538
+ });
3539
+ }
3540
+ return this._initPromise;
3541
+ }
3542
+ async _doInit() {
3543
+ if (!this.apiKeyHash) {
3544
+ this.apiKeyHash = await hashApiKey(this.apiKey);
3545
+ }
3546
+ let pair;
3547
+ try {
3548
+ pair = await dbGet(this.apiKeyHash);
3549
+ if (pair && !isCryptoKeyPair(pair)) pair = void 0;
3550
+ } catch {
3551
+ pair = void 0;
3552
+ }
3553
+ if (!pair) {
3554
+ pair = await globalThis.crypto.subtle.generateKey(
3555
+ { name: "ECDSA", namedCurve: "P-256" },
3556
+ // false → private key non-extractable; per W3C ECDSA spec the public
3557
+ // key is always extractable regardless of this flag.
3558
+ false,
3559
+ ["sign", "verify"]
3560
+ );
3561
+ try {
3562
+ await dbPut(this.apiKeyHash, pair);
3563
+ } catch {
3564
+ }
3565
+ }
3566
+ this.keyPair = pair;
3567
+ this.publicJwk = await this._exportPublicJwk(pair.publicKey);
3568
+ this.thumbprint = await computeJwkThumbprint(this.publicJwk);
3569
+ }
3570
+ /**
3571
+ * Derive the public JWK from a `CryptoKey`. Prefers the `'raw'` export (the
3572
+ * 65-byte uncompressed point `0x04 || X(32) || Y(32)`) and base64url-encodes
3573
+ * the coordinates ourselves — that sidesteps polyfills whose `exportKey('jwk')`
3574
+ * emits non-base64url `x`/`y` (standard base64, `=` padding, or — as seen with
3575
+ * `react-native-quick-crypto` — a stray `.`). Real browsers and most polyfills
3576
+ * support `'raw'` for public EC keys.
3577
+ *
3578
+ * Falls back to the `'jwk'` export (normalized via `canonicalEcJwk`) if `'raw'`
3579
+ * is unsupported or returns an unexpected shape, so this can't regress on a
3580
+ * runtime that only implements the JWK path. Both routes yield identical
3581
+ * coordinate bytes, so the `cnf.jkt` thumbprint is unchanged either way.
3582
+ */
3583
+ async _exportPublicJwk(publicKey) {
3584
+ try {
3585
+ const raw = new Uint8Array(await globalThis.crypto.subtle.exportKey("raw", publicKey));
3586
+ if (raw.length !== 65 || raw[0] !== 4) {
3587
+ throw new Error(`[PollarClient:keys] Unexpected raw EC point (len=${raw.length}, tag=${raw[0]})`);
3588
+ }
3589
+ return {
3590
+ kty: "EC",
3591
+ crv: "P-256",
3592
+ x: base64urlEncode(raw.slice(1, 33)),
3593
+ y: base64urlEncode(raw.slice(33, 65))
3594
+ };
3595
+ } catch {
3596
+ const jwk = await globalThis.crypto.subtle.exportKey("jwk", publicKey);
3597
+ return canonicalEcJwk(jwk);
3598
+ }
3599
+ }
3600
+ async reset() {
3601
+ try {
3602
+ if (this.apiKeyHash) await dbDelete(this.apiKeyHash);
3603
+ } catch {
3604
+ }
3605
+ this.keyPair = null;
3606
+ this.publicJwk = null;
3607
+ this.thumbprint = null;
3608
+ this._initPromise = null;
3609
+ }
3610
+ async getPublicJwk() {
3611
+ if (!this.publicJwk) await this.init();
3612
+ if (!this.publicJwk) {
3613
+ throw new Error("[PollarClient:keys] Keypair initialization failed; getPublicJwk unavailable");
3614
+ }
3615
+ return { kty: this.publicJwk.kty, crv: this.publicJwk.crv, x: this.publicJwk.x, y: this.publicJwk.y };
3616
+ }
3617
+ async getThumbprint() {
3618
+ if (!this.thumbprint) await this.init();
3619
+ if (!this.thumbprint) {
3620
+ throw new Error("[PollarClient:keys] Keypair initialization failed; getThumbprint unavailable");
3621
+ }
3622
+ return this.thumbprint;
3623
+ }
3624
+ async sign(payload) {
3625
+ if (!this.keyPair) await this.init();
3626
+ if (!this.keyPair) {
3627
+ throw new Error("[PollarClient:keys] Keypair initialization failed; sign unavailable");
3628
+ }
3629
+ const sig = await globalThis.crypto.subtle.sign(
3630
+ { name: "ECDSA", hash: "SHA-256" },
3631
+ this.keyPair.privateKey,
3632
+ payload
3633
+ );
3634
+ return new Uint8Array(sig);
3635
+ }
3636
+ };
3637
+
3227
3638
  // src/stellar/StellarClient.ts
3228
3639
  var HORIZON_URLS = {
3229
3640
  mainnet: "https://horizon.stellar.org",
@@ -3254,10 +3665,6 @@ var StellarClient = class {
3254
3665
 
3255
3666
  // src/index.rn.ts
3256
3667
  _setDefaultKeyManagerFactory((storage, apiKey) => {
3257
- const subtle = globalThis.crypto?.subtle;
3258
- if (subtle && typeof subtle.generateKey === "function" && typeof subtle.sign === "function") {
3259
- return new WebCryptoKeyManager(apiKey);
3260
- }
3261
3668
  return new NobleKeyManager(storage, apiKey);
3262
3669
  });
3263
3670
 
@@ -3265,6 +3672,7 @@ exports.AUTH_ERROR_CODES = AUTH_ERROR_CODES;
3265
3672
  exports.AlbedoAdapter = AlbedoAdapter;
3266
3673
  exports.FreighterAdapter = FreighterAdapter;
3267
3674
  exports.NobleKeyManager = NobleKeyManager;
3675
+ exports.POLLAR_CORE_VERSION = POLLAR_CORE_VERSION;
3268
3676
  exports.PollarClient = PollarClient;
3269
3677
  exports.StellarClient = StellarClient;
3270
3678
  exports.WalletType = WalletType;