@pollar/core 0.7.1 → 0.8.1
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 +153 -26
- 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-appstate.d.mts +10 -0
- package/dist/adapters/react-native-appstate.d.ts +10 -0
- package/dist/adapters/react-native-appstate.js +38 -0
- package/dist/adapters/react-native-appstate.js.map +1 -0
- package/dist/adapters/react-native-appstate.mjs +36 -0
- package/dist/adapters/react-native-appstate.mjs.map +1 -0
- 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 +1352 -129
- package/dist/index.d.ts +1352 -129
- package/dist/index.js +761 -140
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +761 -140
- package/dist/index.mjs.map +1 -1
- package/dist/index.rn.d.mts +2 -1
- package/dist/index.rn.d.ts +2 -1
- package/dist/index.rn.js +761 -140
- package/dist/index.rn.js.map +1 -1
- package/dist/index.rn.mjs +761 -140
- package/dist/index.rn.mjs.map +1 -1
- package/dist/types-84G_htcn.d.mts +38 -0
- package/dist/types-84G_htcn.d.ts +38 -0
- package/package.json +16 -3
package/dist/index.js
CHANGED
|
@@ -28,10 +28,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
|
|
29
29
|
// ../../node_modules/@stellar/freighter-api/build/index.min.js
|
|
30
30
|
var require_index_min = __commonJS({
|
|
31
|
-
"../../node_modules/@stellar/freighter-api/build/index.min.js"(exports
|
|
31
|
+
"../../node_modules/@stellar/freighter-api/build/index.min.js"(exports, module) {
|
|
32
32
|
!(function(e, r) {
|
|
33
|
-
"object" == typeof exports
|
|
34
|
-
})(exports
|
|
33
|
+
"object" == typeof exports && "object" == typeof module ? module.exports = r() : "function" == typeof define && define.amd ? define([], r) : "object" == typeof exports ? exports.freighterApi = r() : e.freighterApi = r();
|
|
34
|
+
})(exports, (() => (() => {
|
|
35
35
|
var e, r, E = { d: (e2, r2) => {
|
|
36
36
|
for (var o2 in r2) E.o(r2, o2) && !E.o(e2, o2) && Object.defineProperty(e2, o2, { enumerable: true, get: r2[o2] });
|
|
37
37
|
}, o: (e2, r2) => Object.prototype.hasOwnProperty.call(e2, r2), r: (e2) => {
|
|
@@ -325,9 +325,7 @@ var WebCryptoKeyManager = class {
|
|
|
325
325
|
*/
|
|
326
326
|
this._initPromise = null;
|
|
327
327
|
if (typeof globalThis.crypto === "undefined" || !globalThis.crypto.subtle) {
|
|
328
|
-
throw new Error(
|
|
329
|
-
"[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost)."
|
|
330
|
-
);
|
|
328
|
+
throw new Error("[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost).");
|
|
331
329
|
}
|
|
332
330
|
this.apiKey = apiKey;
|
|
333
331
|
}
|
|
@@ -900,14 +898,18 @@ function createApiClient(baseUrl) {
|
|
|
900
898
|
async function listDistributionRules(api) {
|
|
901
899
|
const { data, error } = await api.GET("/distribution/rules");
|
|
902
900
|
if (!data?.content || error) {
|
|
903
|
-
throw new Error(
|
|
901
|
+
throw new Error(
|
|
902
|
+
error?.code ?? error?.error ?? "Failed to list distribution rules"
|
|
903
|
+
);
|
|
904
904
|
}
|
|
905
905
|
return data.content.rules;
|
|
906
906
|
}
|
|
907
907
|
async function claimDistributionRule(api, body) {
|
|
908
908
|
const { data, error } = await api.POST("/distribution/claim", { body });
|
|
909
909
|
if (!data?.content || error) {
|
|
910
|
-
throw new Error(
|
|
910
|
+
throw new Error(
|
|
911
|
+
error?.code ?? error?.error ?? "Failed to claim distribution rule"
|
|
912
|
+
);
|
|
911
913
|
}
|
|
912
914
|
return data.content;
|
|
913
915
|
}
|
|
@@ -918,18 +920,18 @@ async function getKycStatus(api, providerId) {
|
|
|
918
920
|
params: { query: providerId ? { providerId } : {} }
|
|
919
921
|
});
|
|
920
922
|
if (!data?.content || error) {
|
|
921
|
-
throw new Error(error?.error ?? "Failed to get KYC status");
|
|
923
|
+
throw new Error(error?.code ?? error?.error ?? "Failed to get KYC status");
|
|
922
924
|
}
|
|
923
925
|
return data.content;
|
|
924
926
|
}
|
|
925
927
|
async function getKycProviders(api, country) {
|
|
926
928
|
const { data, error } = await api.GET("/kyc/providers", { params: { query: { country } } });
|
|
927
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to get KYC providers");
|
|
929
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get KYC providers");
|
|
928
930
|
return data.content;
|
|
929
931
|
}
|
|
930
932
|
async function startKyc(api, body) {
|
|
931
933
|
const { data, error } = await api.POST("/kyc/start", { body });
|
|
932
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to start KYC");
|
|
934
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to start KYC");
|
|
933
935
|
return data.content;
|
|
934
936
|
}
|
|
935
937
|
async function resolveKyc(api, providerId, level = "basic") {
|
|
@@ -951,22 +953,22 @@ async function pollKycStatus(api, providerId, { intervalMs = 3e3, timeoutMs = 3e
|
|
|
951
953
|
// src/api/endpoints/ramps.ts
|
|
952
954
|
async function getRampsQuote(api, query) {
|
|
953
955
|
const { data, error } = await api.GET("/ramps/quote", { params: { query } });
|
|
954
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to get ramp quotes");
|
|
956
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get ramp quotes");
|
|
955
957
|
return data.content;
|
|
956
958
|
}
|
|
957
959
|
async function createOnRamp(api, body) {
|
|
958
960
|
const { data, error } = await api.POST("/ramps/onramp", { body });
|
|
959
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to create onramp");
|
|
961
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to create onramp");
|
|
960
962
|
return data.content;
|
|
961
963
|
}
|
|
962
964
|
async function createOffRamp(api, body) {
|
|
963
965
|
const { data, error } = await api.POST("/ramps/offramp", { body });
|
|
964
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to create offramp");
|
|
966
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to create offramp");
|
|
965
967
|
return data.content;
|
|
966
968
|
}
|
|
967
969
|
async function getRampTransaction(api, txId) {
|
|
968
970
|
const { data, error } = await api.GET("/ramps/transaction/{txId}", { params: { path: { txId } } });
|
|
969
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to get transaction");
|
|
971
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get transaction");
|
|
970
972
|
return data.content;
|
|
971
973
|
}
|
|
972
974
|
async function pollRampTransaction(api, txId, { intervalMs = 5e3, timeoutMs = 6e5 } = {}) {
|
|
@@ -979,6 +981,26 @@ async function pollRampTransaction(api, txId, { intervalMs = 5e3, timeoutMs = 6e
|
|
|
979
981
|
throw new Error("Ramp transaction polling timed out");
|
|
980
982
|
}
|
|
981
983
|
|
|
984
|
+
// src/lib/random-uuid.ts
|
|
985
|
+
function randomUUID() {
|
|
986
|
+
const c = globalThis.crypto;
|
|
987
|
+
if (c && typeof c.randomUUID === "function") {
|
|
988
|
+
return c.randomUUID();
|
|
989
|
+
}
|
|
990
|
+
if (c && typeof c.getRandomValues === "function") {
|
|
991
|
+
const bytes = new Uint8Array(16);
|
|
992
|
+
c.getRandomValues(bytes);
|
|
993
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
994
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
995
|
+
const hex = [];
|
|
996
|
+
for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
|
|
997
|
+
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
|
|
998
|
+
}
|
|
999
|
+
throw new Error(
|
|
1000
|
+
"[PollarClient] No secure random source available (crypto.randomUUID / crypto.getRandomValues). DPoP requires a secure context (HTTPS) or, in React Native, the `react-native-get-random-values` polyfill."
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
982
1004
|
// src/dpop.ts
|
|
983
1005
|
async function buildProof(args, keyManager) {
|
|
984
1006
|
const jwk = await keyManager.getPublicJwk();
|
|
@@ -988,7 +1010,7 @@ async function buildProof(args, keyManager) {
|
|
|
988
1010
|
jwk
|
|
989
1011
|
};
|
|
990
1012
|
const payload = {
|
|
991
|
-
jti:
|
|
1013
|
+
jti: randomUUID(),
|
|
992
1014
|
htm: args.htm.toUpperCase(),
|
|
993
1015
|
htu: normalizeHtu(args.htu),
|
|
994
1016
|
iat: Math.floor(Date.now() / 1e3)
|
|
@@ -1022,52 +1044,6 @@ function normalizeHtu(rawUrl) {
|
|
|
1022
1044
|
const portPart = port ? `:${port}` : "";
|
|
1023
1045
|
return `${scheme}//${host}${portPart}${url.pathname}`;
|
|
1024
1046
|
}
|
|
1025
|
-
function generateJti() {
|
|
1026
|
-
const c = globalThis.crypto;
|
|
1027
|
-
if (c && typeof c.randomUUID === "function") {
|
|
1028
|
-
return c.randomUUID();
|
|
1029
|
-
}
|
|
1030
|
-
if (c && typeof c.getRandomValues === "function") {
|
|
1031
|
-
const bytes = new Uint8Array(16);
|
|
1032
|
-
c.getRandomValues(bytes);
|
|
1033
|
-
bytes[6] = bytes[6] & 15 | 64;
|
|
1034
|
-
bytes[8] = bytes[8] & 63 | 128;
|
|
1035
|
-
const hex = [];
|
|
1036
|
-
for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
|
|
1037
|
-
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
|
|
1038
|
-
}
|
|
1039
|
-
throw new Error(
|
|
1040
|
-
"[PollarClient:dpop] No secure random source available (crypto.randomUUID / crypto.getRandomValues). DPoP requires a secure context (HTTPS) or, in React Native, the `react-native-get-random-values` polyfill."
|
|
1041
|
-
);
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// src/stellar/StellarClient.ts
|
|
1045
|
-
var HORIZON_URLS = {
|
|
1046
|
-
mainnet: "https://horizon.stellar.org",
|
|
1047
|
-
testnet: "https://horizon-testnet.stellar.org"
|
|
1048
|
-
};
|
|
1049
|
-
var StellarClient = class {
|
|
1050
|
-
constructor(config) {
|
|
1051
|
-
this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
|
|
1052
|
-
}
|
|
1053
|
-
async submitTransaction(signedXdr) {
|
|
1054
|
-
try {
|
|
1055
|
-
const response = await fetch(`${this.horizonUrl}/transactions`, {
|
|
1056
|
-
method: "POST",
|
|
1057
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1058
|
-
body: new URLSearchParams({ tx: signedXdr })
|
|
1059
|
-
});
|
|
1060
|
-
if (!response.ok) {
|
|
1061
|
-
const body = await response.json().catch(() => ({}));
|
|
1062
|
-
return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
|
|
1063
|
-
}
|
|
1064
|
-
const data = await response.json();
|
|
1065
|
-
return { success: true, hash: data.hash };
|
|
1066
|
-
} catch {
|
|
1067
|
-
return { success: false, errorCode: "NETWORK_ERROR" };
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
};
|
|
1071
1047
|
|
|
1072
1048
|
// src/storage/web.ts
|
|
1073
1049
|
var LOG_PREFIX = "[PollarClient:storage]";
|
|
@@ -1156,9 +1132,62 @@ function defaultStorage(options = {}) {
|
|
|
1156
1132
|
return createLocalStorageAdapter(options);
|
|
1157
1133
|
}
|
|
1158
1134
|
|
|
1135
|
+
// src/visibility/noop.ts
|
|
1136
|
+
function createNoopVisibilityProvider() {
|
|
1137
|
+
return {
|
|
1138
|
+
isVisible: () => true,
|
|
1139
|
+
onChange: () => () => {
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// src/visibility/web.ts
|
|
1145
|
+
function createWebVisibilityProvider() {
|
|
1146
|
+
const isVisibleNow = () => typeof document === "undefined" || document.visibilityState === "visible";
|
|
1147
|
+
return {
|
|
1148
|
+
isVisible: isVisibleNow,
|
|
1149
|
+
onChange: (cb) => {
|
|
1150
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
1151
|
+
return () => {
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
let last = isVisibleNow();
|
|
1155
|
+
const handler = () => {
|
|
1156
|
+
const next = isVisibleNow();
|
|
1157
|
+
if (next !== last) {
|
|
1158
|
+
last = next;
|
|
1159
|
+
cb(next);
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
document.addEventListener("visibilitychange", handler);
|
|
1163
|
+
window.addEventListener("pageshow", handler);
|
|
1164
|
+
window.addEventListener("pagehide", handler);
|
|
1165
|
+
window.addEventListener("focus", handler);
|
|
1166
|
+
window.addEventListener("blur", handler);
|
|
1167
|
+
return () => {
|
|
1168
|
+
document.removeEventListener("visibilitychange", handler);
|
|
1169
|
+
window.removeEventListener("pageshow", handler);
|
|
1170
|
+
window.removeEventListener("pagehide", handler);
|
|
1171
|
+
window.removeEventListener("focus", handler);
|
|
1172
|
+
window.removeEventListener("blur", handler);
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// src/visibility/autodetect.ts
|
|
1179
|
+
function defaultVisibilityProvider() {
|
|
1180
|
+
if (typeof document !== "undefined" && typeof window !== "undefined") {
|
|
1181
|
+
return createWebVisibilityProvider();
|
|
1182
|
+
}
|
|
1183
|
+
return createNoopVisibilityProvider();
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1159
1186
|
// src/types.ts
|
|
1160
1187
|
var AUTH_ERROR_CODES = {
|
|
1161
1188
|
SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
|
|
1189
|
+
SESSION_EXPIRED: "SESSION_EXPIRED",
|
|
1190
|
+
SESSION_INVALID: "SESSION_INVALID",
|
|
1162
1191
|
EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
|
|
1163
1192
|
EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
|
|
1164
1193
|
EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
|
|
@@ -1166,6 +1195,7 @@ var AUTH_ERROR_CODES = {
|
|
|
1166
1195
|
AUTH_FAILED: "AUTH_FAILED",
|
|
1167
1196
|
WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
|
|
1168
1197
|
WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
|
|
1198
|
+
WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
|
|
1169
1199
|
UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
|
|
1170
1200
|
};
|
|
1171
1201
|
var PollarFlowError = class extends Error {
|
|
@@ -1471,7 +1501,32 @@ async function readWalletType(storage, apiKeyHash) {
|
|
|
1471
1501
|
return storage.get(walletTypeStorageKey(apiKeyHash));
|
|
1472
1502
|
}
|
|
1473
1503
|
|
|
1504
|
+
// src/lib/abort.ts
|
|
1505
|
+
function abortError() {
|
|
1506
|
+
if (typeof DOMException !== "undefined") {
|
|
1507
|
+
return new DOMException("Aborted", "AbortError");
|
|
1508
|
+
}
|
|
1509
|
+
const err = new Error("Aborted");
|
|
1510
|
+
err.name = "AbortError";
|
|
1511
|
+
return err;
|
|
1512
|
+
}
|
|
1513
|
+
function throwIfAborted(signal) {
|
|
1514
|
+
if (signal?.aborted) throw abortError();
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1474
1517
|
// src/client/stream.ts
|
|
1518
|
+
var SessionStatusError = class extends Error {
|
|
1519
|
+
constructor(code) {
|
|
1520
|
+
super(`[PollarClient] Session status terminal: ${code}`);
|
|
1521
|
+
this.code = code;
|
|
1522
|
+
this.name = "SessionStatusError";
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
function terminalStatusCode(parsed) {
|
|
1526
|
+
const err = parsed?.error;
|
|
1527
|
+
if (err === "INVALID_CLIENT_SESSION_ID" || err === "EXPIRED_CLIENT_ID") return err;
|
|
1528
|
+
return null;
|
|
1529
|
+
}
|
|
1475
1530
|
function abortableDelay(ms, signal) {
|
|
1476
1531
|
return new Promise((resolve, reject) => {
|
|
1477
1532
|
const t = setTimeout(resolve, ms);
|
|
@@ -1479,7 +1534,7 @@ function abortableDelay(ms, signal) {
|
|
|
1479
1534
|
"abort",
|
|
1480
1535
|
() => {
|
|
1481
1536
|
clearTimeout(t);
|
|
1482
|
-
reject(
|
|
1537
|
+
reject(abortError());
|
|
1483
1538
|
},
|
|
1484
1539
|
{ once: true }
|
|
1485
1540
|
);
|
|
@@ -1494,7 +1549,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
|
|
|
1494
1549
|
else await new Promise((r) => setTimeout(r, ms));
|
|
1495
1550
|
};
|
|
1496
1551
|
while (true) {
|
|
1497
|
-
|
|
1552
|
+
throwIfAborted(signal);
|
|
1498
1553
|
let data, error;
|
|
1499
1554
|
try {
|
|
1500
1555
|
({ data, error } = await api.GET("/auth/session/status/{clientSessionId}", {
|
|
@@ -1517,7 +1572,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
|
|
|
1517
1572
|
let sawAnyChunk = false;
|
|
1518
1573
|
try {
|
|
1519
1574
|
while (true) {
|
|
1520
|
-
|
|
1575
|
+
throwIfAborted(signal);
|
|
1521
1576
|
const { done, value } = await reader.read();
|
|
1522
1577
|
if (done) {
|
|
1523
1578
|
streamDone = true;
|
|
@@ -1528,17 +1583,22 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
|
|
|
1528
1583
|
for (const message of chunk.split("\n\n").filter(Boolean)) {
|
|
1529
1584
|
const dataLine = message.split("\n").find((l) => l.startsWith("data:"));
|
|
1530
1585
|
if (!dataLine) continue;
|
|
1586
|
+
let parsed;
|
|
1531
1587
|
try {
|
|
1532
|
-
|
|
1533
|
-
if (check(parsed)) {
|
|
1534
|
-
return parsed;
|
|
1535
|
-
}
|
|
1588
|
+
parsed = JSON.parse(dataLine.slice("data:".length).trim());
|
|
1536
1589
|
} catch {
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
const terminal = terminalStatusCode(parsed);
|
|
1593
|
+
if (terminal) throw new SessionStatusError(terminal);
|
|
1594
|
+
if (check(parsed)) {
|
|
1595
|
+
return parsed;
|
|
1537
1596
|
}
|
|
1538
1597
|
}
|
|
1539
1598
|
}
|
|
1540
1599
|
} catch (e) {
|
|
1541
1600
|
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
1601
|
+
if (e instanceof SessionStatusError) throw e;
|
|
1542
1602
|
console.warn(e);
|
|
1543
1603
|
} finally {
|
|
1544
1604
|
reader.releaseLock();
|
|
@@ -1549,14 +1609,74 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
|
|
|
1549
1609
|
if (delay) await sleep(delay);
|
|
1550
1610
|
}
|
|
1551
1611
|
}
|
|
1612
|
+
async function pollUntilFound(baseUrl, clientSessionId, check, intervalMs = 500, signal) {
|
|
1613
|
+
const url = `${baseUrl}/auth/session/status/${encodeURIComponent(clientSessionId)}/poll`;
|
|
1614
|
+
let backoff = intervalMs;
|
|
1615
|
+
const sleep = async (ms) => {
|
|
1616
|
+
if (ms <= 0) return;
|
|
1617
|
+
if (signal) await abortableDelay(ms, signal);
|
|
1618
|
+
else await new Promise((r) => setTimeout(r, ms));
|
|
1619
|
+
};
|
|
1620
|
+
while (true) {
|
|
1621
|
+
throwIfAborted(signal);
|
|
1622
|
+
let envelope = null;
|
|
1623
|
+
let httpStatus = 0;
|
|
1624
|
+
try {
|
|
1625
|
+
const response = await fetch(url, { headers: { accept: "application/json" }, signal: signal ?? null });
|
|
1626
|
+
httpStatus = response.status;
|
|
1627
|
+
envelope = await response.json().catch(() => null);
|
|
1628
|
+
} catch (e) {
|
|
1629
|
+
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
1630
|
+
console.warn(e);
|
|
1631
|
+
}
|
|
1632
|
+
if (httpStatus === 404 || envelope?.code === "INVALID_CLIENT_SESSION_ID") {
|
|
1633
|
+
throw new SessionStatusError("INVALID_CLIENT_SESSION_ID");
|
|
1634
|
+
}
|
|
1635
|
+
if (httpStatus === 410 || envelope?.code === "EXPIRED_CLIENT_ID") {
|
|
1636
|
+
throw new SessionStatusError("EXPIRED_CLIENT_ID");
|
|
1637
|
+
}
|
|
1638
|
+
if (envelope?.success && envelope.content && check(envelope.content)) {
|
|
1639
|
+
return envelope.content;
|
|
1640
|
+
}
|
|
1641
|
+
if (envelope) backoff = intervalMs;
|
|
1642
|
+
else backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
1643
|
+
await sleep(backoff);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
function waitForSessionReady(args) {
|
|
1647
|
+
const { api, baseUrl, clientSessionId, check, useStreaming, retryDelayMs, signal } = args;
|
|
1648
|
+
return useStreaming ? streamUntilFound(api, clientSessionId, check, retryDelayMs ?? 200, signal) : pollUntilFound(baseUrl, clientSessionId, check, retryDelayMs ?? 500, signal);
|
|
1649
|
+
}
|
|
1552
1650
|
|
|
1553
1651
|
// src/client/auth/authenticate.ts
|
|
1554
1652
|
async function authenticate(clientSessionId, deps, expectedWallet) {
|
|
1555
|
-
const { api, signal, setAuthState, storeSession, clearSession } = deps;
|
|
1653
|
+
const { api, basePath, useStreaming, signal, setAuthState, storeSession, clearSession } = deps;
|
|
1556
1654
|
setAuthState({ step: "authenticating" });
|
|
1557
|
-
|
|
1655
|
+
try {
|
|
1656
|
+
await waitForSessionReady({
|
|
1657
|
+
api,
|
|
1658
|
+
baseUrl: basePath,
|
|
1659
|
+
clientSessionId,
|
|
1660
|
+
check: (data2) => data2?.status === "READY",
|
|
1661
|
+
useStreaming,
|
|
1662
|
+
signal
|
|
1663
|
+
});
|
|
1664
|
+
} catch (err) {
|
|
1665
|
+
if (err instanceof SessionStatusError) {
|
|
1666
|
+
const expired = err.code === "EXPIRED_CLIENT_ID";
|
|
1667
|
+
setAuthState({
|
|
1668
|
+
step: "error",
|
|
1669
|
+
previousStep: "authenticating",
|
|
1670
|
+
message: expired ? "Login session expired \u2014 please try again" : "Login session is no longer valid \u2014 please try again",
|
|
1671
|
+
errorCode: expired ? AUTH_ERROR_CODES.SESSION_EXPIRED : AUTH_ERROR_CODES.SESSION_INVALID
|
|
1672
|
+
});
|
|
1673
|
+
await clearSession();
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
throw err;
|
|
1677
|
+
}
|
|
1558
1678
|
const dpopJwk = await deps.getPublicJwk();
|
|
1559
|
-
const { data
|
|
1679
|
+
const { data } = await api.POST("/auth/login", {
|
|
1560
1680
|
body: {
|
|
1561
1681
|
clientSessionId,
|
|
1562
1682
|
dpopJwk,
|
|
@@ -1678,26 +1798,36 @@ function severOpener(popup) {
|
|
|
1678
1798
|
} catch {
|
|
1679
1799
|
}
|
|
1680
1800
|
}
|
|
1681
|
-
async
|
|
1682
|
-
const
|
|
1683
|
-
const popup = window.open("about:blank", "_blank");
|
|
1801
|
+
var defaultWebOAuthOpener = async ({ getUrl }) => {
|
|
1802
|
+
const popup = typeof window !== "undefined" ? window.open("about:blank", "_blank") : null;
|
|
1684
1803
|
severOpener(popup);
|
|
1685
|
-
const
|
|
1686
|
-
if (!
|
|
1804
|
+
const url = await getUrl();
|
|
1805
|
+
if (!url) {
|
|
1687
1806
|
popup?.close();
|
|
1688
1807
|
return;
|
|
1689
1808
|
}
|
|
1690
|
-
setAuthState({ step: "opening_oauth", provider });
|
|
1691
|
-
const url = new URL(`${basePath}/auth/${provider}`);
|
|
1692
|
-
url.searchParams.set("api_key", apiKey);
|
|
1693
|
-
url.searchParams.set("client_session_id", clientSessionId);
|
|
1694
|
-
url.searchParams.set("redirect_uri", window.location.origin);
|
|
1695
1809
|
if (popup) {
|
|
1696
|
-
popup.location.href = url
|
|
1810
|
+
popup.location.href = url;
|
|
1697
1811
|
severOpener(popup);
|
|
1698
|
-
} else {
|
|
1699
|
-
window.open(url
|
|
1812
|
+
} else if (typeof window !== "undefined") {
|
|
1813
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
1700
1814
|
}
|
|
1815
|
+
};
|
|
1816
|
+
async function loginOAuth(provider, deps) {
|
|
1817
|
+
const { setAuthState, basePath, apiKey, openAuthUrl, redirectUri, signal } = deps;
|
|
1818
|
+
let clientSessionId = null;
|
|
1819
|
+
const getUrl = async () => {
|
|
1820
|
+
clientSessionId = await createAuthSession(deps);
|
|
1821
|
+
if (!clientSessionId) return null;
|
|
1822
|
+
setAuthState({ step: "opening_oauth", provider });
|
|
1823
|
+
const url = new URL(`${basePath}/auth/${provider}`);
|
|
1824
|
+
url.searchParams.set("api_key", apiKey);
|
|
1825
|
+
url.searchParams.set("client_session_id", clientSessionId);
|
|
1826
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
1827
|
+
return url.toString();
|
|
1828
|
+
};
|
|
1829
|
+
await openAuthUrl({ provider, getUrl, redirectUri, signal });
|
|
1830
|
+
if (!clientSessionId) return;
|
|
1701
1831
|
await authenticate(clientSessionId, deps);
|
|
1702
1832
|
}
|
|
1703
1833
|
|
|
@@ -1707,10 +1837,10 @@ function withSignal(promise, signal) {
|
|
|
1707
1837
|
promise,
|
|
1708
1838
|
new Promise((_, reject) => {
|
|
1709
1839
|
if (signal.aborted) {
|
|
1710
|
-
reject(
|
|
1840
|
+
reject(abortError());
|
|
1711
1841
|
return;
|
|
1712
1842
|
}
|
|
1713
|
-
signal.addEventListener("abort", () => reject(
|
|
1843
|
+
signal.addEventListener("abort", () => reject(abortError()), { once: true });
|
|
1714
1844
|
})
|
|
1715
1845
|
]);
|
|
1716
1846
|
}
|
|
@@ -1721,7 +1851,7 @@ async function loginWallet(type, deps) {
|
|
|
1721
1851
|
let connectedWallet;
|
|
1722
1852
|
try {
|
|
1723
1853
|
setAuthState({ step: "connecting_wallet", walletType: type });
|
|
1724
|
-
const adapter = await deps.resolveWalletAdapter(type);
|
|
1854
|
+
const adapter = await withSignal(deps.resolveWalletAdapter(type), signal);
|
|
1725
1855
|
const available = await withSignal(adapter.isAvailable(), signal);
|
|
1726
1856
|
if (!available) {
|
|
1727
1857
|
setAuthState({ step: "wallet_not_installed", walletType: type });
|
|
@@ -1758,6 +1888,9 @@ async function loginWallet(type, deps) {
|
|
|
1758
1888
|
|
|
1759
1889
|
// src/client/client.ts
|
|
1760
1890
|
var isBrowser = typeof window !== "undefined" && typeof localStorage !== "undefined";
|
|
1891
|
+
var isReactNative = typeof navigator !== "undefined" && navigator.product === "ReactNative";
|
|
1892
|
+
var isClientRuntime = isBrowser || isReactNative;
|
|
1893
|
+
var REFRESH_SKEW_SECONDS = 60;
|
|
1761
1894
|
function warnServerSide(method) {
|
|
1762
1895
|
console.warn(
|
|
1763
1896
|
`[PollarClient] ${method}() called server-side \u2014 browser APIs unavailable. Use PollarClient only in Client Components.`
|
|
@@ -1785,6 +1918,11 @@ var PollarClient = class {
|
|
|
1785
1918
|
/** Singleton in-flight refresh — concurrent 401s coalesce into one /auth/refresh call. */
|
|
1786
1919
|
this._refreshPromise = null;
|
|
1787
1920
|
this._storageEventHandler = null;
|
|
1921
|
+
/** Updated by the request middleware. Read by the silent-refresh scheduler
|
|
1922
|
+
* to skip proactive refreshes after `maxIdleMs` of no HTTP activity. */
|
|
1923
|
+
this._lastRequestAt = Date.now();
|
|
1924
|
+
this._refreshTimer = null;
|
|
1925
|
+
this._visibilityUnsubscribe = null;
|
|
1788
1926
|
this._transactionState = null;
|
|
1789
1927
|
this._transactionStateListeners = /* @__PURE__ */ new Set();
|
|
1790
1928
|
this._txHistoryState = { step: "idle" };
|
|
@@ -1795,19 +1933,38 @@ var PollarClient = class {
|
|
|
1795
1933
|
this._authStateListeners = /* @__PURE__ */ new Set();
|
|
1796
1934
|
this._networkState = { step: "idle" };
|
|
1797
1935
|
this._networkStateListeners = /* @__PURE__ */ new Set();
|
|
1936
|
+
/**
|
|
1937
|
+
* Latched once the storage adapter degrades. We dedupe (the adapter only
|
|
1938
|
+
* fires once anyway) and use it to replay state to late-subscribers — same
|
|
1939
|
+
* pattern as `onAuthStateChange` replaying `_authState` on subscribe.
|
|
1940
|
+
* Only populated when the SDK constructed the default storage adapter; if
|
|
1941
|
+
* the consumer passes `config.storage`, they own degradation notifications.
|
|
1942
|
+
*/
|
|
1943
|
+
this._storageDegraded = null;
|
|
1944
|
+
this._storageDegradeListeners = /* @__PURE__ */ new Set();
|
|
1798
1945
|
this._walletAdapter = null;
|
|
1799
1946
|
this._loginController = null;
|
|
1800
1947
|
this.apiKey = config.apiKey;
|
|
1801
|
-
this.id =
|
|
1948
|
+
this.id = randomUUID();
|
|
1802
1949
|
this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
|
|
1803
|
-
this._storage = config.storage ?? defaultStorage(
|
|
1950
|
+
this._storage = config.storage ?? defaultStorage({
|
|
1951
|
+
onDegrade: (reason, error) => {
|
|
1952
|
+
config.onStorageDegrade?.(reason, error);
|
|
1953
|
+
this._dispatchStorageDegrade(reason, error);
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1804
1956
|
this._keyManager = config.keyManager ?? defaultKeyManager(this._storage, config.apiKey);
|
|
1805
1957
|
this._walletAdapterResolver = config.walletAdapter ?? null;
|
|
1958
|
+
this._walletResolverTimeoutMs = config.walletResolverTimeoutMs ?? 5e3;
|
|
1806
1959
|
this._deviceLabel = config.deviceLabel;
|
|
1960
|
+
this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
|
|
1961
|
+
this._maxIdleMs = config.maxIdleMs;
|
|
1962
|
+
this._openAuthUrl = config.openAuthUrl ?? defaultWebOAuthOpener;
|
|
1963
|
+
this._oauthRedirectUri = config.oauthRedirectUri ?? (isBrowser ? window.location.origin : "");
|
|
1807
1964
|
this._api = createApiClient(this.basePath);
|
|
1808
1965
|
this._wireMiddlewares();
|
|
1809
1966
|
this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
|
|
1810
|
-
if (!
|
|
1967
|
+
if (!isClientRuntime) {
|
|
1811
1968
|
warnServerSide("constructor");
|
|
1812
1969
|
this._initialized = Promise.resolve();
|
|
1813
1970
|
return;
|
|
@@ -1833,7 +1990,7 @@ var PollarClient = class {
|
|
|
1833
1990
|
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
1834
1991
|
async _initialize() {
|
|
1835
1992
|
this._apiKeyHash = await hashApiKey(this.apiKey);
|
|
1836
|
-
if (
|
|
1993
|
+
if (isBrowser) {
|
|
1837
1994
|
const sessionKey = sessionStorageKey(this._apiKeyHash);
|
|
1838
1995
|
const handler = (e) => {
|
|
1839
1996
|
if (e.key === sessionKey) {
|
|
@@ -1849,15 +2006,23 @@ var PollarClient = class {
|
|
|
1849
2006
|
console.warn("[PollarClient] KeyManager init failed; DPoP unavailable for this session", err);
|
|
1850
2007
|
}
|
|
1851
2008
|
await this._restoreSession();
|
|
2009
|
+
this._visibilityUnsubscribe = this._visibilityProvider.onChange((visible) => {
|
|
2010
|
+
if (visible) void this._maybeProactiveRefresh();
|
|
2011
|
+
});
|
|
1852
2012
|
}
|
|
1853
2013
|
/** Detach the cross-tab storage listener and abort any in-flight login. */
|
|
1854
2014
|
destroy() {
|
|
1855
|
-
if (this._storageEventHandler &&
|
|
2015
|
+
if (this._storageEventHandler && isBrowser) {
|
|
1856
2016
|
window.removeEventListener("storage", this._storageEventHandler);
|
|
1857
2017
|
this._storageEventHandler = null;
|
|
1858
2018
|
}
|
|
1859
2019
|
this._loginController?.abort();
|
|
1860
2020
|
this._loginController = null;
|
|
2021
|
+
this._clearRefreshTimer();
|
|
2022
|
+
if (this._visibilityUnsubscribe) {
|
|
2023
|
+
this._visibilityUnsubscribe();
|
|
2024
|
+
this._visibilityUnsubscribe = null;
|
|
2025
|
+
}
|
|
1861
2026
|
}
|
|
1862
2027
|
// ─── Middlewares (DPoP + auto-refresh) ────────────────────────────────────
|
|
1863
2028
|
_wireMiddlewares() {
|
|
@@ -1865,6 +2030,7 @@ var PollarClient = class {
|
|
|
1865
2030
|
this._api.use({
|
|
1866
2031
|
onRequest: async ({ request }) => {
|
|
1867
2032
|
request.headers.set("x-pollar-api-key", self.apiKey);
|
|
2033
|
+
self._lastRequestAt = Date.now();
|
|
1868
2034
|
await self._initialized;
|
|
1869
2035
|
if (request.body !== null) {
|
|
1870
2036
|
try {
|
|
@@ -1895,15 +2061,22 @@ var PollarClient = class {
|
|
|
1895
2061
|
const newNonce = response.headers.get("DPoP-Nonce");
|
|
1896
2062
|
if (newNonce) self._dpopNonce = newNonce;
|
|
1897
2063
|
if (response.status !== 401) return response;
|
|
1898
|
-
if (request.url.includes("/auth/refresh")) return response;
|
|
1899
2064
|
const wwwAuth = response.headers.get("WWW-Authenticate") ?? "";
|
|
1900
2065
|
const isNonceChallenge = wwwAuth.includes("use_dpop_nonce");
|
|
2066
|
+
if (request.url.includes("/auth/refresh")) {
|
|
2067
|
+
if (isNonceChallenge) return self._retryRequest(request);
|
|
2068
|
+
return response;
|
|
2069
|
+
}
|
|
1901
2070
|
if (!isNonceChallenge) {
|
|
1902
2071
|
try {
|
|
1903
2072
|
await self.refresh();
|
|
1904
2073
|
} catch {
|
|
1905
2074
|
return response;
|
|
1906
2075
|
}
|
|
2076
|
+
const method = request.method.toUpperCase();
|
|
2077
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
2078
|
+
return response;
|
|
2079
|
+
}
|
|
1907
2080
|
}
|
|
1908
2081
|
return self._retryRequest(request);
|
|
1909
2082
|
}
|
|
@@ -1928,14 +2101,22 @@ var PollarClient = class {
|
|
|
1928
2101
|
}
|
|
1929
2102
|
async _retryRequest(originalRequest) {
|
|
1930
2103
|
const headers = new Headers(originalRequest.headers);
|
|
1931
|
-
const
|
|
1932
|
-
if (
|
|
1933
|
-
const proof = await this._buildProofForRequest(originalRequest,
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
2104
|
+
const isRefresh = originalRequest.url.includes("/auth/refresh");
|
|
2105
|
+
if (isRefresh) {
|
|
2106
|
+
const proof = await this._buildProofForRequest(originalRequest, void 0);
|
|
2107
|
+
headers.delete("Authorization");
|
|
2108
|
+
if (proof) headers.set("DPoP", proof);
|
|
2109
|
+
else headers.delete("DPoP");
|
|
2110
|
+
} else {
|
|
2111
|
+
const accessToken = this._session?.token?.accessToken;
|
|
2112
|
+
if (accessToken) {
|
|
2113
|
+
const proof = await this._buildProofForRequest(originalRequest, accessToken);
|
|
2114
|
+
if (proof) {
|
|
2115
|
+
headers.set("Authorization", `DPoP ${accessToken}`);
|
|
2116
|
+
headers.set("DPoP", proof);
|
|
2117
|
+
} else {
|
|
2118
|
+
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
2119
|
+
}
|
|
1939
2120
|
}
|
|
1940
2121
|
}
|
|
1941
2122
|
const cachedBody = this._requestBodyCache.get(originalRequest);
|
|
@@ -2006,6 +2187,65 @@ var PollarClient = class {
|
|
|
2006
2187
|
} catch (err) {
|
|
2007
2188
|
console.error("[PollarClient] Failed to persist refreshed session", err);
|
|
2008
2189
|
}
|
|
2190
|
+
this._scheduleNextRefresh();
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
// ─── Silent refresh scheduler ────────────────────────────────────────────────
|
|
2194
|
+
/**
|
|
2195
|
+
* Arm a single setTimeout to fire shortly before the current access token
|
|
2196
|
+
* expires. Idempotent — clearing any previous timer first. Safe to call
|
|
2197
|
+
* from any session-write site (initial login, restore-from-storage, after
|
|
2198
|
+
* a successful rotation). No-op if there's no session in memory.
|
|
2199
|
+
*
|
|
2200
|
+
* Browser/RN background-tab throttling makes long-running setTimeouts
|
|
2201
|
+
* unreliable on their own; the `visibilitychange` listener compensates by
|
|
2202
|
+
* re-invoking `_maybeProactiveRefresh` whenever the app comes back to the
|
|
2203
|
+
* foreground, catching any timer that fired late or never fired at all.
|
|
2204
|
+
*/
|
|
2205
|
+
_scheduleNextRefresh() {
|
|
2206
|
+
this._clearRefreshTimer();
|
|
2207
|
+
const expiresAt = this._session?.token?.expiresAt;
|
|
2208
|
+
if (typeof expiresAt !== "number") return;
|
|
2209
|
+
const dueInMs = Math.max(0, (expiresAt - Math.floor(Date.now() / 1e3) - REFRESH_SKEW_SECONDS) * 1e3);
|
|
2210
|
+
this._refreshTimer = setTimeout(() => {
|
|
2211
|
+
void this._maybeProactiveRefresh();
|
|
2212
|
+
}, dueInMs);
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* Decide whether to actually run a refresh right now. Called both from the
|
|
2216
|
+
* scheduler timer and from the visibility-change listener.
|
|
2217
|
+
*
|
|
2218
|
+
* Skip if:
|
|
2219
|
+
* - no session / no RT (nothing to refresh)
|
|
2220
|
+
* - app is hidden — wait for the visibility listener to re-trigger us
|
|
2221
|
+
* - `maxIdleMs` configured and no client request since that window — let
|
|
2222
|
+
* the next reactive 401-refresh handle it whenever the user comes back
|
|
2223
|
+
* - the AT still has more than `REFRESH_SKEW_SECONDS` of life — reschedule
|
|
2224
|
+
*
|
|
2225
|
+
* Otherwise call `refresh()`, which uses the existing in-flight singleton
|
|
2226
|
+
* so we never collide with a reactive 401-triggered refresh. On failure,
|
|
2227
|
+
* `_doRefresh` already calls `_clearSession`, so auth-state listeners see
|
|
2228
|
+
* `step:'idle'` — no extra event dispatch needed here.
|
|
2229
|
+
*/
|
|
2230
|
+
async _maybeProactiveRefresh() {
|
|
2231
|
+
if (!this._session?.token?.refreshToken) return;
|
|
2232
|
+
if (!this._visibilityProvider.isVisible()) return;
|
|
2233
|
+
if (this._maxIdleMs !== void 0 && Date.now() - this._lastRequestAt > this._maxIdleMs) return;
|
|
2234
|
+
const expiresAt = this._session.token.expiresAt;
|
|
2235
|
+
if (Math.floor(Date.now() / 1e3) < expiresAt - REFRESH_SKEW_SECONDS) {
|
|
2236
|
+
this._scheduleNextRefresh();
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
try {
|
|
2240
|
+
await this.refresh();
|
|
2241
|
+
} catch (err) {
|
|
2242
|
+
console.warn("[PollarClient] Proactive refresh failed; session cleared", err);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
_clearRefreshTimer() {
|
|
2246
|
+
if (this._refreshTimer !== null) {
|
|
2247
|
+
clearTimeout(this._refreshTimer);
|
|
2248
|
+
this._refreshTimer = null;
|
|
2009
2249
|
}
|
|
2010
2250
|
}
|
|
2011
2251
|
// ─── Auth state ──────────────────────────────────────────────────────────────
|
|
@@ -2017,13 +2257,45 @@ var PollarClient = class {
|
|
|
2017
2257
|
cb(this._authState);
|
|
2018
2258
|
return () => this._authStateListeners.delete(cb);
|
|
2019
2259
|
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Subscribe to persistent-storage degradation (Safari private mode,
|
|
2262
|
+
* sandboxed iframes, quota errors, etc.). The SDK keeps running off
|
|
2263
|
+
* in-memory storage after degrade, but sessions won't survive reload — a
|
|
2264
|
+
* host UI typically wants to show "your session won't be saved" so the
|
|
2265
|
+
* user isn't blindsided after a refresh.
|
|
2266
|
+
*
|
|
2267
|
+
* Fires at most once per client lifetime (the underlying adapter dedupes).
|
|
2268
|
+
* Late subscribers receive the latched state synchronously on subscribe.
|
|
2269
|
+
*
|
|
2270
|
+
* Only fires when the SDK constructs the default storage adapter. If you
|
|
2271
|
+
* pass a custom `config.storage`, wire your own notification path through
|
|
2272
|
+
* that adapter's API — the SDK has no hook into it.
|
|
2273
|
+
*/
|
|
2274
|
+
onStorageDegrade(cb) {
|
|
2275
|
+
this._storageDegradeListeners.add(cb);
|
|
2276
|
+
if (this._storageDegraded) {
|
|
2277
|
+
cb(this._storageDegraded.reason, this._storageDegraded.error);
|
|
2278
|
+
}
|
|
2279
|
+
return () => this._storageDegradeListeners.delete(cb);
|
|
2280
|
+
}
|
|
2281
|
+
_dispatchStorageDegrade(reason, error) {
|
|
2282
|
+
if (this._storageDegraded) return;
|
|
2283
|
+
this._storageDegraded = { reason, error };
|
|
2284
|
+
for (const cb of this._storageDegradeListeners) {
|
|
2285
|
+
try {
|
|
2286
|
+
cb(reason, error);
|
|
2287
|
+
} catch (err) {
|
|
2288
|
+
console.error("[PollarClient] onStorageDegrade listener threw", err);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2020
2292
|
/** PII (email, names, avatar, providers). Held in memory only — never persisted. */
|
|
2021
2293
|
getUserProfile() {
|
|
2022
2294
|
return this._profile;
|
|
2023
2295
|
}
|
|
2024
2296
|
// ─── Login (unified entry point) ─────────────────────────────────────────
|
|
2025
2297
|
login(options) {
|
|
2026
|
-
if (!
|
|
2298
|
+
if (!isClientRuntime) {
|
|
2027
2299
|
warnServerSide("login");
|
|
2028
2300
|
return;
|
|
2029
2301
|
}
|
|
@@ -2034,7 +2306,9 @@ var PollarClient = class {
|
|
|
2034
2306
|
loginOAuth(options.provider, {
|
|
2035
2307
|
...deps,
|
|
2036
2308
|
basePath: this.basePath,
|
|
2037
|
-
apiKey: this.apiKey
|
|
2309
|
+
apiKey: this.apiKey,
|
|
2310
|
+
openAuthUrl: this._openAuthUrl,
|
|
2311
|
+
redirectUri: this._oauthRedirectUri
|
|
2038
2312
|
}).catch((err) => this._handleFlowError(err));
|
|
2039
2313
|
} else if (options.provider === "email") {
|
|
2040
2314
|
const { email } = options;
|
|
@@ -2050,7 +2324,7 @@ var PollarClient = class {
|
|
|
2050
2324
|
}
|
|
2051
2325
|
// ─── Email OTP flow (3 steps) ─────────────────────────────────────────────
|
|
2052
2326
|
beginEmailLogin() {
|
|
2053
|
-
if (!
|
|
2327
|
+
if (!isClientRuntime) {
|
|
2054
2328
|
warnServerSide("beginEmailLogin");
|
|
2055
2329
|
return;
|
|
2056
2330
|
}
|
|
@@ -2058,7 +2332,7 @@ var PollarClient = class {
|
|
|
2058
2332
|
initEmailSession(this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
|
|
2059
2333
|
}
|
|
2060
2334
|
sendEmailCode(email) {
|
|
2061
|
-
if (!
|
|
2335
|
+
if (!isClientRuntime) {
|
|
2062
2336
|
warnServerSide("sendEmailCode");
|
|
2063
2337
|
return;
|
|
2064
2338
|
}
|
|
@@ -2070,7 +2344,7 @@ var PollarClient = class {
|
|
|
2070
2344
|
sendEmailCode(email, clientSessionId, this._flowDeps(signal)).catch((err) => this._handleFlowError(err));
|
|
2071
2345
|
}
|
|
2072
2346
|
verifyEmailCode(code) {
|
|
2073
|
-
if (!
|
|
2347
|
+
if (!isClientRuntime) {
|
|
2074
2348
|
warnServerSide("verifyEmailCode");
|
|
2075
2349
|
return;
|
|
2076
2350
|
}
|
|
@@ -2088,7 +2362,7 @@ var PollarClient = class {
|
|
|
2088
2362
|
}
|
|
2089
2363
|
// ─── Wallet flow (single call) ────────────────────────────────────────────
|
|
2090
2364
|
loginWallet(type) {
|
|
2091
|
-
if (!
|
|
2365
|
+
if (!isClientRuntime) {
|
|
2092
2366
|
warnServerSide("loginWallet");
|
|
2093
2367
|
return;
|
|
2094
2368
|
}
|
|
@@ -2114,7 +2388,7 @@ var PollarClient = class {
|
|
|
2114
2388
|
* across all devices.
|
|
2115
2389
|
*/
|
|
2116
2390
|
async logout(options = {}) {
|
|
2117
|
-
if (!
|
|
2391
|
+
if (!isClientRuntime) {
|
|
2118
2392
|
warnServerSide("logout");
|
|
2119
2393
|
return;
|
|
2120
2394
|
}
|
|
@@ -2144,7 +2418,7 @@ var PollarClient = class {
|
|
|
2144
2418
|
* `current` flag identifies which entry corresponds to this client.
|
|
2145
2419
|
*/
|
|
2146
2420
|
async listSessions() {
|
|
2147
|
-
if (!
|
|
2421
|
+
if (!isClientRuntime) {
|
|
2148
2422
|
warnServerSide("listSessions");
|
|
2149
2423
|
return [];
|
|
2150
2424
|
}
|
|
@@ -2163,7 +2437,7 @@ var PollarClient = class {
|
|
|
2163
2437
|
* does NOT clear local state — call `logout()` for that case.
|
|
2164
2438
|
*/
|
|
2165
2439
|
async revokeSession(familyId) {
|
|
2166
|
-
if (!
|
|
2440
|
+
if (!isClientRuntime) {
|
|
2167
2441
|
warnServerSide("revokeSession");
|
|
2168
2442
|
return;
|
|
2169
2443
|
}
|
|
@@ -2253,10 +2527,16 @@ var PollarClient = class {
|
|
|
2253
2527
|
}
|
|
2254
2528
|
}
|
|
2255
2529
|
// ─── Transactions ─────────────────────────────────────────────────────────
|
|
2530
|
+
/**
|
|
2531
|
+
* Builds an unsigned XDR. Drives `_setTransactionState` for modal-style UIs
|
|
2532
|
+
* AND returns a {@link BuildOutcome} so headless callers can `await` and
|
|
2533
|
+
* inspect the result without subscribing to state changes.
|
|
2534
|
+
*/
|
|
2256
2535
|
async buildTx(operation, params, options) {
|
|
2257
2536
|
if (!this._session?.wallet?.publicKey) {
|
|
2258
|
-
|
|
2259
|
-
|
|
2537
|
+
const details = "No wallet connected";
|
|
2538
|
+
this._setTransactionState({ step: "error", phase: "building", details });
|
|
2539
|
+
return { status: "error", details };
|
|
2260
2540
|
}
|
|
2261
2541
|
const body = {
|
|
2262
2542
|
network: this.getNetwork(),
|
|
@@ -2270,40 +2550,194 @@ var PollarClient = class {
|
|
|
2270
2550
|
const { data, error } = await this._api.POST("/tx/build", { body });
|
|
2271
2551
|
if (!error && data?.success && data.content) {
|
|
2272
2552
|
this._setTransactionState({ step: "built", buildData: data.content });
|
|
2273
|
-
|
|
2274
|
-
const details = error?.details;
|
|
2275
|
-
this._setTransactionState({ step: "error", ...details && { details } });
|
|
2553
|
+
return { status: "built", buildData: data.content };
|
|
2276
2554
|
}
|
|
2555
|
+
const details = error?.details;
|
|
2556
|
+
this._setTransactionState({ step: "error", phase: "building", ...details && { details } });
|
|
2557
|
+
return { status: "error", ...details && { details } };
|
|
2277
2558
|
} catch (err) {
|
|
2278
2559
|
console.error("[PollarClient] buildTx failed", err);
|
|
2279
|
-
this._setTransactionState({ step: "error" });
|
|
2560
|
+
this._setTransactionState({ step: "error", phase: "building" });
|
|
2561
|
+
return { status: "error" };
|
|
2280
2562
|
}
|
|
2281
2563
|
}
|
|
2282
2564
|
getWalletType() {
|
|
2283
2565
|
return this._walletAdapter?.type ?? null;
|
|
2284
2566
|
}
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2567
|
+
/**
|
|
2568
|
+
* Signs the given unsigned XDR and returns the signed XDR.
|
|
2569
|
+
*
|
|
2570
|
+
* - External wallets: signs locally via the wallet adapter.
|
|
2571
|
+
* - Custodial wallets: posts to `/tx/sign`. The backend signs (through
|
|
2572
|
+
* wallet-service or the app's customer-managed adapter) and returns the
|
|
2573
|
+
* signed XDR plus an `idempotencyKey` the caller should echo back to
|
|
2574
|
+
* `submitTx`.
|
|
2575
|
+
*
|
|
2576
|
+
* Drives `_setTransactionState`: emits `signing` while in flight and
|
|
2577
|
+
* `signed` on success (or `error[phase: 'signing']` on failure). `buildData`
|
|
2578
|
+
* is threaded through if the consumer previously called `buildTx`.
|
|
2579
|
+
*/
|
|
2580
|
+
async signTx(unsignedXdr) {
|
|
2581
|
+
const buildData = this._currentBuildData();
|
|
2582
|
+
this._setTransactionState({ step: "signing", ...buildData && { buildData } });
|
|
2291
2583
|
if (this._walletAdapter) {
|
|
2584
|
+
const accountToSign = this._session?.wallet?.publicKey;
|
|
2585
|
+
const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
|
|
2292
2586
|
try {
|
|
2293
|
-
const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
|
|
2294
2587
|
const { signedTxXdr } = await this._walletAdapter.signTransaction(unsignedXdr, signOpts);
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2588
|
+
this._setTransactionState({
|
|
2589
|
+
step: "signed",
|
|
2590
|
+
signedXdr: signedTxXdr,
|
|
2591
|
+
...buildData && { buildData }
|
|
2592
|
+
});
|
|
2593
|
+
return { status: "signed", signedXdr: signedTxXdr };
|
|
2594
|
+
} catch (err) {
|
|
2595
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
2596
|
+
this._setTransactionState({
|
|
2597
|
+
step: "error",
|
|
2598
|
+
phase: "signing",
|
|
2599
|
+
...buildData && { buildData },
|
|
2600
|
+
...details && { details }
|
|
2601
|
+
});
|
|
2602
|
+
return { status: "error", ...details && { details } };
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
const publicKey = this._session?.wallet?.publicKey ?? "";
|
|
2606
|
+
try {
|
|
2607
|
+
const { data, error } = await this._api.POST("/tx/sign", {
|
|
2608
|
+
body: { network: this.getNetwork(), publicKey, unsignedXdr }
|
|
2609
|
+
});
|
|
2610
|
+
if (!error && data?.success && data.content?.signedXdr) {
|
|
2611
|
+
const { signedXdr, idempotencyKey } = data.content;
|
|
2612
|
+
this._setTransactionState({
|
|
2613
|
+
step: "signed",
|
|
2614
|
+
signedXdr,
|
|
2615
|
+
submissionToken: idempotencyKey,
|
|
2616
|
+
...buildData && { buildData }
|
|
2617
|
+
});
|
|
2618
|
+
return { status: "signed", signedXdr, submissionToken: idempotencyKey };
|
|
2619
|
+
}
|
|
2620
|
+
const details = error?.details;
|
|
2621
|
+
this._setTransactionState({
|
|
2622
|
+
step: "error",
|
|
2623
|
+
phase: "signing",
|
|
2624
|
+
...buildData && { buildData },
|
|
2625
|
+
...details && { details }
|
|
2626
|
+
});
|
|
2627
|
+
return { status: "error", ...details && { details } };
|
|
2628
|
+
} catch (err) {
|
|
2629
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
2630
|
+
this._setTransactionState({
|
|
2631
|
+
step: "error",
|
|
2632
|
+
phase: "signing",
|
|
2633
|
+
...buildData && { buildData },
|
|
2634
|
+
...details && { details }
|
|
2635
|
+
});
|
|
2636
|
+
return { status: "error", ...details && { details } };
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* Submits a signed XDR via `/tx/submit` regardless of wallet type
|
|
2641
|
+
* (custodial or external). Routing through sdk-api gives us:
|
|
2642
|
+
* - End-to-end tx_records persistence with full phase lifecycle so the
|
|
2643
|
+
* developer dashboard can show every tx (both custodial and external
|
|
2644
|
+
* wallet flows) at `/apps/:id/monitor/transactions`.
|
|
2645
|
+
* - Idempotency tracking via `submissionToken` (returned by `signTx`).
|
|
2646
|
+
* - A single response shape (SUCCESS / PENDING / FAILED) shared by both
|
|
2647
|
+
* flows — previously external wallets could only return SUCCESS or
|
|
2648
|
+
* error since the direct-to-Horizon path was synchronous.
|
|
2649
|
+
*
|
|
2650
|
+
* The extra hop adds ~50–150 ms vs. the legacy direct-Horizon path; the
|
|
2651
|
+
* persistence + observability win is worth it.
|
|
2652
|
+
*
|
|
2653
|
+
* Drives `_setTransactionState`: emits `submitting` while in flight,
|
|
2654
|
+
* `submitted` on Horizon ack (pending), `success` on ledger confirmation,
|
|
2655
|
+
* or `error[phase: 'submitting']` on failure.
|
|
2656
|
+
*/
|
|
2657
|
+
async submitTx(signedXdr, opts) {
|
|
2658
|
+
const buildData = this._currentBuildData();
|
|
2659
|
+
const outcomeExtra = buildData ? { buildData } : {};
|
|
2660
|
+
this._setTransactionState({ step: "submitting", signedXdr, ...buildData && { buildData } });
|
|
2661
|
+
const publicKey = this._session?.wallet?.publicKey ?? "";
|
|
2662
|
+
try {
|
|
2663
|
+
const { data, error } = await this._api.POST("/tx/submit", {
|
|
2664
|
+
body: {
|
|
2665
|
+
network: this.getNetwork(),
|
|
2666
|
+
publicKey,
|
|
2667
|
+
signedXdr,
|
|
2668
|
+
...opts?.submissionToken && { idempotencyKey: opts.submissionToken }
|
|
2301
2669
|
}
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2670
|
+
});
|
|
2671
|
+
if (!error && data?.success && data.content) {
|
|
2672
|
+
const { hash, status: backendStatus, resultCode } = data.content;
|
|
2673
|
+
if (backendStatus === "SUCCESS") {
|
|
2674
|
+
this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
|
|
2675
|
+
return { status: "success", hash, ...outcomeExtra };
|
|
2676
|
+
}
|
|
2677
|
+
if (backendStatus === "PENDING") {
|
|
2678
|
+
this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
|
|
2679
|
+
return { status: "pending", hash, ...outcomeExtra };
|
|
2680
|
+
}
|
|
2681
|
+
this._setTransactionState({
|
|
2682
|
+
step: "error",
|
|
2683
|
+
phase: "submitting",
|
|
2684
|
+
...buildData && { buildData },
|
|
2685
|
+
...resultCode && { details: resultCode }
|
|
2686
|
+
});
|
|
2687
|
+
return {
|
|
2688
|
+
status: "error",
|
|
2689
|
+
hash,
|
|
2690
|
+
...outcomeExtra,
|
|
2691
|
+
...resultCode && { details: resultCode, resultCode }
|
|
2692
|
+
};
|
|
2304
2693
|
}
|
|
2305
|
-
|
|
2694
|
+
const details = error?.details;
|
|
2695
|
+
this._setTransactionState({
|
|
2696
|
+
step: "error",
|
|
2697
|
+
phase: "submitting",
|
|
2698
|
+
...buildData && { buildData },
|
|
2699
|
+
...details && { details }
|
|
2700
|
+
});
|
|
2701
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
2702
|
+
} catch (err) {
|
|
2703
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
2704
|
+
this._setTransactionState({
|
|
2705
|
+
step: "error",
|
|
2706
|
+
phase: "submitting",
|
|
2707
|
+
...buildData && { buildData },
|
|
2708
|
+
...details && { details }
|
|
2709
|
+
});
|
|
2710
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
2306
2711
|
}
|
|
2712
|
+
}
|
|
2713
|
+
/**
|
|
2714
|
+
* Signs and submits in one logical step. Returns a {@link SubmitOutcome}.
|
|
2715
|
+
*
|
|
2716
|
+
* - **External wallets**: composes `signTx` + `submitTx` client-side. State
|
|
2717
|
+
* machine sees the full granular sequence `signing → signed → submitting
|
|
2718
|
+
* → success` because the underlying methods each emit.
|
|
2719
|
+
* - **Custodial wallets**: atomic `/tx/sign-and-send` round-trip. State
|
|
2720
|
+
* machine emits the compound `signing-submitting` step (the SDK can't
|
|
2721
|
+
* observe when one phase ends and the next begins inside that single
|
|
2722
|
+
* backend call) and then transitions to `submitted` (Horizon ack only) or
|
|
2723
|
+
* `success` (ledger-confirmed), or `error[phase: 'signing-submitting']`.
|
|
2724
|
+
*/
|
|
2725
|
+
async signAndSubmitTx(unsignedXdr) {
|
|
2726
|
+
if (this._walletAdapter) {
|
|
2727
|
+
const signed = await this.signTx(unsignedXdr);
|
|
2728
|
+
if (signed.status === "error") {
|
|
2729
|
+
const buildData2 = this._currentBuildData();
|
|
2730
|
+
return {
|
|
2731
|
+
status: "error",
|
|
2732
|
+
...buildData2 && { buildData: buildData2 },
|
|
2733
|
+
...signed.details && { details: signed.details }
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2736
|
+
return this.submitTx(signed.signedXdr);
|
|
2737
|
+
}
|
|
2738
|
+
const buildData = this._currentBuildData();
|
|
2739
|
+
const outcomeExtra = buildData ? { buildData } : {};
|
|
2740
|
+
this._setTransactionState({ step: "signing-submitting", ...buildData && { buildData } });
|
|
2307
2741
|
const body = {
|
|
2308
2742
|
network: this.getNetwork(),
|
|
2309
2743
|
publicKey: this._session?.wallet?.publicKey ?? "",
|
|
@@ -2312,15 +2746,129 @@ var PollarClient = class {
|
|
|
2312
2746
|
try {
|
|
2313
2747
|
const { data, error } = await this._api.POST("/tx/sign-and-send", { body });
|
|
2314
2748
|
if (!error && data?.success && data.content?.hash) {
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2749
|
+
const {
|
|
2750
|
+
hash,
|
|
2751
|
+
status: backendStatus,
|
|
2752
|
+
resultCode
|
|
2753
|
+
} = data.content;
|
|
2754
|
+
if (backendStatus === "SUCCESS") {
|
|
2755
|
+
this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
|
|
2756
|
+
return { status: "success", hash, ...outcomeExtra };
|
|
2757
|
+
}
|
|
2758
|
+
if (backendStatus === "PENDING") {
|
|
2759
|
+
this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
|
|
2760
|
+
return { status: "pending", hash, ...outcomeExtra };
|
|
2761
|
+
}
|
|
2762
|
+
this._setTransactionState({
|
|
2763
|
+
step: "error",
|
|
2764
|
+
phase: "signing-submitting",
|
|
2765
|
+
...buildData && { buildData },
|
|
2766
|
+
...resultCode && { details: resultCode }
|
|
2767
|
+
});
|
|
2768
|
+
return {
|
|
2769
|
+
status: "error",
|
|
2770
|
+
hash,
|
|
2771
|
+
...outcomeExtra,
|
|
2772
|
+
...resultCode && { details: resultCode, resultCode }
|
|
2773
|
+
};
|
|
2319
2774
|
}
|
|
2320
|
-
|
|
2321
|
-
this._setTransactionState({
|
|
2775
|
+
const details = error?.details;
|
|
2776
|
+
this._setTransactionState({
|
|
2777
|
+
step: "error",
|
|
2778
|
+
phase: "signing-submitting",
|
|
2779
|
+
...buildData && { buildData },
|
|
2780
|
+
...details && { details }
|
|
2781
|
+
});
|
|
2782
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
2783
|
+
} catch (err) {
|
|
2784
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
2785
|
+
this._setTransactionState({
|
|
2786
|
+
step: "error",
|
|
2787
|
+
phase: "signing-submitting",
|
|
2788
|
+
...buildData && { buildData },
|
|
2789
|
+
...details && { details }
|
|
2790
|
+
});
|
|
2791
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
2322
2792
|
}
|
|
2323
2793
|
}
|
|
2794
|
+
/**
|
|
2795
|
+
* One-shot: build → sign → submit, returning the final {@link SubmitOutcome}.
|
|
2796
|
+
*
|
|
2797
|
+
* - **External wallets**: composes `buildTx` + `signAndSubmitTx` client-side.
|
|
2798
|
+
* State machine sees the full granular sequence (`building → built →
|
|
2799
|
+
* signing → signed → submitting → success`) because each composed call
|
|
2800
|
+
* emits its own transitions.
|
|
2801
|
+
* - **Custodial wallets**: single round-trip to `/tx/build-sign-submit`. The
|
|
2802
|
+
* signed XDR never leaves the backend. State machine emits the compound
|
|
2803
|
+
* `building-signing-submitting` step (the SDK can't observe individual
|
|
2804
|
+
* phase boundaries inside one atomic call) and then transitions to
|
|
2805
|
+
* `submitted` / `success` / `error[phase: 'building-signing-submitting']`.
|
|
2806
|
+
*
|
|
2807
|
+
* If you need granular UI feedback for custodial flows (separate
|
|
2808
|
+
* "Building…", "Signing…", "Submitting…" indicators), call `buildTx`,
|
|
2809
|
+
* `signTx`, and `submitTx` separately instead.
|
|
2810
|
+
*/
|
|
2811
|
+
async buildAndSignAndSubmitTx(operation, params, options) {
|
|
2812
|
+
if (this._walletAdapter) {
|
|
2813
|
+
const built = await this.buildTx(operation, params, options);
|
|
2814
|
+
if (built.status === "error") {
|
|
2815
|
+
return { status: "error", ...built.details && { details: built.details } };
|
|
2816
|
+
}
|
|
2817
|
+
return this.signAndSubmitTx(built.buildData.unsignedXdr);
|
|
2818
|
+
}
|
|
2819
|
+
if (!this._session?.wallet?.publicKey) {
|
|
2820
|
+
this._setTransactionState({ step: "error", phase: "building-signing-submitting", details: "No wallet connected" });
|
|
2821
|
+
return { status: "error", details: "No wallet connected" };
|
|
2822
|
+
}
|
|
2823
|
+
this._setTransactionState({ step: "building-signing-submitting" });
|
|
2824
|
+
try {
|
|
2825
|
+
const { data, error } = await this._api.POST("/tx/build-sign-submit", {
|
|
2826
|
+
body: {
|
|
2827
|
+
network: this.getNetwork(),
|
|
2828
|
+
publicKey: this._session.wallet.publicKey,
|
|
2829
|
+
operation,
|
|
2830
|
+
params,
|
|
2831
|
+
options: options ?? {}
|
|
2832
|
+
}
|
|
2833
|
+
});
|
|
2834
|
+
if (!error && data?.success && data.content) {
|
|
2835
|
+
const { hash, status: backendStatus, resultCode } = data.content;
|
|
2836
|
+
if (backendStatus === "SUCCESS") {
|
|
2837
|
+
this._setTransactionState({ step: "success", hash });
|
|
2838
|
+
return { status: "success", hash };
|
|
2839
|
+
}
|
|
2840
|
+
if (backendStatus === "PENDING") {
|
|
2841
|
+
this._setTransactionState({ step: "submitted", hash });
|
|
2842
|
+
return { status: "pending", hash };
|
|
2843
|
+
}
|
|
2844
|
+
this._setTransactionState({
|
|
2845
|
+
step: "error",
|
|
2846
|
+
phase: "building-signing-submitting",
|
|
2847
|
+
...resultCode && { details: resultCode }
|
|
2848
|
+
});
|
|
2849
|
+
return { status: "error", hash, ...resultCode && { details: resultCode, resultCode } };
|
|
2850
|
+
}
|
|
2851
|
+
const details = error?.details;
|
|
2852
|
+
this._setTransactionState({
|
|
2853
|
+
step: "error",
|
|
2854
|
+
phase: "building-signing-submitting",
|
|
2855
|
+
...details && { details }
|
|
2856
|
+
});
|
|
2857
|
+
return { status: "error", ...details && { details } };
|
|
2858
|
+
} catch (err) {
|
|
2859
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
2860
|
+
this._setTransactionState({
|
|
2861
|
+
step: "error",
|
|
2862
|
+
phase: "building-signing-submitting",
|
|
2863
|
+
...details && { details }
|
|
2864
|
+
});
|
|
2865
|
+
return { status: "error", ...details && { details } };
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
/** Alias for {@link buildAndSignAndSubmitTx} — shorter "just do the thing" name. */
|
|
2869
|
+
async runTx(operation, params, options) {
|
|
2870
|
+
return this.buildAndSignAndSubmitTx(operation, params, options);
|
|
2871
|
+
}
|
|
2324
2872
|
// ─── App config ───────────────────────────────────────────────────────────
|
|
2325
2873
|
async getAppConfig() {
|
|
2326
2874
|
try {
|
|
@@ -2387,6 +2935,11 @@ var PollarClient = class {
|
|
|
2387
2935
|
_flowDeps(signal) {
|
|
2388
2936
|
return {
|
|
2389
2937
|
api: this._api,
|
|
2938
|
+
basePath: this.basePath,
|
|
2939
|
+
// SSE status streaming works on web; React Native's `fetch` has no
|
|
2940
|
+
// readable `response.body`, so those clients poll the non-streaming
|
|
2941
|
+
// status endpoint instead. `isBrowser` is false in RN and SSR alike.
|
|
2942
|
+
useStreaming: isBrowser,
|
|
2390
2943
|
signal,
|
|
2391
2944
|
setAuthState: this._setAuthState.bind(this),
|
|
2392
2945
|
storeSession: this._storeSession.bind(this),
|
|
@@ -2408,7 +2961,22 @@ var PollarClient = class {
|
|
|
2408
2961
|
*/
|
|
2409
2962
|
async _resolveWalletAdapter(id) {
|
|
2410
2963
|
if (this._walletAdapterResolver) {
|
|
2411
|
-
|
|
2964
|
+
const timeoutMs = this._walletResolverTimeoutMs;
|
|
2965
|
+
let timeoutHandle;
|
|
2966
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
2967
|
+
timeoutHandle = setTimeout(() => {
|
|
2968
|
+
reject(
|
|
2969
|
+
Object.assign(new Error(`[PollarClient] Wallet adapter resolver for "${id}" timed out after ${timeoutMs}ms`), {
|
|
2970
|
+
code: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
|
|
2971
|
+
})
|
|
2972
|
+
);
|
|
2973
|
+
}, timeoutMs);
|
|
2974
|
+
});
|
|
2975
|
+
try {
|
|
2976
|
+
return await Promise.race([Promise.resolve(this._walletAdapterResolver(id)), timeoutPromise]);
|
|
2977
|
+
} finally {
|
|
2978
|
+
if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
|
|
2979
|
+
}
|
|
2412
2980
|
}
|
|
2413
2981
|
if (id === "freighter" /* FREIGHTER */) return new FreighterAdapter();
|
|
2414
2982
|
if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter();
|
|
@@ -2422,6 +2990,16 @@ var PollarClient = class {
|
|
|
2422
2990
|
this._setAuthState({ step: "idle" });
|
|
2423
2991
|
return;
|
|
2424
2992
|
}
|
|
2993
|
+
if (error instanceof Error && error.code === AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT) {
|
|
2994
|
+
console.error("[PollarClient]", error.message);
|
|
2995
|
+
this._setAuthState({
|
|
2996
|
+
step: "error",
|
|
2997
|
+
previousStep: this._authState.step,
|
|
2998
|
+
message: error.message,
|
|
2999
|
+
errorCode: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
|
|
3000
|
+
});
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
2425
3003
|
console.error("[PollarClient] Unexpected error in auth flow", error);
|
|
2426
3004
|
this._setAuthState({
|
|
2427
3005
|
step: "error",
|
|
@@ -2443,6 +3021,7 @@ var PollarClient = class {
|
|
|
2443
3021
|
}
|
|
2444
3022
|
console.info("[PollarClient] Session restored from storage");
|
|
2445
3023
|
this._setAuthState({ step: "authenticated", session: this._session });
|
|
3024
|
+
this._scheduleNextRefresh();
|
|
2446
3025
|
} else {
|
|
2447
3026
|
console.info("[PollarClient] No session in storage");
|
|
2448
3027
|
}
|
|
@@ -2469,9 +3048,11 @@ var PollarClient = class {
|
|
|
2469
3048
|
}
|
|
2470
3049
|
await writeStorage(this._storage, this.apiKeyHash, persisted);
|
|
2471
3050
|
this._setAuthState({ step: "authenticated", session: persisted });
|
|
3051
|
+
this._scheduleNextRefresh();
|
|
2472
3052
|
}
|
|
2473
3053
|
async _clearSession() {
|
|
2474
3054
|
console.info("[PollarClient] Session cleared");
|
|
3055
|
+
this._clearRefreshTimer();
|
|
2475
3056
|
this._session = null;
|
|
2476
3057
|
this._profile = null;
|
|
2477
3058
|
this._walletAdapter = null;
|
|
@@ -2504,6 +3085,46 @@ var PollarClient = class {
|
|
|
2504
3085
|
console.info(`[PollarClient] transaction:${next.step}`);
|
|
2505
3086
|
for (const cb of this._transactionStateListeners) cb(next);
|
|
2506
3087
|
}
|
|
3088
|
+
/**
|
|
3089
|
+
* Threads `buildData` through state transitions. When the user has already
|
|
3090
|
+
* called `buildTx`, every subsequent state (signing, signed, submitting,
|
|
3091
|
+
* submitted, success, error) should carry the build summary so modal UIs
|
|
3092
|
+
* can keep showing "Send 5 USDC to G..." through the whole flow.
|
|
3093
|
+
*/
|
|
3094
|
+
_currentBuildData() {
|
|
3095
|
+
const s = this._transactionState;
|
|
3096
|
+
if (!s) return void 0;
|
|
3097
|
+
if ("buildData" in s && s.buildData) return s.buildData;
|
|
3098
|
+
return void 0;
|
|
3099
|
+
}
|
|
3100
|
+
};
|
|
3101
|
+
|
|
3102
|
+
// src/stellar/StellarClient.ts
|
|
3103
|
+
var HORIZON_URLS = {
|
|
3104
|
+
mainnet: "https://horizon.stellar.org",
|
|
3105
|
+
testnet: "https://horizon-testnet.stellar.org"
|
|
3106
|
+
};
|
|
3107
|
+
var StellarClient = class {
|
|
3108
|
+
constructor(config) {
|
|
3109
|
+
this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
|
|
3110
|
+
}
|
|
3111
|
+
async submitTransaction(signedXdr) {
|
|
3112
|
+
try {
|
|
3113
|
+
const response = await fetch(`${this.horizonUrl}/transactions`, {
|
|
3114
|
+
method: "POST",
|
|
3115
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
3116
|
+
body: new URLSearchParams({ tx: signedXdr })
|
|
3117
|
+
});
|
|
3118
|
+
if (!response.ok) {
|
|
3119
|
+
const body = await response.json().catch(() => ({}));
|
|
3120
|
+
return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
|
|
3121
|
+
}
|
|
3122
|
+
const data = await response.json();
|
|
3123
|
+
return { success: true, hash: data.hash };
|
|
3124
|
+
} catch {
|
|
3125
|
+
return { success: false, errorCode: "NETWORK_ERROR" };
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
2507
3128
|
};
|
|
2508
3129
|
|
|
2509
3130
|
// src/index.ts
|