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