@pollar/core 0.7.1 → 0.8.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.mjs CHANGED
@@ -26,10 +26,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
 
27
27
  // ../../node_modules/@stellar/freighter-api/build/index.min.js
28
28
  var require_index_min = __commonJS({
29
- "../../node_modules/@stellar/freighter-api/build/index.min.js"(exports$1, module) {
29
+ "../../node_modules/@stellar/freighter-api/build/index.min.js"(exports, module) {
30
30
  !(function(e, r) {
31
- "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();
32
- })(exports$1, (() => (() => {
31
+ "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();
32
+ })(exports, (() => (() => {
33
33
  var e, r, E = { d: (e2, r2) => {
34
34
  for (var o2 in r2) E.o(r2, o2) && !E.o(e2, o2) && Object.defineProperty(e2, o2, { enumerable: true, get: r2[o2] });
35
35
  }, o: (e2, r2) => Object.prototype.hasOwnProperty.call(e2, r2), r: (e2) => {
@@ -323,9 +323,7 @@ var WebCryptoKeyManager = class {
323
323
  */
324
324
  this._initPromise = null;
325
325
  if (typeof globalThis.crypto === "undefined" || !globalThis.crypto.subtle) {
326
- throw new Error(
327
- "[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost)."
328
- );
326
+ throw new Error("[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost).");
329
327
  }
330
328
  this.apiKey = apiKey;
331
329
  }
@@ -898,14 +896,18 @@ function createApiClient(baseUrl) {
898
896
  async function listDistributionRules(api) {
899
897
  const { data, error } = await api.GET("/distribution/rules");
900
898
  if (!data?.content || error) {
901
- throw new Error(error?.error ?? "Failed to list distribution rules");
899
+ throw new Error(
900
+ error?.code ?? error?.error ?? "Failed to list distribution rules"
901
+ );
902
902
  }
903
903
  return data.content.rules;
904
904
  }
905
905
  async function claimDistributionRule(api, body) {
906
906
  const { data, error } = await api.POST("/distribution/claim", { body });
907
907
  if (!data?.content || error) {
908
- throw new Error(error?.error ?? "Failed to claim distribution rule");
908
+ throw new Error(
909
+ error?.code ?? error?.error ?? "Failed to claim distribution rule"
910
+ );
909
911
  }
910
912
  return data.content;
911
913
  }
@@ -916,18 +918,18 @@ async function getKycStatus(api, providerId) {
916
918
  params: { query: providerId ? { providerId } : {} }
917
919
  });
918
920
  if (!data?.content || error) {
919
- throw new Error(error?.error ?? "Failed to get KYC status");
921
+ throw new Error(error?.code ?? error?.error ?? "Failed to get KYC status");
920
922
  }
921
923
  return data.content;
922
924
  }
923
925
  async function getKycProviders(api, country) {
924
926
  const { data, error } = await api.GET("/kyc/providers", { params: { query: { country } } });
925
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to get KYC providers");
927
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get KYC providers");
926
928
  return data.content;
927
929
  }
928
930
  async function startKyc(api, body) {
929
931
  const { data, error } = await api.POST("/kyc/start", { body });
930
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to start KYC");
932
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to start KYC");
931
933
  return data.content;
932
934
  }
933
935
  async function resolveKyc(api, providerId, level = "basic") {
@@ -949,22 +951,22 @@ async function pollKycStatus(api, providerId, { intervalMs = 3e3, timeoutMs = 3e
949
951
  // src/api/endpoints/ramps.ts
950
952
  async function getRampsQuote(api, query) {
951
953
  const { data, error } = await api.GET("/ramps/quote", { params: { query } });
952
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to get ramp quotes");
954
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get ramp quotes");
953
955
  return data.content;
954
956
  }
955
957
  async function createOnRamp(api, body) {
956
958
  const { data, error } = await api.POST("/ramps/onramp", { body });
957
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to create onramp");
959
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to create onramp");
958
960
  return data.content;
959
961
  }
960
962
  async function createOffRamp(api, body) {
961
963
  const { data, error } = await api.POST("/ramps/offramp", { body });
962
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to create offramp");
964
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to create offramp");
963
965
  return data.content;
964
966
  }
965
967
  async function getRampTransaction(api, txId) {
966
968
  const { data, error } = await api.GET("/ramps/transaction/{txId}", { params: { path: { txId } } });
967
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to get transaction");
969
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get transaction");
968
970
  return data.content;
969
971
  }
970
972
  async function pollRampTransaction(api, txId, { intervalMs = 5e3, timeoutMs = 6e5 } = {}) {
@@ -1039,34 +1041,6 @@ function generateJti() {
1039
1041
  );
1040
1042
  }
1041
1043
 
1042
- // src/stellar/StellarClient.ts
1043
- var HORIZON_URLS = {
1044
- mainnet: "https://horizon.stellar.org",
1045
- testnet: "https://horizon-testnet.stellar.org"
1046
- };
1047
- var StellarClient = class {
1048
- constructor(config) {
1049
- this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
1050
- }
1051
- async submitTransaction(signedXdr) {
1052
- try {
1053
- const response = await fetch(`${this.horizonUrl}/transactions`, {
1054
- method: "POST",
1055
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1056
- body: new URLSearchParams({ tx: signedXdr })
1057
- });
1058
- if (!response.ok) {
1059
- const body = await response.json().catch(() => ({}));
1060
- return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
1061
- }
1062
- const data = await response.json();
1063
- return { success: true, hash: data.hash };
1064
- } catch {
1065
- return { success: false, errorCode: "NETWORK_ERROR" };
1066
- }
1067
- }
1068
- };
1069
-
1070
1044
  // src/storage/web.ts
1071
1045
  var LOG_PREFIX = "[PollarClient:storage]";
1072
1046
  function createMemoryAdapter() {
@@ -1154,6 +1128,57 @@ function defaultStorage(options = {}) {
1154
1128
  return createLocalStorageAdapter(options);
1155
1129
  }
1156
1130
 
1131
+ // src/visibility/noop.ts
1132
+ function createNoopVisibilityProvider() {
1133
+ return {
1134
+ isVisible: () => true,
1135
+ onChange: () => () => {
1136
+ }
1137
+ };
1138
+ }
1139
+
1140
+ // src/visibility/web.ts
1141
+ function createWebVisibilityProvider() {
1142
+ const isVisibleNow = () => typeof document === "undefined" || document.visibilityState === "visible";
1143
+ return {
1144
+ isVisible: isVisibleNow,
1145
+ onChange: (cb) => {
1146
+ if (typeof window === "undefined" || typeof document === "undefined") {
1147
+ return () => {
1148
+ };
1149
+ }
1150
+ let last = isVisibleNow();
1151
+ const handler = () => {
1152
+ const next = isVisibleNow();
1153
+ if (next !== last) {
1154
+ last = next;
1155
+ cb(next);
1156
+ }
1157
+ };
1158
+ document.addEventListener("visibilitychange", handler);
1159
+ window.addEventListener("pageshow", handler);
1160
+ window.addEventListener("pagehide", handler);
1161
+ window.addEventListener("focus", handler);
1162
+ window.addEventListener("blur", handler);
1163
+ return () => {
1164
+ document.removeEventListener("visibilitychange", handler);
1165
+ window.removeEventListener("pageshow", handler);
1166
+ window.removeEventListener("pagehide", handler);
1167
+ window.removeEventListener("focus", handler);
1168
+ window.removeEventListener("blur", handler);
1169
+ };
1170
+ }
1171
+ };
1172
+ }
1173
+
1174
+ // src/visibility/autodetect.ts
1175
+ function defaultVisibilityProvider() {
1176
+ if (typeof document !== "undefined" && typeof window !== "undefined") {
1177
+ return createWebVisibilityProvider();
1178
+ }
1179
+ return createNoopVisibilityProvider();
1180
+ }
1181
+
1157
1182
  // src/types.ts
1158
1183
  var AUTH_ERROR_CODES = {
1159
1184
  SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
@@ -1164,6 +1189,7 @@ var AUTH_ERROR_CODES = {
1164
1189
  AUTH_FAILED: "AUTH_FAILED",
1165
1190
  WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
1166
1191
  WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
1192
+ WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
1167
1193
  UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
1168
1194
  };
1169
1195
  var PollarFlowError = class extends Error {
@@ -1554,7 +1580,7 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
1554
1580
  setAuthState({ step: "authenticating" });
1555
1581
  await streamUntilFound(api, clientSessionId, (data2) => data2?.status === "READY", 200, signal);
1556
1582
  const dpopJwk = await deps.getPublicJwk();
1557
- const { data, error } = await api.POST("/auth/login", {
1583
+ const { data } = await api.POST("/auth/login", {
1558
1584
  body: {
1559
1585
  clientSessionId,
1560
1586
  dpopJwk,
@@ -1719,7 +1745,7 @@ async function loginWallet(type, deps) {
1719
1745
  let connectedWallet;
1720
1746
  try {
1721
1747
  setAuthState({ step: "connecting_wallet", walletType: type });
1722
- const adapter = await deps.resolveWalletAdapter(type);
1748
+ const adapter = await withSignal(deps.resolveWalletAdapter(type), signal);
1723
1749
  const available = await withSignal(adapter.isAvailable(), signal);
1724
1750
  if (!available) {
1725
1751
  setAuthState({ step: "wallet_not_installed", walletType: type });
@@ -1756,6 +1782,7 @@ async function loginWallet(type, deps) {
1756
1782
 
1757
1783
  // src/client/client.ts
1758
1784
  var isBrowser = typeof window !== "undefined" && typeof localStorage !== "undefined";
1785
+ var REFRESH_SKEW_SECONDS = 60;
1759
1786
  function warnServerSide(method) {
1760
1787
  console.warn(
1761
1788
  `[PollarClient] ${method}() called server-side \u2014 browser APIs unavailable. Use PollarClient only in Client Components.`
@@ -1783,6 +1810,11 @@ var PollarClient = class {
1783
1810
  /** Singleton in-flight refresh — concurrent 401s coalesce into one /auth/refresh call. */
1784
1811
  this._refreshPromise = null;
1785
1812
  this._storageEventHandler = null;
1813
+ /** Updated by the request middleware. Read by the silent-refresh scheduler
1814
+ * to skip proactive refreshes after `maxIdleMs` of no HTTP activity. */
1815
+ this._lastRequestAt = Date.now();
1816
+ this._refreshTimer = null;
1817
+ this._visibilityUnsubscribe = null;
1786
1818
  this._transactionState = null;
1787
1819
  this._transactionStateListeners = /* @__PURE__ */ new Set();
1788
1820
  this._txHistoryState = { step: "idle" };
@@ -1793,15 +1825,32 @@ var PollarClient = class {
1793
1825
  this._authStateListeners = /* @__PURE__ */ new Set();
1794
1826
  this._networkState = { step: "idle" };
1795
1827
  this._networkStateListeners = /* @__PURE__ */ new Set();
1828
+ /**
1829
+ * Latched once the storage adapter degrades. We dedupe (the adapter only
1830
+ * fires once anyway) and use it to replay state to late-subscribers — same
1831
+ * pattern as `onAuthStateChange` replaying `_authState` on subscribe.
1832
+ * Only populated when the SDK constructed the default storage adapter; if
1833
+ * the consumer passes `config.storage`, they own degradation notifications.
1834
+ */
1835
+ this._storageDegraded = null;
1836
+ this._storageDegradeListeners = /* @__PURE__ */ new Set();
1796
1837
  this._walletAdapter = null;
1797
1838
  this._loginController = null;
1798
1839
  this.apiKey = config.apiKey;
1799
1840
  this.id = crypto.randomUUID();
1800
1841
  this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
1801
- this._storage = config.storage ?? defaultStorage(config.onStorageDegrade ? { onDegrade: config.onStorageDegrade } : void 0);
1842
+ this._storage = config.storage ?? defaultStorage({
1843
+ onDegrade: (reason, error) => {
1844
+ config.onStorageDegrade?.(reason, error);
1845
+ this._dispatchStorageDegrade(reason, error);
1846
+ }
1847
+ });
1802
1848
  this._keyManager = config.keyManager ?? defaultKeyManager(this._storage, config.apiKey);
1803
1849
  this._walletAdapterResolver = config.walletAdapter ?? null;
1850
+ this._walletResolverTimeoutMs = config.walletResolverTimeoutMs ?? 5e3;
1804
1851
  this._deviceLabel = config.deviceLabel;
1852
+ this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
1853
+ this._maxIdleMs = config.maxIdleMs;
1805
1854
  this._api = createApiClient(this.basePath);
1806
1855
  this._wireMiddlewares();
1807
1856
  this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
@@ -1847,6 +1896,9 @@ var PollarClient = class {
1847
1896
  console.warn("[PollarClient] KeyManager init failed; DPoP unavailable for this session", err);
1848
1897
  }
1849
1898
  await this._restoreSession();
1899
+ this._visibilityUnsubscribe = this._visibilityProvider.onChange((visible) => {
1900
+ if (visible) void this._maybeProactiveRefresh();
1901
+ });
1850
1902
  }
1851
1903
  /** Detach the cross-tab storage listener and abort any in-flight login. */
1852
1904
  destroy() {
@@ -1856,6 +1908,11 @@ var PollarClient = class {
1856
1908
  }
1857
1909
  this._loginController?.abort();
1858
1910
  this._loginController = null;
1911
+ this._clearRefreshTimer();
1912
+ if (this._visibilityUnsubscribe) {
1913
+ this._visibilityUnsubscribe();
1914
+ this._visibilityUnsubscribe = null;
1915
+ }
1859
1916
  }
1860
1917
  // ─── Middlewares (DPoP + auto-refresh) ────────────────────────────────────
1861
1918
  _wireMiddlewares() {
@@ -1863,6 +1920,7 @@ var PollarClient = class {
1863
1920
  this._api.use({
1864
1921
  onRequest: async ({ request }) => {
1865
1922
  request.headers.set("x-pollar-api-key", self.apiKey);
1923
+ self._lastRequestAt = Date.now();
1866
1924
  await self._initialized;
1867
1925
  if (request.body !== null) {
1868
1926
  try {
@@ -1893,15 +1951,22 @@ var PollarClient = class {
1893
1951
  const newNonce = response.headers.get("DPoP-Nonce");
1894
1952
  if (newNonce) self._dpopNonce = newNonce;
1895
1953
  if (response.status !== 401) return response;
1896
- if (request.url.includes("/auth/refresh")) return response;
1897
1954
  const wwwAuth = response.headers.get("WWW-Authenticate") ?? "";
1898
1955
  const isNonceChallenge = wwwAuth.includes("use_dpop_nonce");
1956
+ if (request.url.includes("/auth/refresh")) {
1957
+ if (isNonceChallenge) return self._retryRequest(request);
1958
+ return response;
1959
+ }
1899
1960
  if (!isNonceChallenge) {
1900
1961
  try {
1901
1962
  await self.refresh();
1902
1963
  } catch {
1903
1964
  return response;
1904
1965
  }
1966
+ const method = request.method.toUpperCase();
1967
+ if (method !== "GET" && method !== "HEAD") {
1968
+ return response;
1969
+ }
1905
1970
  }
1906
1971
  return self._retryRequest(request);
1907
1972
  }
@@ -1926,14 +1991,22 @@ var PollarClient = class {
1926
1991
  }
1927
1992
  async _retryRequest(originalRequest) {
1928
1993
  const headers = new Headers(originalRequest.headers);
1929
- const accessToken = this._session?.token?.accessToken;
1930
- if (accessToken) {
1931
- const proof = await this._buildProofForRequest(originalRequest, accessToken);
1932
- if (proof) {
1933
- headers.set("Authorization", `DPoP ${accessToken}`);
1934
- headers.set("DPoP", proof);
1935
- } else {
1936
- headers.set("Authorization", `Bearer ${accessToken}`);
1994
+ const isRefresh = originalRequest.url.includes("/auth/refresh");
1995
+ if (isRefresh) {
1996
+ const proof = await this._buildProofForRequest(originalRequest, void 0);
1997
+ headers.delete("Authorization");
1998
+ if (proof) headers.set("DPoP", proof);
1999
+ else headers.delete("DPoP");
2000
+ } else {
2001
+ const accessToken = this._session?.token?.accessToken;
2002
+ if (accessToken) {
2003
+ const proof = await this._buildProofForRequest(originalRequest, accessToken);
2004
+ if (proof) {
2005
+ headers.set("Authorization", `DPoP ${accessToken}`);
2006
+ headers.set("DPoP", proof);
2007
+ } else {
2008
+ headers.set("Authorization", `Bearer ${accessToken}`);
2009
+ }
1937
2010
  }
1938
2011
  }
1939
2012
  const cachedBody = this._requestBodyCache.get(originalRequest);
@@ -2004,6 +2077,65 @@ var PollarClient = class {
2004
2077
  } catch (err) {
2005
2078
  console.error("[PollarClient] Failed to persist refreshed session", err);
2006
2079
  }
2080
+ this._scheduleNextRefresh();
2081
+ }
2082
+ }
2083
+ // ─── Silent refresh scheduler ────────────────────────────────────────────────
2084
+ /**
2085
+ * Arm a single setTimeout to fire shortly before the current access token
2086
+ * expires. Idempotent — clearing any previous timer first. Safe to call
2087
+ * from any session-write site (initial login, restore-from-storage, after
2088
+ * a successful rotation). No-op if there's no session in memory.
2089
+ *
2090
+ * Browser/RN background-tab throttling makes long-running setTimeouts
2091
+ * unreliable on their own; the `visibilitychange` listener compensates by
2092
+ * re-invoking `_maybeProactiveRefresh` whenever the app comes back to the
2093
+ * foreground, catching any timer that fired late or never fired at all.
2094
+ */
2095
+ _scheduleNextRefresh() {
2096
+ this._clearRefreshTimer();
2097
+ const expiresAt = this._session?.token?.expiresAt;
2098
+ if (typeof expiresAt !== "number") return;
2099
+ const dueInMs = Math.max(0, (expiresAt - Math.floor(Date.now() / 1e3) - REFRESH_SKEW_SECONDS) * 1e3);
2100
+ this._refreshTimer = setTimeout(() => {
2101
+ void this._maybeProactiveRefresh();
2102
+ }, dueInMs);
2103
+ }
2104
+ /**
2105
+ * Decide whether to actually run a refresh right now. Called both from the
2106
+ * scheduler timer and from the visibility-change listener.
2107
+ *
2108
+ * Skip if:
2109
+ * - no session / no RT (nothing to refresh)
2110
+ * - app is hidden — wait for the visibility listener to re-trigger us
2111
+ * - `maxIdleMs` configured and no client request since that window — let
2112
+ * the next reactive 401-refresh handle it whenever the user comes back
2113
+ * - the AT still has more than `REFRESH_SKEW_SECONDS` of life — reschedule
2114
+ *
2115
+ * Otherwise call `refresh()`, which uses the existing in-flight singleton
2116
+ * so we never collide with a reactive 401-triggered refresh. On failure,
2117
+ * `_doRefresh` already calls `_clearSession`, so auth-state listeners see
2118
+ * `step:'idle'` — no extra event dispatch needed here.
2119
+ */
2120
+ async _maybeProactiveRefresh() {
2121
+ if (!this._session?.token?.refreshToken) return;
2122
+ if (!this._visibilityProvider.isVisible()) return;
2123
+ if (this._maxIdleMs !== void 0 && Date.now() - this._lastRequestAt > this._maxIdleMs) return;
2124
+ const expiresAt = this._session.token.expiresAt;
2125
+ if (Math.floor(Date.now() / 1e3) < expiresAt - REFRESH_SKEW_SECONDS) {
2126
+ this._scheduleNextRefresh();
2127
+ return;
2128
+ }
2129
+ try {
2130
+ await this.refresh();
2131
+ } catch (err) {
2132
+ console.warn("[PollarClient] Proactive refresh failed; session cleared", err);
2133
+ }
2134
+ }
2135
+ _clearRefreshTimer() {
2136
+ if (this._refreshTimer !== null) {
2137
+ clearTimeout(this._refreshTimer);
2138
+ this._refreshTimer = null;
2007
2139
  }
2008
2140
  }
2009
2141
  // ─── Auth state ──────────────────────────────────────────────────────────────
@@ -2015,6 +2147,38 @@ var PollarClient = class {
2015
2147
  cb(this._authState);
2016
2148
  return () => this._authStateListeners.delete(cb);
2017
2149
  }
2150
+ /**
2151
+ * Subscribe to persistent-storage degradation (Safari private mode,
2152
+ * sandboxed iframes, quota errors, etc.). The SDK keeps running off
2153
+ * in-memory storage after degrade, but sessions won't survive reload — a
2154
+ * host UI typically wants to show "your session won't be saved" so the
2155
+ * user isn't blindsided after a refresh.
2156
+ *
2157
+ * Fires at most once per client lifetime (the underlying adapter dedupes).
2158
+ * Late subscribers receive the latched state synchronously on subscribe.
2159
+ *
2160
+ * Only fires when the SDK constructs the default storage adapter. If you
2161
+ * pass a custom `config.storage`, wire your own notification path through
2162
+ * that adapter's API — the SDK has no hook into it.
2163
+ */
2164
+ onStorageDegrade(cb) {
2165
+ this._storageDegradeListeners.add(cb);
2166
+ if (this._storageDegraded) {
2167
+ cb(this._storageDegraded.reason, this._storageDegraded.error);
2168
+ }
2169
+ return () => this._storageDegradeListeners.delete(cb);
2170
+ }
2171
+ _dispatchStorageDegrade(reason, error) {
2172
+ if (this._storageDegraded) return;
2173
+ this._storageDegraded = { reason, error };
2174
+ for (const cb of this._storageDegradeListeners) {
2175
+ try {
2176
+ cb(reason, error);
2177
+ } catch (err) {
2178
+ console.error("[PollarClient] onStorageDegrade listener threw", err);
2179
+ }
2180
+ }
2181
+ }
2018
2182
  /** PII (email, names, avatar, providers). Held in memory only — never persisted. */
2019
2183
  getUserProfile() {
2020
2184
  return this._profile;
@@ -2251,10 +2415,16 @@ var PollarClient = class {
2251
2415
  }
2252
2416
  }
2253
2417
  // ─── Transactions ─────────────────────────────────────────────────────────
2418
+ /**
2419
+ * Builds an unsigned XDR. Drives `_setTransactionState` for modal-style UIs
2420
+ * AND returns a {@link BuildOutcome} so headless callers can `await` and
2421
+ * inspect the result without subscribing to state changes.
2422
+ */
2254
2423
  async buildTx(operation, params, options) {
2255
2424
  if (!this._session?.wallet?.publicKey) {
2256
- this._setTransactionState({ step: "error", details: "No wallet connected" });
2257
- return;
2425
+ const details = "No wallet connected";
2426
+ this._setTransactionState({ step: "error", phase: "building", details });
2427
+ return { status: "error", details };
2258
2428
  }
2259
2429
  const body = {
2260
2430
  network: this.getNetwork(),
@@ -2268,40 +2438,194 @@ var PollarClient = class {
2268
2438
  const { data, error } = await this._api.POST("/tx/build", { body });
2269
2439
  if (!error && data?.success && data.content) {
2270
2440
  this._setTransactionState({ step: "built", buildData: data.content });
2271
- } else {
2272
- const details = error?.details;
2273
- this._setTransactionState({ step: "error", ...details && { details } });
2441
+ return { status: "built", buildData: data.content };
2274
2442
  }
2443
+ const details = error?.details;
2444
+ this._setTransactionState({ step: "error", phase: "building", ...details && { details } });
2445
+ return { status: "error", ...details && { details } };
2275
2446
  } catch (err) {
2276
2447
  console.error("[PollarClient] buildTx failed", err);
2277
- this._setTransactionState({ step: "error" });
2448
+ this._setTransactionState({ step: "error", phase: "building" });
2449
+ return { status: "error" };
2278
2450
  }
2279
2451
  }
2280
2452
  getWalletType() {
2281
2453
  return this._walletAdapter?.type ?? null;
2282
2454
  }
2283
- async signAndSubmitTx(unsignedXdr) {
2284
- const state = this._transactionState;
2285
- const buildData = state?.step === "built" ? state.buildData : state?.step === "error" ? state.buildData : void 0;
2286
- const stateExtra = buildData ? { buildData } : { external: true };
2287
- this._setTransactionState({ step: "signing", ...stateExtra });
2288
- const accountToSign = this._session?.wallet?.publicKey;
2455
+ /**
2456
+ * Signs the given unsigned XDR and returns the signed XDR.
2457
+ *
2458
+ * - External wallets: signs locally via the wallet adapter.
2459
+ * - Custodial wallets: posts to `/tx/sign`. The backend signs (through
2460
+ * wallet-service or the app's customer-managed adapter) and returns the
2461
+ * signed XDR plus an `idempotencyKey` the caller should echo back to
2462
+ * `submitTx`.
2463
+ *
2464
+ * Drives `_setTransactionState`: emits `signing` while in flight and
2465
+ * `signed` on success (or `error[phase: 'signing']` on failure). `buildData`
2466
+ * is threaded through if the consumer previously called `buildTx`.
2467
+ */
2468
+ async signTx(unsignedXdr) {
2469
+ const buildData = this._currentBuildData();
2470
+ this._setTransactionState({ step: "signing", ...buildData && { buildData } });
2289
2471
  if (this._walletAdapter) {
2472
+ const accountToSign = this._session?.wallet?.publicKey;
2473
+ const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
2290
2474
  try {
2291
- const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
2292
2475
  const { signedTxXdr } = await this._walletAdapter.signTransaction(unsignedXdr, signOpts);
2293
- const stellarClient = new StellarClient(this.getNetwork());
2294
- const result = await stellarClient.submitTransaction(signedTxXdr);
2295
- if (result.success) {
2296
- this._setTransactionState({ step: "success", ...stateExtra, hash: result.hash });
2297
- } else {
2298
- this._setTransactionState({ step: "error", ...stateExtra, details: result.errorCode });
2476
+ this._setTransactionState({
2477
+ step: "signed",
2478
+ signedXdr: signedTxXdr,
2479
+ ...buildData && { buildData }
2480
+ });
2481
+ return { status: "signed", signedXdr: signedTxXdr };
2482
+ } catch (err) {
2483
+ const details = err instanceof Error ? err.message : void 0;
2484
+ this._setTransactionState({
2485
+ step: "error",
2486
+ phase: "signing",
2487
+ ...buildData && { buildData },
2488
+ ...details && { details }
2489
+ });
2490
+ return { status: "error", ...details && { details } };
2491
+ }
2492
+ }
2493
+ const publicKey = this._session?.wallet?.publicKey ?? "";
2494
+ try {
2495
+ const { data, error } = await this._api.POST("/tx/sign", {
2496
+ body: { network: this.getNetwork(), publicKey, unsignedXdr }
2497
+ });
2498
+ if (!error && data?.success && data.content?.signedXdr) {
2499
+ const { signedXdr, idempotencyKey } = data.content;
2500
+ this._setTransactionState({
2501
+ step: "signed",
2502
+ signedXdr,
2503
+ submissionToken: idempotencyKey,
2504
+ ...buildData && { buildData }
2505
+ });
2506
+ return { status: "signed", signedXdr, submissionToken: idempotencyKey };
2507
+ }
2508
+ const details = error?.details;
2509
+ this._setTransactionState({
2510
+ step: "error",
2511
+ phase: "signing",
2512
+ ...buildData && { buildData },
2513
+ ...details && { details }
2514
+ });
2515
+ return { status: "error", ...details && { details } };
2516
+ } catch (err) {
2517
+ const details = err instanceof Error ? err.message : void 0;
2518
+ this._setTransactionState({
2519
+ step: "error",
2520
+ phase: "signing",
2521
+ ...buildData && { buildData },
2522
+ ...details && { details }
2523
+ });
2524
+ return { status: "error", ...details && { details } };
2525
+ }
2526
+ }
2527
+ /**
2528
+ * Submits a signed XDR via `/tx/submit` regardless of wallet type
2529
+ * (custodial or external). Routing through sdk-api gives us:
2530
+ * - End-to-end tx_records persistence with full phase lifecycle so the
2531
+ * developer dashboard can show every tx (both custodial and external
2532
+ * wallet flows) at `/apps/:id/monitor/transactions`.
2533
+ * - Idempotency tracking via `submissionToken` (returned by `signTx`).
2534
+ * - A single response shape (SUCCESS / PENDING / FAILED) shared by both
2535
+ * flows — previously external wallets could only return SUCCESS or
2536
+ * error since the direct-to-Horizon path was synchronous.
2537
+ *
2538
+ * The extra hop adds ~50–150 ms vs. the legacy direct-Horizon path; the
2539
+ * persistence + observability win is worth it.
2540
+ *
2541
+ * Drives `_setTransactionState`: emits `submitting` while in flight,
2542
+ * `submitted` on Horizon ack (pending), `success` on ledger confirmation,
2543
+ * or `error[phase: 'submitting']` on failure.
2544
+ */
2545
+ async submitTx(signedXdr, opts) {
2546
+ const buildData = this._currentBuildData();
2547
+ const outcomeExtra = buildData ? { buildData } : {};
2548
+ this._setTransactionState({ step: "submitting", signedXdr, ...buildData && { buildData } });
2549
+ const publicKey = this._session?.wallet?.publicKey ?? "";
2550
+ try {
2551
+ const { data, error } = await this._api.POST("/tx/submit", {
2552
+ body: {
2553
+ network: this.getNetwork(),
2554
+ publicKey,
2555
+ signedXdr,
2556
+ ...opts?.submissionToken && { idempotencyKey: opts.submissionToken }
2299
2557
  }
2300
- } catch {
2301
- this._setTransactionState({ step: "error", ...stateExtra });
2558
+ });
2559
+ if (!error && data?.success && data.content) {
2560
+ const { hash, status: backendStatus, resultCode } = data.content;
2561
+ if (backendStatus === "SUCCESS") {
2562
+ this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
2563
+ return { status: "success", hash, ...outcomeExtra };
2564
+ }
2565
+ if (backendStatus === "PENDING") {
2566
+ this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
2567
+ return { status: "pending", hash, ...outcomeExtra };
2568
+ }
2569
+ this._setTransactionState({
2570
+ step: "error",
2571
+ phase: "submitting",
2572
+ ...buildData && { buildData },
2573
+ ...resultCode && { details: resultCode }
2574
+ });
2575
+ return {
2576
+ status: "error",
2577
+ hash,
2578
+ ...outcomeExtra,
2579
+ ...resultCode && { details: resultCode, resultCode }
2580
+ };
2302
2581
  }
2303
- return;
2582
+ const details = error?.details;
2583
+ this._setTransactionState({
2584
+ step: "error",
2585
+ phase: "submitting",
2586
+ ...buildData && { buildData },
2587
+ ...details && { details }
2588
+ });
2589
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2590
+ } catch (err) {
2591
+ const details = err instanceof Error ? err.message : void 0;
2592
+ this._setTransactionState({
2593
+ step: "error",
2594
+ phase: "submitting",
2595
+ ...buildData && { buildData },
2596
+ ...details && { details }
2597
+ });
2598
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2599
+ }
2600
+ }
2601
+ /**
2602
+ * Signs and submits in one logical step. Returns a {@link SubmitOutcome}.
2603
+ *
2604
+ * - **External wallets**: composes `signTx` + `submitTx` client-side. State
2605
+ * machine sees the full granular sequence `signing → signed → submitting
2606
+ * → success` because the underlying methods each emit.
2607
+ * - **Custodial wallets**: atomic `/tx/sign-and-send` round-trip. State
2608
+ * machine emits the compound `signing-submitting` step (the SDK can't
2609
+ * observe when one phase ends and the next begins inside that single
2610
+ * backend call) and then transitions to `submitted` (Horizon ack only) or
2611
+ * `success` (ledger-confirmed), or `error[phase: 'signing-submitting']`.
2612
+ */
2613
+ async signAndSubmitTx(unsignedXdr) {
2614
+ if (this._walletAdapter) {
2615
+ const signed = await this.signTx(unsignedXdr);
2616
+ if (signed.status === "error") {
2617
+ const buildData2 = this._currentBuildData();
2618
+ return {
2619
+ status: "error",
2620
+ ...buildData2 && { buildData: buildData2 },
2621
+ ...signed.details && { details: signed.details }
2622
+ };
2623
+ }
2624
+ return this.submitTx(signed.signedXdr);
2304
2625
  }
2626
+ const buildData = this._currentBuildData();
2627
+ const outcomeExtra = buildData ? { buildData } : {};
2628
+ this._setTransactionState({ step: "signing-submitting", ...buildData && { buildData } });
2305
2629
  const body = {
2306
2630
  network: this.getNetwork(),
2307
2631
  publicKey: this._session?.wallet?.publicKey ?? "",
@@ -2310,15 +2634,129 @@ var PollarClient = class {
2310
2634
  try {
2311
2635
  const { data, error } = await this._api.POST("/tx/sign-and-send", { body });
2312
2636
  if (!error && data?.success && data.content?.hash) {
2313
- this._setTransactionState({ step: "success", ...stateExtra, hash: data.content.hash });
2314
- } else {
2315
- const details = error?.details;
2316
- this._setTransactionState({ step: "error", ...stateExtra, ...details && { details } });
2637
+ const {
2638
+ hash,
2639
+ status: backendStatus,
2640
+ resultCode
2641
+ } = data.content;
2642
+ if (backendStatus === "SUCCESS") {
2643
+ this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
2644
+ return { status: "success", hash, ...outcomeExtra };
2645
+ }
2646
+ if (backendStatus === "PENDING") {
2647
+ this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
2648
+ return { status: "pending", hash, ...outcomeExtra };
2649
+ }
2650
+ this._setTransactionState({
2651
+ step: "error",
2652
+ phase: "signing-submitting",
2653
+ ...buildData && { buildData },
2654
+ ...resultCode && { details: resultCode }
2655
+ });
2656
+ return {
2657
+ status: "error",
2658
+ hash,
2659
+ ...outcomeExtra,
2660
+ ...resultCode && { details: resultCode, resultCode }
2661
+ };
2317
2662
  }
2318
- } catch {
2319
- this._setTransactionState({ step: "error", ...stateExtra });
2663
+ const details = error?.details;
2664
+ this._setTransactionState({
2665
+ step: "error",
2666
+ phase: "signing-submitting",
2667
+ ...buildData && { buildData },
2668
+ ...details && { details }
2669
+ });
2670
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2671
+ } catch (err) {
2672
+ const details = err instanceof Error ? err.message : void 0;
2673
+ this._setTransactionState({
2674
+ step: "error",
2675
+ phase: "signing-submitting",
2676
+ ...buildData && { buildData },
2677
+ ...details && { details }
2678
+ });
2679
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2680
+ }
2681
+ }
2682
+ /**
2683
+ * One-shot: build → sign → submit, returning the final {@link SubmitOutcome}.
2684
+ *
2685
+ * - **External wallets**: composes `buildTx` + `signAndSubmitTx` client-side.
2686
+ * State machine sees the full granular sequence (`building → built →
2687
+ * signing → signed → submitting → success`) because each composed call
2688
+ * emits its own transitions.
2689
+ * - **Custodial wallets**: single round-trip to `/tx/build-sign-submit`. The
2690
+ * signed XDR never leaves the backend. State machine emits the compound
2691
+ * `building-signing-submitting` step (the SDK can't observe individual
2692
+ * phase boundaries inside one atomic call) and then transitions to
2693
+ * `submitted` / `success` / `error[phase: 'building-signing-submitting']`.
2694
+ *
2695
+ * If you need granular UI feedback for custodial flows (separate
2696
+ * "Building…", "Signing…", "Submitting…" indicators), call `buildTx`,
2697
+ * `signTx`, and `submitTx` separately instead.
2698
+ */
2699
+ async buildAndSignAndSubmitTx(operation, params, options) {
2700
+ if (this._walletAdapter) {
2701
+ const built = await this.buildTx(operation, params, options);
2702
+ if (built.status === "error") {
2703
+ return { status: "error", ...built.details && { details: built.details } };
2704
+ }
2705
+ return this.signAndSubmitTx(built.buildData.unsignedXdr);
2706
+ }
2707
+ if (!this._session?.wallet?.publicKey) {
2708
+ this._setTransactionState({ step: "error", phase: "building-signing-submitting", details: "No wallet connected" });
2709
+ return { status: "error", details: "No wallet connected" };
2710
+ }
2711
+ this._setTransactionState({ step: "building-signing-submitting" });
2712
+ try {
2713
+ const { data, error } = await this._api.POST("/tx/build-sign-submit", {
2714
+ body: {
2715
+ network: this.getNetwork(),
2716
+ publicKey: this._session.wallet.publicKey,
2717
+ operation,
2718
+ params,
2719
+ options: options ?? {}
2720
+ }
2721
+ });
2722
+ if (!error && data?.success && data.content) {
2723
+ const { hash, status: backendStatus, resultCode } = data.content;
2724
+ if (backendStatus === "SUCCESS") {
2725
+ this._setTransactionState({ step: "success", hash });
2726
+ return { status: "success", hash };
2727
+ }
2728
+ if (backendStatus === "PENDING") {
2729
+ this._setTransactionState({ step: "submitted", hash });
2730
+ return { status: "pending", hash };
2731
+ }
2732
+ this._setTransactionState({
2733
+ step: "error",
2734
+ phase: "building-signing-submitting",
2735
+ ...resultCode && { details: resultCode }
2736
+ });
2737
+ return { status: "error", hash, ...resultCode && { details: resultCode, resultCode } };
2738
+ }
2739
+ const details = error?.details;
2740
+ this._setTransactionState({
2741
+ step: "error",
2742
+ phase: "building-signing-submitting",
2743
+ ...details && { details }
2744
+ });
2745
+ return { status: "error", ...details && { details } };
2746
+ } catch (err) {
2747
+ const details = err instanceof Error ? err.message : void 0;
2748
+ this._setTransactionState({
2749
+ step: "error",
2750
+ phase: "building-signing-submitting",
2751
+ ...details && { details }
2752
+ });
2753
+ return { status: "error", ...details && { details } };
2320
2754
  }
2321
2755
  }
2756
+ /** Alias for {@link buildAndSignAndSubmitTx} — shorter "just do the thing" name. */
2757
+ async runTx(operation, params, options) {
2758
+ return this.buildAndSignAndSubmitTx(operation, params, options);
2759
+ }
2322
2760
  // ─── App config ───────────────────────────────────────────────────────────
2323
2761
  async getAppConfig() {
2324
2762
  try {
@@ -2406,7 +2844,22 @@ var PollarClient = class {
2406
2844
  */
2407
2845
  async _resolveWalletAdapter(id) {
2408
2846
  if (this._walletAdapterResolver) {
2409
- return Promise.resolve(this._walletAdapterResolver(id));
2847
+ const timeoutMs = this._walletResolverTimeoutMs;
2848
+ let timeoutHandle;
2849
+ const timeoutPromise = new Promise((_, reject) => {
2850
+ timeoutHandle = setTimeout(() => {
2851
+ reject(
2852
+ Object.assign(new Error(`[PollarClient] Wallet adapter resolver for "${id}" timed out after ${timeoutMs}ms`), {
2853
+ code: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
2854
+ })
2855
+ );
2856
+ }, timeoutMs);
2857
+ });
2858
+ try {
2859
+ return await Promise.race([Promise.resolve(this._walletAdapterResolver(id)), timeoutPromise]);
2860
+ } finally {
2861
+ if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
2862
+ }
2410
2863
  }
2411
2864
  if (id === "freighter" /* FREIGHTER */) return new FreighterAdapter();
2412
2865
  if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter();
@@ -2420,6 +2873,16 @@ var PollarClient = class {
2420
2873
  this._setAuthState({ step: "idle" });
2421
2874
  return;
2422
2875
  }
2876
+ if (error instanceof Error && error.code === AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT) {
2877
+ console.error("[PollarClient]", error.message);
2878
+ this._setAuthState({
2879
+ step: "error",
2880
+ previousStep: this._authState.step,
2881
+ message: error.message,
2882
+ errorCode: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
2883
+ });
2884
+ return;
2885
+ }
2423
2886
  console.error("[PollarClient] Unexpected error in auth flow", error);
2424
2887
  this._setAuthState({
2425
2888
  step: "error",
@@ -2441,6 +2904,7 @@ var PollarClient = class {
2441
2904
  }
2442
2905
  console.info("[PollarClient] Session restored from storage");
2443
2906
  this._setAuthState({ step: "authenticated", session: this._session });
2907
+ this._scheduleNextRefresh();
2444
2908
  } else {
2445
2909
  console.info("[PollarClient] No session in storage");
2446
2910
  }
@@ -2467,9 +2931,11 @@ var PollarClient = class {
2467
2931
  }
2468
2932
  await writeStorage(this._storage, this.apiKeyHash, persisted);
2469
2933
  this._setAuthState({ step: "authenticated", session: persisted });
2934
+ this._scheduleNextRefresh();
2470
2935
  }
2471
2936
  async _clearSession() {
2472
2937
  console.info("[PollarClient] Session cleared");
2938
+ this._clearRefreshTimer();
2473
2939
  this._session = null;
2474
2940
  this._profile = null;
2475
2941
  this._walletAdapter = null;
@@ -2502,6 +2968,46 @@ var PollarClient = class {
2502
2968
  console.info(`[PollarClient] transaction:${next.step}`);
2503
2969
  for (const cb of this._transactionStateListeners) cb(next);
2504
2970
  }
2971
+ /**
2972
+ * Threads `buildData` through state transitions. When the user has already
2973
+ * called `buildTx`, every subsequent state (signing, signed, submitting,
2974
+ * submitted, success, error) should carry the build summary so modal UIs
2975
+ * can keep showing "Send 5 USDC to G..." through the whole flow.
2976
+ */
2977
+ _currentBuildData() {
2978
+ const s = this._transactionState;
2979
+ if (!s) return void 0;
2980
+ if ("buildData" in s && s.buildData) return s.buildData;
2981
+ return void 0;
2982
+ }
2983
+ };
2984
+
2985
+ // src/stellar/StellarClient.ts
2986
+ var HORIZON_URLS = {
2987
+ mainnet: "https://horizon.stellar.org",
2988
+ testnet: "https://horizon-testnet.stellar.org"
2989
+ };
2990
+ var StellarClient = class {
2991
+ constructor(config) {
2992
+ this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
2993
+ }
2994
+ async submitTransaction(signedXdr) {
2995
+ try {
2996
+ const response = await fetch(`${this.horizonUrl}/transactions`, {
2997
+ method: "POST",
2998
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2999
+ body: new URLSearchParams({ tx: signedXdr })
3000
+ });
3001
+ if (!response.ok) {
3002
+ const body = await response.json().catch(() => ({}));
3003
+ return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
3004
+ }
3005
+ const data = await response.json();
3006
+ return { success: true, hash: data.hash };
3007
+ } catch {
3008
+ return { success: false, errorCode: "NETWORK_ERROR" };
3009
+ }
3010
+ }
2505
3011
  };
2506
3012
 
2507
3013
  // src/index.ts