@pollar/core 0.8.1 → 0.9.0-rc.0
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 +24 -16
- package/dist/adapters/expo-secure-store.d.mts +11 -1
- package/dist/adapters/expo-secure-store.d.ts +11 -1
- package/dist/adapters/expo-secure-store.js +11 -3
- package/dist/adapters/expo-secure-store.js.map +1 -1
- package/dist/adapters/expo-secure-store.mjs +11 -4
- package/dist/adapters/expo-secure-store.mjs.map +1 -1
- 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 +947 -100
- package/dist/index.d.ts +947 -100
- package/dist/index.js +471 -60
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +471 -61
- 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 +621 -215
- package/dist/index.rn.js.map +1 -1
- package/dist/index.rn.mjs +618 -213
- 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 +2 -1
package/dist/index.rn.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var nist = require('@noble/curves/nist');
|
|
4
|
+
var sha2 = require('@noble/hashes/sha2');
|
|
4
5
|
|
|
5
6
|
var __create = Object.create;
|
|
6
7
|
var __defProp = Object.defineProperty;
|
|
@@ -186,11 +187,8 @@ function defaultKeyManager(storage, apiKey) {
|
|
|
186
187
|
}
|
|
187
188
|
return _factory(storage, apiKey);
|
|
188
189
|
}
|
|
189
|
-
|
|
190
|
-
// src/lib/sha256.ts
|
|
191
190
|
async function sha256(data) {
|
|
192
|
-
|
|
193
|
-
return new Uint8Array(buf);
|
|
191
|
+
return sha2.sha256(data);
|
|
194
192
|
}
|
|
195
193
|
|
|
196
194
|
// src/lib/api-key-hash.ts
|
|
@@ -269,11 +267,14 @@ async function computeJwkThumbprint(jwk) {
|
|
|
269
267
|
const digest = await sha256(new TextEncoder().encode(canonical));
|
|
270
268
|
return base64urlEncode(digest);
|
|
271
269
|
}
|
|
270
|
+
function toBase64url(value) {
|
|
271
|
+
return value.replace(/\+/g, "-").replace(/\//g, "_").replace(/[^A-Za-z0-9_-]/g, "");
|
|
272
|
+
}
|
|
272
273
|
function canonicalEcJwk(jwk) {
|
|
273
274
|
if (jwk.kty !== "EC" || jwk.crv !== "P-256" || typeof jwk.x !== "string" || typeof jwk.y !== "string") {
|
|
274
275
|
throw new Error("[PollarClient:thumbprint] Source JWK is not an EC P-256 public key");
|
|
275
276
|
}
|
|
276
|
-
return { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y };
|
|
277
|
+
return { kty: "EC", crv: "P-256", x: toBase64url(jwk.x), y: toBase64url(jwk.y) };
|
|
277
278
|
}
|
|
278
279
|
|
|
279
280
|
// src/keys/noble.ts
|
|
@@ -325,14 +326,14 @@ var NobleKeyManager = class {
|
|
|
325
326
|
priv = null;
|
|
326
327
|
}
|
|
327
328
|
if (!priv) {
|
|
328
|
-
priv =
|
|
329
|
+
priv = nist.p256.utils.randomPrivateKey();
|
|
329
330
|
try {
|
|
330
331
|
await this.storage.set(this.storageKey, base64urlEncode(priv));
|
|
331
332
|
} catch {
|
|
332
333
|
}
|
|
333
334
|
}
|
|
334
335
|
this.privateKey = priv;
|
|
335
|
-
const pub =
|
|
336
|
+
const pub = nist.p256.getPublicKey(priv, false);
|
|
336
337
|
if (pub.length !== 65 || pub[0] !== 4) {
|
|
337
338
|
throw new Error("[PollarClient:keys] Unexpected public key format from @noble/curves");
|
|
338
339
|
}
|
|
@@ -374,172 +375,11 @@ var NobleKeyManager = class {
|
|
|
374
375
|
throw new Error("[PollarClient:keys] Keypair initialization failed; sign unavailable");
|
|
375
376
|
}
|
|
376
377
|
const digest = await sha256(payload);
|
|
377
|
-
const signature =
|
|
378
|
+
const signature = nist.p256.sign(digest, this.privateKey, { prehash: false });
|
|
378
379
|
return signature.toCompactRawBytes();
|
|
379
380
|
}
|
|
380
381
|
};
|
|
381
382
|
|
|
382
|
-
// src/keys/web-crypto.ts
|
|
383
|
-
var DB_NAME = "pollar-keys";
|
|
384
|
-
var DB_VERSION = 1;
|
|
385
|
-
var STORE_NAME = "keys";
|
|
386
|
-
function openDb() {
|
|
387
|
-
return new Promise((resolve, reject) => {
|
|
388
|
-
if (typeof indexedDB === "undefined") {
|
|
389
|
-
reject(new Error("[PollarClient:keys] IndexedDB not available"));
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
393
|
-
req.onerror = () => reject(req.error ?? new Error("IDB open failed"));
|
|
394
|
-
req.onsuccess = () => resolve(req.result);
|
|
395
|
-
req.onupgradeneeded = () => {
|
|
396
|
-
const db = req.result;
|
|
397
|
-
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
398
|
-
db.createObjectStore(STORE_NAME);
|
|
399
|
-
}
|
|
400
|
-
};
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
function awaitTx(req) {
|
|
404
|
-
return new Promise((resolve, reject) => {
|
|
405
|
-
req.onsuccess = () => resolve(req.result);
|
|
406
|
-
req.onerror = () => reject(req.error ?? new Error("IDB request failed"));
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
async function dbGet(key) {
|
|
410
|
-
const db = await openDb();
|
|
411
|
-
try {
|
|
412
|
-
const tx = db.transaction(STORE_NAME, "readonly");
|
|
413
|
-
const result = await awaitTx(tx.objectStore(STORE_NAME).get(key));
|
|
414
|
-
return result;
|
|
415
|
-
} finally {
|
|
416
|
-
db.close();
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
async function dbPut(key, value) {
|
|
420
|
-
const db = await openDb();
|
|
421
|
-
try {
|
|
422
|
-
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
423
|
-
await awaitTx(tx.objectStore(STORE_NAME).put(value, key));
|
|
424
|
-
} finally {
|
|
425
|
-
db.close();
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
async function dbDelete(key) {
|
|
429
|
-
const db = await openDb();
|
|
430
|
-
try {
|
|
431
|
-
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
432
|
-
await awaitTx(tx.objectStore(STORE_NAME).delete(key));
|
|
433
|
-
} finally {
|
|
434
|
-
db.close();
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
function isCryptoKeyPair(v) {
|
|
438
|
-
if (typeof v !== "object" || v === null) return false;
|
|
439
|
-
const obj = v;
|
|
440
|
-
return obj.privateKey !== void 0 && obj.publicKey !== void 0;
|
|
441
|
-
}
|
|
442
|
-
var WebCryptoKeyManager = class {
|
|
443
|
-
constructor(apiKey) {
|
|
444
|
-
this.apiKeyHash = null;
|
|
445
|
-
this.keyPair = null;
|
|
446
|
-
this.publicJwk = null;
|
|
447
|
-
this.thumbprint = null;
|
|
448
|
-
/**
|
|
449
|
-
* Cached in-flight init. Lets `init()` be called concurrently (or implicitly
|
|
450
|
-
* from `getPublicJwk` / `sign`) without doing the work twice. Cleared on
|
|
451
|
-
* failure so callers can retry, and cleared on `reset()`.
|
|
452
|
-
*/
|
|
453
|
-
this._initPromise = null;
|
|
454
|
-
if (typeof globalThis.crypto === "undefined" || !globalThis.crypto.subtle) {
|
|
455
|
-
throw new Error("[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost).");
|
|
456
|
-
}
|
|
457
|
-
this.apiKey = apiKey;
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Idempotent and safe under concurrency. The first call kicks off the real
|
|
461
|
-
* init; subsequent (and concurrent) calls return the same in-flight promise.
|
|
462
|
-
* Other methods (`getPublicJwk`, `getThumbprint`, `sign`) auto-await this so
|
|
463
|
-
* the manager is self-healing if `init()` was never explicitly invoked.
|
|
464
|
-
*/
|
|
465
|
-
async init() {
|
|
466
|
-
if (this.keyPair) return;
|
|
467
|
-
if (!this._initPromise) {
|
|
468
|
-
this._initPromise = this._doInit().catch((err) => {
|
|
469
|
-
console.error("[PollarClient:keys] WebCryptoKeyManager init failed", err);
|
|
470
|
-
this._initPromise = null;
|
|
471
|
-
throw err;
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
return this._initPromise;
|
|
475
|
-
}
|
|
476
|
-
async _doInit() {
|
|
477
|
-
if (!this.apiKeyHash) {
|
|
478
|
-
this.apiKeyHash = await hashApiKey(this.apiKey);
|
|
479
|
-
}
|
|
480
|
-
let pair;
|
|
481
|
-
try {
|
|
482
|
-
pair = await dbGet(this.apiKeyHash);
|
|
483
|
-
if (pair && !isCryptoKeyPair(pair)) pair = void 0;
|
|
484
|
-
} catch {
|
|
485
|
-
pair = void 0;
|
|
486
|
-
}
|
|
487
|
-
if (!pair) {
|
|
488
|
-
pair = await globalThis.crypto.subtle.generateKey(
|
|
489
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
490
|
-
// false → private key non-extractable; per W3C ECDSA spec the public
|
|
491
|
-
// key is always extractable regardless of this flag.
|
|
492
|
-
false,
|
|
493
|
-
["sign", "verify"]
|
|
494
|
-
);
|
|
495
|
-
try {
|
|
496
|
-
await dbPut(this.apiKeyHash, pair);
|
|
497
|
-
} catch {
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
this.keyPair = pair;
|
|
501
|
-
const exported = await globalThis.crypto.subtle.exportKey("jwk", pair.publicKey);
|
|
502
|
-
this.publicJwk = canonicalEcJwk(exported);
|
|
503
|
-
this.thumbprint = await computeJwkThumbprint(this.publicJwk);
|
|
504
|
-
}
|
|
505
|
-
async reset() {
|
|
506
|
-
try {
|
|
507
|
-
if (this.apiKeyHash) await dbDelete(this.apiKeyHash);
|
|
508
|
-
} catch {
|
|
509
|
-
}
|
|
510
|
-
this.keyPair = null;
|
|
511
|
-
this.publicJwk = null;
|
|
512
|
-
this.thumbprint = null;
|
|
513
|
-
this._initPromise = null;
|
|
514
|
-
}
|
|
515
|
-
async getPublicJwk() {
|
|
516
|
-
if (!this.publicJwk) await this.init();
|
|
517
|
-
if (!this.publicJwk) {
|
|
518
|
-
throw new Error("[PollarClient:keys] Keypair initialization failed; getPublicJwk unavailable");
|
|
519
|
-
}
|
|
520
|
-
return { kty: this.publicJwk.kty, crv: this.publicJwk.crv, x: this.publicJwk.x, y: this.publicJwk.y };
|
|
521
|
-
}
|
|
522
|
-
async getThumbprint() {
|
|
523
|
-
if (!this.thumbprint) await this.init();
|
|
524
|
-
if (!this.thumbprint) {
|
|
525
|
-
throw new Error("[PollarClient:keys] Keypair initialization failed; getThumbprint unavailable");
|
|
526
|
-
}
|
|
527
|
-
return this.thumbprint;
|
|
528
|
-
}
|
|
529
|
-
async sign(payload) {
|
|
530
|
-
if (!this.keyPair) await this.init();
|
|
531
|
-
if (!this.keyPair) {
|
|
532
|
-
throw new Error("[PollarClient:keys] Keypair initialization failed; sign unavailable");
|
|
533
|
-
}
|
|
534
|
-
const sig = await globalThis.crypto.subtle.sign(
|
|
535
|
-
{ name: "ECDSA", hash: "SHA-256" },
|
|
536
|
-
this.keyPair.privateKey,
|
|
537
|
-
payload
|
|
538
|
-
);
|
|
539
|
-
return new Uint8Array(sig);
|
|
540
|
-
}
|
|
541
|
-
};
|
|
542
|
-
|
|
543
383
|
// ../../node_modules/openapi-fetch/dist/index.mjs
|
|
544
384
|
var PATH_PARAM_RE = /\{[^{}]+\}/g;
|
|
545
385
|
var supportsRequestInitExt = () => {
|
|
@@ -1259,6 +1099,9 @@ function defaultStorage(options = {}) {
|
|
|
1259
1099
|
return createLocalStorageAdapter(options);
|
|
1260
1100
|
}
|
|
1261
1101
|
|
|
1102
|
+
// src/version.ts
|
|
1103
|
+
var POLLAR_CORE_VERSION = "0.9.0-rc.0" ;
|
|
1104
|
+
|
|
1262
1105
|
// src/visibility/noop.ts
|
|
1263
1106
|
function createNoopVisibilityProvider() {
|
|
1264
1107
|
return {
|
|
@@ -1323,6 +1166,7 @@ var AUTH_ERROR_CODES = {
|
|
|
1323
1166
|
WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
|
|
1324
1167
|
WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
|
|
1325
1168
|
WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
|
|
1169
|
+
PASSKEY_FAILED: "PASSKEY_FAILED",
|
|
1326
1170
|
UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
|
|
1327
1171
|
};
|
|
1328
1172
|
var PollarFlowError = class extends Error {
|
|
@@ -1368,7 +1212,7 @@ var FreighterAdapter = class {
|
|
|
1368
1212
|
if (!userInfo?.publicKey) {
|
|
1369
1213
|
throw new Error("Failed to get user information from Freighter");
|
|
1370
1214
|
}
|
|
1371
|
-
return { address: userInfo.publicKey
|
|
1215
|
+
return { address: userInfo.publicKey };
|
|
1372
1216
|
}
|
|
1373
1217
|
async disconnect() {
|
|
1374
1218
|
}
|
|
@@ -1406,6 +1250,19 @@ var FreighterAdapter = class {
|
|
|
1406
1250
|
};
|
|
1407
1251
|
|
|
1408
1252
|
// src/wallets/AlbedoAdapter.ts
|
|
1253
|
+
var PUBLIC_PASSPHRASE = "Public Global Stellar Network ; September 2015";
|
|
1254
|
+
var TESTNET_PASSPHRASE = "Test SDF Network ; September 2015";
|
|
1255
|
+
function albedoNetwork(options, fallback) {
|
|
1256
|
+
switch (options?.networkPassphrase) {
|
|
1257
|
+
case PUBLIC_PASSPHRASE:
|
|
1258
|
+
return "public";
|
|
1259
|
+
case TESTNET_PASSPHRASE:
|
|
1260
|
+
return "testnet";
|
|
1261
|
+
}
|
|
1262
|
+
if (options?.network === "public" || options?.network === "mainnet") return "public";
|
|
1263
|
+
if (options?.network === "testnet") return "testnet";
|
|
1264
|
+
return fallback;
|
|
1265
|
+
}
|
|
1409
1266
|
function openAlbedoPopup(url) {
|
|
1410
1267
|
const popup = window.open(url, "albedo", "width=420,height=720,resizable=yes,scrollbars=yes");
|
|
1411
1268
|
if (!popup) {
|
|
@@ -1444,7 +1301,13 @@ function waitForAlbedoResult() {
|
|
|
1444
1301
|
});
|
|
1445
1302
|
}
|
|
1446
1303
|
var AlbedoAdapter = class {
|
|
1447
|
-
|
|
1304
|
+
/**
|
|
1305
|
+
* Network used for `connect` and `signAuthEntry` (which carry no per-call
|
|
1306
|
+
* network) and as the fallback for `signTransaction`. Defaults to `'testnet'`
|
|
1307
|
+
* to preserve the previous behavior when constructed with no argument.
|
|
1308
|
+
*/
|
|
1309
|
+
constructor(network = "testnet") {
|
|
1310
|
+
this.network = network;
|
|
1448
1311
|
this.type = "albedo" /* ALBEDO */;
|
|
1449
1312
|
}
|
|
1450
1313
|
async isAvailable() {
|
|
@@ -1454,7 +1317,7 @@ var AlbedoAdapter = class {
|
|
|
1454
1317
|
const url = new URL("https://albedo.link");
|
|
1455
1318
|
url.searchParams.set("intent", "public-key");
|
|
1456
1319
|
url.searchParams.set("app_name", "Pollar");
|
|
1457
|
-
url.searchParams.set("network",
|
|
1320
|
+
url.searchParams.set("network", this.network);
|
|
1458
1321
|
url.searchParams.set("callback", `${window.location.origin}/albedo-callback`);
|
|
1459
1322
|
url.searchParams.set("origin", window.location.origin);
|
|
1460
1323
|
openAlbedoPopup(url.toString());
|
|
@@ -1462,7 +1325,7 @@ var AlbedoAdapter = class {
|
|
|
1462
1325
|
if (!result.pubkey) {
|
|
1463
1326
|
throw new Error("Albedo connection rejected");
|
|
1464
1327
|
}
|
|
1465
|
-
return { address: result.pubkey
|
|
1328
|
+
return { address: result.pubkey };
|
|
1466
1329
|
}
|
|
1467
1330
|
async disconnect() {
|
|
1468
1331
|
}
|
|
@@ -1472,12 +1335,12 @@ var AlbedoAdapter = class {
|
|
|
1472
1335
|
async getNetwork() {
|
|
1473
1336
|
throw new Error("Albedo does not expose network");
|
|
1474
1337
|
}
|
|
1475
|
-
async signTransaction(xdr,
|
|
1338
|
+
async signTransaction(xdr, options) {
|
|
1476
1339
|
const url = new URL("https://albedo.link");
|
|
1477
1340
|
url.searchParams.set("intent", "tx");
|
|
1478
1341
|
url.searchParams.set("xdr", xdr);
|
|
1479
1342
|
url.searchParams.set("app_name", "Pollar");
|
|
1480
|
-
url.searchParams.set("network",
|
|
1343
|
+
url.searchParams.set("network", albedoNetwork(options, this.network));
|
|
1481
1344
|
url.searchParams.set("callback", window.location.href);
|
|
1482
1345
|
url.searchParams.set("origin", window.location.origin);
|
|
1483
1346
|
window.location.href = url.toString();
|
|
@@ -1490,7 +1353,7 @@ var AlbedoAdapter = class {
|
|
|
1490
1353
|
url.searchParams.set("intent", "sign-auth-entry");
|
|
1491
1354
|
url.searchParams.set("xdr", entryXdr);
|
|
1492
1355
|
url.searchParams.set("app_name", "Pollar");
|
|
1493
|
-
url.searchParams.set("network",
|
|
1356
|
+
url.searchParams.set("network", this.network);
|
|
1494
1357
|
url.searchParams.set("callback", window.location.href);
|
|
1495
1358
|
url.searchParams.set("origin", window.location.origin);
|
|
1496
1359
|
window.location.href = url.toString();
|
|
@@ -1577,8 +1440,12 @@ function isValidSession(value) {
|
|
|
1577
1440
|
return false;
|
|
1578
1441
|
}
|
|
1579
1442
|
const w = wallet;
|
|
1580
|
-
if (w["
|
|
1581
|
-
console.warn("[PollarClient:session] Invalid session \u2014 wallet.
|
|
1443
|
+
if (w["type"] !== "custodial" && w["type"] !== "smart" && w["type"] !== "external") {
|
|
1444
|
+
console.warn("[PollarClient:session] Invalid session \u2014 wallet.type must be custodial|smart|external");
|
|
1445
|
+
return false;
|
|
1446
|
+
}
|
|
1447
|
+
if (w["address"] !== null && !isBoundedString(w["address"], MAX_WALLET_PUBLIC_KEY)) {
|
|
1448
|
+
console.warn("[PollarClient:session] Invalid session \u2014 wallet.address must be string|null");
|
|
1582
1449
|
return false;
|
|
1583
1450
|
}
|
|
1584
1451
|
if (w["existsOnStellar"] !== void 0 && typeof w["existsOnStellar"] !== "boolean") {
|
|
@@ -1589,6 +1456,10 @@ function isValidSession(value) {
|
|
|
1589
1456
|
console.warn("[PollarClient:session] Invalid session \u2014 wallet.createdAt must be a finite number if present");
|
|
1590
1457
|
return false;
|
|
1591
1458
|
}
|
|
1459
|
+
if (w["linkedAt"] !== void 0 && (typeof w["linkedAt"] !== "number" || !Number.isFinite(w["linkedAt"]))) {
|
|
1460
|
+
console.warn("[PollarClient:session] Invalid session \u2014 wallet.linkedAt must be a finite number if present");
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1592
1463
|
return true;
|
|
1593
1464
|
}
|
|
1594
1465
|
async function readStorage(storage, apiKeyHash) {
|
|
@@ -1596,6 +1467,12 @@ async function readStorage(storage, apiKeyHash) {
|
|
|
1596
1467
|
if (!raw) return null;
|
|
1597
1468
|
try {
|
|
1598
1469
|
const session = JSON.parse(raw);
|
|
1470
|
+
if (typeof session === "object" && session !== null) {
|
|
1471
|
+
const w = session.wallet;
|
|
1472
|
+
if (w && w["address"] == null && typeof w["publicKey"] === "string") {
|
|
1473
|
+
w["address"] = w["publicKey"];
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1599
1476
|
if (!isValidSession(session)) {
|
|
1600
1477
|
await storage.remove(sessionStorageKey(apiKeyHash));
|
|
1601
1478
|
console.warn("[PollarClient:session] Stored session is invalid \u2014 clearing storage");
|
|
@@ -1686,7 +1563,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
|
|
|
1686
1563
|
}));
|
|
1687
1564
|
} catch (e) {
|
|
1688
1565
|
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
1689
|
-
console.warn(e);
|
|
1566
|
+
console.warn("[PollarClient:stream] session-status request failed; will retry", e);
|
|
1690
1567
|
}
|
|
1691
1568
|
if (error || !data) {
|
|
1692
1569
|
await sleep(backoff);
|
|
@@ -1726,7 +1603,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
|
|
|
1726
1603
|
} catch (e) {
|
|
1727
1604
|
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
1728
1605
|
if (e instanceof SessionStatusError) throw e;
|
|
1729
|
-
console.warn(e);
|
|
1606
|
+
console.warn("[PollarClient:stream] session-status stream read failed; will retry", e);
|
|
1730
1607
|
} finally {
|
|
1731
1608
|
reader.releaseLock();
|
|
1732
1609
|
}
|
|
@@ -1754,7 +1631,7 @@ async function pollUntilFound(baseUrl, clientSessionId, check, intervalMs = 500,
|
|
|
1754
1631
|
envelope = await response.json().catch(() => null);
|
|
1755
1632
|
} catch (e) {
|
|
1756
1633
|
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
1757
|
-
console.warn(e);
|
|
1634
|
+
console.warn("[PollarClient:stream] session-status poll failed; will retry", e);
|
|
1758
1635
|
}
|
|
1759
1636
|
if (httpStatus === 404 || envelope?.code === "INVALID_CLIENT_SESSION_ID") {
|
|
1760
1637
|
throw new SessionStatusError("INVALID_CLIENT_SESSION_ID");
|
|
@@ -1958,6 +1835,55 @@ async function loginOAuth(provider, deps) {
|
|
|
1958
1835
|
await authenticate(clientSessionId, deps);
|
|
1959
1836
|
}
|
|
1960
1837
|
|
|
1838
|
+
// src/client/auth/passkeyFlow.ts
|
|
1839
|
+
async function loginSmartWallet(deps) {
|
|
1840
|
+
const { api, signal, setAuthState, passkey } = deps;
|
|
1841
|
+
if (!passkey) {
|
|
1842
|
+
setAuthState({
|
|
1843
|
+
step: "error",
|
|
1844
|
+
previousStep: "creating_session",
|
|
1845
|
+
message: "Passkey support is not configured",
|
|
1846
|
+
errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
|
|
1847
|
+
});
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
const clientSessionId = await createAuthSession(deps);
|
|
1851
|
+
if (!clientSessionId) return;
|
|
1852
|
+
try {
|
|
1853
|
+
const { data: challengeData } = await api.POST("/auth/passkey/challenge", {
|
|
1854
|
+
body: { clientSessionId },
|
|
1855
|
+
signal
|
|
1856
|
+
});
|
|
1857
|
+
const challenge = challengeData?.content?.challenge;
|
|
1858
|
+
if (!challengeData?.success || !challenge) {
|
|
1859
|
+
return failPasskey(setAuthState, "Failed to start passkey");
|
|
1860
|
+
}
|
|
1861
|
+
setAuthState({ step: "creating_passkey" });
|
|
1862
|
+
const ceremony = await passkey({ challenge });
|
|
1863
|
+
const response = ceremony.response;
|
|
1864
|
+
if (ceremony.kind === "register") {
|
|
1865
|
+
setAuthState({ step: "deploying_smart_account" });
|
|
1866
|
+
const { data } = await api.POST("/auth/passkey/register", {
|
|
1867
|
+
body: { clientSessionId, response },
|
|
1868
|
+
signal
|
|
1869
|
+
});
|
|
1870
|
+
if (!data?.success) return failPasskey(setAuthState, "Passkey registration failed");
|
|
1871
|
+
} else {
|
|
1872
|
+
const { data } = await api.POST("/auth/passkey/login", {
|
|
1873
|
+
body: { clientSessionId, response },
|
|
1874
|
+
signal
|
|
1875
|
+
});
|
|
1876
|
+
if (!data?.success) return failPasskey(setAuthState, "Passkey authentication failed");
|
|
1877
|
+
}
|
|
1878
|
+
} catch {
|
|
1879
|
+
return failPasskey(setAuthState, "Passkey login failed");
|
|
1880
|
+
}
|
|
1881
|
+
await authenticate(clientSessionId, deps);
|
|
1882
|
+
}
|
|
1883
|
+
function failPasskey(setAuthState, message) {
|
|
1884
|
+
setAuthState({ step: "error", previousStep: "creating_passkey", message, errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED });
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1961
1887
|
// src/client/auth/walletFlow.ts
|
|
1962
1888
|
function withSignal(promise, signal) {
|
|
1963
1889
|
return Promise.race([
|
|
@@ -1984,12 +1910,12 @@ async function loginWallet(type, deps) {
|
|
|
1984
1910
|
setAuthState({ step: "wallet_not_installed", walletType: type });
|
|
1985
1911
|
return;
|
|
1986
1912
|
}
|
|
1987
|
-
const {
|
|
1988
|
-
connectedWallet =
|
|
1913
|
+
const { address } = await withSignal(adapter.connect(), signal);
|
|
1914
|
+
connectedWallet = address;
|
|
1989
1915
|
deps.storeWalletAdapter(adapter, type);
|
|
1990
1916
|
setAuthState({ step: "authenticating_wallet" });
|
|
1991
1917
|
const { data: walletData, error: walletError } = await api.POST("/auth/wallet", {
|
|
1992
|
-
body: { clientSessionId, walletAddress:
|
|
1918
|
+
body: { clientSessionId, walletAddress: address },
|
|
1993
1919
|
signal
|
|
1994
1920
|
});
|
|
1995
1921
|
if (walletError || !walletData?.success) {
|
|
@@ -2054,8 +1980,12 @@ var PollarClient = class {
|
|
|
2054
1980
|
this._transactionStateListeners = /* @__PURE__ */ new Set();
|
|
2055
1981
|
this._txHistoryState = { step: "idle" };
|
|
2056
1982
|
this._txHistoryStateListeners = /* @__PURE__ */ new Set();
|
|
1983
|
+
this._sessionsState = { step: "idle" };
|
|
1984
|
+
this._sessionsStateListeners = /* @__PURE__ */ new Set();
|
|
2057
1985
|
this._walletBalanceState = { step: "idle" };
|
|
2058
1986
|
this._walletBalanceStateListeners = /* @__PURE__ */ new Set();
|
|
1987
|
+
this._enabledAssetsState = { step: "idle" };
|
|
1988
|
+
this._enabledAssetsStateListeners = /* @__PURE__ */ new Set();
|
|
2059
1989
|
this._authState = { step: "idle" };
|
|
2060
1990
|
this._authStateListeners = /* @__PURE__ */ new Set();
|
|
2061
1991
|
this._networkState = { step: "idle" };
|
|
@@ -2071,6 +2001,8 @@ var PollarClient = class {
|
|
|
2071
2001
|
this._storageDegradeListeners = /* @__PURE__ */ new Set();
|
|
2072
2002
|
this._walletAdapter = null;
|
|
2073
2003
|
this._loginController = null;
|
|
2004
|
+
/** Aborts an in-flight `/auth/session/resume` on destroy() or re-trigger. */
|
|
2005
|
+
this._resumeController = null;
|
|
2074
2006
|
this.apiKey = config.apiKey;
|
|
2075
2007
|
this.id = randomUUID();
|
|
2076
2008
|
this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
|
|
@@ -2083,6 +2015,8 @@ var PollarClient = class {
|
|
|
2083
2015
|
this._keyManager = config.keyManager ?? defaultKeyManager(this._storage, config.apiKey);
|
|
2084
2016
|
this._walletAdapterResolver = config.walletAdapter ?? null;
|
|
2085
2017
|
this._walletResolverTimeoutMs = config.walletResolverTimeoutMs ?? 5e3;
|
|
2018
|
+
this._passkey = config.passkey ?? null;
|
|
2019
|
+
this._passkeySign = config.passkeySign ?? null;
|
|
2086
2020
|
this._deviceLabel = config.deviceLabel;
|
|
2087
2021
|
this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
|
|
2088
2022
|
this._maxIdleMs = config.maxIdleMs;
|
|
@@ -2096,7 +2030,9 @@ var PollarClient = class {
|
|
|
2096
2030
|
this._initialized = Promise.resolve();
|
|
2097
2031
|
return;
|
|
2098
2032
|
}
|
|
2099
|
-
console.info(
|
|
2033
|
+
console.info(
|
|
2034
|
+
`[PollarClient] Initialized v${POLLAR_CORE_VERSION} \u2014 endpoint: ${this.basePath}, network: ${this._networkState.network}`
|
|
2035
|
+
);
|
|
2100
2036
|
this._initialized = this._initialize();
|
|
2101
2037
|
}
|
|
2102
2038
|
/**
|
|
@@ -2134,7 +2070,11 @@ var PollarClient = class {
|
|
|
2134
2070
|
}
|
|
2135
2071
|
await this._restoreSession();
|
|
2136
2072
|
this._visibilityUnsubscribe = this._visibilityProvider.onChange((visible) => {
|
|
2137
|
-
if (visible)
|
|
2073
|
+
if (!visible) return;
|
|
2074
|
+
void this._maybeProactiveRefresh();
|
|
2075
|
+
if (this._authState.step === "authenticated" && !this._authState.verified) {
|
|
2076
|
+
void this._resume();
|
|
2077
|
+
}
|
|
2138
2078
|
});
|
|
2139
2079
|
}
|
|
2140
2080
|
/** Detach the cross-tab storage listener and abort any in-flight login. */
|
|
@@ -2145,6 +2085,8 @@ var PollarClient = class {
|
|
|
2145
2085
|
}
|
|
2146
2086
|
this._loginController?.abort();
|
|
2147
2087
|
this._loginController = null;
|
|
2088
|
+
this._resumeController?.abort();
|
|
2089
|
+
this._resumeController = null;
|
|
2148
2090
|
this._clearRefreshTimer();
|
|
2149
2091
|
if (this._visibilityUnsubscribe) {
|
|
2150
2092
|
this._visibilityUnsubscribe();
|
|
@@ -2159,7 +2101,9 @@ var PollarClient = class {
|
|
|
2159
2101
|
request.headers.set("x-pollar-api-key", self.apiKey);
|
|
2160
2102
|
self._lastRequestAt = Date.now();
|
|
2161
2103
|
await self._initialized;
|
|
2162
|
-
|
|
2104
|
+
const cacheMethod = request.method.toUpperCase();
|
|
2105
|
+
const cacheBodyAllowed = cacheMethod !== "GET" && cacheMethod !== "HEAD";
|
|
2106
|
+
if (cacheBodyAllowed && request.body != null) {
|
|
2163
2107
|
try {
|
|
2164
2108
|
self._requestBodyCache.set(request, await request.clone().arrayBuffer());
|
|
2165
2109
|
} catch (err) {
|
|
@@ -2246,7 +2190,9 @@ var PollarClient = class {
|
|
|
2246
2190
|
}
|
|
2247
2191
|
}
|
|
2248
2192
|
}
|
|
2249
|
-
const
|
|
2193
|
+
const retryMethod = originalRequest.method.toUpperCase();
|
|
2194
|
+
const retryBodyAllowed = retryMethod !== "GET" && retryMethod !== "HEAD";
|
|
2195
|
+
const cachedBody = retryBodyAllowed ? this._requestBodyCache.get(originalRequest) : void 0;
|
|
2250
2196
|
const retried = new Request(originalRequest.url, {
|
|
2251
2197
|
method: originalRequest.method,
|
|
2252
2198
|
headers,
|
|
@@ -2290,19 +2236,26 @@ var PollarClient = class {
|
|
|
2290
2236
|
throw err;
|
|
2291
2237
|
}
|
|
2292
2238
|
if (error || !data) {
|
|
2293
|
-
console.
|
|
2239
|
+
console.error("[PollarClient] /auth/refresh returned error", { error });
|
|
2294
2240
|
await this._clearSession();
|
|
2295
2241
|
throw new Error("Refresh failed");
|
|
2296
2242
|
}
|
|
2297
2243
|
const successData = data;
|
|
2298
2244
|
if (!successData.success || !successData.content?.token) {
|
|
2299
|
-
console.
|
|
2245
|
+
console.error("[PollarClient] /auth/refresh response malformed", {
|
|
2246
|
+
success: successData.success,
|
|
2247
|
+
hasToken: !!successData.content?.token
|
|
2248
|
+
});
|
|
2300
2249
|
await this._clearSession();
|
|
2301
2250
|
throw new Error("Refresh response malformed");
|
|
2302
2251
|
}
|
|
2303
2252
|
const newToken = successData.content.token;
|
|
2304
2253
|
if (typeof newToken.accessToken !== "string" || typeof newToken.refreshToken !== "string" || typeof newToken.expiresAt !== "number") {
|
|
2305
|
-
console.
|
|
2254
|
+
console.error("[PollarClient] /auth/refresh token shape invalid", {
|
|
2255
|
+
accessToken: typeof newToken.accessToken,
|
|
2256
|
+
refreshToken: typeof newToken.refreshToken,
|
|
2257
|
+
expiresAt: typeof newToken.expiresAt
|
|
2258
|
+
});
|
|
2306
2259
|
await this._clearSession();
|
|
2307
2260
|
throw new Error("Refresh response token shape invalid");
|
|
2308
2261
|
}
|
|
@@ -2496,6 +2449,19 @@ var PollarClient = class {
|
|
|
2496
2449
|
const controller = this._newController();
|
|
2497
2450
|
loginWallet(type, this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
|
|
2498
2451
|
}
|
|
2452
|
+
/**
|
|
2453
|
+
* "Smart Wallet" login: runs the passkey (WebAuthn) ceremony and, for a new
|
|
2454
|
+
* user, creates a sponsored smart-account C-address. Requires the `passkey`
|
|
2455
|
+
* ceremony to be configured (e.g. via `@pollar/react`).
|
|
2456
|
+
*/
|
|
2457
|
+
loginSmartWallet() {
|
|
2458
|
+
if (!isClientRuntime) {
|
|
2459
|
+
warnServerSide("loginSmartWallet");
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
const controller = this._newController();
|
|
2463
|
+
loginSmartWallet(this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
|
|
2464
|
+
}
|
|
2499
2465
|
// ─── Cancel ───────────────────────────────────────────────────────────────
|
|
2500
2466
|
cancelLogin() {
|
|
2501
2467
|
this._loginController?.abort();
|
|
@@ -2558,6 +2524,29 @@ var PollarClient = class {
|
|
|
2558
2524
|
}
|
|
2559
2525
|
return data.content.sessions;
|
|
2560
2526
|
}
|
|
2527
|
+
getSessionsState() {
|
|
2528
|
+
return this._sessionsState;
|
|
2529
|
+
}
|
|
2530
|
+
onSessionsStateChange(cb) {
|
|
2531
|
+
this._sessionsStateListeners.add(cb);
|
|
2532
|
+
cb(this._sessionsState);
|
|
2533
|
+
return () => this._sessionsStateListeners.delete(cb);
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* Fire-and-forget variant of {@link listSessions} that drives the observable
|
|
2537
|
+
* `SessionsState` store instead of returning the array. UI layers subscribe
|
|
2538
|
+
* via `onSessionsStateChange` and stay pure readers — mirrors `fetchTxHistory`.
|
|
2539
|
+
*/
|
|
2540
|
+
async fetchSessions() {
|
|
2541
|
+
this._setSessionsState({ step: "loading" });
|
|
2542
|
+
try {
|
|
2543
|
+
const sessions = await this.listSessions();
|
|
2544
|
+
this._setSessionsState({ step: "loaded", sessions });
|
|
2545
|
+
} catch (err) {
|
|
2546
|
+
const message = err instanceof Error ? err.message : "Failed to load sessions";
|
|
2547
|
+
this._setSessionsState({ step: "error", message });
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2561
2550
|
/**
|
|
2562
2551
|
* Revoke a specific refresh-token family (a single device session). Use
|
|
2563
2552
|
* `listSessions` to enumerate the familyIds. Revoking the current session
|
|
@@ -2634,16 +2623,19 @@ var PollarClient = class {
|
|
|
2634
2623
|
cb(this._walletBalanceState);
|
|
2635
2624
|
return () => this._walletBalanceStateListeners.delete(cb);
|
|
2636
2625
|
}
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2626
|
+
/**
|
|
2627
|
+
* Refreshes the balances of the authenticated user's OWN wallet. The wallet
|
|
2628
|
+
* and network are resolved server-side from the session — no arguments. Drives
|
|
2629
|
+
* `walletBalanceState`. For an arbitrary wallet, use {@link getWalletBalance}.
|
|
2630
|
+
*/
|
|
2631
|
+
async refreshBalance() {
|
|
2632
|
+
if (!this._session?.wallet?.address) {
|
|
2640
2633
|
this._setWalletBalanceState({ step: "error", message: "No wallet connected" });
|
|
2641
2634
|
return;
|
|
2642
2635
|
}
|
|
2643
2636
|
this._setWalletBalanceState({ step: "loading" });
|
|
2644
2637
|
try {
|
|
2645
|
-
const
|
|
2646
|
-
const { data, error } = await this._api.GET("/wallet/balance", { params: { query: { publicKey: pk, network } } });
|
|
2638
|
+
const { data, error } = await this._api.GET("/wallet/balance");
|
|
2647
2639
|
if (!error && data?.success && data.content) {
|
|
2648
2640
|
this._setWalletBalanceState({ step: "loaded", data: data.content });
|
|
2649
2641
|
} else {
|
|
@@ -2653,6 +2645,53 @@ var PollarClient = class {
|
|
|
2653
2645
|
this._setWalletBalanceState({ step: "error", message: "Failed to load balance" });
|
|
2654
2646
|
}
|
|
2655
2647
|
}
|
|
2648
|
+
/**
|
|
2649
|
+
* General-purpose balance lookup for ANY wallet on ANY network — not scoped
|
|
2650
|
+
* to this application. Enumerates the account's real on-chain holdings via
|
|
2651
|
+
* Horizon (server-side) and returns the data directly (no reactive state).
|
|
2652
|
+
* `network` defaults to the client's current network.
|
|
2653
|
+
*/
|
|
2654
|
+
async getWalletBalance(publicKey, network) {
|
|
2655
|
+
const { data, error } = await this._api.GET("/wallet/{publicKey}/balance", {
|
|
2656
|
+
params: { path: { publicKey }, query: { network: network ?? this.getNetwork() } }
|
|
2657
|
+
});
|
|
2658
|
+
if (error || !data?.success || !data.content) {
|
|
2659
|
+
throw new Error("[PollarClient] Failed to load wallet balance");
|
|
2660
|
+
}
|
|
2661
|
+
return data.content;
|
|
2662
|
+
}
|
|
2663
|
+
// ─── Enabled assets ───────────────────────────────────────────────────────
|
|
2664
|
+
getEnabledAssetsState() {
|
|
2665
|
+
return this._enabledAssetsState;
|
|
2666
|
+
}
|
|
2667
|
+
onEnabledAssetsStateChange(cb) {
|
|
2668
|
+
this._enabledAssetsStateListeners.add(cb);
|
|
2669
|
+
cb(this._enabledAssetsState);
|
|
2670
|
+
return () => this._enabledAssetsStateListeners.delete(cb);
|
|
2671
|
+
}
|
|
2672
|
+
/**
|
|
2673
|
+
* Loads the application's enabled assets paired with the authenticated
|
|
2674
|
+
* wallet's on-chain trustline state — so the SDK knows which trustlines still
|
|
2675
|
+
* need to be added. Wallet and network are resolved server-side from the
|
|
2676
|
+
* session. Drives `enabledAssetsState`; mirrors {@link refreshBalance}.
|
|
2677
|
+
*/
|
|
2678
|
+
async refreshAssets() {
|
|
2679
|
+
if (!this._session?.wallet?.address) {
|
|
2680
|
+
this._setEnabledAssetsState({ step: "error", message: "No wallet connected" });
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
this._setEnabledAssetsState({ step: "loading" });
|
|
2684
|
+
try {
|
|
2685
|
+
const { data, error } = await this._api.GET("/wallet/assets");
|
|
2686
|
+
if (!error && data?.success && data.content) {
|
|
2687
|
+
this._setEnabledAssetsState({ step: "loaded", data: data.content });
|
|
2688
|
+
} else {
|
|
2689
|
+
this._setEnabledAssetsState({ step: "error", message: "Failed to load assets" });
|
|
2690
|
+
}
|
|
2691
|
+
} catch {
|
|
2692
|
+
this._setEnabledAssetsState({ step: "error", message: "Failed to load assets" });
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2656
2695
|
// ─── Transactions ─────────────────────────────────────────────────────────
|
|
2657
2696
|
/**
|
|
2658
2697
|
* Builds an unsigned XDR. Drives `_setTransactionState` for modal-style UIs
|
|
@@ -2660,14 +2699,14 @@ var PollarClient = class {
|
|
|
2660
2699
|
* inspect the result without subscribing to state changes.
|
|
2661
2700
|
*/
|
|
2662
2701
|
async buildTx(operation, params, options) {
|
|
2663
|
-
if (!this._session?.wallet?.
|
|
2702
|
+
if (!this._session?.wallet?.address) {
|
|
2664
2703
|
const details = "No wallet connected";
|
|
2665
2704
|
this._setTransactionState({ step: "error", phase: "building", details });
|
|
2666
2705
|
return { status: "error", details };
|
|
2667
2706
|
}
|
|
2668
2707
|
const body = {
|
|
2669
2708
|
network: this.getNetwork(),
|
|
2670
|
-
|
|
2709
|
+
address: this._session.wallet.address,
|
|
2671
2710
|
operation,
|
|
2672
2711
|
params,
|
|
2673
2712
|
options: options ?? {}
|
|
@@ -2708,7 +2747,7 @@ var PollarClient = class {
|
|
|
2708
2747
|
const buildData = this._currentBuildData();
|
|
2709
2748
|
this._setTransactionState({ step: "signing", ...buildData && { buildData } });
|
|
2710
2749
|
if (this._walletAdapter) {
|
|
2711
|
-
const accountToSign = this._session?.wallet?.
|
|
2750
|
+
const accountToSign = this._session?.wallet?.address;
|
|
2712
2751
|
const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
|
|
2713
2752
|
try {
|
|
2714
2753
|
const { signedTxXdr } = await this._walletAdapter.signTransaction(unsignedXdr, signOpts);
|
|
@@ -2729,10 +2768,10 @@ var PollarClient = class {
|
|
|
2729
2768
|
return { status: "error", ...details && { details } };
|
|
2730
2769
|
}
|
|
2731
2770
|
}
|
|
2732
|
-
const
|
|
2771
|
+
const address = this._session?.wallet?.address ?? "";
|
|
2733
2772
|
try {
|
|
2734
2773
|
const { data, error } = await this._api.POST("/tx/sign", {
|
|
2735
|
-
body: { network: this.getNetwork(),
|
|
2774
|
+
body: { network: this.getNetwork(), address, unsignedXdr }
|
|
2736
2775
|
});
|
|
2737
2776
|
if (!error && data?.success && data.content?.signedXdr) {
|
|
2738
2777
|
const { signedXdr, idempotencyKey } = data.content;
|
|
@@ -2785,12 +2824,12 @@ var PollarClient = class {
|
|
|
2785
2824
|
const buildData = this._currentBuildData();
|
|
2786
2825
|
const outcomeExtra = buildData ? { buildData } : {};
|
|
2787
2826
|
this._setTransactionState({ step: "submitting", signedXdr, ...buildData && { buildData } });
|
|
2788
|
-
const
|
|
2827
|
+
const address = this._session?.wallet?.address ?? "";
|
|
2789
2828
|
try {
|
|
2790
2829
|
const { data, error } = await this._api.POST("/tx/submit", {
|
|
2791
2830
|
body: {
|
|
2792
2831
|
network: this.getNetwork(),
|
|
2793
|
-
|
|
2832
|
+
address,
|
|
2794
2833
|
signedXdr,
|
|
2795
2834
|
...opts?.submissionToken && { idempotencyKey: opts.submissionToken }
|
|
2796
2835
|
}
|
|
@@ -2850,6 +2889,19 @@ var PollarClient = class {
|
|
|
2850
2889
|
* `success` (ledger-confirmed), or `error[phase: 'signing-submitting']`.
|
|
2851
2890
|
*/
|
|
2852
2891
|
async signAndSubmitTx(unsignedXdr) {
|
|
2892
|
+
if (this._session?.wallet?.type === "smart") {
|
|
2893
|
+
const buildData2 = this._currentBuildData();
|
|
2894
|
+
if (!buildData2?.smart) {
|
|
2895
|
+
const details = "no prepared smart transaction; call buildTx first";
|
|
2896
|
+
this._setTransactionState({ step: "error", phase: "signing", details });
|
|
2897
|
+
return { status: "error", details };
|
|
2898
|
+
}
|
|
2899
|
+
return this._signSubmitSmart(buildData2);
|
|
2900
|
+
}
|
|
2901
|
+
if (!unsignedXdr) {
|
|
2902
|
+
this._setTransactionState({ step: "error", phase: "signing", details: "missing unsigned transaction" });
|
|
2903
|
+
return { status: "error", details: "missing unsigned transaction" };
|
|
2904
|
+
}
|
|
2853
2905
|
if (this._walletAdapter) {
|
|
2854
2906
|
const signed = await this.signTx(unsignedXdr);
|
|
2855
2907
|
if (signed.status === "error") {
|
|
@@ -2867,7 +2919,7 @@ var PollarClient = class {
|
|
|
2867
2919
|
this._setTransactionState({ step: "signing-submitting", ...buildData && { buildData } });
|
|
2868
2920
|
const body = {
|
|
2869
2921
|
network: this.getNetwork(),
|
|
2870
|
-
|
|
2922
|
+
address: this._session?.wallet?.address ?? "",
|
|
2871
2923
|
unsignedXdr
|
|
2872
2924
|
};
|
|
2873
2925
|
try {
|
|
@@ -2936,14 +2988,20 @@ var PollarClient = class {
|
|
|
2936
2988
|
* `signTx`, and `submitTx` separately instead.
|
|
2937
2989
|
*/
|
|
2938
2990
|
async buildAndSignAndSubmitTx(operation, params, options) {
|
|
2991
|
+
if (this._session?.wallet?.type === "smart") {
|
|
2992
|
+
return this._runSmartTx(operation, params, options);
|
|
2993
|
+
}
|
|
2939
2994
|
if (this._walletAdapter) {
|
|
2940
2995
|
const built = await this.buildTx(operation, params, options);
|
|
2941
2996
|
if (built.status === "error") {
|
|
2942
2997
|
return { status: "error", ...built.details && { details: built.details } };
|
|
2943
2998
|
}
|
|
2999
|
+
if (!built.buildData.unsignedXdr) {
|
|
3000
|
+
return { status: "error", details: "build returned no unsigned transaction" };
|
|
3001
|
+
}
|
|
2944
3002
|
return this.signAndSubmitTx(built.buildData.unsignedXdr);
|
|
2945
3003
|
}
|
|
2946
|
-
if (!this._session?.wallet?.
|
|
3004
|
+
if (!this._session?.wallet?.address) {
|
|
2947
3005
|
this._setTransactionState({ step: "error", phase: "building-signing-submitting", details: "No wallet connected" });
|
|
2948
3006
|
return { status: "error", details: "No wallet connected" };
|
|
2949
3007
|
}
|
|
@@ -2952,7 +3010,7 @@ var PollarClient = class {
|
|
|
2952
3010
|
const { data, error } = await this._api.POST("/tx/build-sign-submit", {
|
|
2953
3011
|
body: {
|
|
2954
3012
|
network: this.getNetwork(),
|
|
2955
|
-
|
|
3013
|
+
address: this._session.wallet.address,
|
|
2956
3014
|
operation,
|
|
2957
3015
|
params,
|
|
2958
3016
|
options: options ?? {}
|
|
@@ -2996,6 +3054,113 @@ var PollarClient = class {
|
|
|
2996
3054
|
async runTx(operation, params, options) {
|
|
2997
3055
|
return this.buildAndSignAndSubmitTx(operation, params, options);
|
|
2998
3056
|
}
|
|
3057
|
+
/**
|
|
3058
|
+
* Smart-wallet (passkey / C-address) transaction: build (server prepares the
|
|
3059
|
+
* SAC transfer + returns the auth digest) → sign the digest with the passkey
|
|
3060
|
+
* → submit (server assembles the signed auth entry and broadcasts; the
|
|
3061
|
+
* sponsor pays the fee). State machine: building → built → signing →
|
|
3062
|
+
* submitting → success.
|
|
3063
|
+
*/
|
|
3064
|
+
async _runSmartTx(operation, params, options) {
|
|
3065
|
+
const address = this._session?.wallet?.address;
|
|
3066
|
+
if (!address) {
|
|
3067
|
+
this._setTransactionState({ step: "error", phase: "building", details: "No wallet connected" });
|
|
3068
|
+
return { status: "error", details: "No wallet connected" };
|
|
3069
|
+
}
|
|
3070
|
+
if (!this._passkeySign) {
|
|
3071
|
+
const details = "Passkey signer not configured";
|
|
3072
|
+
this._setTransactionState({ step: "error", phase: "signing", details });
|
|
3073
|
+
return { status: "error", details };
|
|
3074
|
+
}
|
|
3075
|
+
this._setTransactionState({ step: "building" });
|
|
3076
|
+
let buildData;
|
|
3077
|
+
try {
|
|
3078
|
+
const body = {
|
|
3079
|
+
network: this.getNetwork(),
|
|
3080
|
+
address,
|
|
3081
|
+
operation,
|
|
3082
|
+
params,
|
|
3083
|
+
options: options ?? {}
|
|
3084
|
+
};
|
|
3085
|
+
const { data, error } = await this._api.POST("/tx/build", { body });
|
|
3086
|
+
if (error || !data?.success || !data.content?.smart) {
|
|
3087
|
+
const details = error?.details ?? "Failed to build transaction";
|
|
3088
|
+
this._setTransactionState({ step: "error", phase: "building", details });
|
|
3089
|
+
return { status: "error", details };
|
|
3090
|
+
}
|
|
3091
|
+
buildData = data.content;
|
|
3092
|
+
} catch (err) {
|
|
3093
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
3094
|
+
this._setTransactionState({ step: "error", phase: "building", ...details && { details } });
|
|
3095
|
+
return { status: "error", ...details && { details } };
|
|
3096
|
+
}
|
|
3097
|
+
this._setTransactionState({ step: "built", buildData });
|
|
3098
|
+
return this._signSubmitSmart(buildData);
|
|
3099
|
+
}
|
|
3100
|
+
/**
|
|
3101
|
+
* Steps 2–3 of the smart-wallet flow: sign the prepared auth digest with the
|
|
3102
|
+
* passkey, then submit. Shared by `_runSmartTx` (atomic) and `signAndSubmitTx`
|
|
3103
|
+
* (split flow, when a smart build is already on the state machine).
|
|
3104
|
+
*/
|
|
3105
|
+
async _signSubmitSmart(buildData) {
|
|
3106
|
+
const address = this._session?.wallet?.address;
|
|
3107
|
+
const smart = buildData.smart;
|
|
3108
|
+
if (!address || !smart) {
|
|
3109
|
+
const details = "no prepared smart transaction";
|
|
3110
|
+
this._setTransactionState({ step: "error", phase: "signing", buildData, details });
|
|
3111
|
+
return { status: "error", buildData, details };
|
|
3112
|
+
}
|
|
3113
|
+
if (!this._passkeySign) {
|
|
3114
|
+
const details = "Passkey signer not configured";
|
|
3115
|
+
this._setTransactionState({ step: "error", phase: "signing", buildData, details });
|
|
3116
|
+
return { status: "error", buildData, details };
|
|
3117
|
+
}
|
|
3118
|
+
this._setTransactionState({ step: "signing", buildData });
|
|
3119
|
+
let assertion;
|
|
3120
|
+
try {
|
|
3121
|
+
assertion = await this._passkeySign({ credentialId: smart.credentialId, challenge: smart.digest });
|
|
3122
|
+
} catch (err) {
|
|
3123
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
3124
|
+
this._setTransactionState({ step: "error", phase: "signing", buildData, ...details && { details } });
|
|
3125
|
+
return { status: "error", buildData, ...details && { details } };
|
|
3126
|
+
}
|
|
3127
|
+
this._setTransactionState({ step: "submitting", buildData });
|
|
3128
|
+
const outcomeExtra = { buildData };
|
|
3129
|
+
try {
|
|
3130
|
+
const { data, error } = await this._api.POST("/tx/submit", {
|
|
3131
|
+
body: {
|
|
3132
|
+
network: this.getNetwork(),
|
|
3133
|
+
address,
|
|
3134
|
+
smart: { entryXdr: smart.entryXdr, funcXdr: smart.funcXdr, assertion }
|
|
3135
|
+
}
|
|
3136
|
+
});
|
|
3137
|
+
if (!error && data?.success && data.content) {
|
|
3138
|
+
const { hash, status: backendStatus, resultCode } = data.content;
|
|
3139
|
+
if (backendStatus === "SUCCESS") {
|
|
3140
|
+
this._setTransactionState({ step: "success", hash, buildData });
|
|
3141
|
+
return { status: "success", hash, ...outcomeExtra };
|
|
3142
|
+
}
|
|
3143
|
+
if (backendStatus === "PENDING") {
|
|
3144
|
+
this._setTransactionState({ step: "submitted", hash, buildData });
|
|
3145
|
+
return { status: "pending", hash, ...outcomeExtra };
|
|
3146
|
+
}
|
|
3147
|
+
this._setTransactionState({
|
|
3148
|
+
step: "error",
|
|
3149
|
+
phase: "submitting",
|
|
3150
|
+
buildData,
|
|
3151
|
+
...resultCode && { details: resultCode }
|
|
3152
|
+
});
|
|
3153
|
+
return { status: "error", hash, ...outcomeExtra, ...resultCode && { details: resultCode, resultCode } };
|
|
3154
|
+
}
|
|
3155
|
+
const details = error?.details;
|
|
3156
|
+
this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
|
|
3157
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
3158
|
+
} catch (err) {
|
|
3159
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
3160
|
+
this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
|
|
3161
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
2999
3164
|
// ─── App config ───────────────────────────────────────────────────────────
|
|
3000
3165
|
async getAppConfig() {
|
|
3001
3166
|
try {
|
|
@@ -3049,10 +3214,18 @@ var PollarClient = class {
|
|
|
3049
3214
|
this._txHistoryState = next;
|
|
3050
3215
|
for (const cb of this._txHistoryStateListeners) cb(next);
|
|
3051
3216
|
}
|
|
3217
|
+
_setSessionsState(next) {
|
|
3218
|
+
this._sessionsState = next;
|
|
3219
|
+
for (const cb of this._sessionsStateListeners) cb(next);
|
|
3220
|
+
}
|
|
3052
3221
|
_setWalletBalanceState(next) {
|
|
3053
3222
|
this._walletBalanceState = next;
|
|
3054
3223
|
for (const cb of this._walletBalanceStateListeners) cb(next);
|
|
3055
3224
|
}
|
|
3225
|
+
_setEnabledAssetsState(next) {
|
|
3226
|
+
this._enabledAssetsState = next;
|
|
3227
|
+
for (const cb of this._enabledAssetsStateListeners) cb(next);
|
|
3228
|
+
}
|
|
3056
3229
|
// ─── Private ──────────────────────────────────────────────────────────────
|
|
3057
3230
|
_newController() {
|
|
3058
3231
|
this._loginController?.abort();
|
|
@@ -3077,6 +3250,7 @@ var PollarClient = class {
|
|
|
3077
3250
|
this._walletAdapter = adapter;
|
|
3078
3251
|
await writeWalletType(this._storage, this.apiKeyHash, id);
|
|
3079
3252
|
},
|
|
3253
|
+
...this._passkey ? { passkey: this._passkey } : {},
|
|
3080
3254
|
...this._deviceLabel ? { deviceLabel: this._deviceLabel } : {}
|
|
3081
3255
|
};
|
|
3082
3256
|
}
|
|
@@ -3106,7 +3280,7 @@ var PollarClient = class {
|
|
|
3106
3280
|
}
|
|
3107
3281
|
}
|
|
3108
3282
|
if (id === "freighter" /* FREIGHTER */) return new FreighterAdapter();
|
|
3109
|
-
if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter();
|
|
3283
|
+
if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter(this.getNetwork() === "mainnet" ? "public" : "testnet");
|
|
3110
3284
|
throw new Error(
|
|
3111
3285
|
`[PollarClient] No wallet adapter configured for "${id}". Pass a walletAdapter resolver in PollarClientConfig.`
|
|
3112
3286
|
);
|
|
@@ -3147,21 +3321,66 @@ var PollarClient = class {
|
|
|
3147
3321
|
}
|
|
3148
3322
|
}
|
|
3149
3323
|
console.info("[PollarClient] Session restored from storage");
|
|
3150
|
-
this._setAuthState({ step: "authenticated", session: this._session });
|
|
3324
|
+
this._setAuthState({ step: "authenticated", session: this._session, verified: false });
|
|
3151
3325
|
this._scheduleNextRefresh();
|
|
3326
|
+
void this._resume();
|
|
3152
3327
|
} else {
|
|
3153
3328
|
console.info("[PollarClient] No session in storage");
|
|
3154
3329
|
}
|
|
3155
3330
|
}
|
|
3331
|
+
/**
|
|
3332
|
+
* Validate the restored session against the server and repopulate the
|
|
3333
|
+
* in-memory profile (PII is never persisted, so it's null after a cold
|
|
3334
|
+
* reload). Goes through the normal authed client, so it coalesces with any
|
|
3335
|
+
* in-flight refresh (onRequest awaits `_refreshPromise`) and, being a GET,
|
|
3336
|
+
* is auto-retried after a 401-triggered refresh.
|
|
3337
|
+
*
|
|
3338
|
+
* - 200 → store profile, mark the session `verified`.
|
|
3339
|
+
* - 401 → the refresh-on-401 path already ran; if the family was
|
|
3340
|
+
* revoked, refresh failed and `_clearSession()` took us to
|
|
3341
|
+
* idle. Nothing to do here — don't double-handle.
|
|
3342
|
+
* - network error → stay optimistic (do NOT log out); revalidated later on
|
|
3343
|
+
* `visibilitychange` or first use.
|
|
3344
|
+
*/
|
|
3345
|
+
async _resume() {
|
|
3346
|
+
if (!this._session) return;
|
|
3347
|
+
this._resumeController?.abort();
|
|
3348
|
+
const controller = new AbortController();
|
|
3349
|
+
this._resumeController = controller;
|
|
3350
|
+
try {
|
|
3351
|
+
const { data, error } = await this._api.GET("/auth/session/resume", { signal: controller.signal });
|
|
3352
|
+
if (error || !data) return;
|
|
3353
|
+
const content = data.content;
|
|
3354
|
+
if (!content || !this._session) return;
|
|
3355
|
+
this._profile = { ...content };
|
|
3356
|
+
this._setAuthState({ step: "authenticated", session: this._session, verified: true });
|
|
3357
|
+
} catch (err) {
|
|
3358
|
+
if (err?.name === "AbortError") return;
|
|
3359
|
+
console.warn("[PollarClient] resume failed (network); will retry", err);
|
|
3360
|
+
} finally {
|
|
3361
|
+
if (this._resumeController === controller) this._resumeController = null;
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3156
3364
|
async _storeSession(session) {
|
|
3157
3365
|
console.info("[PollarClient] Session stored");
|
|
3366
|
+
const w = session.wallet;
|
|
3158
3367
|
const persisted = {
|
|
3159
3368
|
clientSessionId: session.clientSessionId,
|
|
3160
3369
|
userId: session.userId ?? null,
|
|
3161
3370
|
status: session.status,
|
|
3162
3371
|
token: session.token,
|
|
3163
3372
|
user: session.user,
|
|
3164
|
-
|
|
3373
|
+
// The wire response still carries the legacy `publicKey` alias (kept for
|
|
3374
|
+
// older SDKs); the persisted session standardizes on `address` only.
|
|
3375
|
+
wallet: {
|
|
3376
|
+
type: w.type,
|
|
3377
|
+
address: w.address ?? w.publicKey ?? null,
|
|
3378
|
+
...w.existsOnStellar !== void 0 ? { existsOnStellar: w.existsOnStellar } : {},
|
|
3379
|
+
...w.createdAt !== void 0 ? { createdAt: w.createdAt } : {},
|
|
3380
|
+
...w.linkedAt !== void 0 ? { linkedAt: w.linkedAt } : {},
|
|
3381
|
+
...w.network !== void 0 ? { network: w.network } : {},
|
|
3382
|
+
...w.deployTxHash !== void 0 ? { deployTxHash: w.deployTxHash } : {}
|
|
3383
|
+
}
|
|
3165
3384
|
};
|
|
3166
3385
|
this._session = persisted;
|
|
3167
3386
|
if (session.data) {
|
|
@@ -3174,7 +3393,7 @@ var PollarClient = class {
|
|
|
3174
3393
|
};
|
|
3175
3394
|
}
|
|
3176
3395
|
await writeStorage(this._storage, this.apiKeyHash, persisted);
|
|
3177
|
-
this._setAuthState({ step: "authenticated", session: persisted });
|
|
3396
|
+
this._setAuthState({ step: "authenticated", session: persisted, verified: true });
|
|
3178
3397
|
this._scheduleNextRefresh();
|
|
3179
3398
|
}
|
|
3180
3399
|
async _clearSession() {
|
|
@@ -3226,6 +3445,196 @@ var PollarClient = class {
|
|
|
3226
3445
|
}
|
|
3227
3446
|
};
|
|
3228
3447
|
|
|
3448
|
+
// src/keys/web-crypto.ts
|
|
3449
|
+
var DB_NAME = "pollar-keys";
|
|
3450
|
+
var DB_VERSION = 1;
|
|
3451
|
+
var STORE_NAME = "keys";
|
|
3452
|
+
function openDb() {
|
|
3453
|
+
return new Promise((resolve, reject) => {
|
|
3454
|
+
if (typeof indexedDB === "undefined") {
|
|
3455
|
+
reject(new Error("[PollarClient:keys] IndexedDB not available"));
|
|
3456
|
+
return;
|
|
3457
|
+
}
|
|
3458
|
+
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
3459
|
+
req.onerror = () => reject(req.error ?? new Error("[PollarClient:keys] IDB open failed"));
|
|
3460
|
+
req.onsuccess = () => resolve(req.result);
|
|
3461
|
+
req.onupgradeneeded = () => {
|
|
3462
|
+
const db = req.result;
|
|
3463
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
3464
|
+
db.createObjectStore(STORE_NAME);
|
|
3465
|
+
}
|
|
3466
|
+
};
|
|
3467
|
+
});
|
|
3468
|
+
}
|
|
3469
|
+
function awaitTx(req) {
|
|
3470
|
+
return new Promise((resolve, reject) => {
|
|
3471
|
+
req.onsuccess = () => resolve(req.result);
|
|
3472
|
+
req.onerror = () => reject(req.error ?? new Error("[PollarClient:keys] IDB request failed"));
|
|
3473
|
+
});
|
|
3474
|
+
}
|
|
3475
|
+
async function dbGet(key) {
|
|
3476
|
+
const db = await openDb();
|
|
3477
|
+
try {
|
|
3478
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
3479
|
+
const result = await awaitTx(tx.objectStore(STORE_NAME).get(key));
|
|
3480
|
+
return result;
|
|
3481
|
+
} finally {
|
|
3482
|
+
db.close();
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
async function dbPut(key, value) {
|
|
3486
|
+
const db = await openDb();
|
|
3487
|
+
try {
|
|
3488
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
3489
|
+
await awaitTx(tx.objectStore(STORE_NAME).put(value, key));
|
|
3490
|
+
} finally {
|
|
3491
|
+
db.close();
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
async function dbDelete(key) {
|
|
3495
|
+
const db = await openDb();
|
|
3496
|
+
try {
|
|
3497
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
3498
|
+
await awaitTx(tx.objectStore(STORE_NAME).delete(key));
|
|
3499
|
+
} finally {
|
|
3500
|
+
db.close();
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
function isCryptoKeyPair(v) {
|
|
3504
|
+
if (typeof v !== "object" || v === null) return false;
|
|
3505
|
+
const obj = v;
|
|
3506
|
+
return obj.privateKey !== void 0 && obj.publicKey !== void 0;
|
|
3507
|
+
}
|
|
3508
|
+
var WebCryptoKeyManager = class {
|
|
3509
|
+
constructor(apiKey) {
|
|
3510
|
+
this.apiKeyHash = null;
|
|
3511
|
+
this.keyPair = null;
|
|
3512
|
+
this.publicJwk = null;
|
|
3513
|
+
this.thumbprint = null;
|
|
3514
|
+
/**
|
|
3515
|
+
* Cached in-flight init. Lets `init()` be called concurrently (or implicitly
|
|
3516
|
+
* from `getPublicJwk` / `sign`) without doing the work twice. Cleared on
|
|
3517
|
+
* failure so callers can retry, and cleared on `reset()`.
|
|
3518
|
+
*/
|
|
3519
|
+
this._initPromise = null;
|
|
3520
|
+
if (typeof globalThis.crypto === "undefined" || !globalThis.crypto.subtle) {
|
|
3521
|
+
throw new Error("[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost).");
|
|
3522
|
+
}
|
|
3523
|
+
this.apiKey = apiKey;
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3526
|
+
* Idempotent and safe under concurrency. The first call kicks off the real
|
|
3527
|
+
* init; subsequent (and concurrent) calls return the same in-flight promise.
|
|
3528
|
+
* Other methods (`getPublicJwk`, `getThumbprint`, `sign`) auto-await this so
|
|
3529
|
+
* the manager is self-healing if `init()` was never explicitly invoked.
|
|
3530
|
+
*/
|
|
3531
|
+
async init() {
|
|
3532
|
+
if (this.keyPair) return;
|
|
3533
|
+
if (!this._initPromise) {
|
|
3534
|
+
this._initPromise = this._doInit().catch((err) => {
|
|
3535
|
+
console.error("[PollarClient:keys] WebCryptoKeyManager init failed", err);
|
|
3536
|
+
this._initPromise = null;
|
|
3537
|
+
throw err;
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
return this._initPromise;
|
|
3541
|
+
}
|
|
3542
|
+
async _doInit() {
|
|
3543
|
+
if (!this.apiKeyHash) {
|
|
3544
|
+
this.apiKeyHash = await hashApiKey(this.apiKey);
|
|
3545
|
+
}
|
|
3546
|
+
let pair;
|
|
3547
|
+
try {
|
|
3548
|
+
pair = await dbGet(this.apiKeyHash);
|
|
3549
|
+
if (pair && !isCryptoKeyPair(pair)) pair = void 0;
|
|
3550
|
+
} catch {
|
|
3551
|
+
pair = void 0;
|
|
3552
|
+
}
|
|
3553
|
+
if (!pair) {
|
|
3554
|
+
pair = await globalThis.crypto.subtle.generateKey(
|
|
3555
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
3556
|
+
// false → private key non-extractable; per W3C ECDSA spec the public
|
|
3557
|
+
// key is always extractable regardless of this flag.
|
|
3558
|
+
false,
|
|
3559
|
+
["sign", "verify"]
|
|
3560
|
+
);
|
|
3561
|
+
try {
|
|
3562
|
+
await dbPut(this.apiKeyHash, pair);
|
|
3563
|
+
} catch {
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
this.keyPair = pair;
|
|
3567
|
+
this.publicJwk = await this._exportPublicJwk(pair.publicKey);
|
|
3568
|
+
this.thumbprint = await computeJwkThumbprint(this.publicJwk);
|
|
3569
|
+
}
|
|
3570
|
+
/**
|
|
3571
|
+
* Derive the public JWK from a `CryptoKey`. Prefers the `'raw'` export (the
|
|
3572
|
+
* 65-byte uncompressed point `0x04 || X(32) || Y(32)`) and base64url-encodes
|
|
3573
|
+
* the coordinates ourselves — that sidesteps polyfills whose `exportKey('jwk')`
|
|
3574
|
+
* emits non-base64url `x`/`y` (standard base64, `=` padding, or — as seen with
|
|
3575
|
+
* `react-native-quick-crypto` — a stray `.`). Real browsers and most polyfills
|
|
3576
|
+
* support `'raw'` for public EC keys.
|
|
3577
|
+
*
|
|
3578
|
+
* Falls back to the `'jwk'` export (normalized via `canonicalEcJwk`) if `'raw'`
|
|
3579
|
+
* is unsupported or returns an unexpected shape, so this can't regress on a
|
|
3580
|
+
* runtime that only implements the JWK path. Both routes yield identical
|
|
3581
|
+
* coordinate bytes, so the `cnf.jkt` thumbprint is unchanged either way.
|
|
3582
|
+
*/
|
|
3583
|
+
async _exportPublicJwk(publicKey) {
|
|
3584
|
+
try {
|
|
3585
|
+
const raw = new Uint8Array(await globalThis.crypto.subtle.exportKey("raw", publicKey));
|
|
3586
|
+
if (raw.length !== 65 || raw[0] !== 4) {
|
|
3587
|
+
throw new Error(`[PollarClient:keys] Unexpected raw EC point (len=${raw.length}, tag=${raw[0]})`);
|
|
3588
|
+
}
|
|
3589
|
+
return {
|
|
3590
|
+
kty: "EC",
|
|
3591
|
+
crv: "P-256",
|
|
3592
|
+
x: base64urlEncode(raw.slice(1, 33)),
|
|
3593
|
+
y: base64urlEncode(raw.slice(33, 65))
|
|
3594
|
+
};
|
|
3595
|
+
} catch {
|
|
3596
|
+
const jwk = await globalThis.crypto.subtle.exportKey("jwk", publicKey);
|
|
3597
|
+
return canonicalEcJwk(jwk);
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
async reset() {
|
|
3601
|
+
try {
|
|
3602
|
+
if (this.apiKeyHash) await dbDelete(this.apiKeyHash);
|
|
3603
|
+
} catch {
|
|
3604
|
+
}
|
|
3605
|
+
this.keyPair = null;
|
|
3606
|
+
this.publicJwk = null;
|
|
3607
|
+
this.thumbprint = null;
|
|
3608
|
+
this._initPromise = null;
|
|
3609
|
+
}
|
|
3610
|
+
async getPublicJwk() {
|
|
3611
|
+
if (!this.publicJwk) await this.init();
|
|
3612
|
+
if (!this.publicJwk) {
|
|
3613
|
+
throw new Error("[PollarClient:keys] Keypair initialization failed; getPublicJwk unavailable");
|
|
3614
|
+
}
|
|
3615
|
+
return { kty: this.publicJwk.kty, crv: this.publicJwk.crv, x: this.publicJwk.x, y: this.publicJwk.y };
|
|
3616
|
+
}
|
|
3617
|
+
async getThumbprint() {
|
|
3618
|
+
if (!this.thumbprint) await this.init();
|
|
3619
|
+
if (!this.thumbprint) {
|
|
3620
|
+
throw new Error("[PollarClient:keys] Keypair initialization failed; getThumbprint unavailable");
|
|
3621
|
+
}
|
|
3622
|
+
return this.thumbprint;
|
|
3623
|
+
}
|
|
3624
|
+
async sign(payload) {
|
|
3625
|
+
if (!this.keyPair) await this.init();
|
|
3626
|
+
if (!this.keyPair) {
|
|
3627
|
+
throw new Error("[PollarClient:keys] Keypair initialization failed; sign unavailable");
|
|
3628
|
+
}
|
|
3629
|
+
const sig = await globalThis.crypto.subtle.sign(
|
|
3630
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
3631
|
+
this.keyPair.privateKey,
|
|
3632
|
+
payload
|
|
3633
|
+
);
|
|
3634
|
+
return new Uint8Array(sig);
|
|
3635
|
+
}
|
|
3636
|
+
};
|
|
3637
|
+
|
|
3229
3638
|
// src/stellar/StellarClient.ts
|
|
3230
3639
|
var HORIZON_URLS = {
|
|
3231
3640
|
mainnet: "https://horizon.stellar.org",
|
|
@@ -3256,10 +3665,6 @@ var StellarClient = class {
|
|
|
3256
3665
|
|
|
3257
3666
|
// src/index.rn.ts
|
|
3258
3667
|
_setDefaultKeyManagerFactory((storage, apiKey) => {
|
|
3259
|
-
const subtle = globalThis.crypto?.subtle;
|
|
3260
|
-
if (subtle && typeof subtle.generateKey === "function" && typeof subtle.sign === "function") {
|
|
3261
|
-
return new WebCryptoKeyManager(apiKey);
|
|
3262
|
-
}
|
|
3263
3668
|
return new NobleKeyManager(storage, apiKey);
|
|
3264
3669
|
});
|
|
3265
3670
|
|
|
@@ -3267,6 +3672,7 @@ exports.AUTH_ERROR_CODES = AUTH_ERROR_CODES;
|
|
|
3267
3672
|
exports.AlbedoAdapter = AlbedoAdapter;
|
|
3268
3673
|
exports.FreighterAdapter = FreighterAdapter;
|
|
3269
3674
|
exports.NobleKeyManager = NobleKeyManager;
|
|
3675
|
+
exports.POLLAR_CORE_VERSION = POLLAR_CORE_VERSION;
|
|
3270
3676
|
exports.PollarClient = PollarClient;
|
|
3271
3677
|
exports.StellarClient = StellarClient;
|
|
3272
3678
|
exports.WalletType = WalletType;
|