@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.rn.js CHANGED
@@ -30,10 +30,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
 
31
31
  // ../../node_modules/@stellar/freighter-api/build/index.min.js
32
32
  var require_index_min = __commonJS({
33
- "../../node_modules/@stellar/freighter-api/build/index.min.js"(exports$1, module) {
33
+ "../../node_modules/@stellar/freighter-api/build/index.min.js"(exports, module) {
34
34
  !(function(e, r) {
35
- "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();
36
- })(exports$1, (() => (() => {
35
+ "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();
36
+ })(exports, (() => (() => {
37
37
  var e, r, E = { d: (e2, r2) => {
38
38
  for (var o2 in r2) E.o(r2, o2) && !E.o(e2, o2) && Object.defineProperty(e2, o2, { enumerable: true, get: r2[o2] });
39
39
  }, o: (e2, r2) => Object.prototype.hasOwnProperty.call(e2, r2), r: (e2) => {
@@ -452,9 +452,7 @@ var WebCryptoKeyManager = class {
452
452
  */
453
453
  this._initPromise = null;
454
454
  if (typeof globalThis.crypto === "undefined" || !globalThis.crypto.subtle) {
455
- throw new Error(
456
- "[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost)."
457
- );
455
+ throw new Error("[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost).");
458
456
  }
459
457
  this.apiKey = apiKey;
460
458
  }
@@ -1027,14 +1025,18 @@ function createApiClient(baseUrl) {
1027
1025
  async function listDistributionRules(api) {
1028
1026
  const { data, error } = await api.GET("/distribution/rules");
1029
1027
  if (!data?.content || error) {
1030
- throw new Error(error?.error ?? "Failed to list distribution rules");
1028
+ throw new Error(
1029
+ error?.code ?? error?.error ?? "Failed to list distribution rules"
1030
+ );
1031
1031
  }
1032
1032
  return data.content.rules;
1033
1033
  }
1034
1034
  async function claimDistributionRule(api, body) {
1035
1035
  const { data, error } = await api.POST("/distribution/claim", { body });
1036
1036
  if (!data?.content || error) {
1037
- throw new Error(error?.error ?? "Failed to claim distribution rule");
1037
+ throw new Error(
1038
+ error?.code ?? error?.error ?? "Failed to claim distribution rule"
1039
+ );
1038
1040
  }
1039
1041
  return data.content;
1040
1042
  }
@@ -1045,18 +1047,18 @@ async function getKycStatus(api, providerId) {
1045
1047
  params: { query: providerId ? { providerId } : {} }
1046
1048
  });
1047
1049
  if (!data?.content || error) {
1048
- throw new Error(error?.error ?? "Failed to get KYC status");
1050
+ throw new Error(error?.code ?? error?.error ?? "Failed to get KYC status");
1049
1051
  }
1050
1052
  return data.content;
1051
1053
  }
1052
1054
  async function getKycProviders(api, country) {
1053
1055
  const { data, error } = await api.GET("/kyc/providers", { params: { query: { country } } });
1054
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to get KYC providers");
1056
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get KYC providers");
1055
1057
  return data.content;
1056
1058
  }
1057
1059
  async function startKyc(api, body) {
1058
1060
  const { data, error } = await api.POST("/kyc/start", { body });
1059
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to start KYC");
1061
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to start KYC");
1060
1062
  return data.content;
1061
1063
  }
1062
1064
  async function resolveKyc(api, providerId, level = "basic") {
@@ -1078,22 +1080,22 @@ async function pollKycStatus(api, providerId, { intervalMs = 3e3, timeoutMs = 3e
1078
1080
  // src/api/endpoints/ramps.ts
1079
1081
  async function getRampsQuote(api, query) {
1080
1082
  const { data, error } = await api.GET("/ramps/quote", { params: { query } });
1081
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to get ramp quotes");
1083
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get ramp quotes");
1082
1084
  return data.content;
1083
1085
  }
1084
1086
  async function createOnRamp(api, body) {
1085
1087
  const { data, error } = await api.POST("/ramps/onramp", { body });
1086
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to create onramp");
1088
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to create onramp");
1087
1089
  return data.content;
1088
1090
  }
1089
1091
  async function createOffRamp(api, body) {
1090
1092
  const { data, error } = await api.POST("/ramps/offramp", { body });
1091
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to create offramp");
1093
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to create offramp");
1092
1094
  return data.content;
1093
1095
  }
1094
1096
  async function getRampTransaction(api, txId) {
1095
1097
  const { data, error } = await api.GET("/ramps/transaction/{txId}", { params: { path: { txId } } });
1096
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to get transaction");
1098
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get transaction");
1097
1099
  return data.content;
1098
1100
  }
1099
1101
  async function pollRampTransaction(api, txId, { intervalMs = 5e3, timeoutMs = 6e5 } = {}) {
@@ -1168,34 +1170,6 @@ function generateJti() {
1168
1170
  );
1169
1171
  }
1170
1172
 
1171
- // src/stellar/StellarClient.ts
1172
- var HORIZON_URLS = {
1173
- mainnet: "https://horizon.stellar.org",
1174
- testnet: "https://horizon-testnet.stellar.org"
1175
- };
1176
- var StellarClient = class {
1177
- constructor(config) {
1178
- this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
1179
- }
1180
- async submitTransaction(signedXdr) {
1181
- try {
1182
- const response = await fetch(`${this.horizonUrl}/transactions`, {
1183
- method: "POST",
1184
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1185
- body: new URLSearchParams({ tx: signedXdr })
1186
- });
1187
- if (!response.ok) {
1188
- const body = await response.json().catch(() => ({}));
1189
- return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
1190
- }
1191
- const data = await response.json();
1192
- return { success: true, hash: data.hash };
1193
- } catch {
1194
- return { success: false, errorCode: "NETWORK_ERROR" };
1195
- }
1196
- }
1197
- };
1198
-
1199
1173
  // src/storage/web.ts
1200
1174
  var LOG_PREFIX = "[PollarClient:storage]";
1201
1175
  function createMemoryAdapter() {
@@ -1283,6 +1257,57 @@ function defaultStorage(options = {}) {
1283
1257
  return createLocalStorageAdapter(options);
1284
1258
  }
1285
1259
 
1260
+ // src/visibility/noop.ts
1261
+ function createNoopVisibilityProvider() {
1262
+ return {
1263
+ isVisible: () => true,
1264
+ onChange: () => () => {
1265
+ }
1266
+ };
1267
+ }
1268
+
1269
+ // src/visibility/web.ts
1270
+ function createWebVisibilityProvider() {
1271
+ const isVisibleNow = () => typeof document === "undefined" || document.visibilityState === "visible";
1272
+ return {
1273
+ isVisible: isVisibleNow,
1274
+ onChange: (cb) => {
1275
+ if (typeof window === "undefined" || typeof document === "undefined") {
1276
+ return () => {
1277
+ };
1278
+ }
1279
+ let last = isVisibleNow();
1280
+ const handler = () => {
1281
+ const next = isVisibleNow();
1282
+ if (next !== last) {
1283
+ last = next;
1284
+ cb(next);
1285
+ }
1286
+ };
1287
+ document.addEventListener("visibilitychange", handler);
1288
+ window.addEventListener("pageshow", handler);
1289
+ window.addEventListener("pagehide", handler);
1290
+ window.addEventListener("focus", handler);
1291
+ window.addEventListener("blur", handler);
1292
+ return () => {
1293
+ document.removeEventListener("visibilitychange", handler);
1294
+ window.removeEventListener("pageshow", handler);
1295
+ window.removeEventListener("pagehide", handler);
1296
+ window.removeEventListener("focus", handler);
1297
+ window.removeEventListener("blur", handler);
1298
+ };
1299
+ }
1300
+ };
1301
+ }
1302
+
1303
+ // src/visibility/autodetect.ts
1304
+ function defaultVisibilityProvider() {
1305
+ if (typeof document !== "undefined" && typeof window !== "undefined") {
1306
+ return createWebVisibilityProvider();
1307
+ }
1308
+ return createNoopVisibilityProvider();
1309
+ }
1310
+
1286
1311
  // src/types.ts
1287
1312
  var AUTH_ERROR_CODES = {
1288
1313
  SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
@@ -1293,6 +1318,7 @@ var AUTH_ERROR_CODES = {
1293
1318
  AUTH_FAILED: "AUTH_FAILED",
1294
1319
  WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
1295
1320
  WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
1321
+ WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
1296
1322
  UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
1297
1323
  };
1298
1324
  var PollarFlowError = class extends Error {
@@ -1683,7 +1709,7 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
1683
1709
  setAuthState({ step: "authenticating" });
1684
1710
  await streamUntilFound(api, clientSessionId, (data2) => data2?.status === "READY", 200, signal);
1685
1711
  const dpopJwk = await deps.getPublicJwk();
1686
- const { data, error } = await api.POST("/auth/login", {
1712
+ const { data } = await api.POST("/auth/login", {
1687
1713
  body: {
1688
1714
  clientSessionId,
1689
1715
  dpopJwk,
@@ -1848,7 +1874,7 @@ async function loginWallet(type, deps) {
1848
1874
  let connectedWallet;
1849
1875
  try {
1850
1876
  setAuthState({ step: "connecting_wallet", walletType: type });
1851
- const adapter = await deps.resolveWalletAdapter(type);
1877
+ const adapter = await withSignal(deps.resolveWalletAdapter(type), signal);
1852
1878
  const available = await withSignal(adapter.isAvailable(), signal);
1853
1879
  if (!available) {
1854
1880
  setAuthState({ step: "wallet_not_installed", walletType: type });
@@ -1885,6 +1911,7 @@ async function loginWallet(type, deps) {
1885
1911
 
1886
1912
  // src/client/client.ts
1887
1913
  var isBrowser = typeof window !== "undefined" && typeof localStorage !== "undefined";
1914
+ var REFRESH_SKEW_SECONDS = 60;
1888
1915
  function warnServerSide(method) {
1889
1916
  console.warn(
1890
1917
  `[PollarClient] ${method}() called server-side \u2014 browser APIs unavailable. Use PollarClient only in Client Components.`
@@ -1912,6 +1939,11 @@ var PollarClient = class {
1912
1939
  /** Singleton in-flight refresh — concurrent 401s coalesce into one /auth/refresh call. */
1913
1940
  this._refreshPromise = null;
1914
1941
  this._storageEventHandler = null;
1942
+ /** Updated by the request middleware. Read by the silent-refresh scheduler
1943
+ * to skip proactive refreshes after `maxIdleMs` of no HTTP activity. */
1944
+ this._lastRequestAt = Date.now();
1945
+ this._refreshTimer = null;
1946
+ this._visibilityUnsubscribe = null;
1915
1947
  this._transactionState = null;
1916
1948
  this._transactionStateListeners = /* @__PURE__ */ new Set();
1917
1949
  this._txHistoryState = { step: "idle" };
@@ -1922,15 +1954,32 @@ var PollarClient = class {
1922
1954
  this._authStateListeners = /* @__PURE__ */ new Set();
1923
1955
  this._networkState = { step: "idle" };
1924
1956
  this._networkStateListeners = /* @__PURE__ */ new Set();
1957
+ /**
1958
+ * Latched once the storage adapter degrades. We dedupe (the adapter only
1959
+ * fires once anyway) and use it to replay state to late-subscribers — same
1960
+ * pattern as `onAuthStateChange` replaying `_authState` on subscribe.
1961
+ * Only populated when the SDK constructed the default storage adapter; if
1962
+ * the consumer passes `config.storage`, they own degradation notifications.
1963
+ */
1964
+ this._storageDegraded = null;
1965
+ this._storageDegradeListeners = /* @__PURE__ */ new Set();
1925
1966
  this._walletAdapter = null;
1926
1967
  this._loginController = null;
1927
1968
  this.apiKey = config.apiKey;
1928
1969
  this.id = crypto.randomUUID();
1929
1970
  this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
1930
- this._storage = config.storage ?? defaultStorage(config.onStorageDegrade ? { onDegrade: config.onStorageDegrade } : void 0);
1971
+ this._storage = config.storage ?? defaultStorage({
1972
+ onDegrade: (reason, error) => {
1973
+ config.onStorageDegrade?.(reason, error);
1974
+ this._dispatchStorageDegrade(reason, error);
1975
+ }
1976
+ });
1931
1977
  this._keyManager = config.keyManager ?? defaultKeyManager(this._storage, config.apiKey);
1932
1978
  this._walletAdapterResolver = config.walletAdapter ?? null;
1979
+ this._walletResolverTimeoutMs = config.walletResolverTimeoutMs ?? 5e3;
1933
1980
  this._deviceLabel = config.deviceLabel;
1981
+ this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
1982
+ this._maxIdleMs = config.maxIdleMs;
1934
1983
  this._api = createApiClient(this.basePath);
1935
1984
  this._wireMiddlewares();
1936
1985
  this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
@@ -1976,6 +2025,9 @@ var PollarClient = class {
1976
2025
  console.warn("[PollarClient] KeyManager init failed; DPoP unavailable for this session", err);
1977
2026
  }
1978
2027
  await this._restoreSession();
2028
+ this._visibilityUnsubscribe = this._visibilityProvider.onChange((visible) => {
2029
+ if (visible) void this._maybeProactiveRefresh();
2030
+ });
1979
2031
  }
1980
2032
  /** Detach the cross-tab storage listener and abort any in-flight login. */
1981
2033
  destroy() {
@@ -1985,6 +2037,11 @@ var PollarClient = class {
1985
2037
  }
1986
2038
  this._loginController?.abort();
1987
2039
  this._loginController = null;
2040
+ this._clearRefreshTimer();
2041
+ if (this._visibilityUnsubscribe) {
2042
+ this._visibilityUnsubscribe();
2043
+ this._visibilityUnsubscribe = null;
2044
+ }
1988
2045
  }
1989
2046
  // ─── Middlewares (DPoP + auto-refresh) ────────────────────────────────────
1990
2047
  _wireMiddlewares() {
@@ -1992,6 +2049,7 @@ var PollarClient = class {
1992
2049
  this._api.use({
1993
2050
  onRequest: async ({ request }) => {
1994
2051
  request.headers.set("x-pollar-api-key", self.apiKey);
2052
+ self._lastRequestAt = Date.now();
1995
2053
  await self._initialized;
1996
2054
  if (request.body !== null) {
1997
2055
  try {
@@ -2022,15 +2080,22 @@ var PollarClient = class {
2022
2080
  const newNonce = response.headers.get("DPoP-Nonce");
2023
2081
  if (newNonce) self._dpopNonce = newNonce;
2024
2082
  if (response.status !== 401) return response;
2025
- if (request.url.includes("/auth/refresh")) return response;
2026
2083
  const wwwAuth = response.headers.get("WWW-Authenticate") ?? "";
2027
2084
  const isNonceChallenge = wwwAuth.includes("use_dpop_nonce");
2085
+ if (request.url.includes("/auth/refresh")) {
2086
+ if (isNonceChallenge) return self._retryRequest(request);
2087
+ return response;
2088
+ }
2028
2089
  if (!isNonceChallenge) {
2029
2090
  try {
2030
2091
  await self.refresh();
2031
2092
  } catch {
2032
2093
  return response;
2033
2094
  }
2095
+ const method = request.method.toUpperCase();
2096
+ if (method !== "GET" && method !== "HEAD") {
2097
+ return response;
2098
+ }
2034
2099
  }
2035
2100
  return self._retryRequest(request);
2036
2101
  }
@@ -2055,14 +2120,22 @@ var PollarClient = class {
2055
2120
  }
2056
2121
  async _retryRequest(originalRequest) {
2057
2122
  const headers = new Headers(originalRequest.headers);
2058
- const accessToken = this._session?.token?.accessToken;
2059
- if (accessToken) {
2060
- const proof = await this._buildProofForRequest(originalRequest, accessToken);
2061
- if (proof) {
2062
- headers.set("Authorization", `DPoP ${accessToken}`);
2063
- headers.set("DPoP", proof);
2064
- } else {
2065
- headers.set("Authorization", `Bearer ${accessToken}`);
2123
+ const isRefresh = originalRequest.url.includes("/auth/refresh");
2124
+ if (isRefresh) {
2125
+ const proof = await this._buildProofForRequest(originalRequest, void 0);
2126
+ headers.delete("Authorization");
2127
+ if (proof) headers.set("DPoP", proof);
2128
+ else headers.delete("DPoP");
2129
+ } else {
2130
+ const accessToken = this._session?.token?.accessToken;
2131
+ if (accessToken) {
2132
+ const proof = await this._buildProofForRequest(originalRequest, accessToken);
2133
+ if (proof) {
2134
+ headers.set("Authorization", `DPoP ${accessToken}`);
2135
+ headers.set("DPoP", proof);
2136
+ } else {
2137
+ headers.set("Authorization", `Bearer ${accessToken}`);
2138
+ }
2066
2139
  }
2067
2140
  }
2068
2141
  const cachedBody = this._requestBodyCache.get(originalRequest);
@@ -2133,6 +2206,65 @@ var PollarClient = class {
2133
2206
  } catch (err) {
2134
2207
  console.error("[PollarClient] Failed to persist refreshed session", err);
2135
2208
  }
2209
+ this._scheduleNextRefresh();
2210
+ }
2211
+ }
2212
+ // ─── Silent refresh scheduler ────────────────────────────────────────────────
2213
+ /**
2214
+ * Arm a single setTimeout to fire shortly before the current access token
2215
+ * expires. Idempotent — clearing any previous timer first. Safe to call
2216
+ * from any session-write site (initial login, restore-from-storage, after
2217
+ * a successful rotation). No-op if there's no session in memory.
2218
+ *
2219
+ * Browser/RN background-tab throttling makes long-running setTimeouts
2220
+ * unreliable on their own; the `visibilitychange` listener compensates by
2221
+ * re-invoking `_maybeProactiveRefresh` whenever the app comes back to the
2222
+ * foreground, catching any timer that fired late or never fired at all.
2223
+ */
2224
+ _scheduleNextRefresh() {
2225
+ this._clearRefreshTimer();
2226
+ const expiresAt = this._session?.token?.expiresAt;
2227
+ if (typeof expiresAt !== "number") return;
2228
+ const dueInMs = Math.max(0, (expiresAt - Math.floor(Date.now() / 1e3) - REFRESH_SKEW_SECONDS) * 1e3);
2229
+ this._refreshTimer = setTimeout(() => {
2230
+ void this._maybeProactiveRefresh();
2231
+ }, dueInMs);
2232
+ }
2233
+ /**
2234
+ * Decide whether to actually run a refresh right now. Called both from the
2235
+ * scheduler timer and from the visibility-change listener.
2236
+ *
2237
+ * Skip if:
2238
+ * - no session / no RT (nothing to refresh)
2239
+ * - app is hidden — wait for the visibility listener to re-trigger us
2240
+ * - `maxIdleMs` configured and no client request since that window — let
2241
+ * the next reactive 401-refresh handle it whenever the user comes back
2242
+ * - the AT still has more than `REFRESH_SKEW_SECONDS` of life — reschedule
2243
+ *
2244
+ * Otherwise call `refresh()`, which uses the existing in-flight singleton
2245
+ * so we never collide with a reactive 401-triggered refresh. On failure,
2246
+ * `_doRefresh` already calls `_clearSession`, so auth-state listeners see
2247
+ * `step:'idle'` — no extra event dispatch needed here.
2248
+ */
2249
+ async _maybeProactiveRefresh() {
2250
+ if (!this._session?.token?.refreshToken) return;
2251
+ if (!this._visibilityProvider.isVisible()) return;
2252
+ if (this._maxIdleMs !== void 0 && Date.now() - this._lastRequestAt > this._maxIdleMs) return;
2253
+ const expiresAt = this._session.token.expiresAt;
2254
+ if (Math.floor(Date.now() / 1e3) < expiresAt - REFRESH_SKEW_SECONDS) {
2255
+ this._scheduleNextRefresh();
2256
+ return;
2257
+ }
2258
+ try {
2259
+ await this.refresh();
2260
+ } catch (err) {
2261
+ console.warn("[PollarClient] Proactive refresh failed; session cleared", err);
2262
+ }
2263
+ }
2264
+ _clearRefreshTimer() {
2265
+ if (this._refreshTimer !== null) {
2266
+ clearTimeout(this._refreshTimer);
2267
+ this._refreshTimer = null;
2136
2268
  }
2137
2269
  }
2138
2270
  // ─── Auth state ──────────────────────────────────────────────────────────────
@@ -2144,6 +2276,38 @@ var PollarClient = class {
2144
2276
  cb(this._authState);
2145
2277
  return () => this._authStateListeners.delete(cb);
2146
2278
  }
2279
+ /**
2280
+ * Subscribe to persistent-storage degradation (Safari private mode,
2281
+ * sandboxed iframes, quota errors, etc.). The SDK keeps running off
2282
+ * in-memory storage after degrade, but sessions won't survive reload — a
2283
+ * host UI typically wants to show "your session won't be saved" so the
2284
+ * user isn't blindsided after a refresh.
2285
+ *
2286
+ * Fires at most once per client lifetime (the underlying adapter dedupes).
2287
+ * Late subscribers receive the latched state synchronously on subscribe.
2288
+ *
2289
+ * Only fires when the SDK constructs the default storage adapter. If you
2290
+ * pass a custom `config.storage`, wire your own notification path through
2291
+ * that adapter's API — the SDK has no hook into it.
2292
+ */
2293
+ onStorageDegrade(cb) {
2294
+ this._storageDegradeListeners.add(cb);
2295
+ if (this._storageDegraded) {
2296
+ cb(this._storageDegraded.reason, this._storageDegraded.error);
2297
+ }
2298
+ return () => this._storageDegradeListeners.delete(cb);
2299
+ }
2300
+ _dispatchStorageDegrade(reason, error) {
2301
+ if (this._storageDegraded) return;
2302
+ this._storageDegraded = { reason, error };
2303
+ for (const cb of this._storageDegradeListeners) {
2304
+ try {
2305
+ cb(reason, error);
2306
+ } catch (err) {
2307
+ console.error("[PollarClient] onStorageDegrade listener threw", err);
2308
+ }
2309
+ }
2310
+ }
2147
2311
  /** PII (email, names, avatar, providers). Held in memory only — never persisted. */
2148
2312
  getUserProfile() {
2149
2313
  return this._profile;
@@ -2380,10 +2544,16 @@ var PollarClient = class {
2380
2544
  }
2381
2545
  }
2382
2546
  // ─── Transactions ─────────────────────────────────────────────────────────
2547
+ /**
2548
+ * Builds an unsigned XDR. Drives `_setTransactionState` for modal-style UIs
2549
+ * AND returns a {@link BuildOutcome} so headless callers can `await` and
2550
+ * inspect the result without subscribing to state changes.
2551
+ */
2383
2552
  async buildTx(operation, params, options) {
2384
2553
  if (!this._session?.wallet?.publicKey) {
2385
- this._setTransactionState({ step: "error", details: "No wallet connected" });
2386
- return;
2554
+ const details = "No wallet connected";
2555
+ this._setTransactionState({ step: "error", phase: "building", details });
2556
+ return { status: "error", details };
2387
2557
  }
2388
2558
  const body = {
2389
2559
  network: this.getNetwork(),
@@ -2397,40 +2567,194 @@ var PollarClient = class {
2397
2567
  const { data, error } = await this._api.POST("/tx/build", { body });
2398
2568
  if (!error && data?.success && data.content) {
2399
2569
  this._setTransactionState({ step: "built", buildData: data.content });
2400
- } else {
2401
- const details = error?.details;
2402
- this._setTransactionState({ step: "error", ...details && { details } });
2570
+ return { status: "built", buildData: data.content };
2403
2571
  }
2572
+ const details = error?.details;
2573
+ this._setTransactionState({ step: "error", phase: "building", ...details && { details } });
2574
+ return { status: "error", ...details && { details } };
2404
2575
  } catch (err) {
2405
2576
  console.error("[PollarClient] buildTx failed", err);
2406
- this._setTransactionState({ step: "error" });
2577
+ this._setTransactionState({ step: "error", phase: "building" });
2578
+ return { status: "error" };
2407
2579
  }
2408
2580
  }
2409
2581
  getWalletType() {
2410
2582
  return this._walletAdapter?.type ?? null;
2411
2583
  }
2412
- async signAndSubmitTx(unsignedXdr) {
2413
- const state = this._transactionState;
2414
- const buildData = state?.step === "built" ? state.buildData : state?.step === "error" ? state.buildData : void 0;
2415
- const stateExtra = buildData ? { buildData } : { external: true };
2416
- this._setTransactionState({ step: "signing", ...stateExtra });
2417
- const accountToSign = this._session?.wallet?.publicKey;
2584
+ /**
2585
+ * Signs the given unsigned XDR and returns the signed XDR.
2586
+ *
2587
+ * - External wallets: signs locally via the wallet adapter.
2588
+ * - Custodial wallets: posts to `/tx/sign`. The backend signs (through
2589
+ * wallet-service or the app's customer-managed adapter) and returns the
2590
+ * signed XDR plus an `idempotencyKey` the caller should echo back to
2591
+ * `submitTx`.
2592
+ *
2593
+ * Drives `_setTransactionState`: emits `signing` while in flight and
2594
+ * `signed` on success (or `error[phase: 'signing']` on failure). `buildData`
2595
+ * is threaded through if the consumer previously called `buildTx`.
2596
+ */
2597
+ async signTx(unsignedXdr) {
2598
+ const buildData = this._currentBuildData();
2599
+ this._setTransactionState({ step: "signing", ...buildData && { buildData } });
2418
2600
  if (this._walletAdapter) {
2601
+ const accountToSign = this._session?.wallet?.publicKey;
2602
+ const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
2419
2603
  try {
2420
- const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
2421
2604
  const { signedTxXdr } = await this._walletAdapter.signTransaction(unsignedXdr, signOpts);
2422
- const stellarClient = new StellarClient(this.getNetwork());
2423
- const result = await stellarClient.submitTransaction(signedTxXdr);
2424
- if (result.success) {
2425
- this._setTransactionState({ step: "success", ...stateExtra, hash: result.hash });
2426
- } else {
2427
- this._setTransactionState({ step: "error", ...stateExtra, details: result.errorCode });
2605
+ this._setTransactionState({
2606
+ step: "signed",
2607
+ signedXdr: signedTxXdr,
2608
+ ...buildData && { buildData }
2609
+ });
2610
+ return { status: "signed", signedXdr: signedTxXdr };
2611
+ } catch (err) {
2612
+ const details = err instanceof Error ? err.message : void 0;
2613
+ this._setTransactionState({
2614
+ step: "error",
2615
+ phase: "signing",
2616
+ ...buildData && { buildData },
2617
+ ...details && { details }
2618
+ });
2619
+ return { status: "error", ...details && { details } };
2620
+ }
2621
+ }
2622
+ const publicKey = this._session?.wallet?.publicKey ?? "";
2623
+ try {
2624
+ const { data, error } = await this._api.POST("/tx/sign", {
2625
+ body: { network: this.getNetwork(), publicKey, unsignedXdr }
2626
+ });
2627
+ if (!error && data?.success && data.content?.signedXdr) {
2628
+ const { signedXdr, idempotencyKey } = data.content;
2629
+ this._setTransactionState({
2630
+ step: "signed",
2631
+ signedXdr,
2632
+ submissionToken: idempotencyKey,
2633
+ ...buildData && { buildData }
2634
+ });
2635
+ return { status: "signed", signedXdr, submissionToken: idempotencyKey };
2636
+ }
2637
+ const details = error?.details;
2638
+ this._setTransactionState({
2639
+ step: "error",
2640
+ phase: "signing",
2641
+ ...buildData && { buildData },
2642
+ ...details && { details }
2643
+ });
2644
+ return { status: "error", ...details && { details } };
2645
+ } catch (err) {
2646
+ const details = err instanceof Error ? err.message : void 0;
2647
+ this._setTransactionState({
2648
+ step: "error",
2649
+ phase: "signing",
2650
+ ...buildData && { buildData },
2651
+ ...details && { details }
2652
+ });
2653
+ return { status: "error", ...details && { details } };
2654
+ }
2655
+ }
2656
+ /**
2657
+ * Submits a signed XDR via `/tx/submit` regardless of wallet type
2658
+ * (custodial or external). Routing through sdk-api gives us:
2659
+ * - End-to-end tx_records persistence with full phase lifecycle so the
2660
+ * developer dashboard can show every tx (both custodial and external
2661
+ * wallet flows) at `/apps/:id/monitor/transactions`.
2662
+ * - Idempotency tracking via `submissionToken` (returned by `signTx`).
2663
+ * - A single response shape (SUCCESS / PENDING / FAILED) shared by both
2664
+ * flows — previously external wallets could only return SUCCESS or
2665
+ * error since the direct-to-Horizon path was synchronous.
2666
+ *
2667
+ * The extra hop adds ~50–150 ms vs. the legacy direct-Horizon path; the
2668
+ * persistence + observability win is worth it.
2669
+ *
2670
+ * Drives `_setTransactionState`: emits `submitting` while in flight,
2671
+ * `submitted` on Horizon ack (pending), `success` on ledger confirmation,
2672
+ * or `error[phase: 'submitting']` on failure.
2673
+ */
2674
+ async submitTx(signedXdr, opts) {
2675
+ const buildData = this._currentBuildData();
2676
+ const outcomeExtra = buildData ? { buildData } : {};
2677
+ this._setTransactionState({ step: "submitting", signedXdr, ...buildData && { buildData } });
2678
+ const publicKey = this._session?.wallet?.publicKey ?? "";
2679
+ try {
2680
+ const { data, error } = await this._api.POST("/tx/submit", {
2681
+ body: {
2682
+ network: this.getNetwork(),
2683
+ publicKey,
2684
+ signedXdr,
2685
+ ...opts?.submissionToken && { idempotencyKey: opts.submissionToken }
2428
2686
  }
2429
- } catch {
2430
- this._setTransactionState({ step: "error", ...stateExtra });
2687
+ });
2688
+ if (!error && data?.success && data.content) {
2689
+ const { hash, status: backendStatus, resultCode } = data.content;
2690
+ if (backendStatus === "SUCCESS") {
2691
+ this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
2692
+ return { status: "success", hash, ...outcomeExtra };
2693
+ }
2694
+ if (backendStatus === "PENDING") {
2695
+ this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
2696
+ return { status: "pending", hash, ...outcomeExtra };
2697
+ }
2698
+ this._setTransactionState({
2699
+ step: "error",
2700
+ phase: "submitting",
2701
+ ...buildData && { buildData },
2702
+ ...resultCode && { details: resultCode }
2703
+ });
2704
+ return {
2705
+ status: "error",
2706
+ hash,
2707
+ ...outcomeExtra,
2708
+ ...resultCode && { details: resultCode, resultCode }
2709
+ };
2431
2710
  }
2432
- return;
2711
+ const details = error?.details;
2712
+ this._setTransactionState({
2713
+ step: "error",
2714
+ phase: "submitting",
2715
+ ...buildData && { buildData },
2716
+ ...details && { details }
2717
+ });
2718
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2719
+ } catch (err) {
2720
+ const details = err instanceof Error ? err.message : void 0;
2721
+ this._setTransactionState({
2722
+ step: "error",
2723
+ phase: "submitting",
2724
+ ...buildData && { buildData },
2725
+ ...details && { details }
2726
+ });
2727
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2728
+ }
2729
+ }
2730
+ /**
2731
+ * Signs and submits in one logical step. Returns a {@link SubmitOutcome}.
2732
+ *
2733
+ * - **External wallets**: composes `signTx` + `submitTx` client-side. State
2734
+ * machine sees the full granular sequence `signing → signed → submitting
2735
+ * → success` because the underlying methods each emit.
2736
+ * - **Custodial wallets**: atomic `/tx/sign-and-send` round-trip. State
2737
+ * machine emits the compound `signing-submitting` step (the SDK can't
2738
+ * observe when one phase ends and the next begins inside that single
2739
+ * backend call) and then transitions to `submitted` (Horizon ack only) or
2740
+ * `success` (ledger-confirmed), or `error[phase: 'signing-submitting']`.
2741
+ */
2742
+ async signAndSubmitTx(unsignedXdr) {
2743
+ if (this._walletAdapter) {
2744
+ const signed = await this.signTx(unsignedXdr);
2745
+ if (signed.status === "error") {
2746
+ const buildData2 = this._currentBuildData();
2747
+ return {
2748
+ status: "error",
2749
+ ...buildData2 && { buildData: buildData2 },
2750
+ ...signed.details && { details: signed.details }
2751
+ };
2752
+ }
2753
+ return this.submitTx(signed.signedXdr);
2433
2754
  }
2755
+ const buildData = this._currentBuildData();
2756
+ const outcomeExtra = buildData ? { buildData } : {};
2757
+ this._setTransactionState({ step: "signing-submitting", ...buildData && { buildData } });
2434
2758
  const body = {
2435
2759
  network: this.getNetwork(),
2436
2760
  publicKey: this._session?.wallet?.publicKey ?? "",
@@ -2439,15 +2763,129 @@ var PollarClient = class {
2439
2763
  try {
2440
2764
  const { data, error } = await this._api.POST("/tx/sign-and-send", { body });
2441
2765
  if (!error && data?.success && data.content?.hash) {
2442
- this._setTransactionState({ step: "success", ...stateExtra, hash: data.content.hash });
2443
- } else {
2444
- const details = error?.details;
2445
- this._setTransactionState({ step: "error", ...stateExtra, ...details && { details } });
2766
+ const {
2767
+ hash,
2768
+ status: backendStatus,
2769
+ resultCode
2770
+ } = data.content;
2771
+ if (backendStatus === "SUCCESS") {
2772
+ this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
2773
+ return { status: "success", hash, ...outcomeExtra };
2774
+ }
2775
+ if (backendStatus === "PENDING") {
2776
+ this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
2777
+ return { status: "pending", hash, ...outcomeExtra };
2778
+ }
2779
+ this._setTransactionState({
2780
+ step: "error",
2781
+ phase: "signing-submitting",
2782
+ ...buildData && { buildData },
2783
+ ...resultCode && { details: resultCode }
2784
+ });
2785
+ return {
2786
+ status: "error",
2787
+ hash,
2788
+ ...outcomeExtra,
2789
+ ...resultCode && { details: resultCode, resultCode }
2790
+ };
2446
2791
  }
2447
- } catch {
2448
- this._setTransactionState({ step: "error", ...stateExtra });
2792
+ const details = error?.details;
2793
+ this._setTransactionState({
2794
+ step: "error",
2795
+ phase: "signing-submitting",
2796
+ ...buildData && { buildData },
2797
+ ...details && { details }
2798
+ });
2799
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2800
+ } catch (err) {
2801
+ const details = err instanceof Error ? err.message : void 0;
2802
+ this._setTransactionState({
2803
+ step: "error",
2804
+ phase: "signing-submitting",
2805
+ ...buildData && { buildData },
2806
+ ...details && { details }
2807
+ });
2808
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2809
+ }
2810
+ }
2811
+ /**
2812
+ * One-shot: build → sign → submit, returning the final {@link SubmitOutcome}.
2813
+ *
2814
+ * - **External wallets**: composes `buildTx` + `signAndSubmitTx` client-side.
2815
+ * State machine sees the full granular sequence (`building → built →
2816
+ * signing → signed → submitting → success`) because each composed call
2817
+ * emits its own transitions.
2818
+ * - **Custodial wallets**: single round-trip to `/tx/build-sign-submit`. The
2819
+ * signed XDR never leaves the backend. State machine emits the compound
2820
+ * `building-signing-submitting` step (the SDK can't observe individual
2821
+ * phase boundaries inside one atomic call) and then transitions to
2822
+ * `submitted` / `success` / `error[phase: 'building-signing-submitting']`.
2823
+ *
2824
+ * If you need granular UI feedback for custodial flows (separate
2825
+ * "Building…", "Signing…", "Submitting…" indicators), call `buildTx`,
2826
+ * `signTx`, and `submitTx` separately instead.
2827
+ */
2828
+ async buildAndSignAndSubmitTx(operation, params, options) {
2829
+ if (this._walletAdapter) {
2830
+ const built = await this.buildTx(operation, params, options);
2831
+ if (built.status === "error") {
2832
+ return { status: "error", ...built.details && { details: built.details } };
2833
+ }
2834
+ return this.signAndSubmitTx(built.buildData.unsignedXdr);
2835
+ }
2836
+ if (!this._session?.wallet?.publicKey) {
2837
+ this._setTransactionState({ step: "error", phase: "building-signing-submitting", details: "No wallet connected" });
2838
+ return { status: "error", details: "No wallet connected" };
2839
+ }
2840
+ this._setTransactionState({ step: "building-signing-submitting" });
2841
+ try {
2842
+ const { data, error } = await this._api.POST("/tx/build-sign-submit", {
2843
+ body: {
2844
+ network: this.getNetwork(),
2845
+ publicKey: this._session.wallet.publicKey,
2846
+ operation,
2847
+ params,
2848
+ options: options ?? {}
2849
+ }
2850
+ });
2851
+ if (!error && data?.success && data.content) {
2852
+ const { hash, status: backendStatus, resultCode } = data.content;
2853
+ if (backendStatus === "SUCCESS") {
2854
+ this._setTransactionState({ step: "success", hash });
2855
+ return { status: "success", hash };
2856
+ }
2857
+ if (backendStatus === "PENDING") {
2858
+ this._setTransactionState({ step: "submitted", hash });
2859
+ return { status: "pending", hash };
2860
+ }
2861
+ this._setTransactionState({
2862
+ step: "error",
2863
+ phase: "building-signing-submitting",
2864
+ ...resultCode && { details: resultCode }
2865
+ });
2866
+ return { status: "error", hash, ...resultCode && { details: resultCode, resultCode } };
2867
+ }
2868
+ const details = error?.details;
2869
+ this._setTransactionState({
2870
+ step: "error",
2871
+ phase: "building-signing-submitting",
2872
+ ...details && { details }
2873
+ });
2874
+ return { status: "error", ...details && { details } };
2875
+ } catch (err) {
2876
+ const details = err instanceof Error ? err.message : void 0;
2877
+ this._setTransactionState({
2878
+ step: "error",
2879
+ phase: "building-signing-submitting",
2880
+ ...details && { details }
2881
+ });
2882
+ return { status: "error", ...details && { details } };
2449
2883
  }
2450
2884
  }
2885
+ /** Alias for {@link buildAndSignAndSubmitTx} — shorter "just do the thing" name. */
2886
+ async runTx(operation, params, options) {
2887
+ return this.buildAndSignAndSubmitTx(operation, params, options);
2888
+ }
2451
2889
  // ─── App config ───────────────────────────────────────────────────────────
2452
2890
  async getAppConfig() {
2453
2891
  try {
@@ -2535,7 +2973,22 @@ var PollarClient = class {
2535
2973
  */
2536
2974
  async _resolveWalletAdapter(id) {
2537
2975
  if (this._walletAdapterResolver) {
2538
- return Promise.resolve(this._walletAdapterResolver(id));
2976
+ const timeoutMs = this._walletResolverTimeoutMs;
2977
+ let timeoutHandle;
2978
+ const timeoutPromise = new Promise((_, reject) => {
2979
+ timeoutHandle = setTimeout(() => {
2980
+ reject(
2981
+ Object.assign(new Error(`[PollarClient] Wallet adapter resolver for "${id}" timed out after ${timeoutMs}ms`), {
2982
+ code: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
2983
+ })
2984
+ );
2985
+ }, timeoutMs);
2986
+ });
2987
+ try {
2988
+ return await Promise.race([Promise.resolve(this._walletAdapterResolver(id)), timeoutPromise]);
2989
+ } finally {
2990
+ if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
2991
+ }
2539
2992
  }
2540
2993
  if (id === "freighter" /* FREIGHTER */) return new FreighterAdapter();
2541
2994
  if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter();
@@ -2549,6 +3002,16 @@ var PollarClient = class {
2549
3002
  this._setAuthState({ step: "idle" });
2550
3003
  return;
2551
3004
  }
3005
+ if (error instanceof Error && error.code === AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT) {
3006
+ console.error("[PollarClient]", error.message);
3007
+ this._setAuthState({
3008
+ step: "error",
3009
+ previousStep: this._authState.step,
3010
+ message: error.message,
3011
+ errorCode: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
3012
+ });
3013
+ return;
3014
+ }
2552
3015
  console.error("[PollarClient] Unexpected error in auth flow", error);
2553
3016
  this._setAuthState({
2554
3017
  step: "error",
@@ -2570,6 +3033,7 @@ var PollarClient = class {
2570
3033
  }
2571
3034
  console.info("[PollarClient] Session restored from storage");
2572
3035
  this._setAuthState({ step: "authenticated", session: this._session });
3036
+ this._scheduleNextRefresh();
2573
3037
  } else {
2574
3038
  console.info("[PollarClient] No session in storage");
2575
3039
  }
@@ -2596,9 +3060,11 @@ var PollarClient = class {
2596
3060
  }
2597
3061
  await writeStorage(this._storage, this.apiKeyHash, persisted);
2598
3062
  this._setAuthState({ step: "authenticated", session: persisted });
3063
+ this._scheduleNextRefresh();
2599
3064
  }
2600
3065
  async _clearSession() {
2601
3066
  console.info("[PollarClient] Session cleared");
3067
+ this._clearRefreshTimer();
2602
3068
  this._session = null;
2603
3069
  this._profile = null;
2604
3070
  this._walletAdapter = null;
@@ -2631,6 +3097,46 @@ var PollarClient = class {
2631
3097
  console.info(`[PollarClient] transaction:${next.step}`);
2632
3098
  for (const cb of this._transactionStateListeners) cb(next);
2633
3099
  }
3100
+ /**
3101
+ * Threads `buildData` through state transitions. When the user has already
3102
+ * called `buildTx`, every subsequent state (signing, signed, submitting,
3103
+ * submitted, success, error) should carry the build summary so modal UIs
3104
+ * can keep showing "Send 5 USDC to G..." through the whole flow.
3105
+ */
3106
+ _currentBuildData() {
3107
+ const s = this._transactionState;
3108
+ if (!s) return void 0;
3109
+ if ("buildData" in s && s.buildData) return s.buildData;
3110
+ return void 0;
3111
+ }
3112
+ };
3113
+
3114
+ // src/stellar/StellarClient.ts
3115
+ var HORIZON_URLS = {
3116
+ mainnet: "https://horizon.stellar.org",
3117
+ testnet: "https://horizon-testnet.stellar.org"
3118
+ };
3119
+ var StellarClient = class {
3120
+ constructor(config) {
3121
+ this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
3122
+ }
3123
+ async submitTransaction(signedXdr) {
3124
+ try {
3125
+ const response = await fetch(`${this.horizonUrl}/transactions`, {
3126
+ method: "POST",
3127
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
3128
+ body: new URLSearchParams({ tx: signedXdr })
3129
+ });
3130
+ if (!response.ok) {
3131
+ const body = await response.json().catch(() => ({}));
3132
+ return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
3133
+ }
3134
+ const data = await response.json();
3135
+ return { success: true, hash: data.hash };
3136
+ } catch {
3137
+ return { success: false, errorCode: "NETWORK_ERROR" };
3138
+ }
3139
+ }
2634
3140
  };
2635
3141
 
2636
3142
  // src/index.rn.ts