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