@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.rn.js
CHANGED
|
@@ -30,10 +30,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
|
|
31
31
|
// ../../node_modules/@stellar/freighter-api/build/index.min.js
|
|
32
32
|
var require_index_min = __commonJS({
|
|
33
|
-
"../../node_modules/@stellar/freighter-api/build/index.min.js"(exports
|
|
33
|
+
"../../node_modules/@stellar/freighter-api/build/index.min.js"(exports, module) {
|
|
34
34
|
!(function(e, r) {
|
|
35
|
-
"object" == typeof exports
|
|
36
|
-
})(exports
|
|
35
|
+
"object" == typeof exports && "object" == typeof module ? module.exports = r() : "function" == typeof define && define.amd ? define([], r) : "object" == typeof exports ? exports.freighterApi = r() : e.freighterApi = r();
|
|
36
|
+
})(exports, (() => (() => {
|
|
37
37
|
var e, r, E = { d: (e2, r2) => {
|
|
38
38
|
for (var o2 in r2) E.o(r2, o2) && !E.o(e2, o2) && Object.defineProperty(e2, o2, { enumerable: true, get: r2[o2] });
|
|
39
39
|
}, o: (e2, r2) => Object.prototype.hasOwnProperty.call(e2, r2), r: (e2) => {
|
|
@@ -452,9 +452,7 @@ var WebCryptoKeyManager = class {
|
|
|
452
452
|
*/
|
|
453
453
|
this._initPromise = null;
|
|
454
454
|
if (typeof globalThis.crypto === "undefined" || !globalThis.crypto.subtle) {
|
|
455
|
-
throw new Error(
|
|
456
|
-
"[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost)."
|
|
457
|
-
);
|
|
455
|
+
throw new Error("[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost).");
|
|
458
456
|
}
|
|
459
457
|
this.apiKey = apiKey;
|
|
460
458
|
}
|
|
@@ -569,7 +567,7 @@ function createClient(clientOptions) {
|
|
|
569
567
|
const {
|
|
570
568
|
baseUrl: localBaseUrl,
|
|
571
569
|
fetch: fetch2 = baseFetch,
|
|
572
|
-
Request = CustomRequest,
|
|
570
|
+
Request: Request2 = CustomRequest,
|
|
573
571
|
headers,
|
|
574
572
|
params = {},
|
|
575
573
|
parseAs = "json",
|
|
@@ -621,7 +619,7 @@ function createClient(clientOptions) {
|
|
|
621
619
|
};
|
|
622
620
|
let id;
|
|
623
621
|
let options;
|
|
624
|
-
let request = new
|
|
622
|
+
let request = new Request2(
|
|
625
623
|
createFinalURL(schemaPath, { baseUrl: finalBaseUrl, params, querySerializer, pathSerializer }),
|
|
626
624
|
requestInit
|
|
627
625
|
);
|
|
@@ -651,7 +649,7 @@ function createClient(clientOptions) {
|
|
|
651
649
|
id
|
|
652
650
|
});
|
|
653
651
|
if (result) {
|
|
654
|
-
if (result instanceof
|
|
652
|
+
if (result instanceof Request2) {
|
|
655
653
|
request = result;
|
|
656
654
|
} else if (result instanceof Response) {
|
|
657
655
|
response = result;
|
|
@@ -1023,24 +1021,44 @@ function createApiClient(baseUrl) {
|
|
|
1023
1021
|
return createClient({ baseUrl });
|
|
1024
1022
|
}
|
|
1025
1023
|
|
|
1024
|
+
// src/api/endpoints/distribution.ts
|
|
1025
|
+
async function listDistributionRules(api) {
|
|
1026
|
+
const { data, error } = await api.GET("/distribution/rules");
|
|
1027
|
+
if (!data?.content || error) {
|
|
1028
|
+
throw new Error(
|
|
1029
|
+
error?.code ?? error?.error ?? "Failed to list distribution rules"
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
return data.content.rules;
|
|
1033
|
+
}
|
|
1034
|
+
async function claimDistributionRule(api, body) {
|
|
1035
|
+
const { data, error } = await api.POST("/distribution/claim", { body });
|
|
1036
|
+
if (!data?.content || error) {
|
|
1037
|
+
throw new Error(
|
|
1038
|
+
error?.code ?? error?.error ?? "Failed to claim distribution rule"
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
return data.content;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1026
1044
|
// src/api/endpoints/kyc.ts
|
|
1027
1045
|
async function getKycStatus(api, providerId) {
|
|
1028
1046
|
const { data, error } = await api.GET("/kyc/status", {
|
|
1029
1047
|
params: { query: providerId ? { providerId } : {} }
|
|
1030
1048
|
});
|
|
1031
1049
|
if (!data?.content || error) {
|
|
1032
|
-
throw new Error(error?.error ?? "Failed to get KYC status");
|
|
1050
|
+
throw new Error(error?.code ?? error?.error ?? "Failed to get KYC status");
|
|
1033
1051
|
}
|
|
1034
1052
|
return data.content;
|
|
1035
1053
|
}
|
|
1036
1054
|
async function getKycProviders(api, country) {
|
|
1037
1055
|
const { data, error } = await api.GET("/kyc/providers", { params: { query: { country } } });
|
|
1038
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to get KYC providers");
|
|
1056
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get KYC providers");
|
|
1039
1057
|
return data.content;
|
|
1040
1058
|
}
|
|
1041
1059
|
async function startKyc(api, body) {
|
|
1042
1060
|
const { data, error } = await api.POST("/kyc/start", { body });
|
|
1043
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to start KYC");
|
|
1061
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to start KYC");
|
|
1044
1062
|
return data.content;
|
|
1045
1063
|
}
|
|
1046
1064
|
async function resolveKyc(api, providerId, level = "basic") {
|
|
@@ -1062,22 +1080,22 @@ async function pollKycStatus(api, providerId, { intervalMs = 3e3, timeoutMs = 3e
|
|
|
1062
1080
|
// src/api/endpoints/ramps.ts
|
|
1063
1081
|
async function getRampsQuote(api, query) {
|
|
1064
1082
|
const { data, error } = await api.GET("/ramps/quote", { params: { query } });
|
|
1065
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to get ramp quotes");
|
|
1083
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get ramp quotes");
|
|
1066
1084
|
return data.content;
|
|
1067
1085
|
}
|
|
1068
1086
|
async function createOnRamp(api, body) {
|
|
1069
1087
|
const { data, error } = await api.POST("/ramps/onramp", { body });
|
|
1070
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to create onramp");
|
|
1088
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to create onramp");
|
|
1071
1089
|
return data.content;
|
|
1072
1090
|
}
|
|
1073
1091
|
async function createOffRamp(api, body) {
|
|
1074
1092
|
const { data, error } = await api.POST("/ramps/offramp", { body });
|
|
1075
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to create offramp");
|
|
1093
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to create offramp");
|
|
1076
1094
|
return data.content;
|
|
1077
1095
|
}
|
|
1078
1096
|
async function getRampTransaction(api, txId) {
|
|
1079
1097
|
const { data, error } = await api.GET("/ramps/transaction/{txId}", { params: { path: { txId } } });
|
|
1080
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to get transaction");
|
|
1098
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get transaction");
|
|
1081
1099
|
return data.content;
|
|
1082
1100
|
}
|
|
1083
1101
|
async function pollRampTransaction(api, txId, { intervalMs = 5e3, timeoutMs = 6e5 } = {}) {
|
|
@@ -1152,34 +1170,6 @@ function generateJti() {
|
|
|
1152
1170
|
);
|
|
1153
1171
|
}
|
|
1154
1172
|
|
|
1155
|
-
// src/stellar/StellarClient.ts
|
|
1156
|
-
var HORIZON_URLS = {
|
|
1157
|
-
mainnet: "https://horizon.stellar.org",
|
|
1158
|
-
testnet: "https://horizon-testnet.stellar.org"
|
|
1159
|
-
};
|
|
1160
|
-
var StellarClient = class {
|
|
1161
|
-
constructor(config) {
|
|
1162
|
-
this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
|
|
1163
|
-
}
|
|
1164
|
-
async submitTransaction(signedXdr) {
|
|
1165
|
-
try {
|
|
1166
|
-
const response = await fetch(`${this.horizonUrl}/transactions`, {
|
|
1167
|
-
method: "POST",
|
|
1168
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1169
|
-
body: new URLSearchParams({ tx: signedXdr })
|
|
1170
|
-
});
|
|
1171
|
-
if (!response.ok) {
|
|
1172
|
-
const body = await response.json().catch(() => ({}));
|
|
1173
|
-
return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
|
|
1174
|
-
}
|
|
1175
|
-
const data = await response.json();
|
|
1176
|
-
return { success: true, hash: data.hash };
|
|
1177
|
-
} catch {
|
|
1178
|
-
return { success: false, errorCode: "NETWORK_ERROR" };
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
};
|
|
1182
|
-
|
|
1183
1173
|
// src/storage/web.ts
|
|
1184
1174
|
var LOG_PREFIX = "[PollarClient:storage]";
|
|
1185
1175
|
function createMemoryAdapter() {
|
|
@@ -1267,6 +1257,57 @@ function defaultStorage(options = {}) {
|
|
|
1267
1257
|
return createLocalStorageAdapter(options);
|
|
1268
1258
|
}
|
|
1269
1259
|
|
|
1260
|
+
// src/visibility/noop.ts
|
|
1261
|
+
function createNoopVisibilityProvider() {
|
|
1262
|
+
return {
|
|
1263
|
+
isVisible: () => true,
|
|
1264
|
+
onChange: () => () => {
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/visibility/web.ts
|
|
1270
|
+
function createWebVisibilityProvider() {
|
|
1271
|
+
const isVisibleNow = () => typeof document === "undefined" || document.visibilityState === "visible";
|
|
1272
|
+
return {
|
|
1273
|
+
isVisible: isVisibleNow,
|
|
1274
|
+
onChange: (cb) => {
|
|
1275
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
1276
|
+
return () => {
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
let last = isVisibleNow();
|
|
1280
|
+
const handler = () => {
|
|
1281
|
+
const next = isVisibleNow();
|
|
1282
|
+
if (next !== last) {
|
|
1283
|
+
last = next;
|
|
1284
|
+
cb(next);
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
document.addEventListener("visibilitychange", handler);
|
|
1288
|
+
window.addEventListener("pageshow", handler);
|
|
1289
|
+
window.addEventListener("pagehide", handler);
|
|
1290
|
+
window.addEventListener("focus", handler);
|
|
1291
|
+
window.addEventListener("blur", handler);
|
|
1292
|
+
return () => {
|
|
1293
|
+
document.removeEventListener("visibilitychange", handler);
|
|
1294
|
+
window.removeEventListener("pageshow", handler);
|
|
1295
|
+
window.removeEventListener("pagehide", handler);
|
|
1296
|
+
window.removeEventListener("focus", handler);
|
|
1297
|
+
window.removeEventListener("blur", handler);
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// src/visibility/autodetect.ts
|
|
1304
|
+
function defaultVisibilityProvider() {
|
|
1305
|
+
if (typeof document !== "undefined" && typeof window !== "undefined") {
|
|
1306
|
+
return createWebVisibilityProvider();
|
|
1307
|
+
}
|
|
1308
|
+
return createNoopVisibilityProvider();
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1270
1311
|
// src/types.ts
|
|
1271
1312
|
var AUTH_ERROR_CODES = {
|
|
1272
1313
|
SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
|
|
@@ -1277,6 +1318,7 @@ var AUTH_ERROR_CODES = {
|
|
|
1277
1318
|
AUTH_FAILED: "AUTH_FAILED",
|
|
1278
1319
|
WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
|
|
1279
1320
|
WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
|
|
1321
|
+
WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
|
|
1280
1322
|
UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
|
|
1281
1323
|
};
|
|
1282
1324
|
var PollarFlowError = class extends Error {
|
|
@@ -1667,7 +1709,7 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
|
|
|
1667
1709
|
setAuthState({ step: "authenticating" });
|
|
1668
1710
|
await streamUntilFound(api, clientSessionId, (data2) => data2?.status === "READY", 200, signal);
|
|
1669
1711
|
const dpopJwk = await deps.getPublicJwk();
|
|
1670
|
-
const { data
|
|
1712
|
+
const { data } = await api.POST("/auth/login", {
|
|
1671
1713
|
body: {
|
|
1672
1714
|
clientSessionId,
|
|
1673
1715
|
dpopJwk,
|
|
@@ -1832,7 +1874,7 @@ async function loginWallet(type, deps) {
|
|
|
1832
1874
|
let connectedWallet;
|
|
1833
1875
|
try {
|
|
1834
1876
|
setAuthState({ step: "connecting_wallet", walletType: type });
|
|
1835
|
-
const adapter = await deps.resolveWalletAdapter(type);
|
|
1877
|
+
const adapter = await withSignal(deps.resolveWalletAdapter(type), signal);
|
|
1836
1878
|
const available = await withSignal(adapter.isAvailable(), signal);
|
|
1837
1879
|
if (!available) {
|
|
1838
1880
|
setAuthState({ step: "wallet_not_installed", walletType: type });
|
|
@@ -1869,7 +1911,7 @@ async function loginWallet(type, deps) {
|
|
|
1869
1911
|
|
|
1870
1912
|
// src/client/client.ts
|
|
1871
1913
|
var isBrowser = typeof window !== "undefined" && typeof localStorage !== "undefined";
|
|
1872
|
-
var
|
|
1914
|
+
var REFRESH_SKEW_SECONDS = 60;
|
|
1873
1915
|
function warnServerSide(method) {
|
|
1874
1916
|
console.warn(
|
|
1875
1917
|
`[PollarClient] ${method}() called server-side \u2014 browser APIs unavailable. Use PollarClient only in Client Components.`
|
|
@@ -1887,9 +1929,21 @@ var PollarClient = class {
|
|
|
1887
1929
|
this._profile = null;
|
|
1888
1930
|
/** Last `DPoP-Nonce` we saw from a server response. Carried into the next proof. */
|
|
1889
1931
|
this._dpopNonce = null;
|
|
1932
|
+
/**
|
|
1933
|
+
* Snapshot of each in-flight request's body, taken in `onRequest` before
|
|
1934
|
+
* `fetch()` consumes the stream. Needed because `Request.clone()` throws
|
|
1935
|
+
* once the body is disturbed, so the auto-retry path (DPoP nonce challenge
|
|
1936
|
+
* / 401 refresh) must rebuild the request from scratch instead of cloning.
|
|
1937
|
+
*/
|
|
1938
|
+
this._requestBodyCache = /* @__PURE__ */ new WeakMap();
|
|
1890
1939
|
/** Singleton in-flight refresh — concurrent 401s coalesce into one /auth/refresh call. */
|
|
1891
1940
|
this._refreshPromise = null;
|
|
1892
1941
|
this._storageEventHandler = null;
|
|
1942
|
+
/** Updated by the request middleware. Read by the silent-refresh scheduler
|
|
1943
|
+
* to skip proactive refreshes after `maxIdleMs` of no HTTP activity. */
|
|
1944
|
+
this._lastRequestAt = Date.now();
|
|
1945
|
+
this._refreshTimer = null;
|
|
1946
|
+
this._visibilityUnsubscribe = null;
|
|
1893
1947
|
this._transactionState = null;
|
|
1894
1948
|
this._transactionStateListeners = /* @__PURE__ */ new Set();
|
|
1895
1949
|
this._txHistoryState = { step: "idle" };
|
|
@@ -1900,15 +1954,32 @@ var PollarClient = class {
|
|
|
1900
1954
|
this._authStateListeners = /* @__PURE__ */ new Set();
|
|
1901
1955
|
this._networkState = { step: "idle" };
|
|
1902
1956
|
this._networkStateListeners = /* @__PURE__ */ new Set();
|
|
1957
|
+
/**
|
|
1958
|
+
* Latched once the storage adapter degrades. We dedupe (the adapter only
|
|
1959
|
+
* fires once anyway) and use it to replay state to late-subscribers — same
|
|
1960
|
+
* pattern as `onAuthStateChange` replaying `_authState` on subscribe.
|
|
1961
|
+
* Only populated when the SDK constructed the default storage adapter; if
|
|
1962
|
+
* the consumer passes `config.storage`, they own degradation notifications.
|
|
1963
|
+
*/
|
|
1964
|
+
this._storageDegraded = null;
|
|
1965
|
+
this._storageDegradeListeners = /* @__PURE__ */ new Set();
|
|
1903
1966
|
this._walletAdapter = null;
|
|
1904
1967
|
this._loginController = null;
|
|
1905
1968
|
this.apiKey = config.apiKey;
|
|
1906
1969
|
this.id = crypto.randomUUID();
|
|
1907
1970
|
this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
|
|
1908
|
-
this._storage = config.storage ?? defaultStorage(
|
|
1971
|
+
this._storage = config.storage ?? defaultStorage({
|
|
1972
|
+
onDegrade: (reason, error) => {
|
|
1973
|
+
config.onStorageDegrade?.(reason, error);
|
|
1974
|
+
this._dispatchStorageDegrade(reason, error);
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1909
1977
|
this._keyManager = config.keyManager ?? defaultKeyManager(this._storage, config.apiKey);
|
|
1910
1978
|
this._walletAdapterResolver = config.walletAdapter ?? null;
|
|
1979
|
+
this._walletResolverTimeoutMs = config.walletResolverTimeoutMs ?? 5e3;
|
|
1911
1980
|
this._deviceLabel = config.deviceLabel;
|
|
1981
|
+
this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
|
|
1982
|
+
this._maxIdleMs = config.maxIdleMs;
|
|
1912
1983
|
this._api = createApiClient(this.basePath);
|
|
1913
1984
|
this._wireMiddlewares();
|
|
1914
1985
|
this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
|
|
@@ -1954,6 +2025,9 @@ var PollarClient = class {
|
|
|
1954
2025
|
console.warn("[PollarClient] KeyManager init failed; DPoP unavailable for this session", err);
|
|
1955
2026
|
}
|
|
1956
2027
|
await this._restoreSession();
|
|
2028
|
+
this._visibilityUnsubscribe = this._visibilityProvider.onChange((visible) => {
|
|
2029
|
+
if (visible) void this._maybeProactiveRefresh();
|
|
2030
|
+
});
|
|
1957
2031
|
}
|
|
1958
2032
|
/** Detach the cross-tab storage listener and abort any in-flight login. */
|
|
1959
2033
|
destroy() {
|
|
@@ -1963,6 +2037,11 @@ var PollarClient = class {
|
|
|
1963
2037
|
}
|
|
1964
2038
|
this._loginController?.abort();
|
|
1965
2039
|
this._loginController = null;
|
|
2040
|
+
this._clearRefreshTimer();
|
|
2041
|
+
if (this._visibilityUnsubscribe) {
|
|
2042
|
+
this._visibilityUnsubscribe();
|
|
2043
|
+
this._visibilityUnsubscribe = null;
|
|
2044
|
+
}
|
|
1966
2045
|
}
|
|
1967
2046
|
// ─── Middlewares (DPoP + auto-refresh) ────────────────────────────────────
|
|
1968
2047
|
_wireMiddlewares() {
|
|
@@ -1970,7 +2049,15 @@ var PollarClient = class {
|
|
|
1970
2049
|
this._api.use({
|
|
1971
2050
|
onRequest: async ({ request }) => {
|
|
1972
2051
|
request.headers.set("x-pollar-api-key", self.apiKey);
|
|
2052
|
+
self._lastRequestAt = Date.now();
|
|
1973
2053
|
await self._initialized;
|
|
2054
|
+
if (request.body !== null) {
|
|
2055
|
+
try {
|
|
2056
|
+
self._requestBodyCache.set(request, await request.clone().arrayBuffer());
|
|
2057
|
+
} catch (err) {
|
|
2058
|
+
console.warn("[PollarClient] Could not snapshot request body for retry", err);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
1974
2061
|
const isRefresh = request.url.includes("/auth/refresh");
|
|
1975
2062
|
if (!isRefresh && self._refreshPromise) await self._refreshPromise;
|
|
1976
2063
|
if (isRefresh) {
|
|
@@ -1993,16 +2080,22 @@ var PollarClient = class {
|
|
|
1993
2080
|
const newNonce = response.headers.get("DPoP-Nonce");
|
|
1994
2081
|
if (newNonce) self._dpopNonce = newNonce;
|
|
1995
2082
|
if (response.status !== 401) return response;
|
|
1996
|
-
if (request.headers.get(RETRIED_HEADER)) return response;
|
|
1997
|
-
if (request.url.includes("/auth/refresh")) return response;
|
|
1998
2083
|
const wwwAuth = response.headers.get("WWW-Authenticate") ?? "";
|
|
1999
2084
|
const isNonceChallenge = wwwAuth.includes("use_dpop_nonce");
|
|
2085
|
+
if (request.url.includes("/auth/refresh")) {
|
|
2086
|
+
if (isNonceChallenge) return self._retryRequest(request);
|
|
2087
|
+
return response;
|
|
2088
|
+
}
|
|
2000
2089
|
if (!isNonceChallenge) {
|
|
2001
2090
|
try {
|
|
2002
2091
|
await self.refresh();
|
|
2003
2092
|
} catch {
|
|
2004
2093
|
return response;
|
|
2005
2094
|
}
|
|
2095
|
+
const method = request.method.toUpperCase();
|
|
2096
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
2097
|
+
return response;
|
|
2098
|
+
}
|
|
2006
2099
|
}
|
|
2007
2100
|
return self._retryRequest(request);
|
|
2008
2101
|
}
|
|
@@ -2026,19 +2119,37 @@ var PollarClient = class {
|
|
|
2026
2119
|
}
|
|
2027
2120
|
}
|
|
2028
2121
|
async _retryRequest(originalRequest) {
|
|
2029
|
-
const
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
if (proof)
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2122
|
+
const headers = new Headers(originalRequest.headers);
|
|
2123
|
+
const isRefresh = originalRequest.url.includes("/auth/refresh");
|
|
2124
|
+
if (isRefresh) {
|
|
2125
|
+
const proof = await this._buildProofForRequest(originalRequest, void 0);
|
|
2126
|
+
headers.delete("Authorization");
|
|
2127
|
+
if (proof) headers.set("DPoP", proof);
|
|
2128
|
+
else headers.delete("DPoP");
|
|
2129
|
+
} else {
|
|
2130
|
+
const accessToken = this._session?.token?.accessToken;
|
|
2131
|
+
if (accessToken) {
|
|
2132
|
+
const proof = await this._buildProofForRequest(originalRequest, accessToken);
|
|
2133
|
+
if (proof) {
|
|
2134
|
+
headers.set("Authorization", `DPoP ${accessToken}`);
|
|
2135
|
+
headers.set("DPoP", proof);
|
|
2136
|
+
} else {
|
|
2137
|
+
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
2138
|
+
}
|
|
2039
2139
|
}
|
|
2040
2140
|
}
|
|
2041
|
-
|
|
2141
|
+
const cachedBody = this._requestBodyCache.get(originalRequest);
|
|
2142
|
+
const retried = new Request(originalRequest.url, {
|
|
2143
|
+
method: originalRequest.method,
|
|
2144
|
+
headers,
|
|
2145
|
+
body: cachedBody ?? null,
|
|
2146
|
+
credentials: originalRequest.credentials,
|
|
2147
|
+
mode: originalRequest.mode,
|
|
2148
|
+
redirect: originalRequest.redirect,
|
|
2149
|
+
referrer: originalRequest.referrer,
|
|
2150
|
+
integrity: originalRequest.integrity
|
|
2151
|
+
});
|
|
2152
|
+
return fetch(retried);
|
|
2042
2153
|
}
|
|
2043
2154
|
// ─── Refresh (race-safe singleton) ───────────────────────────────────────
|
|
2044
2155
|
/**
|
|
@@ -2095,6 +2206,65 @@ var PollarClient = class {
|
|
|
2095
2206
|
} catch (err) {
|
|
2096
2207
|
console.error("[PollarClient] Failed to persist refreshed session", err);
|
|
2097
2208
|
}
|
|
2209
|
+
this._scheduleNextRefresh();
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
// ─── Silent refresh scheduler ────────────────────────────────────────────────
|
|
2213
|
+
/**
|
|
2214
|
+
* Arm a single setTimeout to fire shortly before the current access token
|
|
2215
|
+
* expires. Idempotent — clearing any previous timer first. Safe to call
|
|
2216
|
+
* from any session-write site (initial login, restore-from-storage, after
|
|
2217
|
+
* a successful rotation). No-op if there's no session in memory.
|
|
2218
|
+
*
|
|
2219
|
+
* Browser/RN background-tab throttling makes long-running setTimeouts
|
|
2220
|
+
* unreliable on their own; the `visibilitychange` listener compensates by
|
|
2221
|
+
* re-invoking `_maybeProactiveRefresh` whenever the app comes back to the
|
|
2222
|
+
* foreground, catching any timer that fired late or never fired at all.
|
|
2223
|
+
*/
|
|
2224
|
+
_scheduleNextRefresh() {
|
|
2225
|
+
this._clearRefreshTimer();
|
|
2226
|
+
const expiresAt = this._session?.token?.expiresAt;
|
|
2227
|
+
if (typeof expiresAt !== "number") return;
|
|
2228
|
+
const dueInMs = Math.max(0, (expiresAt - Math.floor(Date.now() / 1e3) - REFRESH_SKEW_SECONDS) * 1e3);
|
|
2229
|
+
this._refreshTimer = setTimeout(() => {
|
|
2230
|
+
void this._maybeProactiveRefresh();
|
|
2231
|
+
}, dueInMs);
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Decide whether to actually run a refresh right now. Called both from the
|
|
2235
|
+
* scheduler timer and from the visibility-change listener.
|
|
2236
|
+
*
|
|
2237
|
+
* Skip if:
|
|
2238
|
+
* - no session / no RT (nothing to refresh)
|
|
2239
|
+
* - app is hidden — wait for the visibility listener to re-trigger us
|
|
2240
|
+
* - `maxIdleMs` configured and no client request since that window — let
|
|
2241
|
+
* the next reactive 401-refresh handle it whenever the user comes back
|
|
2242
|
+
* - the AT still has more than `REFRESH_SKEW_SECONDS` of life — reschedule
|
|
2243
|
+
*
|
|
2244
|
+
* Otherwise call `refresh()`, which uses the existing in-flight singleton
|
|
2245
|
+
* so we never collide with a reactive 401-triggered refresh. On failure,
|
|
2246
|
+
* `_doRefresh` already calls `_clearSession`, so auth-state listeners see
|
|
2247
|
+
* `step:'idle'` — no extra event dispatch needed here.
|
|
2248
|
+
*/
|
|
2249
|
+
async _maybeProactiveRefresh() {
|
|
2250
|
+
if (!this._session?.token?.refreshToken) return;
|
|
2251
|
+
if (!this._visibilityProvider.isVisible()) return;
|
|
2252
|
+
if (this._maxIdleMs !== void 0 && Date.now() - this._lastRequestAt > this._maxIdleMs) return;
|
|
2253
|
+
const expiresAt = this._session.token.expiresAt;
|
|
2254
|
+
if (Math.floor(Date.now() / 1e3) < expiresAt - REFRESH_SKEW_SECONDS) {
|
|
2255
|
+
this._scheduleNextRefresh();
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
try {
|
|
2259
|
+
await this.refresh();
|
|
2260
|
+
} catch (err) {
|
|
2261
|
+
console.warn("[PollarClient] Proactive refresh failed; session cleared", err);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
_clearRefreshTimer() {
|
|
2265
|
+
if (this._refreshTimer !== null) {
|
|
2266
|
+
clearTimeout(this._refreshTimer);
|
|
2267
|
+
this._refreshTimer = null;
|
|
2098
2268
|
}
|
|
2099
2269
|
}
|
|
2100
2270
|
// ─── Auth state ──────────────────────────────────────────────────────────────
|
|
@@ -2106,6 +2276,38 @@ var PollarClient = class {
|
|
|
2106
2276
|
cb(this._authState);
|
|
2107
2277
|
return () => this._authStateListeners.delete(cb);
|
|
2108
2278
|
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Subscribe to persistent-storage degradation (Safari private mode,
|
|
2281
|
+
* sandboxed iframes, quota errors, etc.). The SDK keeps running off
|
|
2282
|
+
* in-memory storage after degrade, but sessions won't survive reload — a
|
|
2283
|
+
* host UI typically wants to show "your session won't be saved" so the
|
|
2284
|
+
* user isn't blindsided after a refresh.
|
|
2285
|
+
*
|
|
2286
|
+
* Fires at most once per client lifetime (the underlying adapter dedupes).
|
|
2287
|
+
* Late subscribers receive the latched state synchronously on subscribe.
|
|
2288
|
+
*
|
|
2289
|
+
* Only fires when the SDK constructs the default storage adapter. If you
|
|
2290
|
+
* pass a custom `config.storage`, wire your own notification path through
|
|
2291
|
+
* that adapter's API — the SDK has no hook into it.
|
|
2292
|
+
*/
|
|
2293
|
+
onStorageDegrade(cb) {
|
|
2294
|
+
this._storageDegradeListeners.add(cb);
|
|
2295
|
+
if (this._storageDegraded) {
|
|
2296
|
+
cb(this._storageDegraded.reason, this._storageDegraded.error);
|
|
2297
|
+
}
|
|
2298
|
+
return () => this._storageDegradeListeners.delete(cb);
|
|
2299
|
+
}
|
|
2300
|
+
_dispatchStorageDegrade(reason, error) {
|
|
2301
|
+
if (this._storageDegraded) return;
|
|
2302
|
+
this._storageDegraded = { reason, error };
|
|
2303
|
+
for (const cb of this._storageDegradeListeners) {
|
|
2304
|
+
try {
|
|
2305
|
+
cb(reason, error);
|
|
2306
|
+
} catch (err) {
|
|
2307
|
+
console.error("[PollarClient] onStorageDegrade listener threw", err);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2109
2311
|
/** PII (email, names, avatar, providers). Held in memory only — never persisted. */
|
|
2110
2312
|
getUserProfile() {
|
|
2111
2313
|
return this._profile;
|
|
@@ -2342,10 +2544,16 @@ var PollarClient = class {
|
|
|
2342
2544
|
}
|
|
2343
2545
|
}
|
|
2344
2546
|
// ─── Transactions ─────────────────────────────────────────────────────────
|
|
2547
|
+
/**
|
|
2548
|
+
* Builds an unsigned XDR. Drives `_setTransactionState` for modal-style UIs
|
|
2549
|
+
* AND returns a {@link BuildOutcome} so headless callers can `await` and
|
|
2550
|
+
* inspect the result without subscribing to state changes.
|
|
2551
|
+
*/
|
|
2345
2552
|
async buildTx(operation, params, options) {
|
|
2346
2553
|
if (!this._session?.wallet?.publicKey) {
|
|
2347
|
-
|
|
2348
|
-
|
|
2554
|
+
const details = "No wallet connected";
|
|
2555
|
+
this._setTransactionState({ step: "error", phase: "building", details });
|
|
2556
|
+
return { status: "error", details };
|
|
2349
2557
|
}
|
|
2350
2558
|
const body = {
|
|
2351
2559
|
network: this.getNetwork(),
|
|
@@ -2359,39 +2567,194 @@ var PollarClient = class {
|
|
|
2359
2567
|
const { data, error } = await this._api.POST("/tx/build", { body });
|
|
2360
2568
|
if (!error && data?.success && data.content) {
|
|
2361
2569
|
this._setTransactionState({ step: "built", buildData: data.content });
|
|
2362
|
-
|
|
2363
|
-
const details = error?.details;
|
|
2364
|
-
this._setTransactionState({ step: "error", ...details && { details } });
|
|
2570
|
+
return { status: "built", buildData: data.content };
|
|
2365
2571
|
}
|
|
2366
|
-
|
|
2367
|
-
this._setTransactionState({ step: "error" });
|
|
2572
|
+
const details = error?.details;
|
|
2573
|
+
this._setTransactionState({ step: "error", phase: "building", ...details && { details } });
|
|
2574
|
+
return { status: "error", ...details && { details } };
|
|
2575
|
+
} catch (err) {
|
|
2576
|
+
console.error("[PollarClient] buildTx failed", err);
|
|
2577
|
+
this._setTransactionState({ step: "error", phase: "building" });
|
|
2578
|
+
return { status: "error" };
|
|
2368
2579
|
}
|
|
2369
2580
|
}
|
|
2370
2581
|
getWalletType() {
|
|
2371
2582
|
return this._walletAdapter?.type ?? null;
|
|
2372
2583
|
}
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2584
|
+
/**
|
|
2585
|
+
* Signs the given unsigned XDR and returns the signed XDR.
|
|
2586
|
+
*
|
|
2587
|
+
* - External wallets: signs locally via the wallet adapter.
|
|
2588
|
+
* - Custodial wallets: posts to `/tx/sign`. The backend signs (through
|
|
2589
|
+
* wallet-service or the app's customer-managed adapter) and returns the
|
|
2590
|
+
* signed XDR plus an `idempotencyKey` the caller should echo back to
|
|
2591
|
+
* `submitTx`.
|
|
2592
|
+
*
|
|
2593
|
+
* Drives `_setTransactionState`: emits `signing` while in flight and
|
|
2594
|
+
* `signed` on success (or `error[phase: 'signing']` on failure). `buildData`
|
|
2595
|
+
* is threaded through if the consumer previously called `buildTx`.
|
|
2596
|
+
*/
|
|
2597
|
+
async signTx(unsignedXdr) {
|
|
2598
|
+
const buildData = this._currentBuildData();
|
|
2599
|
+
this._setTransactionState({ step: "signing", ...buildData && { buildData } });
|
|
2379
2600
|
if (this._walletAdapter) {
|
|
2601
|
+
const accountToSign = this._session?.wallet?.publicKey;
|
|
2602
|
+
const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
|
|
2380
2603
|
try {
|
|
2381
|
-
const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
|
|
2382
2604
|
const { signedTxXdr } = await this._walletAdapter.signTransaction(unsignedXdr, signOpts);
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2605
|
+
this._setTransactionState({
|
|
2606
|
+
step: "signed",
|
|
2607
|
+
signedXdr: signedTxXdr,
|
|
2608
|
+
...buildData && { buildData }
|
|
2609
|
+
});
|
|
2610
|
+
return { status: "signed", signedXdr: signedTxXdr };
|
|
2611
|
+
} catch (err) {
|
|
2612
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
2613
|
+
this._setTransactionState({
|
|
2614
|
+
step: "error",
|
|
2615
|
+
phase: "signing",
|
|
2616
|
+
...buildData && { buildData },
|
|
2617
|
+
...details && { details }
|
|
2618
|
+
});
|
|
2619
|
+
return { status: "error", ...details && { details } };
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
const publicKey = this._session?.wallet?.publicKey ?? "";
|
|
2623
|
+
try {
|
|
2624
|
+
const { data, error } = await this._api.POST("/tx/sign", {
|
|
2625
|
+
body: { network: this.getNetwork(), publicKey, unsignedXdr }
|
|
2626
|
+
});
|
|
2627
|
+
if (!error && data?.success && data.content?.signedXdr) {
|
|
2628
|
+
const { signedXdr, idempotencyKey } = data.content;
|
|
2629
|
+
this._setTransactionState({
|
|
2630
|
+
step: "signed",
|
|
2631
|
+
signedXdr,
|
|
2632
|
+
submissionToken: idempotencyKey,
|
|
2633
|
+
...buildData && { buildData }
|
|
2634
|
+
});
|
|
2635
|
+
return { status: "signed", signedXdr, submissionToken: idempotencyKey };
|
|
2636
|
+
}
|
|
2637
|
+
const details = error?.details;
|
|
2638
|
+
this._setTransactionState({
|
|
2639
|
+
step: "error",
|
|
2640
|
+
phase: "signing",
|
|
2641
|
+
...buildData && { buildData },
|
|
2642
|
+
...details && { details }
|
|
2643
|
+
});
|
|
2644
|
+
return { status: "error", ...details && { details } };
|
|
2645
|
+
} catch (err) {
|
|
2646
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
2647
|
+
this._setTransactionState({
|
|
2648
|
+
step: "error",
|
|
2649
|
+
phase: "signing",
|
|
2650
|
+
...buildData && { buildData },
|
|
2651
|
+
...details && { details }
|
|
2652
|
+
});
|
|
2653
|
+
return { status: "error", ...details && { details } };
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
/**
|
|
2657
|
+
* Submits a signed XDR via `/tx/submit` regardless of wallet type
|
|
2658
|
+
* (custodial or external). Routing through sdk-api gives us:
|
|
2659
|
+
* - End-to-end tx_records persistence with full phase lifecycle so the
|
|
2660
|
+
* developer dashboard can show every tx (both custodial and external
|
|
2661
|
+
* wallet flows) at `/apps/:id/monitor/transactions`.
|
|
2662
|
+
* - Idempotency tracking via `submissionToken` (returned by `signTx`).
|
|
2663
|
+
* - A single response shape (SUCCESS / PENDING / FAILED) shared by both
|
|
2664
|
+
* flows — previously external wallets could only return SUCCESS or
|
|
2665
|
+
* error since the direct-to-Horizon path was synchronous.
|
|
2666
|
+
*
|
|
2667
|
+
* The extra hop adds ~50–150 ms vs. the legacy direct-Horizon path; the
|
|
2668
|
+
* persistence + observability win is worth it.
|
|
2669
|
+
*
|
|
2670
|
+
* Drives `_setTransactionState`: emits `submitting` while in flight,
|
|
2671
|
+
* `submitted` on Horizon ack (pending), `success` on ledger confirmation,
|
|
2672
|
+
* or `error[phase: 'submitting']` on failure.
|
|
2673
|
+
*/
|
|
2674
|
+
async submitTx(signedXdr, opts) {
|
|
2675
|
+
const buildData = this._currentBuildData();
|
|
2676
|
+
const outcomeExtra = buildData ? { buildData } : {};
|
|
2677
|
+
this._setTransactionState({ step: "submitting", signedXdr, ...buildData && { buildData } });
|
|
2678
|
+
const publicKey = this._session?.wallet?.publicKey ?? "";
|
|
2679
|
+
try {
|
|
2680
|
+
const { data, error } = await this._api.POST("/tx/submit", {
|
|
2681
|
+
body: {
|
|
2682
|
+
network: this.getNetwork(),
|
|
2683
|
+
publicKey,
|
|
2684
|
+
signedXdr,
|
|
2685
|
+
...opts?.submissionToken && { idempotencyKey: opts.submissionToken }
|
|
2389
2686
|
}
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2687
|
+
});
|
|
2688
|
+
if (!error && data?.success && data.content) {
|
|
2689
|
+
const { hash, status: backendStatus, resultCode } = data.content;
|
|
2690
|
+
if (backendStatus === "SUCCESS") {
|
|
2691
|
+
this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
|
|
2692
|
+
return { status: "success", hash, ...outcomeExtra };
|
|
2693
|
+
}
|
|
2694
|
+
if (backendStatus === "PENDING") {
|
|
2695
|
+
this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
|
|
2696
|
+
return { status: "pending", hash, ...outcomeExtra };
|
|
2697
|
+
}
|
|
2698
|
+
this._setTransactionState({
|
|
2699
|
+
step: "error",
|
|
2700
|
+
phase: "submitting",
|
|
2701
|
+
...buildData && { buildData },
|
|
2702
|
+
...resultCode && { details: resultCode }
|
|
2703
|
+
});
|
|
2704
|
+
return {
|
|
2705
|
+
status: "error",
|
|
2706
|
+
hash,
|
|
2707
|
+
...outcomeExtra,
|
|
2708
|
+
...resultCode && { details: resultCode, resultCode }
|
|
2709
|
+
};
|
|
2392
2710
|
}
|
|
2393
|
-
|
|
2711
|
+
const details = error?.details;
|
|
2712
|
+
this._setTransactionState({
|
|
2713
|
+
step: "error",
|
|
2714
|
+
phase: "submitting",
|
|
2715
|
+
...buildData && { buildData },
|
|
2716
|
+
...details && { details }
|
|
2717
|
+
});
|
|
2718
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
2719
|
+
} catch (err) {
|
|
2720
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
2721
|
+
this._setTransactionState({
|
|
2722
|
+
step: "error",
|
|
2723
|
+
phase: "submitting",
|
|
2724
|
+
...buildData && { buildData },
|
|
2725
|
+
...details && { details }
|
|
2726
|
+
});
|
|
2727
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
2394
2728
|
}
|
|
2729
|
+
}
|
|
2730
|
+
/**
|
|
2731
|
+
* Signs and submits in one logical step. Returns a {@link SubmitOutcome}.
|
|
2732
|
+
*
|
|
2733
|
+
* - **External wallets**: composes `signTx` + `submitTx` client-side. State
|
|
2734
|
+
* machine sees the full granular sequence `signing → signed → submitting
|
|
2735
|
+
* → success` because the underlying methods each emit.
|
|
2736
|
+
* - **Custodial wallets**: atomic `/tx/sign-and-send` round-trip. State
|
|
2737
|
+
* machine emits the compound `signing-submitting` step (the SDK can't
|
|
2738
|
+
* observe when one phase ends and the next begins inside that single
|
|
2739
|
+
* backend call) and then transitions to `submitted` (Horizon ack only) or
|
|
2740
|
+
* `success` (ledger-confirmed), or `error[phase: 'signing-submitting']`.
|
|
2741
|
+
*/
|
|
2742
|
+
async signAndSubmitTx(unsignedXdr) {
|
|
2743
|
+
if (this._walletAdapter) {
|
|
2744
|
+
const signed = await this.signTx(unsignedXdr);
|
|
2745
|
+
if (signed.status === "error") {
|
|
2746
|
+
const buildData2 = this._currentBuildData();
|
|
2747
|
+
return {
|
|
2748
|
+
status: "error",
|
|
2749
|
+
...buildData2 && { buildData: buildData2 },
|
|
2750
|
+
...signed.details && { details: signed.details }
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
return this.submitTx(signed.signedXdr);
|
|
2754
|
+
}
|
|
2755
|
+
const buildData = this._currentBuildData();
|
|
2756
|
+
const outcomeExtra = buildData ? { buildData } : {};
|
|
2757
|
+
this._setTransactionState({ step: "signing-submitting", ...buildData && { buildData } });
|
|
2395
2758
|
const body = {
|
|
2396
2759
|
network: this.getNetwork(),
|
|
2397
2760
|
publicKey: this._session?.wallet?.publicKey ?? "",
|
|
@@ -2400,14 +2763,128 @@ var PollarClient = class {
|
|
|
2400
2763
|
try {
|
|
2401
2764
|
const { data, error } = await this._api.POST("/tx/sign-and-send", { body });
|
|
2402
2765
|
if (!error && data?.success && data.content?.hash) {
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2766
|
+
const {
|
|
2767
|
+
hash,
|
|
2768
|
+
status: backendStatus,
|
|
2769
|
+
resultCode
|
|
2770
|
+
} = data.content;
|
|
2771
|
+
if (backendStatus === "SUCCESS") {
|
|
2772
|
+
this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
|
|
2773
|
+
return { status: "success", hash, ...outcomeExtra };
|
|
2774
|
+
}
|
|
2775
|
+
if (backendStatus === "PENDING") {
|
|
2776
|
+
this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
|
|
2777
|
+
return { status: "pending", hash, ...outcomeExtra };
|
|
2778
|
+
}
|
|
2779
|
+
this._setTransactionState({
|
|
2780
|
+
step: "error",
|
|
2781
|
+
phase: "signing-submitting",
|
|
2782
|
+
...buildData && { buildData },
|
|
2783
|
+
...resultCode && { details: resultCode }
|
|
2784
|
+
});
|
|
2785
|
+
return {
|
|
2786
|
+
status: "error",
|
|
2787
|
+
hash,
|
|
2788
|
+
...outcomeExtra,
|
|
2789
|
+
...resultCode && { details: resultCode, resultCode }
|
|
2790
|
+
};
|
|
2407
2791
|
}
|
|
2408
|
-
|
|
2409
|
-
this._setTransactionState({
|
|
2792
|
+
const details = error?.details;
|
|
2793
|
+
this._setTransactionState({
|
|
2794
|
+
step: "error",
|
|
2795
|
+
phase: "signing-submitting",
|
|
2796
|
+
...buildData && { buildData },
|
|
2797
|
+
...details && { details }
|
|
2798
|
+
});
|
|
2799
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
2800
|
+
} catch (err) {
|
|
2801
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
2802
|
+
this._setTransactionState({
|
|
2803
|
+
step: "error",
|
|
2804
|
+
phase: "signing-submitting",
|
|
2805
|
+
...buildData && { buildData },
|
|
2806
|
+
...details && { details }
|
|
2807
|
+
});
|
|
2808
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
/**
|
|
2812
|
+
* One-shot: build → sign → submit, returning the final {@link SubmitOutcome}.
|
|
2813
|
+
*
|
|
2814
|
+
* - **External wallets**: composes `buildTx` + `signAndSubmitTx` client-side.
|
|
2815
|
+
* State machine sees the full granular sequence (`building → built →
|
|
2816
|
+
* signing → signed → submitting → success`) because each composed call
|
|
2817
|
+
* emits its own transitions.
|
|
2818
|
+
* - **Custodial wallets**: single round-trip to `/tx/build-sign-submit`. The
|
|
2819
|
+
* signed XDR never leaves the backend. State machine emits the compound
|
|
2820
|
+
* `building-signing-submitting` step (the SDK can't observe individual
|
|
2821
|
+
* phase boundaries inside one atomic call) and then transitions to
|
|
2822
|
+
* `submitted` / `success` / `error[phase: 'building-signing-submitting']`.
|
|
2823
|
+
*
|
|
2824
|
+
* If you need granular UI feedback for custodial flows (separate
|
|
2825
|
+
* "Building…", "Signing…", "Submitting…" indicators), call `buildTx`,
|
|
2826
|
+
* `signTx`, and `submitTx` separately instead.
|
|
2827
|
+
*/
|
|
2828
|
+
async buildAndSignAndSubmitTx(operation, params, options) {
|
|
2829
|
+
if (this._walletAdapter) {
|
|
2830
|
+
const built = await this.buildTx(operation, params, options);
|
|
2831
|
+
if (built.status === "error") {
|
|
2832
|
+
return { status: "error", ...built.details && { details: built.details } };
|
|
2833
|
+
}
|
|
2834
|
+
return this.signAndSubmitTx(built.buildData.unsignedXdr);
|
|
2410
2835
|
}
|
|
2836
|
+
if (!this._session?.wallet?.publicKey) {
|
|
2837
|
+
this._setTransactionState({ step: "error", phase: "building-signing-submitting", details: "No wallet connected" });
|
|
2838
|
+
return { status: "error", details: "No wallet connected" };
|
|
2839
|
+
}
|
|
2840
|
+
this._setTransactionState({ step: "building-signing-submitting" });
|
|
2841
|
+
try {
|
|
2842
|
+
const { data, error } = await this._api.POST("/tx/build-sign-submit", {
|
|
2843
|
+
body: {
|
|
2844
|
+
network: this.getNetwork(),
|
|
2845
|
+
publicKey: this._session.wallet.publicKey,
|
|
2846
|
+
operation,
|
|
2847
|
+
params,
|
|
2848
|
+
options: options ?? {}
|
|
2849
|
+
}
|
|
2850
|
+
});
|
|
2851
|
+
if (!error && data?.success && data.content) {
|
|
2852
|
+
const { hash, status: backendStatus, resultCode } = data.content;
|
|
2853
|
+
if (backendStatus === "SUCCESS") {
|
|
2854
|
+
this._setTransactionState({ step: "success", hash });
|
|
2855
|
+
return { status: "success", hash };
|
|
2856
|
+
}
|
|
2857
|
+
if (backendStatus === "PENDING") {
|
|
2858
|
+
this._setTransactionState({ step: "submitted", hash });
|
|
2859
|
+
return { status: "pending", hash };
|
|
2860
|
+
}
|
|
2861
|
+
this._setTransactionState({
|
|
2862
|
+
step: "error",
|
|
2863
|
+
phase: "building-signing-submitting",
|
|
2864
|
+
...resultCode && { details: resultCode }
|
|
2865
|
+
});
|
|
2866
|
+
return { status: "error", hash, ...resultCode && { details: resultCode, resultCode } };
|
|
2867
|
+
}
|
|
2868
|
+
const details = error?.details;
|
|
2869
|
+
this._setTransactionState({
|
|
2870
|
+
step: "error",
|
|
2871
|
+
phase: "building-signing-submitting",
|
|
2872
|
+
...details && { details }
|
|
2873
|
+
});
|
|
2874
|
+
return { status: "error", ...details && { details } };
|
|
2875
|
+
} catch (err) {
|
|
2876
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
2877
|
+
this._setTransactionState({
|
|
2878
|
+
step: "error",
|
|
2879
|
+
phase: "building-signing-submitting",
|
|
2880
|
+
...details && { details }
|
|
2881
|
+
});
|
|
2882
|
+
return { status: "error", ...details && { details } };
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
/** Alias for {@link buildAndSignAndSubmitTx} — shorter "just do the thing" name. */
|
|
2886
|
+
async runTx(operation, params, options) {
|
|
2887
|
+
return this.buildAndSignAndSubmitTx(operation, params, options);
|
|
2411
2888
|
}
|
|
2412
2889
|
// ─── App config ───────────────────────────────────────────────────────────
|
|
2413
2890
|
async getAppConfig() {
|
|
@@ -2451,6 +2928,13 @@ var PollarClient = class {
|
|
|
2451
2928
|
pollRampTransaction(txId, opts) {
|
|
2452
2929
|
return pollRampTransaction(this._api, txId, opts);
|
|
2453
2930
|
}
|
|
2931
|
+
// ─── Distribution ─────────────────────────────────────────────────────────
|
|
2932
|
+
listDistributionRules() {
|
|
2933
|
+
return listDistributionRules(this._api);
|
|
2934
|
+
}
|
|
2935
|
+
claimDistributionRule(body) {
|
|
2936
|
+
return claimDistributionRule(this._api, body);
|
|
2937
|
+
}
|
|
2454
2938
|
_setTxHistoryState(next) {
|
|
2455
2939
|
this._txHistoryState = next;
|
|
2456
2940
|
for (const cb of this._txHistoryStateListeners) cb(next);
|
|
@@ -2489,7 +2973,22 @@ var PollarClient = class {
|
|
|
2489
2973
|
*/
|
|
2490
2974
|
async _resolveWalletAdapter(id) {
|
|
2491
2975
|
if (this._walletAdapterResolver) {
|
|
2492
|
-
|
|
2976
|
+
const timeoutMs = this._walletResolverTimeoutMs;
|
|
2977
|
+
let timeoutHandle;
|
|
2978
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
2979
|
+
timeoutHandle = setTimeout(() => {
|
|
2980
|
+
reject(
|
|
2981
|
+
Object.assign(new Error(`[PollarClient] Wallet adapter resolver for "${id}" timed out after ${timeoutMs}ms`), {
|
|
2982
|
+
code: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
|
|
2983
|
+
})
|
|
2984
|
+
);
|
|
2985
|
+
}, timeoutMs);
|
|
2986
|
+
});
|
|
2987
|
+
try {
|
|
2988
|
+
return await Promise.race([Promise.resolve(this._walletAdapterResolver(id)), timeoutPromise]);
|
|
2989
|
+
} finally {
|
|
2990
|
+
if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
|
|
2991
|
+
}
|
|
2493
2992
|
}
|
|
2494
2993
|
if (id === "freighter" /* FREIGHTER */) return new FreighterAdapter();
|
|
2495
2994
|
if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter();
|
|
@@ -2503,6 +3002,16 @@ var PollarClient = class {
|
|
|
2503
3002
|
this._setAuthState({ step: "idle" });
|
|
2504
3003
|
return;
|
|
2505
3004
|
}
|
|
3005
|
+
if (error instanceof Error && error.code === AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT) {
|
|
3006
|
+
console.error("[PollarClient]", error.message);
|
|
3007
|
+
this._setAuthState({
|
|
3008
|
+
step: "error",
|
|
3009
|
+
previousStep: this._authState.step,
|
|
3010
|
+
message: error.message,
|
|
3011
|
+
errorCode: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
|
|
3012
|
+
});
|
|
3013
|
+
return;
|
|
3014
|
+
}
|
|
2506
3015
|
console.error("[PollarClient] Unexpected error in auth flow", error);
|
|
2507
3016
|
this._setAuthState({
|
|
2508
3017
|
step: "error",
|
|
@@ -2524,6 +3033,7 @@ var PollarClient = class {
|
|
|
2524
3033
|
}
|
|
2525
3034
|
console.info("[PollarClient] Session restored from storage");
|
|
2526
3035
|
this._setAuthState({ step: "authenticated", session: this._session });
|
|
3036
|
+
this._scheduleNextRefresh();
|
|
2527
3037
|
} else {
|
|
2528
3038
|
console.info("[PollarClient] No session in storage");
|
|
2529
3039
|
}
|
|
@@ -2550,9 +3060,11 @@ var PollarClient = class {
|
|
|
2550
3060
|
}
|
|
2551
3061
|
await writeStorage(this._storage, this.apiKeyHash, persisted);
|
|
2552
3062
|
this._setAuthState({ step: "authenticated", session: persisted });
|
|
3063
|
+
this._scheduleNextRefresh();
|
|
2553
3064
|
}
|
|
2554
3065
|
async _clearSession() {
|
|
2555
3066
|
console.info("[PollarClient] Session cleared");
|
|
3067
|
+
this._clearRefreshTimer();
|
|
2556
3068
|
this._session = null;
|
|
2557
3069
|
this._profile = null;
|
|
2558
3070
|
this._walletAdapter = null;
|
|
@@ -2585,6 +3097,46 @@ var PollarClient = class {
|
|
|
2585
3097
|
console.info(`[PollarClient] transaction:${next.step}`);
|
|
2586
3098
|
for (const cb of this._transactionStateListeners) cb(next);
|
|
2587
3099
|
}
|
|
3100
|
+
/**
|
|
3101
|
+
* Threads `buildData` through state transitions. When the user has already
|
|
3102
|
+
* called `buildTx`, every subsequent state (signing, signed, submitting,
|
|
3103
|
+
* submitted, success, error) should carry the build summary so modal UIs
|
|
3104
|
+
* can keep showing "Send 5 USDC to G..." through the whole flow.
|
|
3105
|
+
*/
|
|
3106
|
+
_currentBuildData() {
|
|
3107
|
+
const s = this._transactionState;
|
|
3108
|
+
if (!s) return void 0;
|
|
3109
|
+
if ("buildData" in s && s.buildData) return s.buildData;
|
|
3110
|
+
return void 0;
|
|
3111
|
+
}
|
|
3112
|
+
};
|
|
3113
|
+
|
|
3114
|
+
// src/stellar/StellarClient.ts
|
|
3115
|
+
var HORIZON_URLS = {
|
|
3116
|
+
mainnet: "https://horizon.stellar.org",
|
|
3117
|
+
testnet: "https://horizon-testnet.stellar.org"
|
|
3118
|
+
};
|
|
3119
|
+
var StellarClient = class {
|
|
3120
|
+
constructor(config) {
|
|
3121
|
+
this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
|
|
3122
|
+
}
|
|
3123
|
+
async submitTransaction(signedXdr) {
|
|
3124
|
+
try {
|
|
3125
|
+
const response = await fetch(`${this.horizonUrl}/transactions`, {
|
|
3126
|
+
method: "POST",
|
|
3127
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
3128
|
+
body: new URLSearchParams({ tx: signedXdr })
|
|
3129
|
+
});
|
|
3130
|
+
if (!response.ok) {
|
|
3131
|
+
const body = await response.json().catch(() => ({}));
|
|
3132
|
+
return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
|
|
3133
|
+
}
|
|
3134
|
+
const data = await response.json();
|
|
3135
|
+
return { success: true, hash: data.hash };
|
|
3136
|
+
} catch {
|
|
3137
|
+
return { success: false, errorCode: "NETWORK_ERROR" };
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
2588
3140
|
};
|
|
2589
3141
|
|
|
2590
3142
|
// src/index.rn.ts
|
|
@@ -2606,6 +3158,7 @@ exports.WalletType = WalletType;
|
|
|
2606
3158
|
exports.WebCryptoKeyManager = WebCryptoKeyManager;
|
|
2607
3159
|
exports.buildProof = buildProof;
|
|
2608
3160
|
exports.canonicalEcJwk = canonicalEcJwk;
|
|
3161
|
+
exports.claimDistributionRule = claimDistributionRule;
|
|
2609
3162
|
exports.computeJwkThumbprint = computeJwkThumbprint;
|
|
2610
3163
|
exports.createLocalStorageAdapter = createLocalStorageAdapter;
|
|
2611
3164
|
exports.createMemoryAdapter = createMemoryAdapter;
|
|
@@ -2618,6 +3171,7 @@ exports.getKycStatus = getKycStatus;
|
|
|
2618
3171
|
exports.getRampTransaction = getRampTransaction;
|
|
2619
3172
|
exports.getRampsQuote = getRampsQuote;
|
|
2620
3173
|
exports.isValidSession = isValidSession;
|
|
3174
|
+
exports.listDistributionRules = listDistributionRules;
|
|
2621
3175
|
exports.normalizeHtu = normalizeHtu;
|
|
2622
3176
|
exports.pollKycStatus = pollKycStatus;
|
|
2623
3177
|
exports.pollRampTransaction = pollRampTransaction;
|