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