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