@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.rn.mjs
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) => {
|
|
@@ -450,9 +450,7 @@ var WebCryptoKeyManager = class {
|
|
|
450
450
|
*/
|
|
451
451
|
this._initPromise = null;
|
|
452
452
|
if (typeof globalThis.crypto === "undefined" || !globalThis.crypto.subtle) {
|
|
453
|
-
throw new Error(
|
|
454
|
-
"[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost)."
|
|
455
|
-
);
|
|
453
|
+
throw new Error("[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost).");
|
|
456
454
|
}
|
|
457
455
|
this.apiKey = apiKey;
|
|
458
456
|
}
|
|
@@ -1025,14 +1023,18 @@ function createApiClient(baseUrl) {
|
|
|
1025
1023
|
async function listDistributionRules(api) {
|
|
1026
1024
|
const { data, error } = await api.GET("/distribution/rules");
|
|
1027
1025
|
if (!data?.content || error) {
|
|
1028
|
-
throw new Error(
|
|
1026
|
+
throw new Error(
|
|
1027
|
+
error?.code ?? error?.error ?? "Failed to list distribution rules"
|
|
1028
|
+
);
|
|
1029
1029
|
}
|
|
1030
1030
|
return data.content.rules;
|
|
1031
1031
|
}
|
|
1032
1032
|
async function claimDistributionRule(api, body) {
|
|
1033
1033
|
const { data, error } = await api.POST("/distribution/claim", { body });
|
|
1034
1034
|
if (!data?.content || error) {
|
|
1035
|
-
throw new Error(
|
|
1035
|
+
throw new Error(
|
|
1036
|
+
error?.code ?? error?.error ?? "Failed to claim distribution rule"
|
|
1037
|
+
);
|
|
1036
1038
|
}
|
|
1037
1039
|
return data.content;
|
|
1038
1040
|
}
|
|
@@ -1043,18 +1045,18 @@ async function getKycStatus(api, providerId) {
|
|
|
1043
1045
|
params: { query: providerId ? { providerId } : {} }
|
|
1044
1046
|
});
|
|
1045
1047
|
if (!data?.content || error) {
|
|
1046
|
-
throw new Error(error?.error ?? "Failed to get KYC status");
|
|
1048
|
+
throw new Error(error?.code ?? error?.error ?? "Failed to get KYC status");
|
|
1047
1049
|
}
|
|
1048
1050
|
return data.content;
|
|
1049
1051
|
}
|
|
1050
1052
|
async function getKycProviders(api, country) {
|
|
1051
1053
|
const { data, error } = await api.GET("/kyc/providers", { params: { query: { country } } });
|
|
1052
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to get KYC providers");
|
|
1054
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get KYC providers");
|
|
1053
1055
|
return data.content;
|
|
1054
1056
|
}
|
|
1055
1057
|
async function startKyc(api, body) {
|
|
1056
1058
|
const { data, error } = await api.POST("/kyc/start", { body });
|
|
1057
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to start KYC");
|
|
1059
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to start KYC");
|
|
1058
1060
|
return data.content;
|
|
1059
1061
|
}
|
|
1060
1062
|
async function resolveKyc(api, providerId, level = "basic") {
|
|
@@ -1076,22 +1078,22 @@ async function pollKycStatus(api, providerId, { intervalMs = 3e3, timeoutMs = 3e
|
|
|
1076
1078
|
// src/api/endpoints/ramps.ts
|
|
1077
1079
|
async function getRampsQuote(api, query) {
|
|
1078
1080
|
const { data, error } = await api.GET("/ramps/quote", { params: { query } });
|
|
1079
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to get ramp quotes");
|
|
1081
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get ramp quotes");
|
|
1080
1082
|
return data.content;
|
|
1081
1083
|
}
|
|
1082
1084
|
async function createOnRamp(api, body) {
|
|
1083
1085
|
const { data, error } = await api.POST("/ramps/onramp", { body });
|
|
1084
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to create onramp");
|
|
1086
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to create onramp");
|
|
1085
1087
|
return data.content;
|
|
1086
1088
|
}
|
|
1087
1089
|
async function createOffRamp(api, body) {
|
|
1088
1090
|
const { data, error } = await api.POST("/ramps/offramp", { body });
|
|
1089
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to create offramp");
|
|
1091
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to create offramp");
|
|
1090
1092
|
return data.content;
|
|
1091
1093
|
}
|
|
1092
1094
|
async function getRampTransaction(api, txId) {
|
|
1093
1095
|
const { data, error } = await api.GET("/ramps/transaction/{txId}", { params: { path: { txId } } });
|
|
1094
|
-
if (!data?.content || error) throw new Error(error?.error ?? "Failed to get transaction");
|
|
1096
|
+
if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get transaction");
|
|
1095
1097
|
return data.content;
|
|
1096
1098
|
}
|
|
1097
1099
|
async function pollRampTransaction(api, txId, { intervalMs = 5e3, timeoutMs = 6e5 } = {}) {
|
|
@@ -1104,6 +1106,26 @@ async function pollRampTransaction(api, txId, { intervalMs = 5e3, timeoutMs = 6e
|
|
|
1104
1106
|
throw new Error("Ramp transaction polling timed out");
|
|
1105
1107
|
}
|
|
1106
1108
|
|
|
1109
|
+
// src/lib/random-uuid.ts
|
|
1110
|
+
function randomUUID() {
|
|
1111
|
+
const c = globalThis.crypto;
|
|
1112
|
+
if (c && typeof c.randomUUID === "function") {
|
|
1113
|
+
return c.randomUUID();
|
|
1114
|
+
}
|
|
1115
|
+
if (c && typeof c.getRandomValues === "function") {
|
|
1116
|
+
const bytes = new Uint8Array(16);
|
|
1117
|
+
c.getRandomValues(bytes);
|
|
1118
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
1119
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
1120
|
+
const hex = [];
|
|
1121
|
+
for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
|
|
1122
|
+
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("")}`;
|
|
1123
|
+
}
|
|
1124
|
+
throw new Error(
|
|
1125
|
+
"[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."
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1107
1129
|
// src/dpop.ts
|
|
1108
1130
|
async function buildProof(args, keyManager) {
|
|
1109
1131
|
const jwk = await keyManager.getPublicJwk();
|
|
@@ -1113,7 +1135,7 @@ async function buildProof(args, keyManager) {
|
|
|
1113
1135
|
jwk
|
|
1114
1136
|
};
|
|
1115
1137
|
const payload = {
|
|
1116
|
-
jti:
|
|
1138
|
+
jti: randomUUID(),
|
|
1117
1139
|
htm: args.htm.toUpperCase(),
|
|
1118
1140
|
htu: normalizeHtu(args.htu),
|
|
1119
1141
|
iat: Math.floor(Date.now() / 1e3)
|
|
@@ -1147,52 +1169,6 @@ function normalizeHtu(rawUrl) {
|
|
|
1147
1169
|
const portPart = port ? `:${port}` : "";
|
|
1148
1170
|
return `${scheme}//${host}${portPart}${url.pathname}`;
|
|
1149
1171
|
}
|
|
1150
|
-
function generateJti() {
|
|
1151
|
-
const c = globalThis.crypto;
|
|
1152
|
-
if (c && typeof c.randomUUID === "function") {
|
|
1153
|
-
return c.randomUUID();
|
|
1154
|
-
}
|
|
1155
|
-
if (c && typeof c.getRandomValues === "function") {
|
|
1156
|
-
const bytes = new Uint8Array(16);
|
|
1157
|
-
c.getRandomValues(bytes);
|
|
1158
|
-
bytes[6] = bytes[6] & 15 | 64;
|
|
1159
|
-
bytes[8] = bytes[8] & 63 | 128;
|
|
1160
|
-
const hex = [];
|
|
1161
|
-
for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
|
|
1162
|
-
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("")}`;
|
|
1163
|
-
}
|
|
1164
|
-
throw new Error(
|
|
1165
|
-
"[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."
|
|
1166
|
-
);
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
// src/stellar/StellarClient.ts
|
|
1170
|
-
var HORIZON_URLS = {
|
|
1171
|
-
mainnet: "https://horizon.stellar.org",
|
|
1172
|
-
testnet: "https://horizon-testnet.stellar.org"
|
|
1173
|
-
};
|
|
1174
|
-
var StellarClient = class {
|
|
1175
|
-
constructor(config) {
|
|
1176
|
-
this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
|
|
1177
|
-
}
|
|
1178
|
-
async submitTransaction(signedXdr) {
|
|
1179
|
-
try {
|
|
1180
|
-
const response = await fetch(`${this.horizonUrl}/transactions`, {
|
|
1181
|
-
method: "POST",
|
|
1182
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1183
|
-
body: new URLSearchParams({ tx: signedXdr })
|
|
1184
|
-
});
|
|
1185
|
-
if (!response.ok) {
|
|
1186
|
-
const body = await response.json().catch(() => ({}));
|
|
1187
|
-
return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
|
|
1188
|
-
}
|
|
1189
|
-
const data = await response.json();
|
|
1190
|
-
return { success: true, hash: data.hash };
|
|
1191
|
-
} catch {
|
|
1192
|
-
return { success: false, errorCode: "NETWORK_ERROR" };
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
};
|
|
1196
1172
|
|
|
1197
1173
|
// src/storage/web.ts
|
|
1198
1174
|
var LOG_PREFIX = "[PollarClient:storage]";
|
|
@@ -1281,9 +1257,62 @@ function defaultStorage(options = {}) {
|
|
|
1281
1257
|
return createLocalStorageAdapter(options);
|
|
1282
1258
|
}
|
|
1283
1259
|
|
|
1260
|
+
// src/visibility/noop.ts
|
|
1261
|
+
function createNoopVisibilityProvider() {
|
|
1262
|
+
return {
|
|
1263
|
+
isVisible: () => true,
|
|
1264
|
+
onChange: () => () => {
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/visibility/web.ts
|
|
1270
|
+
function createWebVisibilityProvider() {
|
|
1271
|
+
const isVisibleNow = () => typeof document === "undefined" || document.visibilityState === "visible";
|
|
1272
|
+
return {
|
|
1273
|
+
isVisible: isVisibleNow,
|
|
1274
|
+
onChange: (cb) => {
|
|
1275
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
1276
|
+
return () => {
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
let last = isVisibleNow();
|
|
1280
|
+
const handler = () => {
|
|
1281
|
+
const next = isVisibleNow();
|
|
1282
|
+
if (next !== last) {
|
|
1283
|
+
last = next;
|
|
1284
|
+
cb(next);
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
document.addEventListener("visibilitychange", handler);
|
|
1288
|
+
window.addEventListener("pageshow", handler);
|
|
1289
|
+
window.addEventListener("pagehide", handler);
|
|
1290
|
+
window.addEventListener("focus", handler);
|
|
1291
|
+
window.addEventListener("blur", handler);
|
|
1292
|
+
return () => {
|
|
1293
|
+
document.removeEventListener("visibilitychange", handler);
|
|
1294
|
+
window.removeEventListener("pageshow", handler);
|
|
1295
|
+
window.removeEventListener("pagehide", handler);
|
|
1296
|
+
window.removeEventListener("focus", handler);
|
|
1297
|
+
window.removeEventListener("blur", handler);
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// src/visibility/autodetect.ts
|
|
1304
|
+
function defaultVisibilityProvider() {
|
|
1305
|
+
if (typeof document !== "undefined" && typeof window !== "undefined") {
|
|
1306
|
+
return createWebVisibilityProvider();
|
|
1307
|
+
}
|
|
1308
|
+
return createNoopVisibilityProvider();
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1284
1311
|
// src/types.ts
|
|
1285
1312
|
var AUTH_ERROR_CODES = {
|
|
1286
1313
|
SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
|
|
1314
|
+
SESSION_EXPIRED: "SESSION_EXPIRED",
|
|
1315
|
+
SESSION_INVALID: "SESSION_INVALID",
|
|
1287
1316
|
EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
|
|
1288
1317
|
EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
|
|
1289
1318
|
EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
|
|
@@ -1291,6 +1320,7 @@ var AUTH_ERROR_CODES = {
|
|
|
1291
1320
|
AUTH_FAILED: "AUTH_FAILED",
|
|
1292
1321
|
WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
|
|
1293
1322
|
WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
|
|
1323
|
+
WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
|
|
1294
1324
|
UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
|
|
1295
1325
|
};
|
|
1296
1326
|
var PollarFlowError = class extends Error {
|
|
@@ -1596,7 +1626,32 @@ async function readWalletType(storage, apiKeyHash) {
|
|
|
1596
1626
|
return storage.get(walletTypeStorageKey(apiKeyHash));
|
|
1597
1627
|
}
|
|
1598
1628
|
|
|
1629
|
+
// src/lib/abort.ts
|
|
1630
|
+
function abortError() {
|
|
1631
|
+
if (typeof DOMException !== "undefined") {
|
|
1632
|
+
return new DOMException("Aborted", "AbortError");
|
|
1633
|
+
}
|
|
1634
|
+
const err = new Error("Aborted");
|
|
1635
|
+
err.name = "AbortError";
|
|
1636
|
+
return err;
|
|
1637
|
+
}
|
|
1638
|
+
function throwIfAborted(signal) {
|
|
1639
|
+
if (signal?.aborted) throw abortError();
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1599
1642
|
// src/client/stream.ts
|
|
1643
|
+
var SessionStatusError = class extends Error {
|
|
1644
|
+
constructor(code) {
|
|
1645
|
+
super(`[PollarClient] Session status terminal: ${code}`);
|
|
1646
|
+
this.code = code;
|
|
1647
|
+
this.name = "SessionStatusError";
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
function terminalStatusCode(parsed) {
|
|
1651
|
+
const err = parsed?.error;
|
|
1652
|
+
if (err === "INVALID_CLIENT_SESSION_ID" || err === "EXPIRED_CLIENT_ID") return err;
|
|
1653
|
+
return null;
|
|
1654
|
+
}
|
|
1600
1655
|
function abortableDelay(ms, signal) {
|
|
1601
1656
|
return new Promise((resolve, reject) => {
|
|
1602
1657
|
const t = setTimeout(resolve, ms);
|
|
@@ -1604,7 +1659,7 @@ function abortableDelay(ms, signal) {
|
|
|
1604
1659
|
"abort",
|
|
1605
1660
|
() => {
|
|
1606
1661
|
clearTimeout(t);
|
|
1607
|
-
reject(
|
|
1662
|
+
reject(abortError());
|
|
1608
1663
|
},
|
|
1609
1664
|
{ once: true }
|
|
1610
1665
|
);
|
|
@@ -1619,7 +1674,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
|
|
|
1619
1674
|
else await new Promise((r) => setTimeout(r, ms));
|
|
1620
1675
|
};
|
|
1621
1676
|
while (true) {
|
|
1622
|
-
|
|
1677
|
+
throwIfAborted(signal);
|
|
1623
1678
|
let data, error;
|
|
1624
1679
|
try {
|
|
1625
1680
|
({ data, error } = await api.GET("/auth/session/status/{clientSessionId}", {
|
|
@@ -1642,7 +1697,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
|
|
|
1642
1697
|
let sawAnyChunk = false;
|
|
1643
1698
|
try {
|
|
1644
1699
|
while (true) {
|
|
1645
|
-
|
|
1700
|
+
throwIfAborted(signal);
|
|
1646
1701
|
const { done, value } = await reader.read();
|
|
1647
1702
|
if (done) {
|
|
1648
1703
|
streamDone = true;
|
|
@@ -1653,17 +1708,22 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
|
|
|
1653
1708
|
for (const message of chunk.split("\n\n").filter(Boolean)) {
|
|
1654
1709
|
const dataLine = message.split("\n").find((l) => l.startsWith("data:"));
|
|
1655
1710
|
if (!dataLine) continue;
|
|
1711
|
+
let parsed;
|
|
1656
1712
|
try {
|
|
1657
|
-
|
|
1658
|
-
if (check(parsed)) {
|
|
1659
|
-
return parsed;
|
|
1660
|
-
}
|
|
1713
|
+
parsed = JSON.parse(dataLine.slice("data:".length).trim());
|
|
1661
1714
|
} catch {
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
const terminal = terminalStatusCode(parsed);
|
|
1718
|
+
if (terminal) throw new SessionStatusError(terminal);
|
|
1719
|
+
if (check(parsed)) {
|
|
1720
|
+
return parsed;
|
|
1662
1721
|
}
|
|
1663
1722
|
}
|
|
1664
1723
|
}
|
|
1665
1724
|
} catch (e) {
|
|
1666
1725
|
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
1726
|
+
if (e instanceof SessionStatusError) throw e;
|
|
1667
1727
|
console.warn(e);
|
|
1668
1728
|
} finally {
|
|
1669
1729
|
reader.releaseLock();
|
|
@@ -1674,14 +1734,74 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
|
|
|
1674
1734
|
if (delay) await sleep(delay);
|
|
1675
1735
|
}
|
|
1676
1736
|
}
|
|
1737
|
+
async function pollUntilFound(baseUrl, clientSessionId, check, intervalMs = 500, signal) {
|
|
1738
|
+
const url = `${baseUrl}/auth/session/status/${encodeURIComponent(clientSessionId)}/poll`;
|
|
1739
|
+
let backoff = intervalMs;
|
|
1740
|
+
const sleep = async (ms) => {
|
|
1741
|
+
if (ms <= 0) return;
|
|
1742
|
+
if (signal) await abortableDelay(ms, signal);
|
|
1743
|
+
else await new Promise((r) => setTimeout(r, ms));
|
|
1744
|
+
};
|
|
1745
|
+
while (true) {
|
|
1746
|
+
throwIfAborted(signal);
|
|
1747
|
+
let envelope = null;
|
|
1748
|
+
let httpStatus = 0;
|
|
1749
|
+
try {
|
|
1750
|
+
const response = await fetch(url, { headers: { accept: "application/json" }, signal: signal ?? null });
|
|
1751
|
+
httpStatus = response.status;
|
|
1752
|
+
envelope = await response.json().catch(() => null);
|
|
1753
|
+
} catch (e) {
|
|
1754
|
+
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
1755
|
+
console.warn(e);
|
|
1756
|
+
}
|
|
1757
|
+
if (httpStatus === 404 || envelope?.code === "INVALID_CLIENT_SESSION_ID") {
|
|
1758
|
+
throw new SessionStatusError("INVALID_CLIENT_SESSION_ID");
|
|
1759
|
+
}
|
|
1760
|
+
if (httpStatus === 410 || envelope?.code === "EXPIRED_CLIENT_ID") {
|
|
1761
|
+
throw new SessionStatusError("EXPIRED_CLIENT_ID");
|
|
1762
|
+
}
|
|
1763
|
+
if (envelope?.success && envelope.content && check(envelope.content)) {
|
|
1764
|
+
return envelope.content;
|
|
1765
|
+
}
|
|
1766
|
+
if (envelope) backoff = intervalMs;
|
|
1767
|
+
else backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
1768
|
+
await sleep(backoff);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
function waitForSessionReady(args) {
|
|
1772
|
+
const { api, baseUrl, clientSessionId, check, useStreaming, retryDelayMs, signal } = args;
|
|
1773
|
+
return useStreaming ? streamUntilFound(api, clientSessionId, check, retryDelayMs ?? 200, signal) : pollUntilFound(baseUrl, clientSessionId, check, retryDelayMs ?? 500, signal);
|
|
1774
|
+
}
|
|
1677
1775
|
|
|
1678
1776
|
// src/client/auth/authenticate.ts
|
|
1679
1777
|
async function authenticate(clientSessionId, deps, expectedWallet) {
|
|
1680
|
-
const { api, signal, setAuthState, storeSession, clearSession } = deps;
|
|
1778
|
+
const { api, basePath, useStreaming, signal, setAuthState, storeSession, clearSession } = deps;
|
|
1681
1779
|
setAuthState({ step: "authenticating" });
|
|
1682
|
-
|
|
1780
|
+
try {
|
|
1781
|
+
await waitForSessionReady({
|
|
1782
|
+
api,
|
|
1783
|
+
baseUrl: basePath,
|
|
1784
|
+
clientSessionId,
|
|
1785
|
+
check: (data2) => data2?.status === "READY",
|
|
1786
|
+
useStreaming,
|
|
1787
|
+
signal
|
|
1788
|
+
});
|
|
1789
|
+
} catch (err) {
|
|
1790
|
+
if (err instanceof SessionStatusError) {
|
|
1791
|
+
const expired = err.code === "EXPIRED_CLIENT_ID";
|
|
1792
|
+
setAuthState({
|
|
1793
|
+
step: "error",
|
|
1794
|
+
previousStep: "authenticating",
|
|
1795
|
+
message: expired ? "Login session expired \u2014 please try again" : "Login session is no longer valid \u2014 please try again",
|
|
1796
|
+
errorCode: expired ? AUTH_ERROR_CODES.SESSION_EXPIRED : AUTH_ERROR_CODES.SESSION_INVALID
|
|
1797
|
+
});
|
|
1798
|
+
await clearSession();
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
throw err;
|
|
1802
|
+
}
|
|
1683
1803
|
const dpopJwk = await deps.getPublicJwk();
|
|
1684
|
-
const { data
|
|
1804
|
+
const { data } = await api.POST("/auth/login", {
|
|
1685
1805
|
body: {
|
|
1686
1806
|
clientSessionId,
|
|
1687
1807
|
dpopJwk,
|
|
@@ -1803,26 +1923,36 @@ function severOpener(popup) {
|
|
|
1803
1923
|
} catch {
|
|
1804
1924
|
}
|
|
1805
1925
|
}
|
|
1806
|
-
async
|
|
1807
|
-
const
|
|
1808
|
-
const popup = window.open("about:blank", "_blank");
|
|
1926
|
+
var defaultWebOAuthOpener = async ({ getUrl }) => {
|
|
1927
|
+
const popup = typeof window !== "undefined" ? window.open("about:blank", "_blank") : null;
|
|
1809
1928
|
severOpener(popup);
|
|
1810
|
-
const
|
|
1811
|
-
if (!
|
|
1929
|
+
const url = await getUrl();
|
|
1930
|
+
if (!url) {
|
|
1812
1931
|
popup?.close();
|
|
1813
1932
|
return;
|
|
1814
1933
|
}
|
|
1815
|
-
setAuthState({ step: "opening_oauth", provider });
|
|
1816
|
-
const url = new URL(`${basePath}/auth/${provider}`);
|
|
1817
|
-
url.searchParams.set("api_key", apiKey);
|
|
1818
|
-
url.searchParams.set("client_session_id", clientSessionId);
|
|
1819
|
-
url.searchParams.set("redirect_uri", window.location.origin);
|
|
1820
1934
|
if (popup) {
|
|
1821
|
-
popup.location.href = url
|
|
1935
|
+
popup.location.href = url;
|
|
1822
1936
|
severOpener(popup);
|
|
1823
|
-
} else {
|
|
1824
|
-
window.open(url
|
|
1937
|
+
} else if (typeof window !== "undefined") {
|
|
1938
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
1825
1939
|
}
|
|
1940
|
+
};
|
|
1941
|
+
async function loginOAuth(provider, deps) {
|
|
1942
|
+
const { setAuthState, basePath, apiKey, openAuthUrl, redirectUri, signal } = deps;
|
|
1943
|
+
let clientSessionId = null;
|
|
1944
|
+
const getUrl = async () => {
|
|
1945
|
+
clientSessionId = await createAuthSession(deps);
|
|
1946
|
+
if (!clientSessionId) return null;
|
|
1947
|
+
setAuthState({ step: "opening_oauth", provider });
|
|
1948
|
+
const url = new URL(`${basePath}/auth/${provider}`);
|
|
1949
|
+
url.searchParams.set("api_key", apiKey);
|
|
1950
|
+
url.searchParams.set("client_session_id", clientSessionId);
|
|
1951
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
1952
|
+
return url.toString();
|
|
1953
|
+
};
|
|
1954
|
+
await openAuthUrl({ provider, getUrl, redirectUri, signal });
|
|
1955
|
+
if (!clientSessionId) return;
|
|
1826
1956
|
await authenticate(clientSessionId, deps);
|
|
1827
1957
|
}
|
|
1828
1958
|
|
|
@@ -1832,10 +1962,10 @@ function withSignal(promise, signal) {
|
|
|
1832
1962
|
promise,
|
|
1833
1963
|
new Promise((_, reject) => {
|
|
1834
1964
|
if (signal.aborted) {
|
|
1835
|
-
reject(
|
|
1965
|
+
reject(abortError());
|
|
1836
1966
|
return;
|
|
1837
1967
|
}
|
|
1838
|
-
signal.addEventListener("abort", () => reject(
|
|
1968
|
+
signal.addEventListener("abort", () => reject(abortError()), { once: true });
|
|
1839
1969
|
})
|
|
1840
1970
|
]);
|
|
1841
1971
|
}
|
|
@@ -1846,7 +1976,7 @@ async function loginWallet(type, deps) {
|
|
|
1846
1976
|
let connectedWallet;
|
|
1847
1977
|
try {
|
|
1848
1978
|
setAuthState({ step: "connecting_wallet", walletType: type });
|
|
1849
|
-
const adapter = await deps.resolveWalletAdapter(type);
|
|
1979
|
+
const adapter = await withSignal(deps.resolveWalletAdapter(type), signal);
|
|
1850
1980
|
const available = await withSignal(adapter.isAvailable(), signal);
|
|
1851
1981
|
if (!available) {
|
|
1852
1982
|
setAuthState({ step: "wallet_not_installed", walletType: type });
|
|
@@ -1883,6 +2013,9 @@ async function loginWallet(type, deps) {
|
|
|
1883
2013
|
|
|
1884
2014
|
// src/client/client.ts
|
|
1885
2015
|
var isBrowser = typeof window !== "undefined" && typeof localStorage !== "undefined";
|
|
2016
|
+
var isReactNative = typeof navigator !== "undefined" && navigator.product === "ReactNative";
|
|
2017
|
+
var isClientRuntime = isBrowser || isReactNative;
|
|
2018
|
+
var REFRESH_SKEW_SECONDS = 60;
|
|
1886
2019
|
function warnServerSide(method) {
|
|
1887
2020
|
console.warn(
|
|
1888
2021
|
`[PollarClient] ${method}() called server-side \u2014 browser APIs unavailable. Use PollarClient only in Client Components.`
|
|
@@ -1910,6 +2043,11 @@ var PollarClient = class {
|
|
|
1910
2043
|
/** Singleton in-flight refresh — concurrent 401s coalesce into one /auth/refresh call. */
|
|
1911
2044
|
this._refreshPromise = null;
|
|
1912
2045
|
this._storageEventHandler = null;
|
|
2046
|
+
/** Updated by the request middleware. Read by the silent-refresh scheduler
|
|
2047
|
+
* to skip proactive refreshes after `maxIdleMs` of no HTTP activity. */
|
|
2048
|
+
this._lastRequestAt = Date.now();
|
|
2049
|
+
this._refreshTimer = null;
|
|
2050
|
+
this._visibilityUnsubscribe = null;
|
|
1913
2051
|
this._transactionState = null;
|
|
1914
2052
|
this._transactionStateListeners = /* @__PURE__ */ new Set();
|
|
1915
2053
|
this._txHistoryState = { step: "idle" };
|
|
@@ -1920,19 +2058,38 @@ var PollarClient = class {
|
|
|
1920
2058
|
this._authStateListeners = /* @__PURE__ */ new Set();
|
|
1921
2059
|
this._networkState = { step: "idle" };
|
|
1922
2060
|
this._networkStateListeners = /* @__PURE__ */ new Set();
|
|
2061
|
+
/**
|
|
2062
|
+
* Latched once the storage adapter degrades. We dedupe (the adapter only
|
|
2063
|
+
* fires once anyway) and use it to replay state to late-subscribers — same
|
|
2064
|
+
* pattern as `onAuthStateChange` replaying `_authState` on subscribe.
|
|
2065
|
+
* Only populated when the SDK constructed the default storage adapter; if
|
|
2066
|
+
* the consumer passes `config.storage`, they own degradation notifications.
|
|
2067
|
+
*/
|
|
2068
|
+
this._storageDegraded = null;
|
|
2069
|
+
this._storageDegradeListeners = /* @__PURE__ */ new Set();
|
|
1923
2070
|
this._walletAdapter = null;
|
|
1924
2071
|
this._loginController = null;
|
|
1925
2072
|
this.apiKey = config.apiKey;
|
|
1926
|
-
this.id =
|
|
2073
|
+
this.id = randomUUID();
|
|
1927
2074
|
this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
|
|
1928
|
-
this._storage = config.storage ?? defaultStorage(
|
|
2075
|
+
this._storage = config.storage ?? defaultStorage({
|
|
2076
|
+
onDegrade: (reason, error) => {
|
|
2077
|
+
config.onStorageDegrade?.(reason, error);
|
|
2078
|
+
this._dispatchStorageDegrade(reason, error);
|
|
2079
|
+
}
|
|
2080
|
+
});
|
|
1929
2081
|
this._keyManager = config.keyManager ?? defaultKeyManager(this._storage, config.apiKey);
|
|
1930
2082
|
this._walletAdapterResolver = config.walletAdapter ?? null;
|
|
2083
|
+
this._walletResolverTimeoutMs = config.walletResolverTimeoutMs ?? 5e3;
|
|
1931
2084
|
this._deviceLabel = config.deviceLabel;
|
|
2085
|
+
this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
|
|
2086
|
+
this._maxIdleMs = config.maxIdleMs;
|
|
2087
|
+
this._openAuthUrl = config.openAuthUrl ?? defaultWebOAuthOpener;
|
|
2088
|
+
this._oauthRedirectUri = config.oauthRedirectUri ?? (isBrowser ? window.location.origin : "");
|
|
1932
2089
|
this._api = createApiClient(this.basePath);
|
|
1933
2090
|
this._wireMiddlewares();
|
|
1934
2091
|
this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
|
|
1935
|
-
if (!
|
|
2092
|
+
if (!isClientRuntime) {
|
|
1936
2093
|
warnServerSide("constructor");
|
|
1937
2094
|
this._initialized = Promise.resolve();
|
|
1938
2095
|
return;
|
|
@@ -1958,7 +2115,7 @@ var PollarClient = class {
|
|
|
1958
2115
|
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
1959
2116
|
async _initialize() {
|
|
1960
2117
|
this._apiKeyHash = await hashApiKey(this.apiKey);
|
|
1961
|
-
if (
|
|
2118
|
+
if (isBrowser) {
|
|
1962
2119
|
const sessionKey = sessionStorageKey(this._apiKeyHash);
|
|
1963
2120
|
const handler = (e) => {
|
|
1964
2121
|
if (e.key === sessionKey) {
|
|
@@ -1974,15 +2131,23 @@ var PollarClient = class {
|
|
|
1974
2131
|
console.warn("[PollarClient] KeyManager init failed; DPoP unavailable for this session", err);
|
|
1975
2132
|
}
|
|
1976
2133
|
await this._restoreSession();
|
|
2134
|
+
this._visibilityUnsubscribe = this._visibilityProvider.onChange((visible) => {
|
|
2135
|
+
if (visible) void this._maybeProactiveRefresh();
|
|
2136
|
+
});
|
|
1977
2137
|
}
|
|
1978
2138
|
/** Detach the cross-tab storage listener and abort any in-flight login. */
|
|
1979
2139
|
destroy() {
|
|
1980
|
-
if (this._storageEventHandler &&
|
|
2140
|
+
if (this._storageEventHandler && isBrowser) {
|
|
1981
2141
|
window.removeEventListener("storage", this._storageEventHandler);
|
|
1982
2142
|
this._storageEventHandler = null;
|
|
1983
2143
|
}
|
|
1984
2144
|
this._loginController?.abort();
|
|
1985
2145
|
this._loginController = null;
|
|
2146
|
+
this._clearRefreshTimer();
|
|
2147
|
+
if (this._visibilityUnsubscribe) {
|
|
2148
|
+
this._visibilityUnsubscribe();
|
|
2149
|
+
this._visibilityUnsubscribe = null;
|
|
2150
|
+
}
|
|
1986
2151
|
}
|
|
1987
2152
|
// ─── Middlewares (DPoP + auto-refresh) ────────────────────────────────────
|
|
1988
2153
|
_wireMiddlewares() {
|
|
@@ -1990,6 +2155,7 @@ var PollarClient = class {
|
|
|
1990
2155
|
this._api.use({
|
|
1991
2156
|
onRequest: async ({ request }) => {
|
|
1992
2157
|
request.headers.set("x-pollar-api-key", self.apiKey);
|
|
2158
|
+
self._lastRequestAt = Date.now();
|
|
1993
2159
|
await self._initialized;
|
|
1994
2160
|
if (request.body !== null) {
|
|
1995
2161
|
try {
|
|
@@ -2020,15 +2186,22 @@ var PollarClient = class {
|
|
|
2020
2186
|
const newNonce = response.headers.get("DPoP-Nonce");
|
|
2021
2187
|
if (newNonce) self._dpopNonce = newNonce;
|
|
2022
2188
|
if (response.status !== 401) return response;
|
|
2023
|
-
if (request.url.includes("/auth/refresh")) return response;
|
|
2024
2189
|
const wwwAuth = response.headers.get("WWW-Authenticate") ?? "";
|
|
2025
2190
|
const isNonceChallenge = wwwAuth.includes("use_dpop_nonce");
|
|
2191
|
+
if (request.url.includes("/auth/refresh")) {
|
|
2192
|
+
if (isNonceChallenge) return self._retryRequest(request);
|
|
2193
|
+
return response;
|
|
2194
|
+
}
|
|
2026
2195
|
if (!isNonceChallenge) {
|
|
2027
2196
|
try {
|
|
2028
2197
|
await self.refresh();
|
|
2029
2198
|
} catch {
|
|
2030
2199
|
return response;
|
|
2031
2200
|
}
|
|
2201
|
+
const method = request.method.toUpperCase();
|
|
2202
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
2203
|
+
return response;
|
|
2204
|
+
}
|
|
2032
2205
|
}
|
|
2033
2206
|
return self._retryRequest(request);
|
|
2034
2207
|
}
|
|
@@ -2053,14 +2226,22 @@ var PollarClient = class {
|
|
|
2053
2226
|
}
|
|
2054
2227
|
async _retryRequest(originalRequest) {
|
|
2055
2228
|
const headers = new Headers(originalRequest.headers);
|
|
2056
|
-
const
|
|
2057
|
-
if (
|
|
2058
|
-
const proof = await this._buildProofForRequest(originalRequest,
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2229
|
+
const isRefresh = originalRequest.url.includes("/auth/refresh");
|
|
2230
|
+
if (isRefresh) {
|
|
2231
|
+
const proof = await this._buildProofForRequest(originalRequest, void 0);
|
|
2232
|
+
headers.delete("Authorization");
|
|
2233
|
+
if (proof) headers.set("DPoP", proof);
|
|
2234
|
+
else headers.delete("DPoP");
|
|
2235
|
+
} else {
|
|
2236
|
+
const accessToken = this._session?.token?.accessToken;
|
|
2237
|
+
if (accessToken) {
|
|
2238
|
+
const proof = await this._buildProofForRequest(originalRequest, accessToken);
|
|
2239
|
+
if (proof) {
|
|
2240
|
+
headers.set("Authorization", `DPoP ${accessToken}`);
|
|
2241
|
+
headers.set("DPoP", proof);
|
|
2242
|
+
} else {
|
|
2243
|
+
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
2244
|
+
}
|
|
2064
2245
|
}
|
|
2065
2246
|
}
|
|
2066
2247
|
const cachedBody = this._requestBodyCache.get(originalRequest);
|
|
@@ -2131,6 +2312,65 @@ var PollarClient = class {
|
|
|
2131
2312
|
} catch (err) {
|
|
2132
2313
|
console.error("[PollarClient] Failed to persist refreshed session", err);
|
|
2133
2314
|
}
|
|
2315
|
+
this._scheduleNextRefresh();
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
// ─── Silent refresh scheduler ────────────────────────────────────────────────
|
|
2319
|
+
/**
|
|
2320
|
+
* Arm a single setTimeout to fire shortly before the current access token
|
|
2321
|
+
* expires. Idempotent — clearing any previous timer first. Safe to call
|
|
2322
|
+
* from any session-write site (initial login, restore-from-storage, after
|
|
2323
|
+
* a successful rotation). No-op if there's no session in memory.
|
|
2324
|
+
*
|
|
2325
|
+
* Browser/RN background-tab throttling makes long-running setTimeouts
|
|
2326
|
+
* unreliable on their own; the `visibilitychange` listener compensates by
|
|
2327
|
+
* re-invoking `_maybeProactiveRefresh` whenever the app comes back to the
|
|
2328
|
+
* foreground, catching any timer that fired late or never fired at all.
|
|
2329
|
+
*/
|
|
2330
|
+
_scheduleNextRefresh() {
|
|
2331
|
+
this._clearRefreshTimer();
|
|
2332
|
+
const expiresAt = this._session?.token?.expiresAt;
|
|
2333
|
+
if (typeof expiresAt !== "number") return;
|
|
2334
|
+
const dueInMs = Math.max(0, (expiresAt - Math.floor(Date.now() / 1e3) - REFRESH_SKEW_SECONDS) * 1e3);
|
|
2335
|
+
this._refreshTimer = setTimeout(() => {
|
|
2336
|
+
void this._maybeProactiveRefresh();
|
|
2337
|
+
}, dueInMs);
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Decide whether to actually run a refresh right now. Called both from the
|
|
2341
|
+
* scheduler timer and from the visibility-change listener.
|
|
2342
|
+
*
|
|
2343
|
+
* Skip if:
|
|
2344
|
+
* - no session / no RT (nothing to refresh)
|
|
2345
|
+
* - app is hidden — wait for the visibility listener to re-trigger us
|
|
2346
|
+
* - `maxIdleMs` configured and no client request since that window — let
|
|
2347
|
+
* the next reactive 401-refresh handle it whenever the user comes back
|
|
2348
|
+
* - the AT still has more than `REFRESH_SKEW_SECONDS` of life — reschedule
|
|
2349
|
+
*
|
|
2350
|
+
* Otherwise call `refresh()`, which uses the existing in-flight singleton
|
|
2351
|
+
* so we never collide with a reactive 401-triggered refresh. On failure,
|
|
2352
|
+
* `_doRefresh` already calls `_clearSession`, so auth-state listeners see
|
|
2353
|
+
* `step:'idle'` — no extra event dispatch needed here.
|
|
2354
|
+
*/
|
|
2355
|
+
async _maybeProactiveRefresh() {
|
|
2356
|
+
if (!this._session?.token?.refreshToken) return;
|
|
2357
|
+
if (!this._visibilityProvider.isVisible()) return;
|
|
2358
|
+
if (this._maxIdleMs !== void 0 && Date.now() - this._lastRequestAt > this._maxIdleMs) return;
|
|
2359
|
+
const expiresAt = this._session.token.expiresAt;
|
|
2360
|
+
if (Math.floor(Date.now() / 1e3) < expiresAt - REFRESH_SKEW_SECONDS) {
|
|
2361
|
+
this._scheduleNextRefresh();
|
|
2362
|
+
return;
|
|
2363
|
+
}
|
|
2364
|
+
try {
|
|
2365
|
+
await this.refresh();
|
|
2366
|
+
} catch (err) {
|
|
2367
|
+
console.warn("[PollarClient] Proactive refresh failed; session cleared", err);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
_clearRefreshTimer() {
|
|
2371
|
+
if (this._refreshTimer !== null) {
|
|
2372
|
+
clearTimeout(this._refreshTimer);
|
|
2373
|
+
this._refreshTimer = null;
|
|
2134
2374
|
}
|
|
2135
2375
|
}
|
|
2136
2376
|
// ─── Auth state ──────────────────────────────────────────────────────────────
|
|
@@ -2142,13 +2382,45 @@ var PollarClient = class {
|
|
|
2142
2382
|
cb(this._authState);
|
|
2143
2383
|
return () => this._authStateListeners.delete(cb);
|
|
2144
2384
|
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Subscribe to persistent-storage degradation (Safari private mode,
|
|
2387
|
+
* sandboxed iframes, quota errors, etc.). The SDK keeps running off
|
|
2388
|
+
* in-memory storage after degrade, but sessions won't survive reload — a
|
|
2389
|
+
* host UI typically wants to show "your session won't be saved" so the
|
|
2390
|
+
* user isn't blindsided after a refresh.
|
|
2391
|
+
*
|
|
2392
|
+
* Fires at most once per client lifetime (the underlying adapter dedupes).
|
|
2393
|
+
* Late subscribers receive the latched state synchronously on subscribe.
|
|
2394
|
+
*
|
|
2395
|
+
* Only fires when the SDK constructs the default storage adapter. If you
|
|
2396
|
+
* pass a custom `config.storage`, wire your own notification path through
|
|
2397
|
+
* that adapter's API — the SDK has no hook into it.
|
|
2398
|
+
*/
|
|
2399
|
+
onStorageDegrade(cb) {
|
|
2400
|
+
this._storageDegradeListeners.add(cb);
|
|
2401
|
+
if (this._storageDegraded) {
|
|
2402
|
+
cb(this._storageDegraded.reason, this._storageDegraded.error);
|
|
2403
|
+
}
|
|
2404
|
+
return () => this._storageDegradeListeners.delete(cb);
|
|
2405
|
+
}
|
|
2406
|
+
_dispatchStorageDegrade(reason, error) {
|
|
2407
|
+
if (this._storageDegraded) return;
|
|
2408
|
+
this._storageDegraded = { reason, error };
|
|
2409
|
+
for (const cb of this._storageDegradeListeners) {
|
|
2410
|
+
try {
|
|
2411
|
+
cb(reason, error);
|
|
2412
|
+
} catch (err) {
|
|
2413
|
+
console.error("[PollarClient] onStorageDegrade listener threw", err);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2145
2417
|
/** PII (email, names, avatar, providers). Held in memory only — never persisted. */
|
|
2146
2418
|
getUserProfile() {
|
|
2147
2419
|
return this._profile;
|
|
2148
2420
|
}
|
|
2149
2421
|
// ─── Login (unified entry point) ─────────────────────────────────────────
|
|
2150
2422
|
login(options) {
|
|
2151
|
-
if (!
|
|
2423
|
+
if (!isClientRuntime) {
|
|
2152
2424
|
warnServerSide("login");
|
|
2153
2425
|
return;
|
|
2154
2426
|
}
|
|
@@ -2159,7 +2431,9 @@ var PollarClient = class {
|
|
|
2159
2431
|
loginOAuth(options.provider, {
|
|
2160
2432
|
...deps,
|
|
2161
2433
|
basePath: this.basePath,
|
|
2162
|
-
apiKey: this.apiKey
|
|
2434
|
+
apiKey: this.apiKey,
|
|
2435
|
+
openAuthUrl: this._openAuthUrl,
|
|
2436
|
+
redirectUri: this._oauthRedirectUri
|
|
2163
2437
|
}).catch((err) => this._handleFlowError(err));
|
|
2164
2438
|
} else if (options.provider === "email") {
|
|
2165
2439
|
const { email } = options;
|
|
@@ -2175,7 +2449,7 @@ var PollarClient = class {
|
|
|
2175
2449
|
}
|
|
2176
2450
|
// ─── Email OTP flow (3 steps) ─────────────────────────────────────────────
|
|
2177
2451
|
beginEmailLogin() {
|
|
2178
|
-
if (!
|
|
2452
|
+
if (!isClientRuntime) {
|
|
2179
2453
|
warnServerSide("beginEmailLogin");
|
|
2180
2454
|
return;
|
|
2181
2455
|
}
|
|
@@ -2183,7 +2457,7 @@ var PollarClient = class {
|
|
|
2183
2457
|
initEmailSession(this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
|
|
2184
2458
|
}
|
|
2185
2459
|
sendEmailCode(email) {
|
|
2186
|
-
if (!
|
|
2460
|
+
if (!isClientRuntime) {
|
|
2187
2461
|
warnServerSide("sendEmailCode");
|
|
2188
2462
|
return;
|
|
2189
2463
|
}
|
|
@@ -2195,7 +2469,7 @@ var PollarClient = class {
|
|
|
2195
2469
|
sendEmailCode(email, clientSessionId, this._flowDeps(signal)).catch((err) => this._handleFlowError(err));
|
|
2196
2470
|
}
|
|
2197
2471
|
verifyEmailCode(code) {
|
|
2198
|
-
if (!
|
|
2472
|
+
if (!isClientRuntime) {
|
|
2199
2473
|
warnServerSide("verifyEmailCode");
|
|
2200
2474
|
return;
|
|
2201
2475
|
}
|
|
@@ -2213,7 +2487,7 @@ var PollarClient = class {
|
|
|
2213
2487
|
}
|
|
2214
2488
|
// ─── Wallet flow (single call) ────────────────────────────────────────────
|
|
2215
2489
|
loginWallet(type) {
|
|
2216
|
-
if (!
|
|
2490
|
+
if (!isClientRuntime) {
|
|
2217
2491
|
warnServerSide("loginWallet");
|
|
2218
2492
|
return;
|
|
2219
2493
|
}
|
|
@@ -2239,7 +2513,7 @@ var PollarClient = class {
|
|
|
2239
2513
|
* across all devices.
|
|
2240
2514
|
*/
|
|
2241
2515
|
async logout(options = {}) {
|
|
2242
|
-
if (!
|
|
2516
|
+
if (!isClientRuntime) {
|
|
2243
2517
|
warnServerSide("logout");
|
|
2244
2518
|
return;
|
|
2245
2519
|
}
|
|
@@ -2269,7 +2543,7 @@ var PollarClient = class {
|
|
|
2269
2543
|
* `current` flag identifies which entry corresponds to this client.
|
|
2270
2544
|
*/
|
|
2271
2545
|
async listSessions() {
|
|
2272
|
-
if (!
|
|
2546
|
+
if (!isClientRuntime) {
|
|
2273
2547
|
warnServerSide("listSessions");
|
|
2274
2548
|
return [];
|
|
2275
2549
|
}
|
|
@@ -2288,7 +2562,7 @@ var PollarClient = class {
|
|
|
2288
2562
|
* does NOT clear local state — call `logout()` for that case.
|
|
2289
2563
|
*/
|
|
2290
2564
|
async revokeSession(familyId) {
|
|
2291
|
-
if (!
|
|
2565
|
+
if (!isClientRuntime) {
|
|
2292
2566
|
warnServerSide("revokeSession");
|
|
2293
2567
|
return;
|
|
2294
2568
|
}
|
|
@@ -2378,10 +2652,16 @@ var PollarClient = class {
|
|
|
2378
2652
|
}
|
|
2379
2653
|
}
|
|
2380
2654
|
// ─── Transactions ─────────────────────────────────────────────────────────
|
|
2655
|
+
/**
|
|
2656
|
+
* Builds an unsigned XDR. Drives `_setTransactionState` for modal-style UIs
|
|
2657
|
+
* AND returns a {@link BuildOutcome} so headless callers can `await` and
|
|
2658
|
+
* inspect the result without subscribing to state changes.
|
|
2659
|
+
*/
|
|
2381
2660
|
async buildTx(operation, params, options) {
|
|
2382
2661
|
if (!this._session?.wallet?.publicKey) {
|
|
2383
|
-
|
|
2384
|
-
|
|
2662
|
+
const details = "No wallet connected";
|
|
2663
|
+
this._setTransactionState({ step: "error", phase: "building", details });
|
|
2664
|
+
return { status: "error", details };
|
|
2385
2665
|
}
|
|
2386
2666
|
const body = {
|
|
2387
2667
|
network: this.getNetwork(),
|
|
@@ -2395,40 +2675,194 @@ var PollarClient = class {
|
|
|
2395
2675
|
const { data, error } = await this._api.POST("/tx/build", { body });
|
|
2396
2676
|
if (!error && data?.success && data.content) {
|
|
2397
2677
|
this._setTransactionState({ step: "built", buildData: data.content });
|
|
2398
|
-
|
|
2399
|
-
const details = error?.details;
|
|
2400
|
-
this._setTransactionState({ step: "error", ...details && { details } });
|
|
2678
|
+
return { status: "built", buildData: data.content };
|
|
2401
2679
|
}
|
|
2680
|
+
const details = error?.details;
|
|
2681
|
+
this._setTransactionState({ step: "error", phase: "building", ...details && { details } });
|
|
2682
|
+
return { status: "error", ...details && { details } };
|
|
2402
2683
|
} catch (err) {
|
|
2403
2684
|
console.error("[PollarClient] buildTx failed", err);
|
|
2404
|
-
this._setTransactionState({ step: "error" });
|
|
2685
|
+
this._setTransactionState({ step: "error", phase: "building" });
|
|
2686
|
+
return { status: "error" };
|
|
2405
2687
|
}
|
|
2406
2688
|
}
|
|
2407
2689
|
getWalletType() {
|
|
2408
2690
|
return this._walletAdapter?.type ?? null;
|
|
2409
2691
|
}
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2692
|
+
/**
|
|
2693
|
+
* Signs the given unsigned XDR and returns the signed XDR.
|
|
2694
|
+
*
|
|
2695
|
+
* - External wallets: signs locally via the wallet adapter.
|
|
2696
|
+
* - Custodial wallets: posts to `/tx/sign`. The backend signs (through
|
|
2697
|
+
* wallet-service or the app's customer-managed adapter) and returns the
|
|
2698
|
+
* signed XDR plus an `idempotencyKey` the caller should echo back to
|
|
2699
|
+
* `submitTx`.
|
|
2700
|
+
*
|
|
2701
|
+
* Drives `_setTransactionState`: emits `signing` while in flight and
|
|
2702
|
+
* `signed` on success (or `error[phase: 'signing']` on failure). `buildData`
|
|
2703
|
+
* is threaded through if the consumer previously called `buildTx`.
|
|
2704
|
+
*/
|
|
2705
|
+
async signTx(unsignedXdr) {
|
|
2706
|
+
const buildData = this._currentBuildData();
|
|
2707
|
+
this._setTransactionState({ step: "signing", ...buildData && { buildData } });
|
|
2416
2708
|
if (this._walletAdapter) {
|
|
2709
|
+
const accountToSign = this._session?.wallet?.publicKey;
|
|
2710
|
+
const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
|
|
2417
2711
|
try {
|
|
2418
|
-
const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
|
|
2419
2712
|
const { signedTxXdr } = await this._walletAdapter.signTransaction(unsignedXdr, signOpts);
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
}
|
|
2425
|
-
|
|
2713
|
+
this._setTransactionState({
|
|
2714
|
+
step: "signed",
|
|
2715
|
+
signedXdr: signedTxXdr,
|
|
2716
|
+
...buildData && { buildData }
|
|
2717
|
+
});
|
|
2718
|
+
return { status: "signed", signedXdr: signedTxXdr };
|
|
2719
|
+
} catch (err) {
|
|
2720
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
2721
|
+
this._setTransactionState({
|
|
2722
|
+
step: "error",
|
|
2723
|
+
phase: "signing",
|
|
2724
|
+
...buildData && { buildData },
|
|
2725
|
+
...details && { details }
|
|
2726
|
+
});
|
|
2727
|
+
return { status: "error", ...details && { details } };
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
const publicKey = this._session?.wallet?.publicKey ?? "";
|
|
2731
|
+
try {
|
|
2732
|
+
const { data, error } = await this._api.POST("/tx/sign", {
|
|
2733
|
+
body: { network: this.getNetwork(), publicKey, unsignedXdr }
|
|
2734
|
+
});
|
|
2735
|
+
if (!error && data?.success && data.content?.signedXdr) {
|
|
2736
|
+
const { signedXdr, idempotencyKey } = data.content;
|
|
2737
|
+
this._setTransactionState({
|
|
2738
|
+
step: "signed",
|
|
2739
|
+
signedXdr,
|
|
2740
|
+
submissionToken: idempotencyKey,
|
|
2741
|
+
...buildData && { buildData }
|
|
2742
|
+
});
|
|
2743
|
+
return { status: "signed", signedXdr, submissionToken: idempotencyKey };
|
|
2744
|
+
}
|
|
2745
|
+
const details = error?.details;
|
|
2746
|
+
this._setTransactionState({
|
|
2747
|
+
step: "error",
|
|
2748
|
+
phase: "signing",
|
|
2749
|
+
...buildData && { buildData },
|
|
2750
|
+
...details && { details }
|
|
2751
|
+
});
|
|
2752
|
+
return { status: "error", ...details && { details } };
|
|
2753
|
+
} catch (err) {
|
|
2754
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
2755
|
+
this._setTransactionState({
|
|
2756
|
+
step: "error",
|
|
2757
|
+
phase: "signing",
|
|
2758
|
+
...buildData && { buildData },
|
|
2759
|
+
...details && { details }
|
|
2760
|
+
});
|
|
2761
|
+
return { status: "error", ...details && { details } };
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
/**
|
|
2765
|
+
* Submits a signed XDR via `/tx/submit` regardless of wallet type
|
|
2766
|
+
* (custodial or external). Routing through sdk-api gives us:
|
|
2767
|
+
* - End-to-end tx_records persistence with full phase lifecycle so the
|
|
2768
|
+
* developer dashboard can show every tx (both custodial and external
|
|
2769
|
+
* wallet flows) at `/apps/:id/monitor/transactions`.
|
|
2770
|
+
* - Idempotency tracking via `submissionToken` (returned by `signTx`).
|
|
2771
|
+
* - A single response shape (SUCCESS / PENDING / FAILED) shared by both
|
|
2772
|
+
* flows — previously external wallets could only return SUCCESS or
|
|
2773
|
+
* error since the direct-to-Horizon path was synchronous.
|
|
2774
|
+
*
|
|
2775
|
+
* The extra hop adds ~50–150 ms vs. the legacy direct-Horizon path; the
|
|
2776
|
+
* persistence + observability win is worth it.
|
|
2777
|
+
*
|
|
2778
|
+
* Drives `_setTransactionState`: emits `submitting` while in flight,
|
|
2779
|
+
* `submitted` on Horizon ack (pending), `success` on ledger confirmation,
|
|
2780
|
+
* or `error[phase: 'submitting']` on failure.
|
|
2781
|
+
*/
|
|
2782
|
+
async submitTx(signedXdr, opts) {
|
|
2783
|
+
const buildData = this._currentBuildData();
|
|
2784
|
+
const outcomeExtra = buildData ? { buildData } : {};
|
|
2785
|
+
this._setTransactionState({ step: "submitting", signedXdr, ...buildData && { buildData } });
|
|
2786
|
+
const publicKey = this._session?.wallet?.publicKey ?? "";
|
|
2787
|
+
try {
|
|
2788
|
+
const { data, error } = await this._api.POST("/tx/submit", {
|
|
2789
|
+
body: {
|
|
2790
|
+
network: this.getNetwork(),
|
|
2791
|
+
publicKey,
|
|
2792
|
+
signedXdr,
|
|
2793
|
+
...opts?.submissionToken && { idempotencyKey: opts.submissionToken }
|
|
2426
2794
|
}
|
|
2427
|
-
}
|
|
2428
|
-
|
|
2795
|
+
});
|
|
2796
|
+
if (!error && data?.success && data.content) {
|
|
2797
|
+
const { hash, status: backendStatus, resultCode } = data.content;
|
|
2798
|
+
if (backendStatus === "SUCCESS") {
|
|
2799
|
+
this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
|
|
2800
|
+
return { status: "success", hash, ...outcomeExtra };
|
|
2801
|
+
}
|
|
2802
|
+
if (backendStatus === "PENDING") {
|
|
2803
|
+
this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
|
|
2804
|
+
return { status: "pending", hash, ...outcomeExtra };
|
|
2805
|
+
}
|
|
2806
|
+
this._setTransactionState({
|
|
2807
|
+
step: "error",
|
|
2808
|
+
phase: "submitting",
|
|
2809
|
+
...buildData && { buildData },
|
|
2810
|
+
...resultCode && { details: resultCode }
|
|
2811
|
+
});
|
|
2812
|
+
return {
|
|
2813
|
+
status: "error",
|
|
2814
|
+
hash,
|
|
2815
|
+
...outcomeExtra,
|
|
2816
|
+
...resultCode && { details: resultCode, resultCode }
|
|
2817
|
+
};
|
|
2429
2818
|
}
|
|
2430
|
-
|
|
2819
|
+
const details = error?.details;
|
|
2820
|
+
this._setTransactionState({
|
|
2821
|
+
step: "error",
|
|
2822
|
+
phase: "submitting",
|
|
2823
|
+
...buildData && { buildData },
|
|
2824
|
+
...details && { details }
|
|
2825
|
+
});
|
|
2826
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
2827
|
+
} catch (err) {
|
|
2828
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
2829
|
+
this._setTransactionState({
|
|
2830
|
+
step: "error",
|
|
2831
|
+
phase: "submitting",
|
|
2832
|
+
...buildData && { buildData },
|
|
2833
|
+
...details && { details }
|
|
2834
|
+
});
|
|
2835
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
2431
2836
|
}
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2839
|
+
* Signs and submits in one logical step. Returns a {@link SubmitOutcome}.
|
|
2840
|
+
*
|
|
2841
|
+
* - **External wallets**: composes `signTx` + `submitTx` client-side. State
|
|
2842
|
+
* machine sees the full granular sequence `signing → signed → submitting
|
|
2843
|
+
* → success` because the underlying methods each emit.
|
|
2844
|
+
* - **Custodial wallets**: atomic `/tx/sign-and-send` round-trip. State
|
|
2845
|
+
* machine emits the compound `signing-submitting` step (the SDK can't
|
|
2846
|
+
* observe when one phase ends and the next begins inside that single
|
|
2847
|
+
* backend call) and then transitions to `submitted` (Horizon ack only) or
|
|
2848
|
+
* `success` (ledger-confirmed), or `error[phase: 'signing-submitting']`.
|
|
2849
|
+
*/
|
|
2850
|
+
async signAndSubmitTx(unsignedXdr) {
|
|
2851
|
+
if (this._walletAdapter) {
|
|
2852
|
+
const signed = await this.signTx(unsignedXdr);
|
|
2853
|
+
if (signed.status === "error") {
|
|
2854
|
+
const buildData2 = this._currentBuildData();
|
|
2855
|
+
return {
|
|
2856
|
+
status: "error",
|
|
2857
|
+
...buildData2 && { buildData: buildData2 },
|
|
2858
|
+
...signed.details && { details: signed.details }
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
return this.submitTx(signed.signedXdr);
|
|
2862
|
+
}
|
|
2863
|
+
const buildData = this._currentBuildData();
|
|
2864
|
+
const outcomeExtra = buildData ? { buildData } : {};
|
|
2865
|
+
this._setTransactionState({ step: "signing-submitting", ...buildData && { buildData } });
|
|
2432
2866
|
const body = {
|
|
2433
2867
|
network: this.getNetwork(),
|
|
2434
2868
|
publicKey: this._session?.wallet?.publicKey ?? "",
|
|
@@ -2437,15 +2871,129 @@ var PollarClient = class {
|
|
|
2437
2871
|
try {
|
|
2438
2872
|
const { data, error } = await this._api.POST("/tx/sign-and-send", { body });
|
|
2439
2873
|
if (!error && data?.success && data.content?.hash) {
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2874
|
+
const {
|
|
2875
|
+
hash,
|
|
2876
|
+
status: backendStatus,
|
|
2877
|
+
resultCode
|
|
2878
|
+
} = data.content;
|
|
2879
|
+
if (backendStatus === "SUCCESS") {
|
|
2880
|
+
this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
|
|
2881
|
+
return { status: "success", hash, ...outcomeExtra };
|
|
2882
|
+
}
|
|
2883
|
+
if (backendStatus === "PENDING") {
|
|
2884
|
+
this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
|
|
2885
|
+
return { status: "pending", hash, ...outcomeExtra };
|
|
2886
|
+
}
|
|
2887
|
+
this._setTransactionState({
|
|
2888
|
+
step: "error",
|
|
2889
|
+
phase: "signing-submitting",
|
|
2890
|
+
...buildData && { buildData },
|
|
2891
|
+
...resultCode && { details: resultCode }
|
|
2892
|
+
});
|
|
2893
|
+
return {
|
|
2894
|
+
status: "error",
|
|
2895
|
+
hash,
|
|
2896
|
+
...outcomeExtra,
|
|
2897
|
+
...resultCode && { details: resultCode, resultCode }
|
|
2898
|
+
};
|
|
2444
2899
|
}
|
|
2445
|
-
|
|
2446
|
-
this._setTransactionState({
|
|
2900
|
+
const details = error?.details;
|
|
2901
|
+
this._setTransactionState({
|
|
2902
|
+
step: "error",
|
|
2903
|
+
phase: "signing-submitting",
|
|
2904
|
+
...buildData && { buildData },
|
|
2905
|
+
...details && { details }
|
|
2906
|
+
});
|
|
2907
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
2908
|
+
} catch (err) {
|
|
2909
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
2910
|
+
this._setTransactionState({
|
|
2911
|
+
step: "error",
|
|
2912
|
+
phase: "signing-submitting",
|
|
2913
|
+
...buildData && { buildData },
|
|
2914
|
+
...details && { details }
|
|
2915
|
+
});
|
|
2916
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
2447
2917
|
}
|
|
2448
2918
|
}
|
|
2919
|
+
/**
|
|
2920
|
+
* One-shot: build → sign → submit, returning the final {@link SubmitOutcome}.
|
|
2921
|
+
*
|
|
2922
|
+
* - **External wallets**: composes `buildTx` + `signAndSubmitTx` client-side.
|
|
2923
|
+
* State machine sees the full granular sequence (`building → built →
|
|
2924
|
+
* signing → signed → submitting → success`) because each composed call
|
|
2925
|
+
* emits its own transitions.
|
|
2926
|
+
* - **Custodial wallets**: single round-trip to `/tx/build-sign-submit`. The
|
|
2927
|
+
* signed XDR never leaves the backend. State machine emits the compound
|
|
2928
|
+
* `building-signing-submitting` step (the SDK can't observe individual
|
|
2929
|
+
* phase boundaries inside one atomic call) and then transitions to
|
|
2930
|
+
* `submitted` / `success` / `error[phase: 'building-signing-submitting']`.
|
|
2931
|
+
*
|
|
2932
|
+
* If you need granular UI feedback for custodial flows (separate
|
|
2933
|
+
* "Building…", "Signing…", "Submitting…" indicators), call `buildTx`,
|
|
2934
|
+
* `signTx`, and `submitTx` separately instead.
|
|
2935
|
+
*/
|
|
2936
|
+
async buildAndSignAndSubmitTx(operation, params, options) {
|
|
2937
|
+
if (this._walletAdapter) {
|
|
2938
|
+
const built = await this.buildTx(operation, params, options);
|
|
2939
|
+
if (built.status === "error") {
|
|
2940
|
+
return { status: "error", ...built.details && { details: built.details } };
|
|
2941
|
+
}
|
|
2942
|
+
return this.signAndSubmitTx(built.buildData.unsignedXdr);
|
|
2943
|
+
}
|
|
2944
|
+
if (!this._session?.wallet?.publicKey) {
|
|
2945
|
+
this._setTransactionState({ step: "error", phase: "building-signing-submitting", details: "No wallet connected" });
|
|
2946
|
+
return { status: "error", details: "No wallet connected" };
|
|
2947
|
+
}
|
|
2948
|
+
this._setTransactionState({ step: "building-signing-submitting" });
|
|
2949
|
+
try {
|
|
2950
|
+
const { data, error } = await this._api.POST("/tx/build-sign-submit", {
|
|
2951
|
+
body: {
|
|
2952
|
+
network: this.getNetwork(),
|
|
2953
|
+
publicKey: this._session.wallet.publicKey,
|
|
2954
|
+
operation,
|
|
2955
|
+
params,
|
|
2956
|
+
options: options ?? {}
|
|
2957
|
+
}
|
|
2958
|
+
});
|
|
2959
|
+
if (!error && data?.success && data.content) {
|
|
2960
|
+
const { hash, status: backendStatus, resultCode } = data.content;
|
|
2961
|
+
if (backendStatus === "SUCCESS") {
|
|
2962
|
+
this._setTransactionState({ step: "success", hash });
|
|
2963
|
+
return { status: "success", hash };
|
|
2964
|
+
}
|
|
2965
|
+
if (backendStatus === "PENDING") {
|
|
2966
|
+
this._setTransactionState({ step: "submitted", hash });
|
|
2967
|
+
return { status: "pending", hash };
|
|
2968
|
+
}
|
|
2969
|
+
this._setTransactionState({
|
|
2970
|
+
step: "error",
|
|
2971
|
+
phase: "building-signing-submitting",
|
|
2972
|
+
...resultCode && { details: resultCode }
|
|
2973
|
+
});
|
|
2974
|
+
return { status: "error", hash, ...resultCode && { details: resultCode, resultCode } };
|
|
2975
|
+
}
|
|
2976
|
+
const details = error?.details;
|
|
2977
|
+
this._setTransactionState({
|
|
2978
|
+
step: "error",
|
|
2979
|
+
phase: "building-signing-submitting",
|
|
2980
|
+
...details && { details }
|
|
2981
|
+
});
|
|
2982
|
+
return { status: "error", ...details && { details } };
|
|
2983
|
+
} catch (err) {
|
|
2984
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
2985
|
+
this._setTransactionState({
|
|
2986
|
+
step: "error",
|
|
2987
|
+
phase: "building-signing-submitting",
|
|
2988
|
+
...details && { details }
|
|
2989
|
+
});
|
|
2990
|
+
return { status: "error", ...details && { details } };
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
/** Alias for {@link buildAndSignAndSubmitTx} — shorter "just do the thing" name. */
|
|
2994
|
+
async runTx(operation, params, options) {
|
|
2995
|
+
return this.buildAndSignAndSubmitTx(operation, params, options);
|
|
2996
|
+
}
|
|
2449
2997
|
// ─── App config ───────────────────────────────────────────────────────────
|
|
2450
2998
|
async getAppConfig() {
|
|
2451
2999
|
try {
|
|
@@ -2512,6 +3060,11 @@ var PollarClient = class {
|
|
|
2512
3060
|
_flowDeps(signal) {
|
|
2513
3061
|
return {
|
|
2514
3062
|
api: this._api,
|
|
3063
|
+
basePath: this.basePath,
|
|
3064
|
+
// SSE status streaming works on web; React Native's `fetch` has no
|
|
3065
|
+
// readable `response.body`, so those clients poll the non-streaming
|
|
3066
|
+
// status endpoint instead. `isBrowser` is false in RN and SSR alike.
|
|
3067
|
+
useStreaming: isBrowser,
|
|
2515
3068
|
signal,
|
|
2516
3069
|
setAuthState: this._setAuthState.bind(this),
|
|
2517
3070
|
storeSession: this._storeSession.bind(this),
|
|
@@ -2533,7 +3086,22 @@ var PollarClient = class {
|
|
|
2533
3086
|
*/
|
|
2534
3087
|
async _resolveWalletAdapter(id) {
|
|
2535
3088
|
if (this._walletAdapterResolver) {
|
|
2536
|
-
|
|
3089
|
+
const timeoutMs = this._walletResolverTimeoutMs;
|
|
3090
|
+
let timeoutHandle;
|
|
3091
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
3092
|
+
timeoutHandle = setTimeout(() => {
|
|
3093
|
+
reject(
|
|
3094
|
+
Object.assign(new Error(`[PollarClient] Wallet adapter resolver for "${id}" timed out after ${timeoutMs}ms`), {
|
|
3095
|
+
code: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
|
|
3096
|
+
})
|
|
3097
|
+
);
|
|
3098
|
+
}, timeoutMs);
|
|
3099
|
+
});
|
|
3100
|
+
try {
|
|
3101
|
+
return await Promise.race([Promise.resolve(this._walletAdapterResolver(id)), timeoutPromise]);
|
|
3102
|
+
} finally {
|
|
3103
|
+
if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
|
|
3104
|
+
}
|
|
2537
3105
|
}
|
|
2538
3106
|
if (id === "freighter" /* FREIGHTER */) return new FreighterAdapter();
|
|
2539
3107
|
if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter();
|
|
@@ -2547,6 +3115,16 @@ var PollarClient = class {
|
|
|
2547
3115
|
this._setAuthState({ step: "idle" });
|
|
2548
3116
|
return;
|
|
2549
3117
|
}
|
|
3118
|
+
if (error instanceof Error && error.code === AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT) {
|
|
3119
|
+
console.error("[PollarClient]", error.message);
|
|
3120
|
+
this._setAuthState({
|
|
3121
|
+
step: "error",
|
|
3122
|
+
previousStep: this._authState.step,
|
|
3123
|
+
message: error.message,
|
|
3124
|
+
errorCode: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
|
|
3125
|
+
});
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
2550
3128
|
console.error("[PollarClient] Unexpected error in auth flow", error);
|
|
2551
3129
|
this._setAuthState({
|
|
2552
3130
|
step: "error",
|
|
@@ -2568,6 +3146,7 @@ var PollarClient = class {
|
|
|
2568
3146
|
}
|
|
2569
3147
|
console.info("[PollarClient] Session restored from storage");
|
|
2570
3148
|
this._setAuthState({ step: "authenticated", session: this._session });
|
|
3149
|
+
this._scheduleNextRefresh();
|
|
2571
3150
|
} else {
|
|
2572
3151
|
console.info("[PollarClient] No session in storage");
|
|
2573
3152
|
}
|
|
@@ -2594,9 +3173,11 @@ var PollarClient = class {
|
|
|
2594
3173
|
}
|
|
2595
3174
|
await writeStorage(this._storage, this.apiKeyHash, persisted);
|
|
2596
3175
|
this._setAuthState({ step: "authenticated", session: persisted });
|
|
3176
|
+
this._scheduleNextRefresh();
|
|
2597
3177
|
}
|
|
2598
3178
|
async _clearSession() {
|
|
2599
3179
|
console.info("[PollarClient] Session cleared");
|
|
3180
|
+
this._clearRefreshTimer();
|
|
2600
3181
|
this._session = null;
|
|
2601
3182
|
this._profile = null;
|
|
2602
3183
|
this._walletAdapter = null;
|
|
@@ -2629,6 +3210,46 @@ var PollarClient = class {
|
|
|
2629
3210
|
console.info(`[PollarClient] transaction:${next.step}`);
|
|
2630
3211
|
for (const cb of this._transactionStateListeners) cb(next);
|
|
2631
3212
|
}
|
|
3213
|
+
/**
|
|
3214
|
+
* Threads `buildData` through state transitions. When the user has already
|
|
3215
|
+
* called `buildTx`, every subsequent state (signing, signed, submitting,
|
|
3216
|
+
* submitted, success, error) should carry the build summary so modal UIs
|
|
3217
|
+
* can keep showing "Send 5 USDC to G..." through the whole flow.
|
|
3218
|
+
*/
|
|
3219
|
+
_currentBuildData() {
|
|
3220
|
+
const s = this._transactionState;
|
|
3221
|
+
if (!s) return void 0;
|
|
3222
|
+
if ("buildData" in s && s.buildData) return s.buildData;
|
|
3223
|
+
return void 0;
|
|
3224
|
+
}
|
|
3225
|
+
};
|
|
3226
|
+
|
|
3227
|
+
// src/stellar/StellarClient.ts
|
|
3228
|
+
var HORIZON_URLS = {
|
|
3229
|
+
mainnet: "https://horizon.stellar.org",
|
|
3230
|
+
testnet: "https://horizon-testnet.stellar.org"
|
|
3231
|
+
};
|
|
3232
|
+
var StellarClient = class {
|
|
3233
|
+
constructor(config) {
|
|
3234
|
+
this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
|
|
3235
|
+
}
|
|
3236
|
+
async submitTransaction(signedXdr) {
|
|
3237
|
+
try {
|
|
3238
|
+
const response = await fetch(`${this.horizonUrl}/transactions`, {
|
|
3239
|
+
method: "POST",
|
|
3240
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
3241
|
+
body: new URLSearchParams({ tx: signedXdr })
|
|
3242
|
+
});
|
|
3243
|
+
if (!response.ok) {
|
|
3244
|
+
const body = await response.json().catch(() => ({}));
|
|
3245
|
+
return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
|
|
3246
|
+
}
|
|
3247
|
+
const data = await response.json();
|
|
3248
|
+
return { success: true, hash: data.hash };
|
|
3249
|
+
} catch {
|
|
3250
|
+
return { success: false, errorCode: "NETWORK_ERROR" };
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
2632
3253
|
};
|
|
2633
3254
|
|
|
2634
3255
|
// src/index.rn.ts
|