@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/README.md +36 -21
- package/dist/adapters/expo-secure-store.js.map +1 -1
- package/dist/adapters/expo-secure-store.mjs.map +1 -1
- package/dist/adapters/react-native-keychain.js.map +1 -1
- package/dist/adapters/react-native-keychain.mjs.map +1 -1
- package/dist/index.d.mts +1174 -106
- package/dist/index.d.ts +1174 -106
- package/dist/index.js +590 -84
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +590 -84
- package/dist/index.mjs.map +1 -1
- package/dist/index.rn.d.mts +1 -1
- package/dist/index.rn.d.ts +1 -1
- package/dist/index.rn.js +590 -84
- package/dist/index.rn.js.map +1 -1
- package/dist/index.rn.mjs +590 -84
- package/dist/index.rn.mjs.map +1 -1
- package/package.json +6 -3
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
|
|
33
|
+
"../../node_modules/@stellar/freighter-api/build/index.min.js"(exports, module) {
|
|
34
34
|
!(function(e, r) {
|
|
35
|
-
"object" == typeof exports
|
|
36
|
-
})(exports
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
2059
|
-
if (
|
|
2060
|
-
const proof = await this._buildProofForRequest(originalRequest,
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
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
|
-
|
|
2386
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
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
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
}
|
|
2427
|
-
|
|
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
|
-
}
|
|
2430
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
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
|
-
|
|
2448
|
-
this._setTransactionState({
|
|
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
|
-
|
|
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
|