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