@pollar/core 0.7.0 → 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 +1516 -142
- package/dist/index.d.ts +1516 -142
- package/dist/index.js +645 -91
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +644 -92
- 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 +645 -91
- package/dist/index.rn.js.map +1 -1
- package/dist/index.rn.mjs +644 -92
- 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
|
}
|
|
@@ -442,7 +440,7 @@ function createClient(clientOptions) {
|
|
|
442
440
|
const {
|
|
443
441
|
baseUrl: localBaseUrl,
|
|
444
442
|
fetch: fetch2 = baseFetch,
|
|
445
|
-
Request = CustomRequest,
|
|
443
|
+
Request: Request2 = CustomRequest,
|
|
446
444
|
headers,
|
|
447
445
|
params = {},
|
|
448
446
|
parseAs = "json",
|
|
@@ -494,7 +492,7 @@ function createClient(clientOptions) {
|
|
|
494
492
|
};
|
|
495
493
|
let id;
|
|
496
494
|
let options;
|
|
497
|
-
let request = new
|
|
495
|
+
let request = new Request2(
|
|
498
496
|
createFinalURL(schemaPath, { baseUrl: finalBaseUrl, params, querySerializer, pathSerializer }),
|
|
499
497
|
requestInit
|
|
500
498
|
);
|
|
@@ -524,7 +522,7 @@ function createClient(clientOptions) {
|
|
|
524
522
|
id
|
|
525
523
|
});
|
|
526
524
|
if (result) {
|
|
527
|
-
if (result instanceof
|
|
525
|
+
if (result instanceof Request2) {
|
|
528
526
|
request = result;
|
|
529
527
|
} else if (result instanceof Response) {
|
|
530
528
|
response = result;
|
|
@@ -896,24 +894,44 @@ function createApiClient(baseUrl) {
|
|
|
896
894
|
return createClient({ baseUrl });
|
|
897
895
|
}
|
|
898
896
|
|
|
897
|
+
// src/api/endpoints/distribution.ts
|
|
898
|
+
async function listDistributionRules(api) {
|
|
899
|
+
const { data, error } = await api.GET("/distribution/rules");
|
|
900
|
+
if (!data?.content || error) {
|
|
901
|
+
throw new Error(
|
|
902
|
+
error?.code ?? error?.error ?? "Failed to list distribution rules"
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
return data.content.rules;
|
|
906
|
+
}
|
|
907
|
+
async function claimDistributionRule(api, body) {
|
|
908
|
+
const { data, error } = await api.POST("/distribution/claim", { body });
|
|
909
|
+
if (!data?.content || error) {
|
|
910
|
+
throw new Error(
|
|
911
|
+
error?.code ?? error?.error ?? "Failed to claim distribution rule"
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
return data.content;
|
|
915
|
+
}
|
|
916
|
+
|
|
899
917
|
// src/api/endpoints/kyc.ts
|
|
900
918
|
async function getKycStatus(api, providerId) {
|
|
901
919
|
const { data, error } = await api.GET("/kyc/status", {
|
|
902
920
|
params: { query: providerId ? { providerId } : {} }
|
|
903
921
|
});
|
|
904
922
|
if (!data?.content || error) {
|
|
905
|
-
throw new Error(error?.error ?? "Failed to get KYC status");
|
|
923
|
+
throw new Error(error?.code ?? error?.error ?? "Failed to get KYC status");
|
|
906
924
|
}
|
|
907
925
|
return data.content;
|
|
908
926
|
}
|
|
909
927
|
async function getKycProviders(api, country) {
|
|
910
928
|
const { data, error } = await api.GET("/kyc/providers", { params: { query: { country } } });
|
|
911
|
-
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");
|
|
912
930
|
return data.content;
|
|
913
931
|
}
|
|
914
932
|
async function startKyc(api, body) {
|
|
915
933
|
const { data, error } = await api.POST("/kyc/start", { body });
|
|
916
|
-
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");
|
|
917
935
|
return data.content;
|
|
918
936
|
}
|
|
919
937
|
async function resolveKyc(api, providerId, level = "basic") {
|
|
@@ -935,22 +953,22 @@ async function pollKycStatus(api, providerId, { intervalMs = 3e3, timeoutMs = 3e
|
|
|
935
953
|
// src/api/endpoints/ramps.ts
|
|
936
954
|
async function getRampsQuote(api, query) {
|
|
937
955
|
const { data, error } = await api.GET("/ramps/quote", { params: { query } });
|
|
938
|
-
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");
|
|
939
957
|
return data.content;
|
|
940
958
|
}
|
|
941
959
|
async function createOnRamp(api, body) {
|
|
942
960
|
const { data, error } = await api.POST("/ramps/onramp", { body });
|
|
943
|
-
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");
|
|
944
962
|
return data.content;
|
|
945
963
|
}
|
|
946
964
|
async function createOffRamp(api, body) {
|
|
947
965
|
const { data, error } = await api.POST("/ramps/offramp", { body });
|
|
948
|
-
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");
|
|
949
967
|
return data.content;
|
|
950
968
|
}
|
|
951
969
|
async function getRampTransaction(api, txId) {
|
|
952
970
|
const { data, error } = await api.GET("/ramps/transaction/{txId}", { params: { path: { txId } } });
|
|
953
|
-
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");
|
|
954
972
|
return data.content;
|
|
955
973
|
}
|
|
956
974
|
async function pollRampTransaction(api, txId, { intervalMs = 5e3, timeoutMs = 6e5 } = {}) {
|
|
@@ -1025,34 +1043,6 @@ function generateJti() {
|
|
|
1025
1043
|
);
|
|
1026
1044
|
}
|
|
1027
1045
|
|
|
1028
|
-
// src/stellar/StellarClient.ts
|
|
1029
|
-
var HORIZON_URLS = {
|
|
1030
|
-
mainnet: "https://horizon.stellar.org",
|
|
1031
|
-
testnet: "https://horizon-testnet.stellar.org"
|
|
1032
|
-
};
|
|
1033
|
-
var StellarClient = class {
|
|
1034
|
-
constructor(config) {
|
|
1035
|
-
this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
|
|
1036
|
-
}
|
|
1037
|
-
async submitTransaction(signedXdr) {
|
|
1038
|
-
try {
|
|
1039
|
-
const response = await fetch(`${this.horizonUrl}/transactions`, {
|
|
1040
|
-
method: "POST",
|
|
1041
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1042
|
-
body: new URLSearchParams({ tx: signedXdr })
|
|
1043
|
-
});
|
|
1044
|
-
if (!response.ok) {
|
|
1045
|
-
const body = await response.json().catch(() => ({}));
|
|
1046
|
-
return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
|
|
1047
|
-
}
|
|
1048
|
-
const data = await response.json();
|
|
1049
|
-
return { success: true, hash: data.hash };
|
|
1050
|
-
} catch {
|
|
1051
|
-
return { success: false, errorCode: "NETWORK_ERROR" };
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
};
|
|
1055
|
-
|
|
1056
1046
|
// src/storage/web.ts
|
|
1057
1047
|
var LOG_PREFIX = "[PollarClient:storage]";
|
|
1058
1048
|
function createMemoryAdapter() {
|
|
@@ -1140,6 +1130,57 @@ function defaultStorage(options = {}) {
|
|
|
1140
1130
|
return createLocalStorageAdapter(options);
|
|
1141
1131
|
}
|
|
1142
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
|
+
|
|
1143
1184
|
// src/types.ts
|
|
1144
1185
|
var AUTH_ERROR_CODES = {
|
|
1145
1186
|
SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
|
|
@@ -1150,6 +1191,7 @@ var AUTH_ERROR_CODES = {
|
|
|
1150
1191
|
AUTH_FAILED: "AUTH_FAILED",
|
|
1151
1192
|
WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
|
|
1152
1193
|
WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
|
|
1194
|
+
WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
|
|
1153
1195
|
UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
|
|
1154
1196
|
};
|
|
1155
1197
|
var PollarFlowError = class extends Error {
|
|
@@ -1540,7 +1582,7 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
|
|
|
1540
1582
|
setAuthState({ step: "authenticating" });
|
|
1541
1583
|
await streamUntilFound(api, clientSessionId, (data2) => data2?.status === "READY", 200, signal);
|
|
1542
1584
|
const dpopJwk = await deps.getPublicJwk();
|
|
1543
|
-
const { data
|
|
1585
|
+
const { data } = await api.POST("/auth/login", {
|
|
1544
1586
|
body: {
|
|
1545
1587
|
clientSessionId,
|
|
1546
1588
|
dpopJwk,
|
|
@@ -1705,7 +1747,7 @@ async function loginWallet(type, deps) {
|
|
|
1705
1747
|
let connectedWallet;
|
|
1706
1748
|
try {
|
|
1707
1749
|
setAuthState({ step: "connecting_wallet", walletType: type });
|
|
1708
|
-
const adapter = await deps.resolveWalletAdapter(type);
|
|
1750
|
+
const adapter = await withSignal(deps.resolveWalletAdapter(type), signal);
|
|
1709
1751
|
const available = await withSignal(adapter.isAvailable(), signal);
|
|
1710
1752
|
if (!available) {
|
|
1711
1753
|
setAuthState({ step: "wallet_not_installed", walletType: type });
|
|
@@ -1742,7 +1784,7 @@ async function loginWallet(type, deps) {
|
|
|
1742
1784
|
|
|
1743
1785
|
// src/client/client.ts
|
|
1744
1786
|
var isBrowser = typeof window !== "undefined" && typeof localStorage !== "undefined";
|
|
1745
|
-
var
|
|
1787
|
+
var REFRESH_SKEW_SECONDS = 60;
|
|
1746
1788
|
function warnServerSide(method) {
|
|
1747
1789
|
console.warn(
|
|
1748
1790
|
`[PollarClient] ${method}() called server-side \u2014 browser APIs unavailable. Use PollarClient only in Client Components.`
|
|
@@ -1760,9 +1802,21 @@ var PollarClient = class {
|
|
|
1760
1802
|
this._profile = null;
|
|
1761
1803
|
/** Last `DPoP-Nonce` we saw from a server response. Carried into the next proof. */
|
|
1762
1804
|
this._dpopNonce = null;
|
|
1805
|
+
/**
|
|
1806
|
+
* Snapshot of each in-flight request's body, taken in `onRequest` before
|
|
1807
|
+
* `fetch()` consumes the stream. Needed because `Request.clone()` throws
|
|
1808
|
+
* once the body is disturbed, so the auto-retry path (DPoP nonce challenge
|
|
1809
|
+
* / 401 refresh) must rebuild the request from scratch instead of cloning.
|
|
1810
|
+
*/
|
|
1811
|
+
this._requestBodyCache = /* @__PURE__ */ new WeakMap();
|
|
1763
1812
|
/** Singleton in-flight refresh — concurrent 401s coalesce into one /auth/refresh call. */
|
|
1764
1813
|
this._refreshPromise = null;
|
|
1765
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;
|
|
1766
1820
|
this._transactionState = null;
|
|
1767
1821
|
this._transactionStateListeners = /* @__PURE__ */ new Set();
|
|
1768
1822
|
this._txHistoryState = { step: "idle" };
|
|
@@ -1773,15 +1827,32 @@ var PollarClient = class {
|
|
|
1773
1827
|
this._authStateListeners = /* @__PURE__ */ new Set();
|
|
1774
1828
|
this._networkState = { step: "idle" };
|
|
1775
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();
|
|
1776
1839
|
this._walletAdapter = null;
|
|
1777
1840
|
this._loginController = null;
|
|
1778
1841
|
this.apiKey = config.apiKey;
|
|
1779
1842
|
this.id = crypto.randomUUID();
|
|
1780
1843
|
this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
|
|
1781
|
-
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
|
+
});
|
|
1782
1850
|
this._keyManager = config.keyManager ?? defaultKeyManager(this._storage, config.apiKey);
|
|
1783
1851
|
this._walletAdapterResolver = config.walletAdapter ?? null;
|
|
1852
|
+
this._walletResolverTimeoutMs = config.walletResolverTimeoutMs ?? 5e3;
|
|
1784
1853
|
this._deviceLabel = config.deviceLabel;
|
|
1854
|
+
this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
|
|
1855
|
+
this._maxIdleMs = config.maxIdleMs;
|
|
1785
1856
|
this._api = createApiClient(this.basePath);
|
|
1786
1857
|
this._wireMiddlewares();
|
|
1787
1858
|
this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
|
|
@@ -1827,6 +1898,9 @@ var PollarClient = class {
|
|
|
1827
1898
|
console.warn("[PollarClient] KeyManager init failed; DPoP unavailable for this session", err);
|
|
1828
1899
|
}
|
|
1829
1900
|
await this._restoreSession();
|
|
1901
|
+
this._visibilityUnsubscribe = this._visibilityProvider.onChange((visible) => {
|
|
1902
|
+
if (visible) void this._maybeProactiveRefresh();
|
|
1903
|
+
});
|
|
1830
1904
|
}
|
|
1831
1905
|
/** Detach the cross-tab storage listener and abort any in-flight login. */
|
|
1832
1906
|
destroy() {
|
|
@@ -1836,6 +1910,11 @@ var PollarClient = class {
|
|
|
1836
1910
|
}
|
|
1837
1911
|
this._loginController?.abort();
|
|
1838
1912
|
this._loginController = null;
|
|
1913
|
+
this._clearRefreshTimer();
|
|
1914
|
+
if (this._visibilityUnsubscribe) {
|
|
1915
|
+
this._visibilityUnsubscribe();
|
|
1916
|
+
this._visibilityUnsubscribe = null;
|
|
1917
|
+
}
|
|
1839
1918
|
}
|
|
1840
1919
|
// ─── Middlewares (DPoP + auto-refresh) ────────────────────────────────────
|
|
1841
1920
|
_wireMiddlewares() {
|
|
@@ -1843,7 +1922,15 @@ var PollarClient = class {
|
|
|
1843
1922
|
this._api.use({
|
|
1844
1923
|
onRequest: async ({ request }) => {
|
|
1845
1924
|
request.headers.set("x-pollar-api-key", self.apiKey);
|
|
1925
|
+
self._lastRequestAt = Date.now();
|
|
1846
1926
|
await self._initialized;
|
|
1927
|
+
if (request.body !== null) {
|
|
1928
|
+
try {
|
|
1929
|
+
self._requestBodyCache.set(request, await request.clone().arrayBuffer());
|
|
1930
|
+
} catch (err) {
|
|
1931
|
+
console.warn("[PollarClient] Could not snapshot request body for retry", err);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1847
1934
|
const isRefresh = request.url.includes("/auth/refresh");
|
|
1848
1935
|
if (!isRefresh && self._refreshPromise) await self._refreshPromise;
|
|
1849
1936
|
if (isRefresh) {
|
|
@@ -1866,16 +1953,22 @@ var PollarClient = class {
|
|
|
1866
1953
|
const newNonce = response.headers.get("DPoP-Nonce");
|
|
1867
1954
|
if (newNonce) self._dpopNonce = newNonce;
|
|
1868
1955
|
if (response.status !== 401) return response;
|
|
1869
|
-
if (request.headers.get(RETRIED_HEADER)) return response;
|
|
1870
|
-
if (request.url.includes("/auth/refresh")) return response;
|
|
1871
1956
|
const wwwAuth = response.headers.get("WWW-Authenticate") ?? "";
|
|
1872
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
|
+
}
|
|
1873
1962
|
if (!isNonceChallenge) {
|
|
1874
1963
|
try {
|
|
1875
1964
|
await self.refresh();
|
|
1876
1965
|
} catch {
|
|
1877
1966
|
return response;
|
|
1878
1967
|
}
|
|
1968
|
+
const method = request.method.toUpperCase();
|
|
1969
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
1970
|
+
return response;
|
|
1971
|
+
}
|
|
1879
1972
|
}
|
|
1880
1973
|
return self._retryRequest(request);
|
|
1881
1974
|
}
|
|
@@ -1899,19 +1992,37 @@ var PollarClient = class {
|
|
|
1899
1992
|
}
|
|
1900
1993
|
}
|
|
1901
1994
|
async _retryRequest(originalRequest) {
|
|
1902
|
-
const
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
if (proof)
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1995
|
+
const headers = new Headers(originalRequest.headers);
|
|
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
|
+
}
|
|
1912
2012
|
}
|
|
1913
2013
|
}
|
|
1914
|
-
|
|
2014
|
+
const cachedBody = this._requestBodyCache.get(originalRequest);
|
|
2015
|
+
const retried = new Request(originalRequest.url, {
|
|
2016
|
+
method: originalRequest.method,
|
|
2017
|
+
headers,
|
|
2018
|
+
body: cachedBody ?? null,
|
|
2019
|
+
credentials: originalRequest.credentials,
|
|
2020
|
+
mode: originalRequest.mode,
|
|
2021
|
+
redirect: originalRequest.redirect,
|
|
2022
|
+
referrer: originalRequest.referrer,
|
|
2023
|
+
integrity: originalRequest.integrity
|
|
2024
|
+
});
|
|
2025
|
+
return fetch(retried);
|
|
1915
2026
|
}
|
|
1916
2027
|
// ─── Refresh (race-safe singleton) ───────────────────────────────────────
|
|
1917
2028
|
/**
|
|
@@ -1968,6 +2079,65 @@ var PollarClient = class {
|
|
|
1968
2079
|
} catch (err) {
|
|
1969
2080
|
console.error("[PollarClient] Failed to persist refreshed session", err);
|
|
1970
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;
|
|
1971
2141
|
}
|
|
1972
2142
|
}
|
|
1973
2143
|
// ─── Auth state ──────────────────────────────────────────────────────────────
|
|
@@ -1979,6 +2149,38 @@ var PollarClient = class {
|
|
|
1979
2149
|
cb(this._authState);
|
|
1980
2150
|
return () => this._authStateListeners.delete(cb);
|
|
1981
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
|
+
}
|
|
1982
2184
|
/** PII (email, names, avatar, providers). Held in memory only — never persisted. */
|
|
1983
2185
|
getUserProfile() {
|
|
1984
2186
|
return this._profile;
|
|
@@ -2215,10 +2417,16 @@ var PollarClient = class {
|
|
|
2215
2417
|
}
|
|
2216
2418
|
}
|
|
2217
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
|
+
*/
|
|
2218
2425
|
async buildTx(operation, params, options) {
|
|
2219
2426
|
if (!this._session?.wallet?.publicKey) {
|
|
2220
|
-
|
|
2221
|
-
|
|
2427
|
+
const details = "No wallet connected";
|
|
2428
|
+
this._setTransactionState({ step: "error", phase: "building", details });
|
|
2429
|
+
return { status: "error", details };
|
|
2222
2430
|
}
|
|
2223
2431
|
const body = {
|
|
2224
2432
|
network: this.getNetwork(),
|
|
@@ -2232,39 +2440,194 @@ var PollarClient = class {
|
|
|
2232
2440
|
const { data, error } = await this._api.POST("/tx/build", { body });
|
|
2233
2441
|
if (!error && data?.success && data.content) {
|
|
2234
2442
|
this._setTransactionState({ step: "built", buildData: data.content });
|
|
2235
|
-
|
|
2236
|
-
const details = error?.details;
|
|
2237
|
-
this._setTransactionState({ step: "error", ...details && { details } });
|
|
2443
|
+
return { status: "built", buildData: data.content };
|
|
2238
2444
|
}
|
|
2239
|
-
|
|
2240
|
-
this._setTransactionState({ step: "error" });
|
|
2445
|
+
const details = error?.details;
|
|
2446
|
+
this._setTransactionState({ step: "error", phase: "building", ...details && { details } });
|
|
2447
|
+
return { status: "error", ...details && { details } };
|
|
2448
|
+
} catch (err) {
|
|
2449
|
+
console.error("[PollarClient] buildTx failed", err);
|
|
2450
|
+
this._setTransactionState({ step: "error", phase: "building" });
|
|
2451
|
+
return { status: "error" };
|
|
2241
2452
|
}
|
|
2242
2453
|
}
|
|
2243
2454
|
getWalletType() {
|
|
2244
2455
|
return this._walletAdapter?.type ?? null;
|
|
2245
2456
|
}
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
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 } });
|
|
2252
2473
|
if (this._walletAdapter) {
|
|
2474
|
+
const accountToSign = this._session?.wallet?.publicKey;
|
|
2475
|
+
const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
|
|
2253
2476
|
try {
|
|
2254
|
-
const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
|
|
2255
2477
|
const { signedTxXdr } = await this._walletAdapter.signTransaction(unsignedXdr, signOpts);
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
}
|
|
2261
|
-
|
|
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 }
|
|
2262
2559
|
}
|
|
2263
|
-
}
|
|
2264
|
-
|
|
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
|
+
};
|
|
2265
2583
|
}
|
|
2266
|
-
|
|
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 } };
|
|
2267
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);
|
|
2627
|
+
}
|
|
2628
|
+
const buildData = this._currentBuildData();
|
|
2629
|
+
const outcomeExtra = buildData ? { buildData } : {};
|
|
2630
|
+
this._setTransactionState({ step: "signing-submitting", ...buildData && { buildData } });
|
|
2268
2631
|
const body = {
|
|
2269
2632
|
network: this.getNetwork(),
|
|
2270
2633
|
publicKey: this._session?.wallet?.publicKey ?? "",
|
|
@@ -2273,14 +2636,128 @@ var PollarClient = class {
|
|
|
2273
2636
|
try {
|
|
2274
2637
|
const { data, error } = await this._api.POST("/tx/sign-and-send", { body });
|
|
2275
2638
|
if (!error && data?.success && data.content?.hash) {
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
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
|
+
};
|
|
2280
2664
|
}
|
|
2281
|
-
|
|
2282
|
-
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);
|
|
2283
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 } };
|
|
2756
|
+
}
|
|
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);
|
|
2284
2761
|
}
|
|
2285
2762
|
// ─── App config ───────────────────────────────────────────────────────────
|
|
2286
2763
|
async getAppConfig() {
|
|
@@ -2324,6 +2801,13 @@ var PollarClient = class {
|
|
|
2324
2801
|
pollRampTransaction(txId, opts) {
|
|
2325
2802
|
return pollRampTransaction(this._api, txId, opts);
|
|
2326
2803
|
}
|
|
2804
|
+
// ─── Distribution ─────────────────────────────────────────────────────────
|
|
2805
|
+
listDistributionRules() {
|
|
2806
|
+
return listDistributionRules(this._api);
|
|
2807
|
+
}
|
|
2808
|
+
claimDistributionRule(body) {
|
|
2809
|
+
return claimDistributionRule(this._api, body);
|
|
2810
|
+
}
|
|
2327
2811
|
_setTxHistoryState(next) {
|
|
2328
2812
|
this._txHistoryState = next;
|
|
2329
2813
|
for (const cb of this._txHistoryStateListeners) cb(next);
|
|
@@ -2362,7 +2846,22 @@ var PollarClient = class {
|
|
|
2362
2846
|
*/
|
|
2363
2847
|
async _resolveWalletAdapter(id) {
|
|
2364
2848
|
if (this._walletAdapterResolver) {
|
|
2365
|
-
|
|
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
|
+
}
|
|
2366
2865
|
}
|
|
2367
2866
|
if (id === "freighter" /* FREIGHTER */) return new FreighterAdapter();
|
|
2368
2867
|
if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter();
|
|
@@ -2376,6 +2875,16 @@ var PollarClient = class {
|
|
|
2376
2875
|
this._setAuthState({ step: "idle" });
|
|
2377
2876
|
return;
|
|
2378
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
|
+
}
|
|
2379
2888
|
console.error("[PollarClient] Unexpected error in auth flow", error);
|
|
2380
2889
|
this._setAuthState({
|
|
2381
2890
|
step: "error",
|
|
@@ -2397,6 +2906,7 @@ var PollarClient = class {
|
|
|
2397
2906
|
}
|
|
2398
2907
|
console.info("[PollarClient] Session restored from storage");
|
|
2399
2908
|
this._setAuthState({ step: "authenticated", session: this._session });
|
|
2909
|
+
this._scheduleNextRefresh();
|
|
2400
2910
|
} else {
|
|
2401
2911
|
console.info("[PollarClient] No session in storage");
|
|
2402
2912
|
}
|
|
@@ -2423,9 +2933,11 @@ var PollarClient = class {
|
|
|
2423
2933
|
}
|
|
2424
2934
|
await writeStorage(this._storage, this.apiKeyHash, persisted);
|
|
2425
2935
|
this._setAuthState({ step: "authenticated", session: persisted });
|
|
2936
|
+
this._scheduleNextRefresh();
|
|
2426
2937
|
}
|
|
2427
2938
|
async _clearSession() {
|
|
2428
2939
|
console.info("[PollarClient] Session cleared");
|
|
2940
|
+
this._clearRefreshTimer();
|
|
2429
2941
|
this._session = null;
|
|
2430
2942
|
this._profile = null;
|
|
2431
2943
|
this._walletAdapter = null;
|
|
@@ -2458,6 +2970,46 @@ var PollarClient = class {
|
|
|
2458
2970
|
console.info(`[PollarClient] transaction:${next.step}`);
|
|
2459
2971
|
for (const cb of this._transactionStateListeners) cb(next);
|
|
2460
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
|
+
}
|
|
2461
3013
|
};
|
|
2462
3014
|
|
|
2463
3015
|
// src/index.ts
|
|
@@ -2472,6 +3024,7 @@ exports.WalletType = WalletType;
|
|
|
2472
3024
|
exports.WebCryptoKeyManager = WebCryptoKeyManager;
|
|
2473
3025
|
exports.buildProof = buildProof;
|
|
2474
3026
|
exports.canonicalEcJwk = canonicalEcJwk;
|
|
3027
|
+
exports.claimDistributionRule = claimDistributionRule;
|
|
2475
3028
|
exports.computeJwkThumbprint = computeJwkThumbprint;
|
|
2476
3029
|
exports.createLocalStorageAdapter = createLocalStorageAdapter;
|
|
2477
3030
|
exports.createMemoryAdapter = createMemoryAdapter;
|
|
@@ -2484,6 +3037,7 @@ exports.getKycStatus = getKycStatus;
|
|
|
2484
3037
|
exports.getRampTransaction = getRampTransaction;
|
|
2485
3038
|
exports.getRampsQuote = getRampsQuote;
|
|
2486
3039
|
exports.isValidSession = isValidSession;
|
|
3040
|
+
exports.listDistributionRules = listDistributionRules;
|
|
2487
3041
|
exports.normalizeHtu = normalizeHtu;
|
|
2488
3042
|
exports.pollKycStatus = pollKycStatus;
|
|
2489
3043
|
exports.pollRampTransaction = pollRampTransaction;
|