@pollar/core 0.8.2 → 0.9.0-rc.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 +35 -0
- package/dist/adapters/react-native-appstate.d.mts +1 -1
- package/dist/adapters/react-native-appstate.d.ts +1 -1
- package/dist/index.d.mts +996 -96
- package/dist/index.d.ts +996 -96
- package/dist/index.js +546 -112
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +545 -113
- package/dist/index.mjs.map +1 -1
- package/dist/index.rn.d.mts +2 -2
- package/dist/index.rn.d.ts +2 -2
- package/dist/index.rn.js +691 -262
- package/dist/index.rn.js.map +1 -1
- package/dist/index.rn.mjs +690 -263
- package/dist/index.rn.mjs.map +1 -1
- package/dist/{types-84G_htcn.d.mts → types-Dyky8g0p.d.mts} +5 -12
- package/dist/{types-84G_htcn.d.ts → types-Dyky8g0p.d.ts} +5 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -186,17 +186,6 @@ function defaultKeyManager(storage, apiKey) {
|
|
|
186
186
|
}
|
|
187
187
|
return _factory(storage, apiKey);
|
|
188
188
|
}
|
|
189
|
-
async function sha256(data) {
|
|
190
|
-
return sha2.sha256(data);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// src/lib/api-key-hash.ts
|
|
194
|
-
async function hashApiKey(apiKey) {
|
|
195
|
-
const digest = await sha256(new TextEncoder().encode(apiKey));
|
|
196
|
-
let hex = "";
|
|
197
|
-
for (let i = 0; i < 4; i++) hex += digest[i].toString(16).padStart(2, "0");
|
|
198
|
-
return hex;
|
|
199
|
-
}
|
|
200
189
|
|
|
201
190
|
// src/lib/base64url.ts
|
|
202
191
|
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
@@ -234,6 +223,17 @@ function base64urlEncode(bytes) {
|
|
|
234
223
|
function base64urlEncodeString(s) {
|
|
235
224
|
return base64urlEncode(new TextEncoder().encode(s));
|
|
236
225
|
}
|
|
226
|
+
async function sha256(data) {
|
|
227
|
+
return sha2.sha256(data);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/lib/api-key-hash.ts
|
|
231
|
+
async function hashApiKey(apiKey) {
|
|
232
|
+
const digest = await sha256(new TextEncoder().encode(apiKey));
|
|
233
|
+
let hex = "";
|
|
234
|
+
for (let i = 0; i < 4; i++) hex += digest[i].toString(16).padStart(2, "0");
|
|
235
|
+
return hex;
|
|
236
|
+
}
|
|
237
237
|
|
|
238
238
|
// src/keys/thumbprint.ts
|
|
239
239
|
async function computeJwkThumbprint(jwk) {
|
|
@@ -244,11 +244,14 @@ async function computeJwkThumbprint(jwk) {
|
|
|
244
244
|
const digest = await sha256(new TextEncoder().encode(canonical));
|
|
245
245
|
return base64urlEncode(digest);
|
|
246
246
|
}
|
|
247
|
+
function toBase64url(value) {
|
|
248
|
+
return value.replace(/\+/g, "-").replace(/\//g, "_").replace(/[^A-Za-z0-9_-]/g, "");
|
|
249
|
+
}
|
|
247
250
|
function canonicalEcJwk(jwk) {
|
|
248
251
|
if (jwk.kty !== "EC" || jwk.crv !== "P-256" || typeof jwk.x !== "string" || typeof jwk.y !== "string") {
|
|
249
252
|
throw new Error("[PollarClient:thumbprint] Source JWK is not an EC P-256 public key");
|
|
250
253
|
}
|
|
251
|
-
return { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y };
|
|
254
|
+
return { kty: "EC", crv: "P-256", x: toBase64url(jwk.x), y: toBase64url(jwk.y) };
|
|
252
255
|
}
|
|
253
256
|
|
|
254
257
|
// src/keys/web-crypto.ts
|
|
@@ -262,7 +265,7 @@ function openDb() {
|
|
|
262
265
|
return;
|
|
263
266
|
}
|
|
264
267
|
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
265
|
-
req.onerror = () => reject(req.error ?? new Error("IDB open failed"));
|
|
268
|
+
req.onerror = () => reject(req.error ?? new Error("[PollarClient:keys] IDB open failed"));
|
|
266
269
|
req.onsuccess = () => resolve(req.result);
|
|
267
270
|
req.onupgradeneeded = () => {
|
|
268
271
|
const db = req.result;
|
|
@@ -275,7 +278,7 @@ function openDb() {
|
|
|
275
278
|
function awaitTx(req) {
|
|
276
279
|
return new Promise((resolve, reject) => {
|
|
277
280
|
req.onsuccess = () => resolve(req.result);
|
|
278
|
-
req.onerror = () => reject(req.error ?? new Error("IDB request failed"));
|
|
281
|
+
req.onerror = () => reject(req.error ?? new Error("[PollarClient:keys] IDB request failed"));
|
|
279
282
|
});
|
|
280
283
|
}
|
|
281
284
|
async function dbGet(key) {
|
|
@@ -338,7 +341,6 @@ var WebCryptoKeyManager = class {
|
|
|
338
341
|
if (this.keyPair) return;
|
|
339
342
|
if (!this._initPromise) {
|
|
340
343
|
this._initPromise = this._doInit().catch((err) => {
|
|
341
|
-
console.error("[PollarClient:keys] WebCryptoKeyManager init failed", err);
|
|
342
344
|
this._initPromise = null;
|
|
343
345
|
throw err;
|
|
344
346
|
});
|
|
@@ -370,10 +372,39 @@ var WebCryptoKeyManager = class {
|
|
|
370
372
|
}
|
|
371
373
|
}
|
|
372
374
|
this.keyPair = pair;
|
|
373
|
-
|
|
374
|
-
this.publicJwk = canonicalEcJwk(exported);
|
|
375
|
+
this.publicJwk = await this._exportPublicJwk(pair.publicKey);
|
|
375
376
|
this.thumbprint = await computeJwkThumbprint(this.publicJwk);
|
|
376
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* Derive the public JWK from a `CryptoKey`. Prefers the `'raw'` export (the
|
|
380
|
+
* 65-byte uncompressed point `0x04 || X(32) || Y(32)`) and base64url-encodes
|
|
381
|
+
* the coordinates ourselves — that sidesteps polyfills whose `exportKey('jwk')`
|
|
382
|
+
* emits non-base64url `x`/`y` (standard base64, `=` padding, or — as seen with
|
|
383
|
+
* `react-native-quick-crypto` — a stray `.`). Real browsers and most polyfills
|
|
384
|
+
* support `'raw'` for public EC keys.
|
|
385
|
+
*
|
|
386
|
+
* Falls back to the `'jwk'` export (normalized via `canonicalEcJwk`) if `'raw'`
|
|
387
|
+
* is unsupported or returns an unexpected shape, so this can't regress on a
|
|
388
|
+
* runtime that only implements the JWK path. Both routes yield identical
|
|
389
|
+
* coordinate bytes, so the `cnf.jkt` thumbprint is unchanged either way.
|
|
390
|
+
*/
|
|
391
|
+
async _exportPublicJwk(publicKey) {
|
|
392
|
+
try {
|
|
393
|
+
const raw = new Uint8Array(await globalThis.crypto.subtle.exportKey("raw", publicKey));
|
|
394
|
+
if (raw.length !== 65 || raw[0] !== 4) {
|
|
395
|
+
throw new Error(`[PollarClient:keys] Unexpected raw EC point (len=${raw.length}, tag=${raw[0]})`);
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
kty: "EC",
|
|
399
|
+
crv: "P-256",
|
|
400
|
+
x: base64urlEncode(raw.slice(1, 33)),
|
|
401
|
+
y: base64urlEncode(raw.slice(33, 65))
|
|
402
|
+
};
|
|
403
|
+
} catch {
|
|
404
|
+
const jwk = await globalThis.crypto.subtle.exportKey("jwk", publicKey);
|
|
405
|
+
return canonicalEcJwk(jwk);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
377
408
|
async reset() {
|
|
378
409
|
try {
|
|
379
410
|
if (this.apiKeyHash) await dbDelete(this.apiKeyHash);
|
|
@@ -1044,6 +1075,16 @@ function normalizeHtu(rawUrl) {
|
|
|
1044
1075
|
return `${scheme}//${host}${portPart}${url.pathname}`;
|
|
1045
1076
|
}
|
|
1046
1077
|
|
|
1078
|
+
// src/lib/logger.ts
|
|
1079
|
+
var RANK = { silent: 0, error: 1, warn: 2, info: 3, debug: 4 };
|
|
1080
|
+
function createLogger(level = "info", sink = console) {
|
|
1081
|
+
const threshold = RANK[level];
|
|
1082
|
+
const gate = (lvl) => (...args) => {
|
|
1083
|
+
if (threshold >= RANK[lvl]) sink[lvl](...args);
|
|
1084
|
+
};
|
|
1085
|
+
return { error: gate("error"), warn: gate("warn"), info: gate("info"), debug: gate("debug") };
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1047
1088
|
// src/storage/web.ts
|
|
1048
1089
|
var LOG_PREFIX = "[PollarClient:storage]";
|
|
1049
1090
|
function createMemoryAdapter() {
|
|
@@ -1067,7 +1108,7 @@ function createLocalStorageAdapter(options = {}) {
|
|
|
1067
1108
|
function degrade(reason, error) {
|
|
1068
1109
|
if (degraded) return;
|
|
1069
1110
|
degraded = true;
|
|
1070
|
-
console.warn(`${LOG_PREFIX} localStorage unavailable (${reason}); degrading to in-memory storage`);
|
|
1111
|
+
(options.logger ?? console).warn(`${LOG_PREFIX} localStorage unavailable (${reason}); degrading to in-memory storage`);
|
|
1071
1112
|
options.onDegrade?.(reason, error);
|
|
1072
1113
|
}
|
|
1073
1114
|
return {
|
|
@@ -1131,6 +1172,9 @@ function defaultStorage(options = {}) {
|
|
|
1131
1172
|
return createLocalStorageAdapter(options);
|
|
1132
1173
|
}
|
|
1133
1174
|
|
|
1175
|
+
// src/version.ts
|
|
1176
|
+
var POLLAR_CORE_VERSION = "0.9.0-rc.1" ;
|
|
1177
|
+
|
|
1134
1178
|
// src/visibility/noop.ts
|
|
1135
1179
|
function createNoopVisibilityProvider() {
|
|
1136
1180
|
return {
|
|
@@ -1195,6 +1239,7 @@ var AUTH_ERROR_CODES = {
|
|
|
1195
1239
|
WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
|
|
1196
1240
|
WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
|
|
1197
1241
|
WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
|
|
1242
|
+
PASSKEY_FAILED: "PASSKEY_FAILED",
|
|
1198
1243
|
UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
|
|
1199
1244
|
};
|
|
1200
1245
|
var PollarFlowError = class extends Error {
|
|
@@ -1240,7 +1285,7 @@ var FreighterAdapter = class {
|
|
|
1240
1285
|
if (!userInfo?.publicKey) {
|
|
1241
1286
|
throw new Error("Failed to get user information from Freighter");
|
|
1242
1287
|
}
|
|
1243
|
-
return { address: userInfo.publicKey
|
|
1288
|
+
return { address: userInfo.publicKey };
|
|
1244
1289
|
}
|
|
1245
1290
|
async disconnect() {
|
|
1246
1291
|
}
|
|
@@ -1278,6 +1323,19 @@ var FreighterAdapter = class {
|
|
|
1278
1323
|
};
|
|
1279
1324
|
|
|
1280
1325
|
// src/wallets/AlbedoAdapter.ts
|
|
1326
|
+
var PUBLIC_PASSPHRASE = "Public Global Stellar Network ; September 2015";
|
|
1327
|
+
var TESTNET_PASSPHRASE = "Test SDF Network ; September 2015";
|
|
1328
|
+
function albedoNetwork(options, fallback) {
|
|
1329
|
+
switch (options?.networkPassphrase) {
|
|
1330
|
+
case PUBLIC_PASSPHRASE:
|
|
1331
|
+
return "public";
|
|
1332
|
+
case TESTNET_PASSPHRASE:
|
|
1333
|
+
return "testnet";
|
|
1334
|
+
}
|
|
1335
|
+
if (options?.network === "public" || options?.network === "mainnet") return "public";
|
|
1336
|
+
if (options?.network === "testnet") return "testnet";
|
|
1337
|
+
return fallback;
|
|
1338
|
+
}
|
|
1281
1339
|
function openAlbedoPopup(url) {
|
|
1282
1340
|
const popup = window.open(url, "albedo", "width=420,height=720,resizable=yes,scrollbars=yes");
|
|
1283
1341
|
if (!popup) {
|
|
@@ -1316,7 +1374,13 @@ function waitForAlbedoResult() {
|
|
|
1316
1374
|
});
|
|
1317
1375
|
}
|
|
1318
1376
|
var AlbedoAdapter = class {
|
|
1319
|
-
|
|
1377
|
+
/**
|
|
1378
|
+
* Network used for `connect` and `signAuthEntry` (which carry no per-call
|
|
1379
|
+
* network) and as the fallback for `signTransaction`. Defaults to `'testnet'`
|
|
1380
|
+
* to preserve the previous behavior when constructed with no argument.
|
|
1381
|
+
*/
|
|
1382
|
+
constructor(network = "testnet") {
|
|
1383
|
+
this.network = network;
|
|
1320
1384
|
this.type = "albedo" /* ALBEDO */;
|
|
1321
1385
|
}
|
|
1322
1386
|
async isAvailable() {
|
|
@@ -1326,7 +1390,7 @@ var AlbedoAdapter = class {
|
|
|
1326
1390
|
const url = new URL("https://albedo.link");
|
|
1327
1391
|
url.searchParams.set("intent", "public-key");
|
|
1328
1392
|
url.searchParams.set("app_name", "Pollar");
|
|
1329
|
-
url.searchParams.set("network",
|
|
1393
|
+
url.searchParams.set("network", this.network);
|
|
1330
1394
|
url.searchParams.set("callback", `${window.location.origin}/albedo-callback`);
|
|
1331
1395
|
url.searchParams.set("origin", window.location.origin);
|
|
1332
1396
|
openAlbedoPopup(url.toString());
|
|
@@ -1334,7 +1398,7 @@ var AlbedoAdapter = class {
|
|
|
1334
1398
|
if (!result.pubkey) {
|
|
1335
1399
|
throw new Error("Albedo connection rejected");
|
|
1336
1400
|
}
|
|
1337
|
-
return { address: result.pubkey
|
|
1401
|
+
return { address: result.pubkey };
|
|
1338
1402
|
}
|
|
1339
1403
|
async disconnect() {
|
|
1340
1404
|
}
|
|
@@ -1344,12 +1408,12 @@ var AlbedoAdapter = class {
|
|
|
1344
1408
|
async getNetwork() {
|
|
1345
1409
|
throw new Error("Albedo does not expose network");
|
|
1346
1410
|
}
|
|
1347
|
-
async signTransaction(xdr,
|
|
1411
|
+
async signTransaction(xdr, options) {
|
|
1348
1412
|
const url = new URL("https://albedo.link");
|
|
1349
1413
|
url.searchParams.set("intent", "tx");
|
|
1350
1414
|
url.searchParams.set("xdr", xdr);
|
|
1351
1415
|
url.searchParams.set("app_name", "Pollar");
|
|
1352
|
-
url.searchParams.set("network",
|
|
1416
|
+
url.searchParams.set("network", albedoNetwork(options, this.network));
|
|
1353
1417
|
url.searchParams.set("callback", window.location.href);
|
|
1354
1418
|
url.searchParams.set("origin", window.location.origin);
|
|
1355
1419
|
window.location.href = url.toString();
|
|
@@ -1362,7 +1426,7 @@ var AlbedoAdapter = class {
|
|
|
1362
1426
|
url.searchParams.set("intent", "sign-auth-entry");
|
|
1363
1427
|
url.searchParams.set("xdr", entryXdr);
|
|
1364
1428
|
url.searchParams.set("app_name", "Pollar");
|
|
1365
|
-
url.searchParams.set("network",
|
|
1429
|
+
url.searchParams.set("network", this.network);
|
|
1366
1430
|
url.searchParams.set("callback", window.location.href);
|
|
1367
1431
|
url.searchParams.set("origin", window.location.origin);
|
|
1368
1432
|
window.location.href = url.toString();
|
|
@@ -1393,84 +1457,98 @@ function isBoundedString(v, max, allowEmpty = false) {
|
|
|
1393
1457
|
if (!allowEmpty && v.length === 0) return false;
|
|
1394
1458
|
return v.length <= max;
|
|
1395
1459
|
}
|
|
1396
|
-
function isValidSession(value) {
|
|
1460
|
+
function isValidSession(value, logger = console) {
|
|
1397
1461
|
if (typeof value !== "object" || value === null) {
|
|
1398
|
-
|
|
1462
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 value is not an object");
|
|
1399
1463
|
return false;
|
|
1400
1464
|
}
|
|
1401
1465
|
const s = value;
|
|
1402
1466
|
if (!isBoundedString(s["clientSessionId"], MAX_CLIENT_SESSION_ID)) {
|
|
1403
|
-
|
|
1467
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 clientSessionId missing/empty/too long");
|
|
1404
1468
|
return false;
|
|
1405
1469
|
}
|
|
1406
1470
|
if (s["userId"] !== null && !isBoundedString(s["userId"], MAX_USER_ID)) {
|
|
1407
|
-
|
|
1471
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 userId must be string|null");
|
|
1408
1472
|
return false;
|
|
1409
1473
|
}
|
|
1410
1474
|
if (!isBoundedString(s["status"], MAX_STATUS)) {
|
|
1411
|
-
|
|
1475
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 status must be string");
|
|
1412
1476
|
return false;
|
|
1413
1477
|
}
|
|
1414
1478
|
const token = s["token"];
|
|
1415
1479
|
if (typeof token !== "object" || token === null) {
|
|
1416
|
-
|
|
1480
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 token missing or not an object");
|
|
1417
1481
|
return false;
|
|
1418
1482
|
}
|
|
1419
1483
|
const t = token;
|
|
1420
1484
|
if (!isBoundedString(t["accessToken"], MAX_ACCESS_TOKEN)) {
|
|
1421
|
-
|
|
1485
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 token.accessToken missing/empty/too long");
|
|
1422
1486
|
return false;
|
|
1423
1487
|
}
|
|
1424
1488
|
if (!isBoundedString(t["refreshToken"], MAX_REFRESH_TOKEN)) {
|
|
1425
|
-
|
|
1489
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 token.refreshToken missing/empty/too long");
|
|
1426
1490
|
return false;
|
|
1427
1491
|
}
|
|
1428
1492
|
if (typeof t["expiresAt"] !== "number" || !Number.isFinite(t["expiresAt"])) {
|
|
1429
|
-
|
|
1493
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 token.expiresAt must be a finite number");
|
|
1430
1494
|
return false;
|
|
1431
1495
|
}
|
|
1432
1496
|
const user = s["user"];
|
|
1433
1497
|
if (typeof user !== "object" || user === null) {
|
|
1434
|
-
|
|
1498
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 user missing or not an object");
|
|
1435
1499
|
return false;
|
|
1436
1500
|
}
|
|
1437
1501
|
const u = user;
|
|
1438
1502
|
if (u["id"] !== void 0 && !isBoundedString(u["id"], MAX_USER_ID)) {
|
|
1439
|
-
|
|
1503
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 user.id must be string if present");
|
|
1440
1504
|
return false;
|
|
1441
1505
|
}
|
|
1442
1506
|
if (typeof u["ready"] !== "boolean") {
|
|
1443
|
-
|
|
1507
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 user.ready must be boolean");
|
|
1444
1508
|
return false;
|
|
1445
1509
|
}
|
|
1446
1510
|
const wallet = s["wallet"];
|
|
1447
1511
|
if (typeof wallet !== "object" || wallet === null) {
|
|
1448
|
-
|
|
1512
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 wallet missing or not an object");
|
|
1449
1513
|
return false;
|
|
1450
1514
|
}
|
|
1451
1515
|
const w = wallet;
|
|
1452
|
-
if (w["
|
|
1453
|
-
|
|
1516
|
+
if (w["type"] !== "custodial" && w["type"] !== "smart" && w["type"] !== "external") {
|
|
1517
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 wallet.type must be custodial|smart|external");
|
|
1518
|
+
return false;
|
|
1519
|
+
}
|
|
1520
|
+
if (w["address"] !== null && !isBoundedString(w["address"], MAX_WALLET_PUBLIC_KEY)) {
|
|
1521
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 wallet.address must be string|null");
|
|
1454
1522
|
return false;
|
|
1455
1523
|
}
|
|
1456
1524
|
if (w["existsOnStellar"] !== void 0 && typeof w["existsOnStellar"] !== "boolean") {
|
|
1457
|
-
|
|
1525
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 wallet.existsOnStellar must be boolean if present");
|
|
1458
1526
|
return false;
|
|
1459
1527
|
}
|
|
1460
1528
|
if (w["createdAt"] !== void 0 && (typeof w["createdAt"] !== "number" || !Number.isFinite(w["createdAt"]))) {
|
|
1461
|
-
|
|
1529
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 wallet.createdAt must be a finite number if present");
|
|
1530
|
+
return false;
|
|
1531
|
+
}
|
|
1532
|
+
if (w["linkedAt"] !== void 0 && (typeof w["linkedAt"] !== "number" || !Number.isFinite(w["linkedAt"]))) {
|
|
1533
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 wallet.linkedAt must be a finite number if present");
|
|
1462
1534
|
return false;
|
|
1463
1535
|
}
|
|
1464
1536
|
return true;
|
|
1465
1537
|
}
|
|
1466
|
-
async function readStorage(storage, apiKeyHash) {
|
|
1538
|
+
async function readStorage(storage, apiKeyHash, logger = console) {
|
|
1467
1539
|
const raw = await storage.get(sessionStorageKey(apiKeyHash));
|
|
1468
1540
|
if (!raw) return null;
|
|
1469
1541
|
try {
|
|
1470
1542
|
const session = JSON.parse(raw);
|
|
1471
|
-
if (
|
|
1543
|
+
if (typeof session === "object" && session !== null) {
|
|
1544
|
+
const w = session.wallet;
|
|
1545
|
+
if (w && w["address"] == null && typeof w["publicKey"] === "string") {
|
|
1546
|
+
w["address"] = w["publicKey"];
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
if (!isValidSession(session, logger)) {
|
|
1472
1550
|
await storage.remove(sessionStorageKey(apiKeyHash));
|
|
1473
|
-
|
|
1551
|
+
logger.warn("[PollarClient:session] Stored session is invalid \u2014 clearing storage");
|
|
1474
1552
|
return null;
|
|
1475
1553
|
}
|
|
1476
1554
|
if (session.token.expiresAt * 1e3 < Date.now()) {
|
|
@@ -1478,7 +1556,7 @@ async function readStorage(storage, apiKeyHash) {
|
|
|
1478
1556
|
}
|
|
1479
1557
|
return session;
|
|
1480
1558
|
} catch (error) {
|
|
1481
|
-
|
|
1559
|
+
logger.error("[PollarClient:session] Failed to parse session from storage", error);
|
|
1482
1560
|
await storage.remove(sessionStorageKey(apiKeyHash));
|
|
1483
1561
|
return null;
|
|
1484
1562
|
}
|
|
@@ -1540,7 +1618,7 @@ function abortableDelay(ms, signal) {
|
|
|
1540
1618
|
});
|
|
1541
1619
|
}
|
|
1542
1620
|
var MAX_BACKOFF_MS = 5e3;
|
|
1543
|
-
async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200, signal) {
|
|
1621
|
+
async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200, signal, logger = console) {
|
|
1544
1622
|
let backoff = retryDelayMs;
|
|
1545
1623
|
const sleep = async (ms) => {
|
|
1546
1624
|
if (ms <= 0) return;
|
|
@@ -1558,7 +1636,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
|
|
|
1558
1636
|
}));
|
|
1559
1637
|
} catch (e) {
|
|
1560
1638
|
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
1561
|
-
|
|
1639
|
+
logger.debug("[PollarClient:stream] session-status request failed; will retry", e);
|
|
1562
1640
|
}
|
|
1563
1641
|
if (error || !data) {
|
|
1564
1642
|
await sleep(backoff);
|
|
@@ -1598,7 +1676,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
|
|
|
1598
1676
|
} catch (e) {
|
|
1599
1677
|
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
1600
1678
|
if (e instanceof SessionStatusError) throw e;
|
|
1601
|
-
|
|
1679
|
+
logger.debug("[PollarClient:stream] session-status stream read failed; will retry", e);
|
|
1602
1680
|
} finally {
|
|
1603
1681
|
reader.releaseLock();
|
|
1604
1682
|
}
|
|
@@ -1608,7 +1686,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
|
|
|
1608
1686
|
if (delay) await sleep(delay);
|
|
1609
1687
|
}
|
|
1610
1688
|
}
|
|
1611
|
-
async function pollUntilFound(baseUrl, clientSessionId, check, intervalMs = 500, signal) {
|
|
1689
|
+
async function pollUntilFound(baseUrl, clientSessionId, check, intervalMs = 500, signal, logger = console) {
|
|
1612
1690
|
const url = `${baseUrl}/auth/session/status/${encodeURIComponent(clientSessionId)}/poll`;
|
|
1613
1691
|
let backoff = intervalMs;
|
|
1614
1692
|
const sleep = async (ms) => {
|
|
@@ -1626,7 +1704,7 @@ async function pollUntilFound(baseUrl, clientSessionId, check, intervalMs = 500,
|
|
|
1626
1704
|
envelope = await response.json().catch(() => null);
|
|
1627
1705
|
} catch (e) {
|
|
1628
1706
|
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
1629
|
-
|
|
1707
|
+
logger.debug("[PollarClient:stream] session-status poll failed; will retry", e);
|
|
1630
1708
|
}
|
|
1631
1709
|
if (httpStatus === 404 || envelope?.code === "INVALID_CLIENT_SESSION_ID") {
|
|
1632
1710
|
throw new SessionStatusError("INVALID_CLIENT_SESSION_ID");
|
|
@@ -1643,13 +1721,13 @@ async function pollUntilFound(baseUrl, clientSessionId, check, intervalMs = 500,
|
|
|
1643
1721
|
}
|
|
1644
1722
|
}
|
|
1645
1723
|
function waitForSessionReady(args) {
|
|
1646
|
-
const { api, baseUrl, clientSessionId, check, useStreaming, retryDelayMs, signal } = args;
|
|
1647
|
-
return useStreaming ? streamUntilFound(api, clientSessionId, check, retryDelayMs ?? 200, signal) : pollUntilFound(baseUrl, clientSessionId, check, retryDelayMs ?? 500, signal);
|
|
1724
|
+
const { api, baseUrl, clientSessionId, check, useStreaming, retryDelayMs, signal, logger = console } = args;
|
|
1725
|
+
return useStreaming ? streamUntilFound(api, clientSessionId, check, retryDelayMs ?? 200, signal, logger) : pollUntilFound(baseUrl, clientSessionId, check, retryDelayMs ?? 500, signal, logger);
|
|
1648
1726
|
}
|
|
1649
1727
|
|
|
1650
1728
|
// src/client/auth/authenticate.ts
|
|
1651
1729
|
async function authenticate(clientSessionId, deps, expectedWallet) {
|
|
1652
|
-
const { api, basePath, useStreaming, signal, setAuthState, storeSession, clearSession } = deps;
|
|
1730
|
+
const { api, logger, basePath, useStreaming, signal, setAuthState, storeSession, clearSession } = deps;
|
|
1653
1731
|
setAuthState({ step: "authenticating" });
|
|
1654
1732
|
try {
|
|
1655
1733
|
await waitForSessionReady({
|
|
@@ -1658,7 +1736,8 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
|
|
|
1658
1736
|
clientSessionId,
|
|
1659
1737
|
check: (data2) => data2?.status === "READY",
|
|
1660
1738
|
useStreaming,
|
|
1661
|
-
signal
|
|
1739
|
+
signal,
|
|
1740
|
+
logger
|
|
1662
1741
|
});
|
|
1663
1742
|
} catch (err) {
|
|
1664
1743
|
if (err instanceof SessionStatusError) {
|
|
@@ -1683,7 +1762,7 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
|
|
|
1683
1762
|
},
|
|
1684
1763
|
signal
|
|
1685
1764
|
});
|
|
1686
|
-
if (data?.code === "SDK_LOGIN_SUCCESS" && isValidSession(data?.content)) {
|
|
1765
|
+
if (data?.code === "SDK_LOGIN_SUCCESS" && isValidSession(data?.content, logger)) {
|
|
1687
1766
|
if (expectedWallet && data.content.data.providers.wallet?.address !== expectedWallet) {
|
|
1688
1767
|
setAuthState({
|
|
1689
1768
|
step: "error",
|
|
@@ -1830,6 +1909,55 @@ async function loginOAuth(provider, deps) {
|
|
|
1830
1909
|
await authenticate(clientSessionId, deps);
|
|
1831
1910
|
}
|
|
1832
1911
|
|
|
1912
|
+
// src/client/auth/passkeyFlow.ts
|
|
1913
|
+
async function loginSmartWallet(deps) {
|
|
1914
|
+
const { api, signal, setAuthState, passkey } = deps;
|
|
1915
|
+
if (!passkey) {
|
|
1916
|
+
setAuthState({
|
|
1917
|
+
step: "error",
|
|
1918
|
+
previousStep: "creating_session",
|
|
1919
|
+
message: "Passkey support is not configured",
|
|
1920
|
+
errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
|
|
1921
|
+
});
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
const clientSessionId = await createAuthSession(deps);
|
|
1925
|
+
if (!clientSessionId) return;
|
|
1926
|
+
try {
|
|
1927
|
+
const { data: challengeData } = await api.POST("/auth/passkey/challenge", {
|
|
1928
|
+
body: { clientSessionId },
|
|
1929
|
+
signal
|
|
1930
|
+
});
|
|
1931
|
+
const challenge = challengeData?.content?.challenge;
|
|
1932
|
+
if (!challengeData?.success || !challenge) {
|
|
1933
|
+
return failPasskey(setAuthState, "Failed to start passkey");
|
|
1934
|
+
}
|
|
1935
|
+
setAuthState({ step: "creating_passkey" });
|
|
1936
|
+
const ceremony = await passkey({ challenge });
|
|
1937
|
+
const response = ceremony.response;
|
|
1938
|
+
if (ceremony.kind === "register") {
|
|
1939
|
+
setAuthState({ step: "deploying_smart_account" });
|
|
1940
|
+
const { data } = await api.POST("/auth/passkey/register", {
|
|
1941
|
+
body: { clientSessionId, response },
|
|
1942
|
+
signal
|
|
1943
|
+
});
|
|
1944
|
+
if (!data?.success) return failPasskey(setAuthState, "Passkey registration failed");
|
|
1945
|
+
} else {
|
|
1946
|
+
const { data } = await api.POST("/auth/passkey/login", {
|
|
1947
|
+
body: { clientSessionId, response },
|
|
1948
|
+
signal
|
|
1949
|
+
});
|
|
1950
|
+
if (!data?.success) return failPasskey(setAuthState, "Passkey authentication failed");
|
|
1951
|
+
}
|
|
1952
|
+
} catch {
|
|
1953
|
+
return failPasskey(setAuthState, "Passkey login failed");
|
|
1954
|
+
}
|
|
1955
|
+
await authenticate(clientSessionId, deps);
|
|
1956
|
+
}
|
|
1957
|
+
function failPasskey(setAuthState, message) {
|
|
1958
|
+
setAuthState({ step: "error", previousStep: "creating_passkey", message, errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED });
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1833
1961
|
// src/client/auth/walletFlow.ts
|
|
1834
1962
|
function withSignal(promise, signal) {
|
|
1835
1963
|
return Promise.race([
|
|
@@ -1856,12 +1984,12 @@ async function loginWallet(type, deps) {
|
|
|
1856
1984
|
setAuthState({ step: "wallet_not_installed", walletType: type });
|
|
1857
1985
|
return;
|
|
1858
1986
|
}
|
|
1859
|
-
const {
|
|
1860
|
-
connectedWallet =
|
|
1987
|
+
const { address } = await withSignal(adapter.connect(), signal);
|
|
1988
|
+
connectedWallet = address;
|
|
1861
1989
|
deps.storeWalletAdapter(adapter, type);
|
|
1862
1990
|
setAuthState({ step: "authenticating_wallet" });
|
|
1863
1991
|
const { data: walletData, error: walletError } = await api.POST("/auth/wallet", {
|
|
1864
|
-
body: { clientSessionId, walletAddress:
|
|
1992
|
+
body: { clientSessionId, walletAddress: address },
|
|
1865
1993
|
signal
|
|
1866
1994
|
});
|
|
1867
1995
|
if (walletError || !walletData?.success) {
|
|
@@ -1926,8 +2054,12 @@ var PollarClient = class {
|
|
|
1926
2054
|
this._transactionStateListeners = /* @__PURE__ */ new Set();
|
|
1927
2055
|
this._txHistoryState = { step: "idle" };
|
|
1928
2056
|
this._txHistoryStateListeners = /* @__PURE__ */ new Set();
|
|
2057
|
+
this._sessionsState = { step: "idle" };
|
|
2058
|
+
this._sessionsStateListeners = /* @__PURE__ */ new Set();
|
|
1929
2059
|
this._walletBalanceState = { step: "idle" };
|
|
1930
2060
|
this._walletBalanceStateListeners = /* @__PURE__ */ new Set();
|
|
2061
|
+
this._enabledAssetsState = { step: "idle" };
|
|
2062
|
+
this._enabledAssetsStateListeners = /* @__PURE__ */ new Set();
|
|
1931
2063
|
this._authState = { step: "idle" };
|
|
1932
2064
|
this._authStateListeners = /* @__PURE__ */ new Set();
|
|
1933
2065
|
this._networkState = { step: "idle" };
|
|
@@ -1943,10 +2075,14 @@ var PollarClient = class {
|
|
|
1943
2075
|
this._storageDegradeListeners = /* @__PURE__ */ new Set();
|
|
1944
2076
|
this._walletAdapter = null;
|
|
1945
2077
|
this._loginController = null;
|
|
2078
|
+
/** Aborts an in-flight `/auth/session/resume` on destroy() or re-trigger. */
|
|
2079
|
+
this._resumeController = null;
|
|
1946
2080
|
this.apiKey = config.apiKey;
|
|
1947
2081
|
this.id = randomUUID();
|
|
1948
2082
|
this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
|
|
2083
|
+
this._log = createLogger(config.logLevel ?? "info", config.logger);
|
|
1949
2084
|
this._storage = config.storage ?? defaultStorage({
|
|
2085
|
+
logger: this._log,
|
|
1950
2086
|
onDegrade: (reason, error) => {
|
|
1951
2087
|
config.onStorageDegrade?.(reason, error);
|
|
1952
2088
|
this._dispatchStorageDegrade(reason, error);
|
|
@@ -1955,6 +2091,8 @@ var PollarClient = class {
|
|
|
1955
2091
|
this._keyManager = config.keyManager ?? defaultKeyManager(this._storage, config.apiKey);
|
|
1956
2092
|
this._walletAdapterResolver = config.walletAdapter ?? null;
|
|
1957
2093
|
this._walletResolverTimeoutMs = config.walletResolverTimeoutMs ?? 5e3;
|
|
2094
|
+
this._passkey = config.passkey ?? null;
|
|
2095
|
+
this._passkeySign = config.passkeySign ?? null;
|
|
1958
2096
|
this._deviceLabel = config.deviceLabel;
|
|
1959
2097
|
this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
|
|
1960
2098
|
this._maxIdleMs = config.maxIdleMs;
|
|
@@ -1968,7 +2106,9 @@ var PollarClient = class {
|
|
|
1968
2106
|
this._initialized = Promise.resolve();
|
|
1969
2107
|
return;
|
|
1970
2108
|
}
|
|
1971
|
-
|
|
2109
|
+
this._log.info(
|
|
2110
|
+
`[PollarClient] Initialized v${POLLAR_CORE_VERSION} \u2014 endpoint: ${this.basePath}, network: ${this._networkState.network}`
|
|
2111
|
+
);
|
|
1972
2112
|
this._initialized = this._initialize();
|
|
1973
2113
|
}
|
|
1974
2114
|
/**
|
|
@@ -1993,7 +2133,7 @@ var PollarClient = class {
|
|
|
1993
2133
|
const sessionKey = sessionStorageKey(this._apiKeyHash);
|
|
1994
2134
|
const handler = (e) => {
|
|
1995
2135
|
if (e.key === sessionKey) {
|
|
1996
|
-
this._restoreSession().catch((err) =>
|
|
2136
|
+
this._restoreSession().catch((err) => this._log.error("[PollarClient] Cross-tab restore failed", err));
|
|
1997
2137
|
}
|
|
1998
2138
|
};
|
|
1999
2139
|
window.addEventListener("storage", handler);
|
|
@@ -2002,11 +2142,15 @@ var PollarClient = class {
|
|
|
2002
2142
|
try {
|
|
2003
2143
|
await this._keyManager.init();
|
|
2004
2144
|
} catch (err) {
|
|
2005
|
-
|
|
2145
|
+
this._log.warn("[PollarClient] KeyManager init failed; DPoP unavailable for this session", err);
|
|
2006
2146
|
}
|
|
2007
2147
|
await this._restoreSession();
|
|
2008
2148
|
this._visibilityUnsubscribe = this._visibilityProvider.onChange((visible) => {
|
|
2009
|
-
if (visible)
|
|
2149
|
+
if (!visible) return;
|
|
2150
|
+
void this._maybeProactiveRefresh();
|
|
2151
|
+
if (this._authState.step === "authenticated" && !this._authState.verified) {
|
|
2152
|
+
void this._resume();
|
|
2153
|
+
}
|
|
2010
2154
|
});
|
|
2011
2155
|
}
|
|
2012
2156
|
/** Detach the cross-tab storage listener and abort any in-flight login. */
|
|
@@ -2017,6 +2161,8 @@ var PollarClient = class {
|
|
|
2017
2161
|
}
|
|
2018
2162
|
this._loginController?.abort();
|
|
2019
2163
|
this._loginController = null;
|
|
2164
|
+
this._resumeController?.abort();
|
|
2165
|
+
this._resumeController = null;
|
|
2020
2166
|
this._clearRefreshTimer();
|
|
2021
2167
|
if (this._visibilityUnsubscribe) {
|
|
2022
2168
|
this._visibilityUnsubscribe();
|
|
@@ -2031,11 +2177,13 @@ var PollarClient = class {
|
|
|
2031
2177
|
request.headers.set("x-pollar-api-key", self.apiKey);
|
|
2032
2178
|
self._lastRequestAt = Date.now();
|
|
2033
2179
|
await self._initialized;
|
|
2034
|
-
|
|
2180
|
+
const cacheMethod = request.method.toUpperCase();
|
|
2181
|
+
const cacheBodyAllowed = cacheMethod !== "GET" && cacheMethod !== "HEAD";
|
|
2182
|
+
if (cacheBodyAllowed && request.body != null) {
|
|
2035
2183
|
try {
|
|
2036
2184
|
self._requestBodyCache.set(request, await request.clone().arrayBuffer());
|
|
2037
2185
|
} catch (err) {
|
|
2038
|
-
|
|
2186
|
+
this._log.warn("[PollarClient] Could not snapshot request body for retry", err);
|
|
2039
2187
|
}
|
|
2040
2188
|
}
|
|
2041
2189
|
const isRefresh = request.url.includes("/auth/refresh");
|
|
@@ -2094,7 +2242,7 @@ var PollarClient = class {
|
|
|
2094
2242
|
this._keyManager
|
|
2095
2243
|
);
|
|
2096
2244
|
} catch (err) {
|
|
2097
|
-
|
|
2245
|
+
this._log.warn("[PollarClient] DPoP proof build failed", err);
|
|
2098
2246
|
return null;
|
|
2099
2247
|
}
|
|
2100
2248
|
}
|
|
@@ -2118,7 +2266,9 @@ var PollarClient = class {
|
|
|
2118
2266
|
}
|
|
2119
2267
|
}
|
|
2120
2268
|
}
|
|
2121
|
-
const
|
|
2269
|
+
const retryMethod = originalRequest.method.toUpperCase();
|
|
2270
|
+
const retryBodyAllowed = retryMethod !== "GET" && retryMethod !== "HEAD";
|
|
2271
|
+
const cachedBody = retryBodyAllowed ? this._requestBodyCache.get(originalRequest) : void 0;
|
|
2122
2272
|
const retried = new Request(originalRequest.url, {
|
|
2123
2273
|
method: originalRequest.method,
|
|
2124
2274
|
headers,
|
|
@@ -2146,7 +2296,7 @@ var PollarClient = class {
|
|
|
2146
2296
|
async _doRefresh() {
|
|
2147
2297
|
const refreshToken = this._session?.token?.refreshToken;
|
|
2148
2298
|
if (!refreshToken) {
|
|
2149
|
-
|
|
2299
|
+
this._log.warn("[PollarClient] Refresh skipped: no refresh token in session");
|
|
2150
2300
|
await this._clearSession();
|
|
2151
2301
|
throw new Error("No refresh token available");
|
|
2152
2302
|
}
|
|
@@ -2157,24 +2307,31 @@ var PollarClient = class {
|
|
|
2157
2307
|
data = response.data;
|
|
2158
2308
|
error = response.error;
|
|
2159
2309
|
} catch (err) {
|
|
2160
|
-
|
|
2310
|
+
this._log.error("[PollarClient] /auth/refresh request threw", err);
|
|
2161
2311
|
await this._clearSession();
|
|
2162
2312
|
throw err;
|
|
2163
2313
|
}
|
|
2164
2314
|
if (error || !data) {
|
|
2165
|
-
|
|
2315
|
+
this._log.error("[PollarClient] /auth/refresh returned error", { error });
|
|
2166
2316
|
await this._clearSession();
|
|
2167
2317
|
throw new Error("Refresh failed");
|
|
2168
2318
|
}
|
|
2169
2319
|
const successData = data;
|
|
2170
2320
|
if (!successData.success || !successData.content?.token) {
|
|
2171
|
-
|
|
2321
|
+
this._log.error("[PollarClient] /auth/refresh response malformed", {
|
|
2322
|
+
success: successData.success,
|
|
2323
|
+
hasToken: !!successData.content?.token
|
|
2324
|
+
});
|
|
2172
2325
|
await this._clearSession();
|
|
2173
2326
|
throw new Error("Refresh response malformed");
|
|
2174
2327
|
}
|
|
2175
2328
|
const newToken = successData.content.token;
|
|
2176
2329
|
if (typeof newToken.accessToken !== "string" || typeof newToken.refreshToken !== "string" || typeof newToken.expiresAt !== "number") {
|
|
2177
|
-
|
|
2330
|
+
this._log.error("[PollarClient] /auth/refresh token shape invalid", {
|
|
2331
|
+
accessToken: typeof newToken.accessToken,
|
|
2332
|
+
refreshToken: typeof newToken.refreshToken,
|
|
2333
|
+
expiresAt: typeof newToken.expiresAt
|
|
2334
|
+
});
|
|
2178
2335
|
await this._clearSession();
|
|
2179
2336
|
throw new Error("Refresh response token shape invalid");
|
|
2180
2337
|
}
|
|
@@ -2182,9 +2339,9 @@ var PollarClient = class {
|
|
|
2182
2339
|
try {
|
|
2183
2340
|
this._session = { ...this._session, token: newToken };
|
|
2184
2341
|
await writeStorage(this._storage, this.apiKeyHash, this._session);
|
|
2185
|
-
|
|
2342
|
+
this._log.info("[PollarClient] Tokens refreshed");
|
|
2186
2343
|
} catch (err) {
|
|
2187
|
-
|
|
2344
|
+
this._log.error("[PollarClient] Failed to persist refreshed session", err);
|
|
2188
2345
|
}
|
|
2189
2346
|
this._scheduleNextRefresh();
|
|
2190
2347
|
}
|
|
@@ -2238,7 +2395,7 @@ var PollarClient = class {
|
|
|
2238
2395
|
try {
|
|
2239
2396
|
await this.refresh();
|
|
2240
2397
|
} catch (err) {
|
|
2241
|
-
|
|
2398
|
+
this._log.warn("[PollarClient] Proactive refresh failed; session cleared", err);
|
|
2242
2399
|
}
|
|
2243
2400
|
}
|
|
2244
2401
|
_clearRefreshTimer() {
|
|
@@ -2284,7 +2441,7 @@ var PollarClient = class {
|
|
|
2284
2441
|
try {
|
|
2285
2442
|
cb(reason, error);
|
|
2286
2443
|
} catch (err) {
|
|
2287
|
-
|
|
2444
|
+
this._log.error("[PollarClient] onStorageDegrade listener threw", err);
|
|
2288
2445
|
}
|
|
2289
2446
|
}
|
|
2290
2447
|
}
|
|
@@ -2368,6 +2525,19 @@ var PollarClient = class {
|
|
|
2368
2525
|
const controller = this._newController();
|
|
2369
2526
|
loginWallet(type, this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
|
|
2370
2527
|
}
|
|
2528
|
+
/**
|
|
2529
|
+
* "Smart Wallet" login: runs the passkey (WebAuthn) ceremony and, for a new
|
|
2530
|
+
* user, creates a sponsored smart-account C-address. Requires the `passkey`
|
|
2531
|
+
* ceremony to be configured (e.g. via `@pollar/react`).
|
|
2532
|
+
*/
|
|
2533
|
+
loginSmartWallet() {
|
|
2534
|
+
if (!isClientRuntime) {
|
|
2535
|
+
warnServerSide("loginSmartWallet");
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
const controller = this._newController();
|
|
2539
|
+
loginSmartWallet(this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
|
|
2540
|
+
}
|
|
2371
2541
|
// ─── Cancel ───────────────────────────────────────────────────────────────
|
|
2372
2542
|
cancelLogin() {
|
|
2373
2543
|
this._loginController?.abort();
|
|
@@ -2391,20 +2561,20 @@ var PollarClient = class {
|
|
|
2391
2561
|
warnServerSide("logout");
|
|
2392
2562
|
return;
|
|
2393
2563
|
}
|
|
2394
|
-
|
|
2564
|
+
this._log.info("[PollarClient] Logout requested", { everywhere: !!options.everywhere });
|
|
2395
2565
|
if (this._session?.token?.accessToken) {
|
|
2396
2566
|
try {
|
|
2397
2567
|
await this._api.POST("/auth/logout", {
|
|
2398
2568
|
body: options.everywhere ? { everywhere: true } : {}
|
|
2399
2569
|
});
|
|
2400
2570
|
} catch (err) {
|
|
2401
|
-
|
|
2571
|
+
this._log.warn("[PollarClient] Server logout failed (continuing with local clear)", err);
|
|
2402
2572
|
}
|
|
2403
2573
|
}
|
|
2404
2574
|
try {
|
|
2405
2575
|
await this._clearSession();
|
|
2406
2576
|
} catch (err) {
|
|
2407
|
-
|
|
2577
|
+
this._log.warn("[PollarClient] Local logout cleanup failed", err);
|
|
2408
2578
|
}
|
|
2409
2579
|
}
|
|
2410
2580
|
/** Convenience: revoke every active session for this user (all devices). */
|
|
@@ -2430,6 +2600,29 @@ var PollarClient = class {
|
|
|
2430
2600
|
}
|
|
2431
2601
|
return data.content.sessions;
|
|
2432
2602
|
}
|
|
2603
|
+
getSessionsState() {
|
|
2604
|
+
return this._sessionsState;
|
|
2605
|
+
}
|
|
2606
|
+
onSessionsStateChange(cb) {
|
|
2607
|
+
this._sessionsStateListeners.add(cb);
|
|
2608
|
+
cb(this._sessionsState);
|
|
2609
|
+
return () => this._sessionsStateListeners.delete(cb);
|
|
2610
|
+
}
|
|
2611
|
+
/**
|
|
2612
|
+
* Fire-and-forget variant of {@link listSessions} that drives the observable
|
|
2613
|
+
* `SessionsState` store instead of returning the array. UI layers subscribe
|
|
2614
|
+
* via `onSessionsStateChange` and stay pure readers — mirrors `fetchTxHistory`.
|
|
2615
|
+
*/
|
|
2616
|
+
async fetchSessions() {
|
|
2617
|
+
this._setSessionsState({ step: "loading" });
|
|
2618
|
+
try {
|
|
2619
|
+
const sessions = await this.listSessions();
|
|
2620
|
+
this._setSessionsState({ step: "loaded", sessions });
|
|
2621
|
+
} catch (err) {
|
|
2622
|
+
const message = err instanceof Error ? err.message : "Failed to load sessions";
|
|
2623
|
+
this._setSessionsState({ step: "error", message });
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2433
2626
|
/**
|
|
2434
2627
|
* Revoke a specific refresh-token family (a single device session). Use
|
|
2435
2628
|
* `listSessions` to enumerate the familyIds. Revoking the current session
|
|
@@ -2457,6 +2650,14 @@ var PollarClient = class {
|
|
|
2457
2650
|
getNetworkState() {
|
|
2458
2651
|
return this._networkState;
|
|
2459
2652
|
}
|
|
2653
|
+
/**
|
|
2654
|
+
* The client's level-gated logger (built from `logLevel` / `logger`). Exposed
|
|
2655
|
+
* so the runtime layer (`@pollar/react`) can route its own logs through the
|
|
2656
|
+
* same level and sink instead of calling `console` directly.
|
|
2657
|
+
*/
|
|
2658
|
+
getLogger() {
|
|
2659
|
+
return this._log;
|
|
2660
|
+
}
|
|
2460
2661
|
setNetwork(network) {
|
|
2461
2662
|
this._setNetworkState({ step: "connected", network });
|
|
2462
2663
|
}
|
|
@@ -2506,16 +2707,19 @@ var PollarClient = class {
|
|
|
2506
2707
|
cb(this._walletBalanceState);
|
|
2507
2708
|
return () => this._walletBalanceStateListeners.delete(cb);
|
|
2508
2709
|
}
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2710
|
+
/**
|
|
2711
|
+
* Refreshes the balances of the authenticated user's OWN wallet. The wallet
|
|
2712
|
+
* and network are resolved server-side from the session — no arguments. Drives
|
|
2713
|
+
* `walletBalanceState`. For an arbitrary wallet, use {@link getWalletBalance}.
|
|
2714
|
+
*/
|
|
2715
|
+
async refreshBalance() {
|
|
2716
|
+
if (!this._session?.wallet?.address) {
|
|
2512
2717
|
this._setWalletBalanceState({ step: "error", message: "No wallet connected" });
|
|
2513
2718
|
return;
|
|
2514
2719
|
}
|
|
2515
2720
|
this._setWalletBalanceState({ step: "loading" });
|
|
2516
2721
|
try {
|
|
2517
|
-
const
|
|
2518
|
-
const { data, error } = await this._api.GET("/wallet/balance", { params: { query: { publicKey: pk, network } } });
|
|
2722
|
+
const { data, error } = await this._api.GET("/wallet/balance");
|
|
2519
2723
|
if (!error && data?.success && data.content) {
|
|
2520
2724
|
this._setWalletBalanceState({ step: "loaded", data: data.content });
|
|
2521
2725
|
} else {
|
|
@@ -2525,6 +2729,53 @@ var PollarClient = class {
|
|
|
2525
2729
|
this._setWalletBalanceState({ step: "error", message: "Failed to load balance" });
|
|
2526
2730
|
}
|
|
2527
2731
|
}
|
|
2732
|
+
/**
|
|
2733
|
+
* General-purpose balance lookup for ANY wallet on ANY network — not scoped
|
|
2734
|
+
* to this application. Enumerates the account's real on-chain holdings via
|
|
2735
|
+
* Horizon (server-side) and returns the data directly (no reactive state).
|
|
2736
|
+
* `network` defaults to the client's current network.
|
|
2737
|
+
*/
|
|
2738
|
+
async getWalletBalance(publicKey, network) {
|
|
2739
|
+
const { data, error } = await this._api.GET("/wallet/{publicKey}/balance", {
|
|
2740
|
+
params: { path: { publicKey }, query: { network: network ?? this.getNetwork() } }
|
|
2741
|
+
});
|
|
2742
|
+
if (error || !data?.success || !data.content) {
|
|
2743
|
+
throw new Error("[PollarClient] Failed to load wallet balance");
|
|
2744
|
+
}
|
|
2745
|
+
return data.content;
|
|
2746
|
+
}
|
|
2747
|
+
// ─── Enabled assets ───────────────────────────────────────────────────────
|
|
2748
|
+
getEnabledAssetsState() {
|
|
2749
|
+
return this._enabledAssetsState;
|
|
2750
|
+
}
|
|
2751
|
+
onEnabledAssetsStateChange(cb) {
|
|
2752
|
+
this._enabledAssetsStateListeners.add(cb);
|
|
2753
|
+
cb(this._enabledAssetsState);
|
|
2754
|
+
return () => this._enabledAssetsStateListeners.delete(cb);
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* Loads the application's enabled assets paired with the authenticated
|
|
2758
|
+
* wallet's on-chain trustline state — so the SDK knows which trustlines still
|
|
2759
|
+
* need to be added. Wallet and network are resolved server-side from the
|
|
2760
|
+
* session. Drives `enabledAssetsState`; mirrors {@link refreshBalance}.
|
|
2761
|
+
*/
|
|
2762
|
+
async refreshAssets() {
|
|
2763
|
+
if (!this._session?.wallet?.address) {
|
|
2764
|
+
this._setEnabledAssetsState({ step: "error", message: "No wallet connected" });
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
this._setEnabledAssetsState({ step: "loading" });
|
|
2768
|
+
try {
|
|
2769
|
+
const { data, error } = await this._api.GET("/wallet/assets");
|
|
2770
|
+
if (!error && data?.success && data.content) {
|
|
2771
|
+
this._setEnabledAssetsState({ step: "loaded", data: data.content });
|
|
2772
|
+
} else {
|
|
2773
|
+
this._setEnabledAssetsState({ step: "error", message: "Failed to load assets" });
|
|
2774
|
+
}
|
|
2775
|
+
} catch {
|
|
2776
|
+
this._setEnabledAssetsState({ step: "error", message: "Failed to load assets" });
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2528
2779
|
// ─── Transactions ─────────────────────────────────────────────────────────
|
|
2529
2780
|
/**
|
|
2530
2781
|
* Builds an unsigned XDR. Drives `_setTransactionState` for modal-style UIs
|
|
@@ -2532,14 +2783,14 @@ var PollarClient = class {
|
|
|
2532
2783
|
* inspect the result without subscribing to state changes.
|
|
2533
2784
|
*/
|
|
2534
2785
|
async buildTx(operation, params, options) {
|
|
2535
|
-
if (!this._session?.wallet?.
|
|
2786
|
+
if (!this._session?.wallet?.address) {
|
|
2536
2787
|
const details = "No wallet connected";
|
|
2537
2788
|
this._setTransactionState({ step: "error", phase: "building", details });
|
|
2538
2789
|
return { status: "error", details };
|
|
2539
2790
|
}
|
|
2540
2791
|
const body = {
|
|
2541
2792
|
network: this.getNetwork(),
|
|
2542
|
-
|
|
2793
|
+
address: this._session.wallet.address,
|
|
2543
2794
|
operation,
|
|
2544
2795
|
params,
|
|
2545
2796
|
options: options ?? {}
|
|
@@ -2555,7 +2806,7 @@ var PollarClient = class {
|
|
|
2555
2806
|
this._setTransactionState({ step: "error", phase: "building", ...details && { details } });
|
|
2556
2807
|
return { status: "error", ...details && { details } };
|
|
2557
2808
|
} catch (err) {
|
|
2558
|
-
|
|
2809
|
+
this._log.error("[PollarClient] buildTx failed", err);
|
|
2559
2810
|
this._setTransactionState({ step: "error", phase: "building" });
|
|
2560
2811
|
return { status: "error" };
|
|
2561
2812
|
}
|
|
@@ -2580,7 +2831,7 @@ var PollarClient = class {
|
|
|
2580
2831
|
const buildData = this._currentBuildData();
|
|
2581
2832
|
this._setTransactionState({ step: "signing", ...buildData && { buildData } });
|
|
2582
2833
|
if (this._walletAdapter) {
|
|
2583
|
-
const accountToSign = this._session?.wallet?.
|
|
2834
|
+
const accountToSign = this._session?.wallet?.address;
|
|
2584
2835
|
const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
|
|
2585
2836
|
try {
|
|
2586
2837
|
const { signedTxXdr } = await this._walletAdapter.signTransaction(unsignedXdr, signOpts);
|
|
@@ -2601,10 +2852,10 @@ var PollarClient = class {
|
|
|
2601
2852
|
return { status: "error", ...details && { details } };
|
|
2602
2853
|
}
|
|
2603
2854
|
}
|
|
2604
|
-
const
|
|
2855
|
+
const address = this._session?.wallet?.address ?? "";
|
|
2605
2856
|
try {
|
|
2606
2857
|
const { data, error } = await this._api.POST("/tx/sign", {
|
|
2607
|
-
body: { network: this.getNetwork(),
|
|
2858
|
+
body: { network: this.getNetwork(), address, unsignedXdr }
|
|
2608
2859
|
});
|
|
2609
2860
|
if (!error && data?.success && data.content?.signedXdr) {
|
|
2610
2861
|
const { signedXdr, idempotencyKey } = data.content;
|
|
@@ -2657,12 +2908,12 @@ var PollarClient = class {
|
|
|
2657
2908
|
const buildData = this._currentBuildData();
|
|
2658
2909
|
const outcomeExtra = buildData ? { buildData } : {};
|
|
2659
2910
|
this._setTransactionState({ step: "submitting", signedXdr, ...buildData && { buildData } });
|
|
2660
|
-
const
|
|
2911
|
+
const address = this._session?.wallet?.address ?? "";
|
|
2661
2912
|
try {
|
|
2662
2913
|
const { data, error } = await this._api.POST("/tx/submit", {
|
|
2663
2914
|
body: {
|
|
2664
2915
|
network: this.getNetwork(),
|
|
2665
|
-
|
|
2916
|
+
address,
|
|
2666
2917
|
signedXdr,
|
|
2667
2918
|
...opts?.submissionToken && { idempotencyKey: opts.submissionToken }
|
|
2668
2919
|
}
|
|
@@ -2722,6 +2973,19 @@ var PollarClient = class {
|
|
|
2722
2973
|
* `success` (ledger-confirmed), or `error[phase: 'signing-submitting']`.
|
|
2723
2974
|
*/
|
|
2724
2975
|
async signAndSubmitTx(unsignedXdr) {
|
|
2976
|
+
if (this._session?.wallet?.type === "smart") {
|
|
2977
|
+
const buildData2 = this._currentBuildData();
|
|
2978
|
+
if (!buildData2?.smart) {
|
|
2979
|
+
const details = "no prepared smart transaction; call buildTx first";
|
|
2980
|
+
this._setTransactionState({ step: "error", phase: "signing", details });
|
|
2981
|
+
return { status: "error", details };
|
|
2982
|
+
}
|
|
2983
|
+
return this._signSubmitSmart(buildData2);
|
|
2984
|
+
}
|
|
2985
|
+
if (!unsignedXdr) {
|
|
2986
|
+
this._setTransactionState({ step: "error", phase: "signing", details: "missing unsigned transaction" });
|
|
2987
|
+
return { status: "error", details: "missing unsigned transaction" };
|
|
2988
|
+
}
|
|
2725
2989
|
if (this._walletAdapter) {
|
|
2726
2990
|
const signed = await this.signTx(unsignedXdr);
|
|
2727
2991
|
if (signed.status === "error") {
|
|
@@ -2739,7 +3003,7 @@ var PollarClient = class {
|
|
|
2739
3003
|
this._setTransactionState({ step: "signing-submitting", ...buildData && { buildData } });
|
|
2740
3004
|
const body = {
|
|
2741
3005
|
network: this.getNetwork(),
|
|
2742
|
-
|
|
3006
|
+
address: this._session?.wallet?.address ?? "",
|
|
2743
3007
|
unsignedXdr
|
|
2744
3008
|
};
|
|
2745
3009
|
try {
|
|
@@ -2808,14 +3072,20 @@ var PollarClient = class {
|
|
|
2808
3072
|
* `signTx`, and `submitTx` separately instead.
|
|
2809
3073
|
*/
|
|
2810
3074
|
async buildAndSignAndSubmitTx(operation, params, options) {
|
|
3075
|
+
if (this._session?.wallet?.type === "smart") {
|
|
3076
|
+
return this._runSmartTx(operation, params, options);
|
|
3077
|
+
}
|
|
2811
3078
|
if (this._walletAdapter) {
|
|
2812
3079
|
const built = await this.buildTx(operation, params, options);
|
|
2813
3080
|
if (built.status === "error") {
|
|
2814
3081
|
return { status: "error", ...built.details && { details: built.details } };
|
|
2815
3082
|
}
|
|
3083
|
+
if (!built.buildData.unsignedXdr) {
|
|
3084
|
+
return { status: "error", details: "build returned no unsigned transaction" };
|
|
3085
|
+
}
|
|
2816
3086
|
return this.signAndSubmitTx(built.buildData.unsignedXdr);
|
|
2817
3087
|
}
|
|
2818
|
-
if (!this._session?.wallet?.
|
|
3088
|
+
if (!this._session?.wallet?.address) {
|
|
2819
3089
|
this._setTransactionState({ step: "error", phase: "building-signing-submitting", details: "No wallet connected" });
|
|
2820
3090
|
return { status: "error", details: "No wallet connected" };
|
|
2821
3091
|
}
|
|
@@ -2824,7 +3094,7 @@ var PollarClient = class {
|
|
|
2824
3094
|
const { data, error } = await this._api.POST("/tx/build-sign-submit", {
|
|
2825
3095
|
body: {
|
|
2826
3096
|
network: this.getNetwork(),
|
|
2827
|
-
|
|
3097
|
+
address: this._session.wallet.address,
|
|
2828
3098
|
operation,
|
|
2829
3099
|
params,
|
|
2830
3100
|
options: options ?? {}
|
|
@@ -2868,6 +3138,113 @@ var PollarClient = class {
|
|
|
2868
3138
|
async runTx(operation, params, options) {
|
|
2869
3139
|
return this.buildAndSignAndSubmitTx(operation, params, options);
|
|
2870
3140
|
}
|
|
3141
|
+
/**
|
|
3142
|
+
* Smart-wallet (passkey / C-address) transaction: build (server prepares the
|
|
3143
|
+
* SAC transfer + returns the auth digest) → sign the digest with the passkey
|
|
3144
|
+
* → submit (server assembles the signed auth entry and broadcasts; the
|
|
3145
|
+
* sponsor pays the fee). State machine: building → built → signing →
|
|
3146
|
+
* submitting → success.
|
|
3147
|
+
*/
|
|
3148
|
+
async _runSmartTx(operation, params, options) {
|
|
3149
|
+
const address = this._session?.wallet?.address;
|
|
3150
|
+
if (!address) {
|
|
3151
|
+
this._setTransactionState({ step: "error", phase: "building", details: "No wallet connected" });
|
|
3152
|
+
return { status: "error", details: "No wallet connected" };
|
|
3153
|
+
}
|
|
3154
|
+
if (!this._passkeySign) {
|
|
3155
|
+
const details = "Passkey signer not configured";
|
|
3156
|
+
this._setTransactionState({ step: "error", phase: "signing", details });
|
|
3157
|
+
return { status: "error", details };
|
|
3158
|
+
}
|
|
3159
|
+
this._setTransactionState({ step: "building" });
|
|
3160
|
+
let buildData;
|
|
3161
|
+
try {
|
|
3162
|
+
const body = {
|
|
3163
|
+
network: this.getNetwork(),
|
|
3164
|
+
address,
|
|
3165
|
+
operation,
|
|
3166
|
+
params,
|
|
3167
|
+
options: options ?? {}
|
|
3168
|
+
};
|
|
3169
|
+
const { data, error } = await this._api.POST("/tx/build", { body });
|
|
3170
|
+
if (error || !data?.success || !data.content?.smart) {
|
|
3171
|
+
const details = error?.details ?? "Failed to build transaction";
|
|
3172
|
+
this._setTransactionState({ step: "error", phase: "building", details });
|
|
3173
|
+
return { status: "error", details };
|
|
3174
|
+
}
|
|
3175
|
+
buildData = data.content;
|
|
3176
|
+
} catch (err) {
|
|
3177
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
3178
|
+
this._setTransactionState({ step: "error", phase: "building", ...details && { details } });
|
|
3179
|
+
return { status: "error", ...details && { details } };
|
|
3180
|
+
}
|
|
3181
|
+
this._setTransactionState({ step: "built", buildData });
|
|
3182
|
+
return this._signSubmitSmart(buildData);
|
|
3183
|
+
}
|
|
3184
|
+
/**
|
|
3185
|
+
* Steps 2–3 of the smart-wallet flow: sign the prepared auth digest with the
|
|
3186
|
+
* passkey, then submit. Shared by `_runSmartTx` (atomic) and `signAndSubmitTx`
|
|
3187
|
+
* (split flow, when a smart build is already on the state machine).
|
|
3188
|
+
*/
|
|
3189
|
+
async _signSubmitSmart(buildData) {
|
|
3190
|
+
const address = this._session?.wallet?.address;
|
|
3191
|
+
const smart = buildData.smart;
|
|
3192
|
+
if (!address || !smart) {
|
|
3193
|
+
const details = "no prepared smart transaction";
|
|
3194
|
+
this._setTransactionState({ step: "error", phase: "signing", buildData, details });
|
|
3195
|
+
return { status: "error", buildData, details };
|
|
3196
|
+
}
|
|
3197
|
+
if (!this._passkeySign) {
|
|
3198
|
+
const details = "Passkey signer not configured";
|
|
3199
|
+
this._setTransactionState({ step: "error", phase: "signing", buildData, details });
|
|
3200
|
+
return { status: "error", buildData, details };
|
|
3201
|
+
}
|
|
3202
|
+
this._setTransactionState({ step: "signing", buildData });
|
|
3203
|
+
let assertion;
|
|
3204
|
+
try {
|
|
3205
|
+
assertion = await this._passkeySign({ credentialId: smart.credentialId, challenge: smart.digest });
|
|
3206
|
+
} catch (err) {
|
|
3207
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
3208
|
+
this._setTransactionState({ step: "error", phase: "signing", buildData, ...details && { details } });
|
|
3209
|
+
return { status: "error", buildData, ...details && { details } };
|
|
3210
|
+
}
|
|
3211
|
+
this._setTransactionState({ step: "submitting", buildData });
|
|
3212
|
+
const outcomeExtra = { buildData };
|
|
3213
|
+
try {
|
|
3214
|
+
const { data, error } = await this._api.POST("/tx/submit", {
|
|
3215
|
+
body: {
|
|
3216
|
+
network: this.getNetwork(),
|
|
3217
|
+
address,
|
|
3218
|
+
smart: { entryXdr: smart.entryXdr, funcXdr: smart.funcXdr, assertion }
|
|
3219
|
+
}
|
|
3220
|
+
});
|
|
3221
|
+
if (!error && data?.success && data.content) {
|
|
3222
|
+
const { hash, status: backendStatus, resultCode } = data.content;
|
|
3223
|
+
if (backendStatus === "SUCCESS") {
|
|
3224
|
+
this._setTransactionState({ step: "success", hash, buildData });
|
|
3225
|
+
return { status: "success", hash, ...outcomeExtra };
|
|
3226
|
+
}
|
|
3227
|
+
if (backendStatus === "PENDING") {
|
|
3228
|
+
this._setTransactionState({ step: "submitted", hash, buildData });
|
|
3229
|
+
return { status: "pending", hash, ...outcomeExtra };
|
|
3230
|
+
}
|
|
3231
|
+
this._setTransactionState({
|
|
3232
|
+
step: "error",
|
|
3233
|
+
phase: "submitting",
|
|
3234
|
+
buildData,
|
|
3235
|
+
...resultCode && { details: resultCode }
|
|
3236
|
+
});
|
|
3237
|
+
return { status: "error", hash, ...outcomeExtra, ...resultCode && { details: resultCode, resultCode } };
|
|
3238
|
+
}
|
|
3239
|
+
const details = error?.details;
|
|
3240
|
+
this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
|
|
3241
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
3242
|
+
} catch (err) {
|
|
3243
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
3244
|
+
this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
|
|
3245
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
2871
3248
|
// ─── App config ───────────────────────────────────────────────────────────
|
|
2872
3249
|
async getAppConfig() {
|
|
2873
3250
|
try {
|
|
@@ -2921,10 +3298,18 @@ var PollarClient = class {
|
|
|
2921
3298
|
this._txHistoryState = next;
|
|
2922
3299
|
for (const cb of this._txHistoryStateListeners) cb(next);
|
|
2923
3300
|
}
|
|
3301
|
+
_setSessionsState(next) {
|
|
3302
|
+
this._sessionsState = next;
|
|
3303
|
+
for (const cb of this._sessionsStateListeners) cb(next);
|
|
3304
|
+
}
|
|
2924
3305
|
_setWalletBalanceState(next) {
|
|
2925
3306
|
this._walletBalanceState = next;
|
|
2926
3307
|
for (const cb of this._walletBalanceStateListeners) cb(next);
|
|
2927
3308
|
}
|
|
3309
|
+
_setEnabledAssetsState(next) {
|
|
3310
|
+
this._enabledAssetsState = next;
|
|
3311
|
+
for (const cb of this._enabledAssetsStateListeners) cb(next);
|
|
3312
|
+
}
|
|
2928
3313
|
// ─── Private ──────────────────────────────────────────────────────────────
|
|
2929
3314
|
_newController() {
|
|
2930
3315
|
this._loginController?.abort();
|
|
@@ -2934,6 +3319,7 @@ var PollarClient = class {
|
|
|
2934
3319
|
_flowDeps(signal) {
|
|
2935
3320
|
return {
|
|
2936
3321
|
api: this._api,
|
|
3322
|
+
logger: this._log,
|
|
2937
3323
|
basePath: this.basePath,
|
|
2938
3324
|
// SSE status streaming works on web; React Native's `fetch` has no
|
|
2939
3325
|
// readable `response.body`, so those clients poll the non-streaming
|
|
@@ -2949,6 +3335,7 @@ var PollarClient = class {
|
|
|
2949
3335
|
this._walletAdapter = adapter;
|
|
2950
3336
|
await writeWalletType(this._storage, this.apiKeyHash, id);
|
|
2951
3337
|
},
|
|
3338
|
+
...this._passkey ? { passkey: this._passkey } : {},
|
|
2952
3339
|
...this._deviceLabel ? { deviceLabel: this._deviceLabel } : {}
|
|
2953
3340
|
};
|
|
2954
3341
|
}
|
|
@@ -2978,19 +3365,19 @@ var PollarClient = class {
|
|
|
2978
3365
|
}
|
|
2979
3366
|
}
|
|
2980
3367
|
if (id === "freighter" /* FREIGHTER */) return new FreighterAdapter();
|
|
2981
|
-
if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter();
|
|
3368
|
+
if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter(this.getNetwork() === "mainnet" ? "public" : "testnet");
|
|
2982
3369
|
throw new Error(
|
|
2983
3370
|
`[PollarClient] No wallet adapter configured for "${id}". Pass a walletAdapter resolver in PollarClientConfig.`
|
|
2984
3371
|
);
|
|
2985
3372
|
}
|
|
2986
3373
|
_handleFlowError(error) {
|
|
2987
3374
|
if (error instanceof Error && error.name === "AbortError") {
|
|
2988
|
-
|
|
3375
|
+
this._log.debug("[PollarClient] Login cancelled");
|
|
2989
3376
|
this._setAuthState({ step: "idle" });
|
|
2990
3377
|
return;
|
|
2991
3378
|
}
|
|
2992
3379
|
if (error instanceof Error && error.code === AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT) {
|
|
2993
|
-
|
|
3380
|
+
this._log.error("[PollarClient]", error.message);
|
|
2994
3381
|
this._setAuthState({
|
|
2995
3382
|
step: "error",
|
|
2996
3383
|
previousStep: this._authState.step,
|
|
@@ -2999,7 +3386,7 @@ var PollarClient = class {
|
|
|
2999
3386
|
});
|
|
3000
3387
|
return;
|
|
3001
3388
|
}
|
|
3002
|
-
|
|
3389
|
+
this._log.error("[PollarClient] Unexpected error in auth flow", error);
|
|
3003
3390
|
this._setAuthState({
|
|
3004
3391
|
step: "error",
|
|
3005
3392
|
previousStep: this._authState.step,
|
|
@@ -3008,32 +3395,77 @@ var PollarClient = class {
|
|
|
3008
3395
|
});
|
|
3009
3396
|
}
|
|
3010
3397
|
async _restoreSession() {
|
|
3011
|
-
this._session = await readStorage(this._storage, this.apiKeyHash);
|
|
3398
|
+
this._session = await readStorage(this._storage, this.apiKeyHash, this._log);
|
|
3012
3399
|
if (this._session) {
|
|
3013
3400
|
const storedType = await readWalletType(this._storage, this.apiKeyHash);
|
|
3014
3401
|
if (storedType) {
|
|
3015
3402
|
try {
|
|
3016
3403
|
this._walletAdapter = await this._resolveWalletAdapter(storedType);
|
|
3017
3404
|
} catch (err) {
|
|
3018
|
-
|
|
3405
|
+
this._log.warn("[PollarClient] Could not restore wallet adapter for stored id", { id: storedType, err });
|
|
3019
3406
|
}
|
|
3020
3407
|
}
|
|
3021
|
-
|
|
3022
|
-
this._setAuthState({ step: "authenticated", session: this._session });
|
|
3408
|
+
this._log.info("[PollarClient] Session restored from storage");
|
|
3409
|
+
this._setAuthState({ step: "authenticated", session: this._session, verified: false });
|
|
3023
3410
|
this._scheduleNextRefresh();
|
|
3411
|
+
void this._resume();
|
|
3024
3412
|
} else {
|
|
3025
|
-
|
|
3413
|
+
this._log.info("[PollarClient] No session in storage");
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
/**
|
|
3417
|
+
* Validate the restored session against the server and repopulate the
|
|
3418
|
+
* in-memory profile (PII is never persisted, so it's null after a cold
|
|
3419
|
+
* reload). Goes through the normal authed client, so it coalesces with any
|
|
3420
|
+
* in-flight refresh (onRequest awaits `_refreshPromise`) and, being a GET,
|
|
3421
|
+
* is auto-retried after a 401-triggered refresh.
|
|
3422
|
+
*
|
|
3423
|
+
* - 200 → store profile, mark the session `verified`.
|
|
3424
|
+
* - 401 → the refresh-on-401 path already ran; if the family was
|
|
3425
|
+
* revoked, refresh failed and `_clearSession()` took us to
|
|
3426
|
+
* idle. Nothing to do here — don't double-handle.
|
|
3427
|
+
* - network error → stay optimistic (do NOT log out); revalidated later on
|
|
3428
|
+
* `visibilitychange` or first use.
|
|
3429
|
+
*/
|
|
3430
|
+
async _resume() {
|
|
3431
|
+
if (!this._session) return;
|
|
3432
|
+
this._resumeController?.abort();
|
|
3433
|
+
const controller = new AbortController();
|
|
3434
|
+
this._resumeController = controller;
|
|
3435
|
+
try {
|
|
3436
|
+
const { data, error } = await this._api.GET("/auth/session/resume", { signal: controller.signal });
|
|
3437
|
+
if (error || !data) return;
|
|
3438
|
+
const content = data.content;
|
|
3439
|
+
if (!content || !this._session) return;
|
|
3440
|
+
this._profile = { ...content };
|
|
3441
|
+
this._setAuthState({ step: "authenticated", session: this._session, verified: true });
|
|
3442
|
+
} catch (err) {
|
|
3443
|
+
if (err?.name === "AbortError") return;
|
|
3444
|
+
this._log.warn("[PollarClient] resume failed (network); will retry", err);
|
|
3445
|
+
} finally {
|
|
3446
|
+
if (this._resumeController === controller) this._resumeController = null;
|
|
3026
3447
|
}
|
|
3027
3448
|
}
|
|
3028
3449
|
async _storeSession(session) {
|
|
3029
|
-
|
|
3450
|
+
this._log.info("[PollarClient] Session stored");
|
|
3451
|
+
const w = session.wallet;
|
|
3030
3452
|
const persisted = {
|
|
3031
3453
|
clientSessionId: session.clientSessionId,
|
|
3032
3454
|
userId: session.userId ?? null,
|
|
3033
3455
|
status: session.status,
|
|
3034
3456
|
token: session.token,
|
|
3035
3457
|
user: session.user,
|
|
3036
|
-
|
|
3458
|
+
// The wire response still carries the legacy `publicKey` alias (kept for
|
|
3459
|
+
// older SDKs); the persisted session standardizes on `address` only.
|
|
3460
|
+
wallet: {
|
|
3461
|
+
type: w.type,
|
|
3462
|
+
address: w.address ?? w.publicKey ?? null,
|
|
3463
|
+
...w.existsOnStellar !== void 0 ? { existsOnStellar: w.existsOnStellar } : {},
|
|
3464
|
+
...w.createdAt !== void 0 ? { createdAt: w.createdAt } : {},
|
|
3465
|
+
...w.linkedAt !== void 0 ? { linkedAt: w.linkedAt } : {},
|
|
3466
|
+
...w.network !== void 0 ? { network: w.network } : {},
|
|
3467
|
+
...w.deployTxHash !== void 0 ? { deployTxHash: w.deployTxHash } : {}
|
|
3468
|
+
}
|
|
3037
3469
|
};
|
|
3038
3470
|
this._session = persisted;
|
|
3039
3471
|
if (session.data) {
|
|
@@ -3046,11 +3478,11 @@ var PollarClient = class {
|
|
|
3046
3478
|
};
|
|
3047
3479
|
}
|
|
3048
3480
|
await writeStorage(this._storage, this.apiKeyHash, persisted);
|
|
3049
|
-
this._setAuthState({ step: "authenticated", session: persisted });
|
|
3481
|
+
this._setAuthState({ step: "authenticated", session: persisted, verified: true });
|
|
3050
3482
|
this._scheduleNextRefresh();
|
|
3051
3483
|
}
|
|
3052
3484
|
async _clearSession() {
|
|
3053
|
-
|
|
3485
|
+
this._log.info("[PollarClient] Session cleared");
|
|
3054
3486
|
this._clearRefreshTimer();
|
|
3055
3487
|
this._session = null;
|
|
3056
3488
|
this._profile = null;
|
|
@@ -3059,7 +3491,7 @@ var PollarClient = class {
|
|
|
3059
3491
|
try {
|
|
3060
3492
|
await this._keyManager.reset();
|
|
3061
3493
|
} catch (err) {
|
|
3062
|
-
|
|
3494
|
+
this._log.warn("[PollarClient] KeyManager reset failed during clearSession", err);
|
|
3063
3495
|
}
|
|
3064
3496
|
await removeStorage(this._storage, this.apiKeyHash);
|
|
3065
3497
|
this._transactionState = null;
|
|
@@ -3071,17 +3503,17 @@ var PollarClient = class {
|
|
|
3071
3503
|
_setNetworkState(next) {
|
|
3072
3504
|
this._networkState = next;
|
|
3073
3505
|
const label = next.step === "connected" ? next.network : next.step;
|
|
3074
|
-
|
|
3506
|
+
this._log.debug(`[PollarClient] network:${label}`);
|
|
3075
3507
|
for (const cb of this._networkStateListeners) cb(next);
|
|
3076
3508
|
}
|
|
3077
3509
|
_setAuthState(next) {
|
|
3078
3510
|
this._authState = next;
|
|
3079
|
-
|
|
3511
|
+
this._log.debug(`[PollarClient] auth:${next.step}`);
|
|
3080
3512
|
for (const cb of this._authStateListeners) cb(next);
|
|
3081
3513
|
}
|
|
3082
3514
|
_setTransactionState(next) {
|
|
3083
3515
|
this._transactionState = next;
|
|
3084
|
-
|
|
3516
|
+
this._log.debug(`[PollarClient] transaction:${next.step}`);
|
|
3085
3517
|
for (const cb of this._transactionStateListeners) cb(next);
|
|
3086
3518
|
}
|
|
3087
3519
|
/**
|
|
@@ -3132,6 +3564,7 @@ _setDefaultKeyManagerFactory((_storage, apiKey) => new WebCryptoKeyManager(apiKe
|
|
|
3132
3564
|
exports.AUTH_ERROR_CODES = AUTH_ERROR_CODES;
|
|
3133
3565
|
exports.AlbedoAdapter = AlbedoAdapter;
|
|
3134
3566
|
exports.FreighterAdapter = FreighterAdapter;
|
|
3567
|
+
exports.POLLAR_CORE_VERSION = POLLAR_CORE_VERSION;
|
|
3135
3568
|
exports.PollarClient = PollarClient;
|
|
3136
3569
|
exports.StellarClient = StellarClient;
|
|
3137
3570
|
exports.WalletType = WalletType;
|
|
@@ -3141,6 +3574,7 @@ exports.canonicalEcJwk = canonicalEcJwk;
|
|
|
3141
3574
|
exports.claimDistributionRule = claimDistributionRule;
|
|
3142
3575
|
exports.computeJwkThumbprint = computeJwkThumbprint;
|
|
3143
3576
|
exports.createLocalStorageAdapter = createLocalStorageAdapter;
|
|
3577
|
+
exports.createLogger = createLogger;
|
|
3144
3578
|
exports.createMemoryAdapter = createMemoryAdapter;
|
|
3145
3579
|
exports.createOffRamp = createOffRamp;
|
|
3146
3580
|
exports.createOnRamp = createOnRamp;
|