@pollar/core 0.7.1 → 0.8.1

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.js CHANGED
@@ -28,10 +28,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
 
29
29
  // ../../node_modules/@stellar/freighter-api/build/index.min.js
30
30
  var require_index_min = __commonJS({
31
- "../../node_modules/@stellar/freighter-api/build/index.min.js"(exports$1, module) {
31
+ "../../node_modules/@stellar/freighter-api/build/index.min.js"(exports, module) {
32
32
  !(function(e, r) {
33
- "object" == typeof exports$1 && "object" == typeof module ? module.exports = r() : "function" == typeof define && define.amd ? define([], r) : "object" == typeof exports$1 ? exports$1.freighterApi = r() : e.freighterApi = r();
34
- })(exports$1, (() => (() => {
33
+ "object" == typeof exports && "object" == typeof module ? module.exports = r() : "function" == typeof define && define.amd ? define([], r) : "object" == typeof exports ? exports.freighterApi = r() : e.freighterApi = r();
34
+ })(exports, (() => (() => {
35
35
  var e, r, E = { d: (e2, r2) => {
36
36
  for (var o2 in r2) E.o(r2, o2) && !E.o(e2, o2) && Object.defineProperty(e2, o2, { enumerable: true, get: r2[o2] });
37
37
  }, o: (e2, r2) => Object.prototype.hasOwnProperty.call(e2, r2), r: (e2) => {
@@ -325,9 +325,7 @@ var WebCryptoKeyManager = class {
325
325
  */
326
326
  this._initPromise = null;
327
327
  if (typeof globalThis.crypto === "undefined" || !globalThis.crypto.subtle) {
328
- throw new Error(
329
- "[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost)."
330
- );
328
+ throw new Error("[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost).");
331
329
  }
332
330
  this.apiKey = apiKey;
333
331
  }
@@ -900,14 +898,18 @@ function createApiClient(baseUrl) {
900
898
  async function listDistributionRules(api) {
901
899
  const { data, error } = await api.GET("/distribution/rules");
902
900
  if (!data?.content || error) {
903
- throw new Error(error?.error ?? "Failed to list distribution rules");
901
+ throw new Error(
902
+ error?.code ?? error?.error ?? "Failed to list distribution rules"
903
+ );
904
904
  }
905
905
  return data.content.rules;
906
906
  }
907
907
  async function claimDistributionRule(api, body) {
908
908
  const { data, error } = await api.POST("/distribution/claim", { body });
909
909
  if (!data?.content || error) {
910
- throw new Error(error?.error ?? "Failed to claim distribution rule");
910
+ throw new Error(
911
+ error?.code ?? error?.error ?? "Failed to claim distribution rule"
912
+ );
911
913
  }
912
914
  return data.content;
913
915
  }
@@ -918,18 +920,18 @@ async function getKycStatus(api, providerId) {
918
920
  params: { query: providerId ? { providerId } : {} }
919
921
  });
920
922
  if (!data?.content || error) {
921
- throw new Error(error?.error ?? "Failed to get KYC status");
923
+ throw new Error(error?.code ?? error?.error ?? "Failed to get KYC status");
922
924
  }
923
925
  return data.content;
924
926
  }
925
927
  async function getKycProviders(api, country) {
926
928
  const { data, error } = await api.GET("/kyc/providers", { params: { query: { country } } });
927
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to get KYC providers");
929
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get KYC providers");
928
930
  return data.content;
929
931
  }
930
932
  async function startKyc(api, body) {
931
933
  const { data, error } = await api.POST("/kyc/start", { body });
932
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to start KYC");
934
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to start KYC");
933
935
  return data.content;
934
936
  }
935
937
  async function resolveKyc(api, providerId, level = "basic") {
@@ -951,22 +953,22 @@ async function pollKycStatus(api, providerId, { intervalMs = 3e3, timeoutMs = 3e
951
953
  // src/api/endpoints/ramps.ts
952
954
  async function getRampsQuote(api, query) {
953
955
  const { data, error } = await api.GET("/ramps/quote", { params: { query } });
954
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to get ramp quotes");
956
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get ramp quotes");
955
957
  return data.content;
956
958
  }
957
959
  async function createOnRamp(api, body) {
958
960
  const { data, error } = await api.POST("/ramps/onramp", { body });
959
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to create onramp");
961
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to create onramp");
960
962
  return data.content;
961
963
  }
962
964
  async function createOffRamp(api, body) {
963
965
  const { data, error } = await api.POST("/ramps/offramp", { body });
964
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to create offramp");
966
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to create offramp");
965
967
  return data.content;
966
968
  }
967
969
  async function getRampTransaction(api, txId) {
968
970
  const { data, error } = await api.GET("/ramps/transaction/{txId}", { params: { path: { txId } } });
969
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to get transaction");
971
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get transaction");
970
972
  return data.content;
971
973
  }
972
974
  async function pollRampTransaction(api, txId, { intervalMs = 5e3, timeoutMs = 6e5 } = {}) {
@@ -979,6 +981,26 @@ async function pollRampTransaction(api, txId, { intervalMs = 5e3, timeoutMs = 6e
979
981
  throw new Error("Ramp transaction polling timed out");
980
982
  }
981
983
 
984
+ // src/lib/random-uuid.ts
985
+ function randomUUID() {
986
+ const c = globalThis.crypto;
987
+ if (c && typeof c.randomUUID === "function") {
988
+ return c.randomUUID();
989
+ }
990
+ if (c && typeof c.getRandomValues === "function") {
991
+ const bytes = new Uint8Array(16);
992
+ c.getRandomValues(bytes);
993
+ bytes[6] = bytes[6] & 15 | 64;
994
+ bytes[8] = bytes[8] & 63 | 128;
995
+ const hex = [];
996
+ for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
997
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
998
+ }
999
+ throw new Error(
1000
+ "[PollarClient] No secure random source available (crypto.randomUUID / crypto.getRandomValues). DPoP requires a secure context (HTTPS) or, in React Native, the `react-native-get-random-values` polyfill."
1001
+ );
1002
+ }
1003
+
982
1004
  // src/dpop.ts
983
1005
  async function buildProof(args, keyManager) {
984
1006
  const jwk = await keyManager.getPublicJwk();
@@ -988,7 +1010,7 @@ async function buildProof(args, keyManager) {
988
1010
  jwk
989
1011
  };
990
1012
  const payload = {
991
- jti: generateJti(),
1013
+ jti: randomUUID(),
992
1014
  htm: args.htm.toUpperCase(),
993
1015
  htu: normalizeHtu(args.htu),
994
1016
  iat: Math.floor(Date.now() / 1e3)
@@ -1022,52 +1044,6 @@ function normalizeHtu(rawUrl) {
1022
1044
  const portPart = port ? `:${port}` : "";
1023
1045
  return `${scheme}//${host}${portPart}${url.pathname}`;
1024
1046
  }
1025
- function generateJti() {
1026
- const c = globalThis.crypto;
1027
- if (c && typeof c.randomUUID === "function") {
1028
- return c.randomUUID();
1029
- }
1030
- if (c && typeof c.getRandomValues === "function") {
1031
- const bytes = new Uint8Array(16);
1032
- c.getRandomValues(bytes);
1033
- bytes[6] = bytes[6] & 15 | 64;
1034
- bytes[8] = bytes[8] & 63 | 128;
1035
- const hex = [];
1036
- for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
1037
- return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
1038
- }
1039
- throw new Error(
1040
- "[PollarClient:dpop] No secure random source available (crypto.randomUUID / crypto.getRandomValues). DPoP requires a secure context (HTTPS) or, in React Native, the `react-native-get-random-values` polyfill."
1041
- );
1042
- }
1043
-
1044
- // src/stellar/StellarClient.ts
1045
- var HORIZON_URLS = {
1046
- mainnet: "https://horizon.stellar.org",
1047
- testnet: "https://horizon-testnet.stellar.org"
1048
- };
1049
- var StellarClient = class {
1050
- constructor(config) {
1051
- this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
1052
- }
1053
- async submitTransaction(signedXdr) {
1054
- try {
1055
- const response = await fetch(`${this.horizonUrl}/transactions`, {
1056
- method: "POST",
1057
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1058
- body: new URLSearchParams({ tx: signedXdr })
1059
- });
1060
- if (!response.ok) {
1061
- const body = await response.json().catch(() => ({}));
1062
- return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
1063
- }
1064
- const data = await response.json();
1065
- return { success: true, hash: data.hash };
1066
- } catch {
1067
- return { success: false, errorCode: "NETWORK_ERROR" };
1068
- }
1069
- }
1070
- };
1071
1047
 
1072
1048
  // src/storage/web.ts
1073
1049
  var LOG_PREFIX = "[PollarClient:storage]";
@@ -1156,9 +1132,62 @@ function defaultStorage(options = {}) {
1156
1132
  return createLocalStorageAdapter(options);
1157
1133
  }
1158
1134
 
1135
+ // src/visibility/noop.ts
1136
+ function createNoopVisibilityProvider() {
1137
+ return {
1138
+ isVisible: () => true,
1139
+ onChange: () => () => {
1140
+ }
1141
+ };
1142
+ }
1143
+
1144
+ // src/visibility/web.ts
1145
+ function createWebVisibilityProvider() {
1146
+ const isVisibleNow = () => typeof document === "undefined" || document.visibilityState === "visible";
1147
+ return {
1148
+ isVisible: isVisibleNow,
1149
+ onChange: (cb) => {
1150
+ if (typeof window === "undefined" || typeof document === "undefined") {
1151
+ return () => {
1152
+ };
1153
+ }
1154
+ let last = isVisibleNow();
1155
+ const handler = () => {
1156
+ const next = isVisibleNow();
1157
+ if (next !== last) {
1158
+ last = next;
1159
+ cb(next);
1160
+ }
1161
+ };
1162
+ document.addEventListener("visibilitychange", handler);
1163
+ window.addEventListener("pageshow", handler);
1164
+ window.addEventListener("pagehide", handler);
1165
+ window.addEventListener("focus", handler);
1166
+ window.addEventListener("blur", handler);
1167
+ return () => {
1168
+ document.removeEventListener("visibilitychange", handler);
1169
+ window.removeEventListener("pageshow", handler);
1170
+ window.removeEventListener("pagehide", handler);
1171
+ window.removeEventListener("focus", handler);
1172
+ window.removeEventListener("blur", handler);
1173
+ };
1174
+ }
1175
+ };
1176
+ }
1177
+
1178
+ // src/visibility/autodetect.ts
1179
+ function defaultVisibilityProvider() {
1180
+ if (typeof document !== "undefined" && typeof window !== "undefined") {
1181
+ return createWebVisibilityProvider();
1182
+ }
1183
+ return createNoopVisibilityProvider();
1184
+ }
1185
+
1159
1186
  // src/types.ts
1160
1187
  var AUTH_ERROR_CODES = {
1161
1188
  SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
1189
+ SESSION_EXPIRED: "SESSION_EXPIRED",
1190
+ SESSION_INVALID: "SESSION_INVALID",
1162
1191
  EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
1163
1192
  EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
1164
1193
  EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
@@ -1166,6 +1195,7 @@ var AUTH_ERROR_CODES = {
1166
1195
  AUTH_FAILED: "AUTH_FAILED",
1167
1196
  WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
1168
1197
  WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
1198
+ WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
1169
1199
  UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
1170
1200
  };
1171
1201
  var PollarFlowError = class extends Error {
@@ -1471,7 +1501,32 @@ async function readWalletType(storage, apiKeyHash) {
1471
1501
  return storage.get(walletTypeStorageKey(apiKeyHash));
1472
1502
  }
1473
1503
 
1504
+ // src/lib/abort.ts
1505
+ function abortError() {
1506
+ if (typeof DOMException !== "undefined") {
1507
+ return new DOMException("Aborted", "AbortError");
1508
+ }
1509
+ const err = new Error("Aborted");
1510
+ err.name = "AbortError";
1511
+ return err;
1512
+ }
1513
+ function throwIfAborted(signal) {
1514
+ if (signal?.aborted) throw abortError();
1515
+ }
1516
+
1474
1517
  // src/client/stream.ts
1518
+ var SessionStatusError = class extends Error {
1519
+ constructor(code) {
1520
+ super(`[PollarClient] Session status terminal: ${code}`);
1521
+ this.code = code;
1522
+ this.name = "SessionStatusError";
1523
+ }
1524
+ };
1525
+ function terminalStatusCode(parsed) {
1526
+ const err = parsed?.error;
1527
+ if (err === "INVALID_CLIENT_SESSION_ID" || err === "EXPIRED_CLIENT_ID") return err;
1528
+ return null;
1529
+ }
1475
1530
  function abortableDelay(ms, signal) {
1476
1531
  return new Promise((resolve, reject) => {
1477
1532
  const t = setTimeout(resolve, ms);
@@ -1479,7 +1534,7 @@ function abortableDelay(ms, signal) {
1479
1534
  "abort",
1480
1535
  () => {
1481
1536
  clearTimeout(t);
1482
- reject(new DOMException("Aborted", "AbortError"));
1537
+ reject(abortError());
1483
1538
  },
1484
1539
  { once: true }
1485
1540
  );
@@ -1494,7 +1549,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1494
1549
  else await new Promise((r) => setTimeout(r, ms));
1495
1550
  };
1496
1551
  while (true) {
1497
- signal?.throwIfAborted();
1552
+ throwIfAborted(signal);
1498
1553
  let data, error;
1499
1554
  try {
1500
1555
  ({ data, error } = await api.GET("/auth/session/status/{clientSessionId}", {
@@ -1517,7 +1572,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1517
1572
  let sawAnyChunk = false;
1518
1573
  try {
1519
1574
  while (true) {
1520
- signal?.throwIfAborted();
1575
+ throwIfAborted(signal);
1521
1576
  const { done, value } = await reader.read();
1522
1577
  if (done) {
1523
1578
  streamDone = true;
@@ -1528,17 +1583,22 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1528
1583
  for (const message of chunk.split("\n\n").filter(Boolean)) {
1529
1584
  const dataLine = message.split("\n").find((l) => l.startsWith("data:"));
1530
1585
  if (!dataLine) continue;
1586
+ let parsed;
1531
1587
  try {
1532
- const parsed = JSON.parse(dataLine.slice("data:".length).trim());
1533
- if (check(parsed)) {
1534
- return parsed;
1535
- }
1588
+ parsed = JSON.parse(dataLine.slice("data:".length).trim());
1536
1589
  } catch {
1590
+ continue;
1591
+ }
1592
+ const terminal = terminalStatusCode(parsed);
1593
+ if (terminal) throw new SessionStatusError(terminal);
1594
+ if (check(parsed)) {
1595
+ return parsed;
1537
1596
  }
1538
1597
  }
1539
1598
  }
1540
1599
  } catch (e) {
1541
1600
  if (e instanceof Error && e.name === "AbortError") throw e;
1601
+ if (e instanceof SessionStatusError) throw e;
1542
1602
  console.warn(e);
1543
1603
  } finally {
1544
1604
  reader.releaseLock();
@@ -1549,14 +1609,74 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1549
1609
  if (delay) await sleep(delay);
1550
1610
  }
1551
1611
  }
1612
+ async function pollUntilFound(baseUrl, clientSessionId, check, intervalMs = 500, signal) {
1613
+ const url = `${baseUrl}/auth/session/status/${encodeURIComponent(clientSessionId)}/poll`;
1614
+ let backoff = intervalMs;
1615
+ const sleep = async (ms) => {
1616
+ if (ms <= 0) return;
1617
+ if (signal) await abortableDelay(ms, signal);
1618
+ else await new Promise((r) => setTimeout(r, ms));
1619
+ };
1620
+ while (true) {
1621
+ throwIfAborted(signal);
1622
+ let envelope = null;
1623
+ let httpStatus = 0;
1624
+ try {
1625
+ const response = await fetch(url, { headers: { accept: "application/json" }, signal: signal ?? null });
1626
+ httpStatus = response.status;
1627
+ envelope = await response.json().catch(() => null);
1628
+ } catch (e) {
1629
+ if (e instanceof Error && e.name === "AbortError") throw e;
1630
+ console.warn(e);
1631
+ }
1632
+ if (httpStatus === 404 || envelope?.code === "INVALID_CLIENT_SESSION_ID") {
1633
+ throw new SessionStatusError("INVALID_CLIENT_SESSION_ID");
1634
+ }
1635
+ if (httpStatus === 410 || envelope?.code === "EXPIRED_CLIENT_ID") {
1636
+ throw new SessionStatusError("EXPIRED_CLIENT_ID");
1637
+ }
1638
+ if (envelope?.success && envelope.content && check(envelope.content)) {
1639
+ return envelope.content;
1640
+ }
1641
+ if (envelope) backoff = intervalMs;
1642
+ else backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
1643
+ await sleep(backoff);
1644
+ }
1645
+ }
1646
+ function waitForSessionReady(args) {
1647
+ const { api, baseUrl, clientSessionId, check, useStreaming, retryDelayMs, signal } = args;
1648
+ return useStreaming ? streamUntilFound(api, clientSessionId, check, retryDelayMs ?? 200, signal) : pollUntilFound(baseUrl, clientSessionId, check, retryDelayMs ?? 500, signal);
1649
+ }
1552
1650
 
1553
1651
  // src/client/auth/authenticate.ts
1554
1652
  async function authenticate(clientSessionId, deps, expectedWallet) {
1555
- const { api, signal, setAuthState, storeSession, clearSession } = deps;
1653
+ const { api, basePath, useStreaming, signal, setAuthState, storeSession, clearSession } = deps;
1556
1654
  setAuthState({ step: "authenticating" });
1557
- await streamUntilFound(api, clientSessionId, (data2) => data2?.status === "READY", 200, signal);
1655
+ try {
1656
+ await waitForSessionReady({
1657
+ api,
1658
+ baseUrl: basePath,
1659
+ clientSessionId,
1660
+ check: (data2) => data2?.status === "READY",
1661
+ useStreaming,
1662
+ signal
1663
+ });
1664
+ } catch (err) {
1665
+ if (err instanceof SessionStatusError) {
1666
+ const expired = err.code === "EXPIRED_CLIENT_ID";
1667
+ setAuthState({
1668
+ step: "error",
1669
+ previousStep: "authenticating",
1670
+ message: expired ? "Login session expired \u2014 please try again" : "Login session is no longer valid \u2014 please try again",
1671
+ errorCode: expired ? AUTH_ERROR_CODES.SESSION_EXPIRED : AUTH_ERROR_CODES.SESSION_INVALID
1672
+ });
1673
+ await clearSession();
1674
+ return;
1675
+ }
1676
+ throw err;
1677
+ }
1558
1678
  const dpopJwk = await deps.getPublicJwk();
1559
- const { data, error } = await api.POST("/auth/login", {
1679
+ const { data } = await api.POST("/auth/login", {
1560
1680
  body: {
1561
1681
  clientSessionId,
1562
1682
  dpopJwk,
@@ -1678,26 +1798,36 @@ function severOpener(popup) {
1678
1798
  } catch {
1679
1799
  }
1680
1800
  }
1681
- async function loginOAuth(provider, deps) {
1682
- const { setAuthState, basePath, apiKey } = deps;
1683
- const popup = window.open("about:blank", "_blank");
1801
+ var defaultWebOAuthOpener = async ({ getUrl }) => {
1802
+ const popup = typeof window !== "undefined" ? window.open("about:blank", "_blank") : null;
1684
1803
  severOpener(popup);
1685
- const clientSessionId = await createAuthSession(deps);
1686
- if (!clientSessionId) {
1804
+ const url = await getUrl();
1805
+ if (!url) {
1687
1806
  popup?.close();
1688
1807
  return;
1689
1808
  }
1690
- setAuthState({ step: "opening_oauth", provider });
1691
- const url = new URL(`${basePath}/auth/${provider}`);
1692
- url.searchParams.set("api_key", apiKey);
1693
- url.searchParams.set("client_session_id", clientSessionId);
1694
- url.searchParams.set("redirect_uri", window.location.origin);
1695
1809
  if (popup) {
1696
- popup.location.href = url.toString();
1810
+ popup.location.href = url;
1697
1811
  severOpener(popup);
1698
- } else {
1699
- window.open(url.toString(), "_blank", "noopener,noreferrer");
1812
+ } else if (typeof window !== "undefined") {
1813
+ window.open(url, "_blank", "noopener,noreferrer");
1700
1814
  }
1815
+ };
1816
+ async function loginOAuth(provider, deps) {
1817
+ const { setAuthState, basePath, apiKey, openAuthUrl, redirectUri, signal } = deps;
1818
+ let clientSessionId = null;
1819
+ const getUrl = async () => {
1820
+ clientSessionId = await createAuthSession(deps);
1821
+ if (!clientSessionId) return null;
1822
+ setAuthState({ step: "opening_oauth", provider });
1823
+ const url = new URL(`${basePath}/auth/${provider}`);
1824
+ url.searchParams.set("api_key", apiKey);
1825
+ url.searchParams.set("client_session_id", clientSessionId);
1826
+ url.searchParams.set("redirect_uri", redirectUri);
1827
+ return url.toString();
1828
+ };
1829
+ await openAuthUrl({ provider, getUrl, redirectUri, signal });
1830
+ if (!clientSessionId) return;
1701
1831
  await authenticate(clientSessionId, deps);
1702
1832
  }
1703
1833
 
@@ -1707,10 +1837,10 @@ function withSignal(promise, signal) {
1707
1837
  promise,
1708
1838
  new Promise((_, reject) => {
1709
1839
  if (signal.aborted) {
1710
- reject(new DOMException("Aborted", "AbortError"));
1840
+ reject(abortError());
1711
1841
  return;
1712
1842
  }
1713
- signal.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")), { once: true });
1843
+ signal.addEventListener("abort", () => reject(abortError()), { once: true });
1714
1844
  })
1715
1845
  ]);
1716
1846
  }
@@ -1721,7 +1851,7 @@ async function loginWallet(type, deps) {
1721
1851
  let connectedWallet;
1722
1852
  try {
1723
1853
  setAuthState({ step: "connecting_wallet", walletType: type });
1724
- const adapter = await deps.resolveWalletAdapter(type);
1854
+ const adapter = await withSignal(deps.resolveWalletAdapter(type), signal);
1725
1855
  const available = await withSignal(adapter.isAvailable(), signal);
1726
1856
  if (!available) {
1727
1857
  setAuthState({ step: "wallet_not_installed", walletType: type });
@@ -1758,6 +1888,9 @@ async function loginWallet(type, deps) {
1758
1888
 
1759
1889
  // src/client/client.ts
1760
1890
  var isBrowser = typeof window !== "undefined" && typeof localStorage !== "undefined";
1891
+ var isReactNative = typeof navigator !== "undefined" && navigator.product === "ReactNative";
1892
+ var isClientRuntime = isBrowser || isReactNative;
1893
+ var REFRESH_SKEW_SECONDS = 60;
1761
1894
  function warnServerSide(method) {
1762
1895
  console.warn(
1763
1896
  `[PollarClient] ${method}() called server-side \u2014 browser APIs unavailable. Use PollarClient only in Client Components.`
@@ -1785,6 +1918,11 @@ var PollarClient = class {
1785
1918
  /** Singleton in-flight refresh — concurrent 401s coalesce into one /auth/refresh call. */
1786
1919
  this._refreshPromise = null;
1787
1920
  this._storageEventHandler = null;
1921
+ /** Updated by the request middleware. Read by the silent-refresh scheduler
1922
+ * to skip proactive refreshes after `maxIdleMs` of no HTTP activity. */
1923
+ this._lastRequestAt = Date.now();
1924
+ this._refreshTimer = null;
1925
+ this._visibilityUnsubscribe = null;
1788
1926
  this._transactionState = null;
1789
1927
  this._transactionStateListeners = /* @__PURE__ */ new Set();
1790
1928
  this._txHistoryState = { step: "idle" };
@@ -1795,19 +1933,38 @@ var PollarClient = class {
1795
1933
  this._authStateListeners = /* @__PURE__ */ new Set();
1796
1934
  this._networkState = { step: "idle" };
1797
1935
  this._networkStateListeners = /* @__PURE__ */ new Set();
1936
+ /**
1937
+ * Latched once the storage adapter degrades. We dedupe (the adapter only
1938
+ * fires once anyway) and use it to replay state to late-subscribers — same
1939
+ * pattern as `onAuthStateChange` replaying `_authState` on subscribe.
1940
+ * Only populated when the SDK constructed the default storage adapter; if
1941
+ * the consumer passes `config.storage`, they own degradation notifications.
1942
+ */
1943
+ this._storageDegraded = null;
1944
+ this._storageDegradeListeners = /* @__PURE__ */ new Set();
1798
1945
  this._walletAdapter = null;
1799
1946
  this._loginController = null;
1800
1947
  this.apiKey = config.apiKey;
1801
- this.id = crypto.randomUUID();
1948
+ this.id = randomUUID();
1802
1949
  this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
1803
- this._storage = config.storage ?? defaultStorage(config.onStorageDegrade ? { onDegrade: config.onStorageDegrade } : void 0);
1950
+ this._storage = config.storage ?? defaultStorage({
1951
+ onDegrade: (reason, error) => {
1952
+ config.onStorageDegrade?.(reason, error);
1953
+ this._dispatchStorageDegrade(reason, error);
1954
+ }
1955
+ });
1804
1956
  this._keyManager = config.keyManager ?? defaultKeyManager(this._storage, config.apiKey);
1805
1957
  this._walletAdapterResolver = config.walletAdapter ?? null;
1958
+ this._walletResolverTimeoutMs = config.walletResolverTimeoutMs ?? 5e3;
1806
1959
  this._deviceLabel = config.deviceLabel;
1960
+ this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
1961
+ this._maxIdleMs = config.maxIdleMs;
1962
+ this._openAuthUrl = config.openAuthUrl ?? defaultWebOAuthOpener;
1963
+ this._oauthRedirectUri = config.oauthRedirectUri ?? (isBrowser ? window.location.origin : "");
1807
1964
  this._api = createApiClient(this.basePath);
1808
1965
  this._wireMiddlewares();
1809
1966
  this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
1810
- if (!isBrowser) {
1967
+ if (!isClientRuntime) {
1811
1968
  warnServerSide("constructor");
1812
1969
  this._initialized = Promise.resolve();
1813
1970
  return;
@@ -1833,7 +1990,7 @@ var PollarClient = class {
1833
1990
  // ─── Lifecycle ────────────────────────────────────────────────────────────
1834
1991
  async _initialize() {
1835
1992
  this._apiKeyHash = await hashApiKey(this.apiKey);
1836
- if (typeof window !== "undefined") {
1993
+ if (isBrowser) {
1837
1994
  const sessionKey = sessionStorageKey(this._apiKeyHash);
1838
1995
  const handler = (e) => {
1839
1996
  if (e.key === sessionKey) {
@@ -1849,15 +2006,23 @@ var PollarClient = class {
1849
2006
  console.warn("[PollarClient] KeyManager init failed; DPoP unavailable for this session", err);
1850
2007
  }
1851
2008
  await this._restoreSession();
2009
+ this._visibilityUnsubscribe = this._visibilityProvider.onChange((visible) => {
2010
+ if (visible) void this._maybeProactiveRefresh();
2011
+ });
1852
2012
  }
1853
2013
  /** Detach the cross-tab storage listener and abort any in-flight login. */
1854
2014
  destroy() {
1855
- if (this._storageEventHandler && typeof window !== "undefined") {
2015
+ if (this._storageEventHandler && isBrowser) {
1856
2016
  window.removeEventListener("storage", this._storageEventHandler);
1857
2017
  this._storageEventHandler = null;
1858
2018
  }
1859
2019
  this._loginController?.abort();
1860
2020
  this._loginController = null;
2021
+ this._clearRefreshTimer();
2022
+ if (this._visibilityUnsubscribe) {
2023
+ this._visibilityUnsubscribe();
2024
+ this._visibilityUnsubscribe = null;
2025
+ }
1861
2026
  }
1862
2027
  // ─── Middlewares (DPoP + auto-refresh) ────────────────────────────────────
1863
2028
  _wireMiddlewares() {
@@ -1865,6 +2030,7 @@ var PollarClient = class {
1865
2030
  this._api.use({
1866
2031
  onRequest: async ({ request }) => {
1867
2032
  request.headers.set("x-pollar-api-key", self.apiKey);
2033
+ self._lastRequestAt = Date.now();
1868
2034
  await self._initialized;
1869
2035
  if (request.body !== null) {
1870
2036
  try {
@@ -1895,15 +2061,22 @@ var PollarClient = class {
1895
2061
  const newNonce = response.headers.get("DPoP-Nonce");
1896
2062
  if (newNonce) self._dpopNonce = newNonce;
1897
2063
  if (response.status !== 401) return response;
1898
- if (request.url.includes("/auth/refresh")) return response;
1899
2064
  const wwwAuth = response.headers.get("WWW-Authenticate") ?? "";
1900
2065
  const isNonceChallenge = wwwAuth.includes("use_dpop_nonce");
2066
+ if (request.url.includes("/auth/refresh")) {
2067
+ if (isNonceChallenge) return self._retryRequest(request);
2068
+ return response;
2069
+ }
1901
2070
  if (!isNonceChallenge) {
1902
2071
  try {
1903
2072
  await self.refresh();
1904
2073
  } catch {
1905
2074
  return response;
1906
2075
  }
2076
+ const method = request.method.toUpperCase();
2077
+ if (method !== "GET" && method !== "HEAD") {
2078
+ return response;
2079
+ }
1907
2080
  }
1908
2081
  return self._retryRequest(request);
1909
2082
  }
@@ -1928,14 +2101,22 @@ var PollarClient = class {
1928
2101
  }
1929
2102
  async _retryRequest(originalRequest) {
1930
2103
  const headers = new Headers(originalRequest.headers);
1931
- const accessToken = this._session?.token?.accessToken;
1932
- if (accessToken) {
1933
- const proof = await this._buildProofForRequest(originalRequest, accessToken);
1934
- if (proof) {
1935
- headers.set("Authorization", `DPoP ${accessToken}`);
1936
- headers.set("DPoP", proof);
1937
- } else {
1938
- headers.set("Authorization", `Bearer ${accessToken}`);
2104
+ const isRefresh = originalRequest.url.includes("/auth/refresh");
2105
+ if (isRefresh) {
2106
+ const proof = await this._buildProofForRequest(originalRequest, void 0);
2107
+ headers.delete("Authorization");
2108
+ if (proof) headers.set("DPoP", proof);
2109
+ else headers.delete("DPoP");
2110
+ } else {
2111
+ const accessToken = this._session?.token?.accessToken;
2112
+ if (accessToken) {
2113
+ const proof = await this._buildProofForRequest(originalRequest, accessToken);
2114
+ if (proof) {
2115
+ headers.set("Authorization", `DPoP ${accessToken}`);
2116
+ headers.set("DPoP", proof);
2117
+ } else {
2118
+ headers.set("Authorization", `Bearer ${accessToken}`);
2119
+ }
1939
2120
  }
1940
2121
  }
1941
2122
  const cachedBody = this._requestBodyCache.get(originalRequest);
@@ -2006,6 +2187,65 @@ var PollarClient = class {
2006
2187
  } catch (err) {
2007
2188
  console.error("[PollarClient] Failed to persist refreshed session", err);
2008
2189
  }
2190
+ this._scheduleNextRefresh();
2191
+ }
2192
+ }
2193
+ // ─── Silent refresh scheduler ────────────────────────────────────────────────
2194
+ /**
2195
+ * Arm a single setTimeout to fire shortly before the current access token
2196
+ * expires. Idempotent — clearing any previous timer first. Safe to call
2197
+ * from any session-write site (initial login, restore-from-storage, after
2198
+ * a successful rotation). No-op if there's no session in memory.
2199
+ *
2200
+ * Browser/RN background-tab throttling makes long-running setTimeouts
2201
+ * unreliable on their own; the `visibilitychange` listener compensates by
2202
+ * re-invoking `_maybeProactiveRefresh` whenever the app comes back to the
2203
+ * foreground, catching any timer that fired late or never fired at all.
2204
+ */
2205
+ _scheduleNextRefresh() {
2206
+ this._clearRefreshTimer();
2207
+ const expiresAt = this._session?.token?.expiresAt;
2208
+ if (typeof expiresAt !== "number") return;
2209
+ const dueInMs = Math.max(0, (expiresAt - Math.floor(Date.now() / 1e3) - REFRESH_SKEW_SECONDS) * 1e3);
2210
+ this._refreshTimer = setTimeout(() => {
2211
+ void this._maybeProactiveRefresh();
2212
+ }, dueInMs);
2213
+ }
2214
+ /**
2215
+ * Decide whether to actually run a refresh right now. Called both from the
2216
+ * scheduler timer and from the visibility-change listener.
2217
+ *
2218
+ * Skip if:
2219
+ * - no session / no RT (nothing to refresh)
2220
+ * - app is hidden — wait for the visibility listener to re-trigger us
2221
+ * - `maxIdleMs` configured and no client request since that window — let
2222
+ * the next reactive 401-refresh handle it whenever the user comes back
2223
+ * - the AT still has more than `REFRESH_SKEW_SECONDS` of life — reschedule
2224
+ *
2225
+ * Otherwise call `refresh()`, which uses the existing in-flight singleton
2226
+ * so we never collide with a reactive 401-triggered refresh. On failure,
2227
+ * `_doRefresh` already calls `_clearSession`, so auth-state listeners see
2228
+ * `step:'idle'` — no extra event dispatch needed here.
2229
+ */
2230
+ async _maybeProactiveRefresh() {
2231
+ if (!this._session?.token?.refreshToken) return;
2232
+ if (!this._visibilityProvider.isVisible()) return;
2233
+ if (this._maxIdleMs !== void 0 && Date.now() - this._lastRequestAt > this._maxIdleMs) return;
2234
+ const expiresAt = this._session.token.expiresAt;
2235
+ if (Math.floor(Date.now() / 1e3) < expiresAt - REFRESH_SKEW_SECONDS) {
2236
+ this._scheduleNextRefresh();
2237
+ return;
2238
+ }
2239
+ try {
2240
+ await this.refresh();
2241
+ } catch (err) {
2242
+ console.warn("[PollarClient] Proactive refresh failed; session cleared", err);
2243
+ }
2244
+ }
2245
+ _clearRefreshTimer() {
2246
+ if (this._refreshTimer !== null) {
2247
+ clearTimeout(this._refreshTimer);
2248
+ this._refreshTimer = null;
2009
2249
  }
2010
2250
  }
2011
2251
  // ─── Auth state ──────────────────────────────────────────────────────────────
@@ -2017,13 +2257,45 @@ var PollarClient = class {
2017
2257
  cb(this._authState);
2018
2258
  return () => this._authStateListeners.delete(cb);
2019
2259
  }
2260
+ /**
2261
+ * Subscribe to persistent-storage degradation (Safari private mode,
2262
+ * sandboxed iframes, quota errors, etc.). The SDK keeps running off
2263
+ * in-memory storage after degrade, but sessions won't survive reload — a
2264
+ * host UI typically wants to show "your session won't be saved" so the
2265
+ * user isn't blindsided after a refresh.
2266
+ *
2267
+ * Fires at most once per client lifetime (the underlying adapter dedupes).
2268
+ * Late subscribers receive the latched state synchronously on subscribe.
2269
+ *
2270
+ * Only fires when the SDK constructs the default storage adapter. If you
2271
+ * pass a custom `config.storage`, wire your own notification path through
2272
+ * that adapter's API — the SDK has no hook into it.
2273
+ */
2274
+ onStorageDegrade(cb) {
2275
+ this._storageDegradeListeners.add(cb);
2276
+ if (this._storageDegraded) {
2277
+ cb(this._storageDegraded.reason, this._storageDegraded.error);
2278
+ }
2279
+ return () => this._storageDegradeListeners.delete(cb);
2280
+ }
2281
+ _dispatchStorageDegrade(reason, error) {
2282
+ if (this._storageDegraded) return;
2283
+ this._storageDegraded = { reason, error };
2284
+ for (const cb of this._storageDegradeListeners) {
2285
+ try {
2286
+ cb(reason, error);
2287
+ } catch (err) {
2288
+ console.error("[PollarClient] onStorageDegrade listener threw", err);
2289
+ }
2290
+ }
2291
+ }
2020
2292
  /** PII (email, names, avatar, providers). Held in memory only — never persisted. */
2021
2293
  getUserProfile() {
2022
2294
  return this._profile;
2023
2295
  }
2024
2296
  // ─── Login (unified entry point) ─────────────────────────────────────────
2025
2297
  login(options) {
2026
- if (!isBrowser) {
2298
+ if (!isClientRuntime) {
2027
2299
  warnServerSide("login");
2028
2300
  return;
2029
2301
  }
@@ -2034,7 +2306,9 @@ var PollarClient = class {
2034
2306
  loginOAuth(options.provider, {
2035
2307
  ...deps,
2036
2308
  basePath: this.basePath,
2037
- apiKey: this.apiKey
2309
+ apiKey: this.apiKey,
2310
+ openAuthUrl: this._openAuthUrl,
2311
+ redirectUri: this._oauthRedirectUri
2038
2312
  }).catch((err) => this._handleFlowError(err));
2039
2313
  } else if (options.provider === "email") {
2040
2314
  const { email } = options;
@@ -2050,7 +2324,7 @@ var PollarClient = class {
2050
2324
  }
2051
2325
  // ─── Email OTP flow (3 steps) ─────────────────────────────────────────────
2052
2326
  beginEmailLogin() {
2053
- if (!isBrowser) {
2327
+ if (!isClientRuntime) {
2054
2328
  warnServerSide("beginEmailLogin");
2055
2329
  return;
2056
2330
  }
@@ -2058,7 +2332,7 @@ var PollarClient = class {
2058
2332
  initEmailSession(this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
2059
2333
  }
2060
2334
  sendEmailCode(email) {
2061
- if (!isBrowser) {
2335
+ if (!isClientRuntime) {
2062
2336
  warnServerSide("sendEmailCode");
2063
2337
  return;
2064
2338
  }
@@ -2070,7 +2344,7 @@ var PollarClient = class {
2070
2344
  sendEmailCode(email, clientSessionId, this._flowDeps(signal)).catch((err) => this._handleFlowError(err));
2071
2345
  }
2072
2346
  verifyEmailCode(code) {
2073
- if (!isBrowser) {
2347
+ if (!isClientRuntime) {
2074
2348
  warnServerSide("verifyEmailCode");
2075
2349
  return;
2076
2350
  }
@@ -2088,7 +2362,7 @@ var PollarClient = class {
2088
2362
  }
2089
2363
  // ─── Wallet flow (single call) ────────────────────────────────────────────
2090
2364
  loginWallet(type) {
2091
- if (!isBrowser) {
2365
+ if (!isClientRuntime) {
2092
2366
  warnServerSide("loginWallet");
2093
2367
  return;
2094
2368
  }
@@ -2114,7 +2388,7 @@ var PollarClient = class {
2114
2388
  * across all devices.
2115
2389
  */
2116
2390
  async logout(options = {}) {
2117
- if (!isBrowser) {
2391
+ if (!isClientRuntime) {
2118
2392
  warnServerSide("logout");
2119
2393
  return;
2120
2394
  }
@@ -2144,7 +2418,7 @@ var PollarClient = class {
2144
2418
  * `current` flag identifies which entry corresponds to this client.
2145
2419
  */
2146
2420
  async listSessions() {
2147
- if (!isBrowser) {
2421
+ if (!isClientRuntime) {
2148
2422
  warnServerSide("listSessions");
2149
2423
  return [];
2150
2424
  }
@@ -2163,7 +2437,7 @@ var PollarClient = class {
2163
2437
  * does NOT clear local state — call `logout()` for that case.
2164
2438
  */
2165
2439
  async revokeSession(familyId) {
2166
- if (!isBrowser) {
2440
+ if (!isClientRuntime) {
2167
2441
  warnServerSide("revokeSession");
2168
2442
  return;
2169
2443
  }
@@ -2253,10 +2527,16 @@ var PollarClient = class {
2253
2527
  }
2254
2528
  }
2255
2529
  // ─── Transactions ─────────────────────────────────────────────────────────
2530
+ /**
2531
+ * Builds an unsigned XDR. Drives `_setTransactionState` for modal-style UIs
2532
+ * AND returns a {@link BuildOutcome} so headless callers can `await` and
2533
+ * inspect the result without subscribing to state changes.
2534
+ */
2256
2535
  async buildTx(operation, params, options) {
2257
2536
  if (!this._session?.wallet?.publicKey) {
2258
- this._setTransactionState({ step: "error", details: "No wallet connected" });
2259
- return;
2537
+ const details = "No wallet connected";
2538
+ this._setTransactionState({ step: "error", phase: "building", details });
2539
+ return { status: "error", details };
2260
2540
  }
2261
2541
  const body = {
2262
2542
  network: this.getNetwork(),
@@ -2270,40 +2550,194 @@ var PollarClient = class {
2270
2550
  const { data, error } = await this._api.POST("/tx/build", { body });
2271
2551
  if (!error && data?.success && data.content) {
2272
2552
  this._setTransactionState({ step: "built", buildData: data.content });
2273
- } else {
2274
- const details = error?.details;
2275
- this._setTransactionState({ step: "error", ...details && { details } });
2553
+ return { status: "built", buildData: data.content };
2276
2554
  }
2555
+ const details = error?.details;
2556
+ this._setTransactionState({ step: "error", phase: "building", ...details && { details } });
2557
+ return { status: "error", ...details && { details } };
2277
2558
  } catch (err) {
2278
2559
  console.error("[PollarClient] buildTx failed", err);
2279
- this._setTransactionState({ step: "error" });
2560
+ this._setTransactionState({ step: "error", phase: "building" });
2561
+ return { status: "error" };
2280
2562
  }
2281
2563
  }
2282
2564
  getWalletType() {
2283
2565
  return this._walletAdapter?.type ?? null;
2284
2566
  }
2285
- async signAndSubmitTx(unsignedXdr) {
2286
- const state = this._transactionState;
2287
- const buildData = state?.step === "built" ? state.buildData : state?.step === "error" ? state.buildData : void 0;
2288
- const stateExtra = buildData ? { buildData } : { external: true };
2289
- this._setTransactionState({ step: "signing", ...stateExtra });
2290
- const accountToSign = this._session?.wallet?.publicKey;
2567
+ /**
2568
+ * Signs the given unsigned XDR and returns the signed XDR.
2569
+ *
2570
+ * - External wallets: signs locally via the wallet adapter.
2571
+ * - Custodial wallets: posts to `/tx/sign`. The backend signs (through
2572
+ * wallet-service or the app's customer-managed adapter) and returns the
2573
+ * signed XDR plus an `idempotencyKey` the caller should echo back to
2574
+ * `submitTx`.
2575
+ *
2576
+ * Drives `_setTransactionState`: emits `signing` while in flight and
2577
+ * `signed` on success (or `error[phase: 'signing']` on failure). `buildData`
2578
+ * is threaded through if the consumer previously called `buildTx`.
2579
+ */
2580
+ async signTx(unsignedXdr) {
2581
+ const buildData = this._currentBuildData();
2582
+ this._setTransactionState({ step: "signing", ...buildData && { buildData } });
2291
2583
  if (this._walletAdapter) {
2584
+ const accountToSign = this._session?.wallet?.publicKey;
2585
+ const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
2292
2586
  try {
2293
- const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
2294
2587
  const { signedTxXdr } = await this._walletAdapter.signTransaction(unsignedXdr, signOpts);
2295
- const stellarClient = new StellarClient(this.getNetwork());
2296
- const result = await stellarClient.submitTransaction(signedTxXdr);
2297
- if (result.success) {
2298
- this._setTransactionState({ step: "success", ...stateExtra, hash: result.hash });
2299
- } else {
2300
- this._setTransactionState({ step: "error", ...stateExtra, details: result.errorCode });
2588
+ this._setTransactionState({
2589
+ step: "signed",
2590
+ signedXdr: signedTxXdr,
2591
+ ...buildData && { buildData }
2592
+ });
2593
+ return { status: "signed", signedXdr: signedTxXdr };
2594
+ } catch (err) {
2595
+ const details = err instanceof Error ? err.message : void 0;
2596
+ this._setTransactionState({
2597
+ step: "error",
2598
+ phase: "signing",
2599
+ ...buildData && { buildData },
2600
+ ...details && { details }
2601
+ });
2602
+ return { status: "error", ...details && { details } };
2603
+ }
2604
+ }
2605
+ const publicKey = this._session?.wallet?.publicKey ?? "";
2606
+ try {
2607
+ const { data, error } = await this._api.POST("/tx/sign", {
2608
+ body: { network: this.getNetwork(), publicKey, unsignedXdr }
2609
+ });
2610
+ if (!error && data?.success && data.content?.signedXdr) {
2611
+ const { signedXdr, idempotencyKey } = data.content;
2612
+ this._setTransactionState({
2613
+ step: "signed",
2614
+ signedXdr,
2615
+ submissionToken: idempotencyKey,
2616
+ ...buildData && { buildData }
2617
+ });
2618
+ return { status: "signed", signedXdr, submissionToken: idempotencyKey };
2619
+ }
2620
+ const details = error?.details;
2621
+ this._setTransactionState({
2622
+ step: "error",
2623
+ phase: "signing",
2624
+ ...buildData && { buildData },
2625
+ ...details && { details }
2626
+ });
2627
+ return { status: "error", ...details && { details } };
2628
+ } catch (err) {
2629
+ const details = err instanceof Error ? err.message : void 0;
2630
+ this._setTransactionState({
2631
+ step: "error",
2632
+ phase: "signing",
2633
+ ...buildData && { buildData },
2634
+ ...details && { details }
2635
+ });
2636
+ return { status: "error", ...details && { details } };
2637
+ }
2638
+ }
2639
+ /**
2640
+ * Submits a signed XDR via `/tx/submit` regardless of wallet type
2641
+ * (custodial or external). Routing through sdk-api gives us:
2642
+ * - End-to-end tx_records persistence with full phase lifecycle so the
2643
+ * developer dashboard can show every tx (both custodial and external
2644
+ * wallet flows) at `/apps/:id/monitor/transactions`.
2645
+ * - Idempotency tracking via `submissionToken` (returned by `signTx`).
2646
+ * - A single response shape (SUCCESS / PENDING / FAILED) shared by both
2647
+ * flows — previously external wallets could only return SUCCESS or
2648
+ * error since the direct-to-Horizon path was synchronous.
2649
+ *
2650
+ * The extra hop adds ~50–150 ms vs. the legacy direct-Horizon path; the
2651
+ * persistence + observability win is worth it.
2652
+ *
2653
+ * Drives `_setTransactionState`: emits `submitting` while in flight,
2654
+ * `submitted` on Horizon ack (pending), `success` on ledger confirmation,
2655
+ * or `error[phase: 'submitting']` on failure.
2656
+ */
2657
+ async submitTx(signedXdr, opts) {
2658
+ const buildData = this._currentBuildData();
2659
+ const outcomeExtra = buildData ? { buildData } : {};
2660
+ this._setTransactionState({ step: "submitting", signedXdr, ...buildData && { buildData } });
2661
+ const publicKey = this._session?.wallet?.publicKey ?? "";
2662
+ try {
2663
+ const { data, error } = await this._api.POST("/tx/submit", {
2664
+ body: {
2665
+ network: this.getNetwork(),
2666
+ publicKey,
2667
+ signedXdr,
2668
+ ...opts?.submissionToken && { idempotencyKey: opts.submissionToken }
2301
2669
  }
2302
- } catch {
2303
- this._setTransactionState({ step: "error", ...stateExtra });
2670
+ });
2671
+ if (!error && data?.success && data.content) {
2672
+ const { hash, status: backendStatus, resultCode } = data.content;
2673
+ if (backendStatus === "SUCCESS") {
2674
+ this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
2675
+ return { status: "success", hash, ...outcomeExtra };
2676
+ }
2677
+ if (backendStatus === "PENDING") {
2678
+ this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
2679
+ return { status: "pending", hash, ...outcomeExtra };
2680
+ }
2681
+ this._setTransactionState({
2682
+ step: "error",
2683
+ phase: "submitting",
2684
+ ...buildData && { buildData },
2685
+ ...resultCode && { details: resultCode }
2686
+ });
2687
+ return {
2688
+ status: "error",
2689
+ hash,
2690
+ ...outcomeExtra,
2691
+ ...resultCode && { details: resultCode, resultCode }
2692
+ };
2304
2693
  }
2305
- return;
2694
+ const details = error?.details;
2695
+ this._setTransactionState({
2696
+ step: "error",
2697
+ phase: "submitting",
2698
+ ...buildData && { buildData },
2699
+ ...details && { details }
2700
+ });
2701
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2702
+ } catch (err) {
2703
+ const details = err instanceof Error ? err.message : void 0;
2704
+ this._setTransactionState({
2705
+ step: "error",
2706
+ phase: "submitting",
2707
+ ...buildData && { buildData },
2708
+ ...details && { details }
2709
+ });
2710
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2306
2711
  }
2712
+ }
2713
+ /**
2714
+ * Signs and submits in one logical step. Returns a {@link SubmitOutcome}.
2715
+ *
2716
+ * - **External wallets**: composes `signTx` + `submitTx` client-side. State
2717
+ * machine sees the full granular sequence `signing → signed → submitting
2718
+ * → success` because the underlying methods each emit.
2719
+ * - **Custodial wallets**: atomic `/tx/sign-and-send` round-trip. State
2720
+ * machine emits the compound `signing-submitting` step (the SDK can't
2721
+ * observe when one phase ends and the next begins inside that single
2722
+ * backend call) and then transitions to `submitted` (Horizon ack only) or
2723
+ * `success` (ledger-confirmed), or `error[phase: 'signing-submitting']`.
2724
+ */
2725
+ async signAndSubmitTx(unsignedXdr) {
2726
+ if (this._walletAdapter) {
2727
+ const signed = await this.signTx(unsignedXdr);
2728
+ if (signed.status === "error") {
2729
+ const buildData2 = this._currentBuildData();
2730
+ return {
2731
+ status: "error",
2732
+ ...buildData2 && { buildData: buildData2 },
2733
+ ...signed.details && { details: signed.details }
2734
+ };
2735
+ }
2736
+ return this.submitTx(signed.signedXdr);
2737
+ }
2738
+ const buildData = this._currentBuildData();
2739
+ const outcomeExtra = buildData ? { buildData } : {};
2740
+ this._setTransactionState({ step: "signing-submitting", ...buildData && { buildData } });
2307
2741
  const body = {
2308
2742
  network: this.getNetwork(),
2309
2743
  publicKey: this._session?.wallet?.publicKey ?? "",
@@ -2312,15 +2746,129 @@ var PollarClient = class {
2312
2746
  try {
2313
2747
  const { data, error } = await this._api.POST("/tx/sign-and-send", { body });
2314
2748
  if (!error && data?.success && data.content?.hash) {
2315
- this._setTransactionState({ step: "success", ...stateExtra, hash: data.content.hash });
2316
- } else {
2317
- const details = error?.details;
2318
- this._setTransactionState({ step: "error", ...stateExtra, ...details && { details } });
2749
+ const {
2750
+ hash,
2751
+ status: backendStatus,
2752
+ resultCode
2753
+ } = data.content;
2754
+ if (backendStatus === "SUCCESS") {
2755
+ this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
2756
+ return { status: "success", hash, ...outcomeExtra };
2757
+ }
2758
+ if (backendStatus === "PENDING") {
2759
+ this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
2760
+ return { status: "pending", hash, ...outcomeExtra };
2761
+ }
2762
+ this._setTransactionState({
2763
+ step: "error",
2764
+ phase: "signing-submitting",
2765
+ ...buildData && { buildData },
2766
+ ...resultCode && { details: resultCode }
2767
+ });
2768
+ return {
2769
+ status: "error",
2770
+ hash,
2771
+ ...outcomeExtra,
2772
+ ...resultCode && { details: resultCode, resultCode }
2773
+ };
2319
2774
  }
2320
- } catch {
2321
- this._setTransactionState({ step: "error", ...stateExtra });
2775
+ const details = error?.details;
2776
+ this._setTransactionState({
2777
+ step: "error",
2778
+ phase: "signing-submitting",
2779
+ ...buildData && { buildData },
2780
+ ...details && { details }
2781
+ });
2782
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2783
+ } catch (err) {
2784
+ const details = err instanceof Error ? err.message : void 0;
2785
+ this._setTransactionState({
2786
+ step: "error",
2787
+ phase: "signing-submitting",
2788
+ ...buildData && { buildData },
2789
+ ...details && { details }
2790
+ });
2791
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2322
2792
  }
2323
2793
  }
2794
+ /**
2795
+ * One-shot: build → sign → submit, returning the final {@link SubmitOutcome}.
2796
+ *
2797
+ * - **External wallets**: composes `buildTx` + `signAndSubmitTx` client-side.
2798
+ * State machine sees the full granular sequence (`building → built →
2799
+ * signing → signed → submitting → success`) because each composed call
2800
+ * emits its own transitions.
2801
+ * - **Custodial wallets**: single round-trip to `/tx/build-sign-submit`. The
2802
+ * signed XDR never leaves the backend. State machine emits the compound
2803
+ * `building-signing-submitting` step (the SDK can't observe individual
2804
+ * phase boundaries inside one atomic call) and then transitions to
2805
+ * `submitted` / `success` / `error[phase: 'building-signing-submitting']`.
2806
+ *
2807
+ * If you need granular UI feedback for custodial flows (separate
2808
+ * "Building…", "Signing…", "Submitting…" indicators), call `buildTx`,
2809
+ * `signTx`, and `submitTx` separately instead.
2810
+ */
2811
+ async buildAndSignAndSubmitTx(operation, params, options) {
2812
+ if (this._walletAdapter) {
2813
+ const built = await this.buildTx(operation, params, options);
2814
+ if (built.status === "error") {
2815
+ return { status: "error", ...built.details && { details: built.details } };
2816
+ }
2817
+ return this.signAndSubmitTx(built.buildData.unsignedXdr);
2818
+ }
2819
+ if (!this._session?.wallet?.publicKey) {
2820
+ this._setTransactionState({ step: "error", phase: "building-signing-submitting", details: "No wallet connected" });
2821
+ return { status: "error", details: "No wallet connected" };
2822
+ }
2823
+ this._setTransactionState({ step: "building-signing-submitting" });
2824
+ try {
2825
+ const { data, error } = await this._api.POST("/tx/build-sign-submit", {
2826
+ body: {
2827
+ network: this.getNetwork(),
2828
+ publicKey: this._session.wallet.publicKey,
2829
+ operation,
2830
+ params,
2831
+ options: options ?? {}
2832
+ }
2833
+ });
2834
+ if (!error && data?.success && data.content) {
2835
+ const { hash, status: backendStatus, resultCode } = data.content;
2836
+ if (backendStatus === "SUCCESS") {
2837
+ this._setTransactionState({ step: "success", hash });
2838
+ return { status: "success", hash };
2839
+ }
2840
+ if (backendStatus === "PENDING") {
2841
+ this._setTransactionState({ step: "submitted", hash });
2842
+ return { status: "pending", hash };
2843
+ }
2844
+ this._setTransactionState({
2845
+ step: "error",
2846
+ phase: "building-signing-submitting",
2847
+ ...resultCode && { details: resultCode }
2848
+ });
2849
+ return { status: "error", hash, ...resultCode && { details: resultCode, resultCode } };
2850
+ }
2851
+ const details = error?.details;
2852
+ this._setTransactionState({
2853
+ step: "error",
2854
+ phase: "building-signing-submitting",
2855
+ ...details && { details }
2856
+ });
2857
+ return { status: "error", ...details && { details } };
2858
+ } catch (err) {
2859
+ const details = err instanceof Error ? err.message : void 0;
2860
+ this._setTransactionState({
2861
+ step: "error",
2862
+ phase: "building-signing-submitting",
2863
+ ...details && { details }
2864
+ });
2865
+ return { status: "error", ...details && { details } };
2866
+ }
2867
+ }
2868
+ /** Alias for {@link buildAndSignAndSubmitTx} — shorter "just do the thing" name. */
2869
+ async runTx(operation, params, options) {
2870
+ return this.buildAndSignAndSubmitTx(operation, params, options);
2871
+ }
2324
2872
  // ─── App config ───────────────────────────────────────────────────────────
2325
2873
  async getAppConfig() {
2326
2874
  try {
@@ -2387,6 +2935,11 @@ var PollarClient = class {
2387
2935
  _flowDeps(signal) {
2388
2936
  return {
2389
2937
  api: this._api,
2938
+ basePath: this.basePath,
2939
+ // SSE status streaming works on web; React Native's `fetch` has no
2940
+ // readable `response.body`, so those clients poll the non-streaming
2941
+ // status endpoint instead. `isBrowser` is false in RN and SSR alike.
2942
+ useStreaming: isBrowser,
2390
2943
  signal,
2391
2944
  setAuthState: this._setAuthState.bind(this),
2392
2945
  storeSession: this._storeSession.bind(this),
@@ -2408,7 +2961,22 @@ var PollarClient = class {
2408
2961
  */
2409
2962
  async _resolveWalletAdapter(id) {
2410
2963
  if (this._walletAdapterResolver) {
2411
- return Promise.resolve(this._walletAdapterResolver(id));
2964
+ const timeoutMs = this._walletResolverTimeoutMs;
2965
+ let timeoutHandle;
2966
+ const timeoutPromise = new Promise((_, reject) => {
2967
+ timeoutHandle = setTimeout(() => {
2968
+ reject(
2969
+ Object.assign(new Error(`[PollarClient] Wallet adapter resolver for "${id}" timed out after ${timeoutMs}ms`), {
2970
+ code: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
2971
+ })
2972
+ );
2973
+ }, timeoutMs);
2974
+ });
2975
+ try {
2976
+ return await Promise.race([Promise.resolve(this._walletAdapterResolver(id)), timeoutPromise]);
2977
+ } finally {
2978
+ if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
2979
+ }
2412
2980
  }
2413
2981
  if (id === "freighter" /* FREIGHTER */) return new FreighterAdapter();
2414
2982
  if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter();
@@ -2422,6 +2990,16 @@ var PollarClient = class {
2422
2990
  this._setAuthState({ step: "idle" });
2423
2991
  return;
2424
2992
  }
2993
+ if (error instanceof Error && error.code === AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT) {
2994
+ console.error("[PollarClient]", error.message);
2995
+ this._setAuthState({
2996
+ step: "error",
2997
+ previousStep: this._authState.step,
2998
+ message: error.message,
2999
+ errorCode: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
3000
+ });
3001
+ return;
3002
+ }
2425
3003
  console.error("[PollarClient] Unexpected error in auth flow", error);
2426
3004
  this._setAuthState({
2427
3005
  step: "error",
@@ -2443,6 +3021,7 @@ var PollarClient = class {
2443
3021
  }
2444
3022
  console.info("[PollarClient] Session restored from storage");
2445
3023
  this._setAuthState({ step: "authenticated", session: this._session });
3024
+ this._scheduleNextRefresh();
2446
3025
  } else {
2447
3026
  console.info("[PollarClient] No session in storage");
2448
3027
  }
@@ -2469,9 +3048,11 @@ var PollarClient = class {
2469
3048
  }
2470
3049
  await writeStorage(this._storage, this.apiKeyHash, persisted);
2471
3050
  this._setAuthState({ step: "authenticated", session: persisted });
3051
+ this._scheduleNextRefresh();
2472
3052
  }
2473
3053
  async _clearSession() {
2474
3054
  console.info("[PollarClient] Session cleared");
3055
+ this._clearRefreshTimer();
2475
3056
  this._session = null;
2476
3057
  this._profile = null;
2477
3058
  this._walletAdapter = null;
@@ -2504,6 +3085,46 @@ var PollarClient = class {
2504
3085
  console.info(`[PollarClient] transaction:${next.step}`);
2505
3086
  for (const cb of this._transactionStateListeners) cb(next);
2506
3087
  }
3088
+ /**
3089
+ * Threads `buildData` through state transitions. When the user has already
3090
+ * called `buildTx`, every subsequent state (signing, signed, submitting,
3091
+ * submitted, success, error) should carry the build summary so modal UIs
3092
+ * can keep showing "Send 5 USDC to G..." through the whole flow.
3093
+ */
3094
+ _currentBuildData() {
3095
+ const s = this._transactionState;
3096
+ if (!s) return void 0;
3097
+ if ("buildData" in s && s.buildData) return s.buildData;
3098
+ return void 0;
3099
+ }
3100
+ };
3101
+
3102
+ // src/stellar/StellarClient.ts
3103
+ var HORIZON_URLS = {
3104
+ mainnet: "https://horizon.stellar.org",
3105
+ testnet: "https://horizon-testnet.stellar.org"
3106
+ };
3107
+ var StellarClient = class {
3108
+ constructor(config) {
3109
+ this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
3110
+ }
3111
+ async submitTransaction(signedXdr) {
3112
+ try {
3113
+ const response = await fetch(`${this.horizonUrl}/transactions`, {
3114
+ method: "POST",
3115
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
3116
+ body: new URLSearchParams({ tx: signedXdr })
3117
+ });
3118
+ if (!response.ok) {
3119
+ const body = await response.json().catch(() => ({}));
3120
+ return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
3121
+ }
3122
+ const data = await response.json();
3123
+ return { success: true, hash: data.hash };
3124
+ } catch {
3125
+ return { success: false, errorCode: "NETWORK_ERROR" };
3126
+ }
3127
+ }
2507
3128
  };
2508
3129
 
2509
3130
  // src/index.ts