@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.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var sha2 = require('@noble/hashes/sha2');
|
|
4
|
+
|
|
3
5
|
var __create = Object.create;
|
|
4
6
|
var __defProp = Object.defineProperty;
|
|
5
7
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -185,20 +187,6 @@ function defaultKeyManager(storage, apiKey) {
|
|
|
185
187
|
return _factory(storage, apiKey);
|
|
186
188
|
}
|
|
187
189
|
|
|
188
|
-
// src/lib/sha256.ts
|
|
189
|
-
async function sha256(data) {
|
|
190
|
-
const buf = await crypto.subtle.digest("SHA-256", data);
|
|
191
|
-
return new Uint8Array(buf);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// src/lib/api-key-hash.ts
|
|
195
|
-
async function hashApiKey(apiKey) {
|
|
196
|
-
const digest = await sha256(new TextEncoder().encode(apiKey));
|
|
197
|
-
let hex = "";
|
|
198
|
-
for (let i = 0; i < 4; i++) hex += digest[i].toString(16).padStart(2, "0");
|
|
199
|
-
return hex;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
190
|
// src/lib/base64url.ts
|
|
203
191
|
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
204
192
|
(() => {
|
|
@@ -235,6 +223,17 @@ function base64urlEncode(bytes) {
|
|
|
235
223
|
function base64urlEncodeString(s) {
|
|
236
224
|
return base64urlEncode(new TextEncoder().encode(s));
|
|
237
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
|
+
}
|
|
238
237
|
|
|
239
238
|
// src/keys/thumbprint.ts
|
|
240
239
|
async function computeJwkThumbprint(jwk) {
|
|
@@ -245,11 +244,14 @@ async function computeJwkThumbprint(jwk) {
|
|
|
245
244
|
const digest = await sha256(new TextEncoder().encode(canonical));
|
|
246
245
|
return base64urlEncode(digest);
|
|
247
246
|
}
|
|
247
|
+
function toBase64url(value) {
|
|
248
|
+
return value.replace(/\+/g, "-").replace(/\//g, "_").replace(/[^A-Za-z0-9_-]/g, "");
|
|
249
|
+
}
|
|
248
250
|
function canonicalEcJwk(jwk) {
|
|
249
251
|
if (jwk.kty !== "EC" || jwk.crv !== "P-256" || typeof jwk.x !== "string" || typeof jwk.y !== "string") {
|
|
250
252
|
throw new Error("[PollarClient:thumbprint] Source JWK is not an EC P-256 public key");
|
|
251
253
|
}
|
|
252
|
-
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) };
|
|
253
255
|
}
|
|
254
256
|
|
|
255
257
|
// src/keys/web-crypto.ts
|
|
@@ -263,7 +265,7 @@ function openDb() {
|
|
|
263
265
|
return;
|
|
264
266
|
}
|
|
265
267
|
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
266
|
-
req.onerror = () => reject(req.error ?? new Error("IDB open failed"));
|
|
268
|
+
req.onerror = () => reject(req.error ?? new Error("[PollarClient:keys] IDB open failed"));
|
|
267
269
|
req.onsuccess = () => resolve(req.result);
|
|
268
270
|
req.onupgradeneeded = () => {
|
|
269
271
|
const db = req.result;
|
|
@@ -276,7 +278,7 @@ function openDb() {
|
|
|
276
278
|
function awaitTx(req) {
|
|
277
279
|
return new Promise((resolve, reject) => {
|
|
278
280
|
req.onsuccess = () => resolve(req.result);
|
|
279
|
-
req.onerror = () => reject(req.error ?? new Error("IDB request failed"));
|
|
281
|
+
req.onerror = () => reject(req.error ?? new Error("[PollarClient:keys] IDB request failed"));
|
|
280
282
|
});
|
|
281
283
|
}
|
|
282
284
|
async function dbGet(key) {
|
|
@@ -371,10 +373,39 @@ var WebCryptoKeyManager = class {
|
|
|
371
373
|
}
|
|
372
374
|
}
|
|
373
375
|
this.keyPair = pair;
|
|
374
|
-
|
|
375
|
-
this.publicJwk = canonicalEcJwk(exported);
|
|
376
|
+
this.publicJwk = await this._exportPublicJwk(pair.publicKey);
|
|
376
377
|
this.thumbprint = await computeJwkThumbprint(this.publicJwk);
|
|
377
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Derive the public JWK from a `CryptoKey`. Prefers the `'raw'` export (the
|
|
381
|
+
* 65-byte uncompressed point `0x04 || X(32) || Y(32)`) and base64url-encodes
|
|
382
|
+
* the coordinates ourselves — that sidesteps polyfills whose `exportKey('jwk')`
|
|
383
|
+
* emits non-base64url `x`/`y` (standard base64, `=` padding, or — as seen with
|
|
384
|
+
* `react-native-quick-crypto` — a stray `.`). Real browsers and most polyfills
|
|
385
|
+
* support `'raw'` for public EC keys.
|
|
386
|
+
*
|
|
387
|
+
* Falls back to the `'jwk'` export (normalized via `canonicalEcJwk`) if `'raw'`
|
|
388
|
+
* is unsupported or returns an unexpected shape, so this can't regress on a
|
|
389
|
+
* runtime that only implements the JWK path. Both routes yield identical
|
|
390
|
+
* coordinate bytes, so the `cnf.jkt` thumbprint is unchanged either way.
|
|
391
|
+
*/
|
|
392
|
+
async _exportPublicJwk(publicKey) {
|
|
393
|
+
try {
|
|
394
|
+
const raw = new Uint8Array(await globalThis.crypto.subtle.exportKey("raw", publicKey));
|
|
395
|
+
if (raw.length !== 65 || raw[0] !== 4) {
|
|
396
|
+
throw new Error(`[PollarClient:keys] Unexpected raw EC point (len=${raw.length}, tag=${raw[0]})`);
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
kty: "EC",
|
|
400
|
+
crv: "P-256",
|
|
401
|
+
x: base64urlEncode(raw.slice(1, 33)),
|
|
402
|
+
y: base64urlEncode(raw.slice(33, 65))
|
|
403
|
+
};
|
|
404
|
+
} catch {
|
|
405
|
+
const jwk = await globalThis.crypto.subtle.exportKey("jwk", publicKey);
|
|
406
|
+
return canonicalEcJwk(jwk);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
378
409
|
async reset() {
|
|
379
410
|
try {
|
|
380
411
|
if (this.apiKeyHash) await dbDelete(this.apiKeyHash);
|
|
@@ -1132,6 +1163,9 @@ function defaultStorage(options = {}) {
|
|
|
1132
1163
|
return createLocalStorageAdapter(options);
|
|
1133
1164
|
}
|
|
1134
1165
|
|
|
1166
|
+
// src/version.ts
|
|
1167
|
+
var POLLAR_CORE_VERSION = "0.9.0-rc.0" ;
|
|
1168
|
+
|
|
1135
1169
|
// src/visibility/noop.ts
|
|
1136
1170
|
function createNoopVisibilityProvider() {
|
|
1137
1171
|
return {
|
|
@@ -1196,6 +1230,7 @@ var AUTH_ERROR_CODES = {
|
|
|
1196
1230
|
WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
|
|
1197
1231
|
WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
|
|
1198
1232
|
WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
|
|
1233
|
+
PASSKEY_FAILED: "PASSKEY_FAILED",
|
|
1199
1234
|
UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
|
|
1200
1235
|
};
|
|
1201
1236
|
var PollarFlowError = class extends Error {
|
|
@@ -1241,7 +1276,7 @@ var FreighterAdapter = class {
|
|
|
1241
1276
|
if (!userInfo?.publicKey) {
|
|
1242
1277
|
throw new Error("Failed to get user information from Freighter");
|
|
1243
1278
|
}
|
|
1244
|
-
return { address: userInfo.publicKey
|
|
1279
|
+
return { address: userInfo.publicKey };
|
|
1245
1280
|
}
|
|
1246
1281
|
async disconnect() {
|
|
1247
1282
|
}
|
|
@@ -1279,6 +1314,19 @@ var FreighterAdapter = class {
|
|
|
1279
1314
|
};
|
|
1280
1315
|
|
|
1281
1316
|
// src/wallets/AlbedoAdapter.ts
|
|
1317
|
+
var PUBLIC_PASSPHRASE = "Public Global Stellar Network ; September 2015";
|
|
1318
|
+
var TESTNET_PASSPHRASE = "Test SDF Network ; September 2015";
|
|
1319
|
+
function albedoNetwork(options, fallback) {
|
|
1320
|
+
switch (options?.networkPassphrase) {
|
|
1321
|
+
case PUBLIC_PASSPHRASE:
|
|
1322
|
+
return "public";
|
|
1323
|
+
case TESTNET_PASSPHRASE:
|
|
1324
|
+
return "testnet";
|
|
1325
|
+
}
|
|
1326
|
+
if (options?.network === "public" || options?.network === "mainnet") return "public";
|
|
1327
|
+
if (options?.network === "testnet") return "testnet";
|
|
1328
|
+
return fallback;
|
|
1329
|
+
}
|
|
1282
1330
|
function openAlbedoPopup(url) {
|
|
1283
1331
|
const popup = window.open(url, "albedo", "width=420,height=720,resizable=yes,scrollbars=yes");
|
|
1284
1332
|
if (!popup) {
|
|
@@ -1317,7 +1365,13 @@ function waitForAlbedoResult() {
|
|
|
1317
1365
|
});
|
|
1318
1366
|
}
|
|
1319
1367
|
var AlbedoAdapter = class {
|
|
1320
|
-
|
|
1368
|
+
/**
|
|
1369
|
+
* Network used for `connect` and `signAuthEntry` (which carry no per-call
|
|
1370
|
+
* network) and as the fallback for `signTransaction`. Defaults to `'testnet'`
|
|
1371
|
+
* to preserve the previous behavior when constructed with no argument.
|
|
1372
|
+
*/
|
|
1373
|
+
constructor(network = "testnet") {
|
|
1374
|
+
this.network = network;
|
|
1321
1375
|
this.type = "albedo" /* ALBEDO */;
|
|
1322
1376
|
}
|
|
1323
1377
|
async isAvailable() {
|
|
@@ -1327,7 +1381,7 @@ var AlbedoAdapter = class {
|
|
|
1327
1381
|
const url = new URL("https://albedo.link");
|
|
1328
1382
|
url.searchParams.set("intent", "public-key");
|
|
1329
1383
|
url.searchParams.set("app_name", "Pollar");
|
|
1330
|
-
url.searchParams.set("network",
|
|
1384
|
+
url.searchParams.set("network", this.network);
|
|
1331
1385
|
url.searchParams.set("callback", `${window.location.origin}/albedo-callback`);
|
|
1332
1386
|
url.searchParams.set("origin", window.location.origin);
|
|
1333
1387
|
openAlbedoPopup(url.toString());
|
|
@@ -1335,7 +1389,7 @@ var AlbedoAdapter = class {
|
|
|
1335
1389
|
if (!result.pubkey) {
|
|
1336
1390
|
throw new Error("Albedo connection rejected");
|
|
1337
1391
|
}
|
|
1338
|
-
return { address: result.pubkey
|
|
1392
|
+
return { address: result.pubkey };
|
|
1339
1393
|
}
|
|
1340
1394
|
async disconnect() {
|
|
1341
1395
|
}
|
|
@@ -1345,12 +1399,12 @@ var AlbedoAdapter = class {
|
|
|
1345
1399
|
async getNetwork() {
|
|
1346
1400
|
throw new Error("Albedo does not expose network");
|
|
1347
1401
|
}
|
|
1348
|
-
async signTransaction(xdr,
|
|
1402
|
+
async signTransaction(xdr, options) {
|
|
1349
1403
|
const url = new URL("https://albedo.link");
|
|
1350
1404
|
url.searchParams.set("intent", "tx");
|
|
1351
1405
|
url.searchParams.set("xdr", xdr);
|
|
1352
1406
|
url.searchParams.set("app_name", "Pollar");
|
|
1353
|
-
url.searchParams.set("network",
|
|
1407
|
+
url.searchParams.set("network", albedoNetwork(options, this.network));
|
|
1354
1408
|
url.searchParams.set("callback", window.location.href);
|
|
1355
1409
|
url.searchParams.set("origin", window.location.origin);
|
|
1356
1410
|
window.location.href = url.toString();
|
|
@@ -1363,7 +1417,7 @@ var AlbedoAdapter = class {
|
|
|
1363
1417
|
url.searchParams.set("intent", "sign-auth-entry");
|
|
1364
1418
|
url.searchParams.set("xdr", entryXdr);
|
|
1365
1419
|
url.searchParams.set("app_name", "Pollar");
|
|
1366
|
-
url.searchParams.set("network",
|
|
1420
|
+
url.searchParams.set("network", this.network);
|
|
1367
1421
|
url.searchParams.set("callback", window.location.href);
|
|
1368
1422
|
url.searchParams.set("origin", window.location.origin);
|
|
1369
1423
|
window.location.href = url.toString();
|
|
@@ -1450,8 +1504,12 @@ function isValidSession(value) {
|
|
|
1450
1504
|
return false;
|
|
1451
1505
|
}
|
|
1452
1506
|
const w = wallet;
|
|
1453
|
-
if (w["
|
|
1454
|
-
console.warn("[PollarClient:session] Invalid session \u2014 wallet.
|
|
1507
|
+
if (w["type"] !== "custodial" && w["type"] !== "smart" && w["type"] !== "external") {
|
|
1508
|
+
console.warn("[PollarClient:session] Invalid session \u2014 wallet.type must be custodial|smart|external");
|
|
1509
|
+
return false;
|
|
1510
|
+
}
|
|
1511
|
+
if (w["address"] !== null && !isBoundedString(w["address"], MAX_WALLET_PUBLIC_KEY)) {
|
|
1512
|
+
console.warn("[PollarClient:session] Invalid session \u2014 wallet.address must be string|null");
|
|
1455
1513
|
return false;
|
|
1456
1514
|
}
|
|
1457
1515
|
if (w["existsOnStellar"] !== void 0 && typeof w["existsOnStellar"] !== "boolean") {
|
|
@@ -1462,6 +1520,10 @@ function isValidSession(value) {
|
|
|
1462
1520
|
console.warn("[PollarClient:session] Invalid session \u2014 wallet.createdAt must be a finite number if present");
|
|
1463
1521
|
return false;
|
|
1464
1522
|
}
|
|
1523
|
+
if (w["linkedAt"] !== void 0 && (typeof w["linkedAt"] !== "number" || !Number.isFinite(w["linkedAt"]))) {
|
|
1524
|
+
console.warn("[PollarClient:session] Invalid session \u2014 wallet.linkedAt must be a finite number if present");
|
|
1525
|
+
return false;
|
|
1526
|
+
}
|
|
1465
1527
|
return true;
|
|
1466
1528
|
}
|
|
1467
1529
|
async function readStorage(storage, apiKeyHash) {
|
|
@@ -1469,6 +1531,12 @@ async function readStorage(storage, apiKeyHash) {
|
|
|
1469
1531
|
if (!raw) return null;
|
|
1470
1532
|
try {
|
|
1471
1533
|
const session = JSON.parse(raw);
|
|
1534
|
+
if (typeof session === "object" && session !== null) {
|
|
1535
|
+
const w = session.wallet;
|
|
1536
|
+
if (w && w["address"] == null && typeof w["publicKey"] === "string") {
|
|
1537
|
+
w["address"] = w["publicKey"];
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1472
1540
|
if (!isValidSession(session)) {
|
|
1473
1541
|
await storage.remove(sessionStorageKey(apiKeyHash));
|
|
1474
1542
|
console.warn("[PollarClient:session] Stored session is invalid \u2014 clearing storage");
|
|
@@ -1559,7 +1627,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
|
|
|
1559
1627
|
}));
|
|
1560
1628
|
} catch (e) {
|
|
1561
1629
|
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
1562
|
-
console.warn(e);
|
|
1630
|
+
console.warn("[PollarClient:stream] session-status request failed; will retry", e);
|
|
1563
1631
|
}
|
|
1564
1632
|
if (error || !data) {
|
|
1565
1633
|
await sleep(backoff);
|
|
@@ -1599,7 +1667,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
|
|
|
1599
1667
|
} catch (e) {
|
|
1600
1668
|
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
1601
1669
|
if (e instanceof SessionStatusError) throw e;
|
|
1602
|
-
console.warn(e);
|
|
1670
|
+
console.warn("[PollarClient:stream] session-status stream read failed; will retry", e);
|
|
1603
1671
|
} finally {
|
|
1604
1672
|
reader.releaseLock();
|
|
1605
1673
|
}
|
|
@@ -1627,7 +1695,7 @@ async function pollUntilFound(baseUrl, clientSessionId, check, intervalMs = 500,
|
|
|
1627
1695
|
envelope = await response.json().catch(() => null);
|
|
1628
1696
|
} catch (e) {
|
|
1629
1697
|
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
1630
|
-
console.warn(e);
|
|
1698
|
+
console.warn("[PollarClient:stream] session-status poll failed; will retry", e);
|
|
1631
1699
|
}
|
|
1632
1700
|
if (httpStatus === 404 || envelope?.code === "INVALID_CLIENT_SESSION_ID") {
|
|
1633
1701
|
throw new SessionStatusError("INVALID_CLIENT_SESSION_ID");
|
|
@@ -1831,6 +1899,55 @@ async function loginOAuth(provider, deps) {
|
|
|
1831
1899
|
await authenticate(clientSessionId, deps);
|
|
1832
1900
|
}
|
|
1833
1901
|
|
|
1902
|
+
// src/client/auth/passkeyFlow.ts
|
|
1903
|
+
async function loginSmartWallet(deps) {
|
|
1904
|
+
const { api, signal, setAuthState, passkey } = deps;
|
|
1905
|
+
if (!passkey) {
|
|
1906
|
+
setAuthState({
|
|
1907
|
+
step: "error",
|
|
1908
|
+
previousStep: "creating_session",
|
|
1909
|
+
message: "Passkey support is not configured",
|
|
1910
|
+
errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
|
|
1911
|
+
});
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
const clientSessionId = await createAuthSession(deps);
|
|
1915
|
+
if (!clientSessionId) return;
|
|
1916
|
+
try {
|
|
1917
|
+
const { data: challengeData } = await api.POST("/auth/passkey/challenge", {
|
|
1918
|
+
body: { clientSessionId },
|
|
1919
|
+
signal
|
|
1920
|
+
});
|
|
1921
|
+
const challenge = challengeData?.content?.challenge;
|
|
1922
|
+
if (!challengeData?.success || !challenge) {
|
|
1923
|
+
return failPasskey(setAuthState, "Failed to start passkey");
|
|
1924
|
+
}
|
|
1925
|
+
setAuthState({ step: "creating_passkey" });
|
|
1926
|
+
const ceremony = await passkey({ challenge });
|
|
1927
|
+
const response = ceremony.response;
|
|
1928
|
+
if (ceremony.kind === "register") {
|
|
1929
|
+
setAuthState({ step: "deploying_smart_account" });
|
|
1930
|
+
const { data } = await api.POST("/auth/passkey/register", {
|
|
1931
|
+
body: { clientSessionId, response },
|
|
1932
|
+
signal
|
|
1933
|
+
});
|
|
1934
|
+
if (!data?.success) return failPasskey(setAuthState, "Passkey registration failed");
|
|
1935
|
+
} else {
|
|
1936
|
+
const { data } = await api.POST("/auth/passkey/login", {
|
|
1937
|
+
body: { clientSessionId, response },
|
|
1938
|
+
signal
|
|
1939
|
+
});
|
|
1940
|
+
if (!data?.success) return failPasskey(setAuthState, "Passkey authentication failed");
|
|
1941
|
+
}
|
|
1942
|
+
} catch {
|
|
1943
|
+
return failPasskey(setAuthState, "Passkey login failed");
|
|
1944
|
+
}
|
|
1945
|
+
await authenticate(clientSessionId, deps);
|
|
1946
|
+
}
|
|
1947
|
+
function failPasskey(setAuthState, message) {
|
|
1948
|
+
setAuthState({ step: "error", previousStep: "creating_passkey", message, errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED });
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1834
1951
|
// src/client/auth/walletFlow.ts
|
|
1835
1952
|
function withSignal(promise, signal) {
|
|
1836
1953
|
return Promise.race([
|
|
@@ -1857,12 +1974,12 @@ async function loginWallet(type, deps) {
|
|
|
1857
1974
|
setAuthState({ step: "wallet_not_installed", walletType: type });
|
|
1858
1975
|
return;
|
|
1859
1976
|
}
|
|
1860
|
-
const {
|
|
1861
|
-
connectedWallet =
|
|
1977
|
+
const { address } = await withSignal(adapter.connect(), signal);
|
|
1978
|
+
connectedWallet = address;
|
|
1862
1979
|
deps.storeWalletAdapter(adapter, type);
|
|
1863
1980
|
setAuthState({ step: "authenticating_wallet" });
|
|
1864
1981
|
const { data: walletData, error: walletError } = await api.POST("/auth/wallet", {
|
|
1865
|
-
body: { clientSessionId, walletAddress:
|
|
1982
|
+
body: { clientSessionId, walletAddress: address },
|
|
1866
1983
|
signal
|
|
1867
1984
|
});
|
|
1868
1985
|
if (walletError || !walletData?.success) {
|
|
@@ -1927,8 +2044,12 @@ var PollarClient = class {
|
|
|
1927
2044
|
this._transactionStateListeners = /* @__PURE__ */ new Set();
|
|
1928
2045
|
this._txHistoryState = { step: "idle" };
|
|
1929
2046
|
this._txHistoryStateListeners = /* @__PURE__ */ new Set();
|
|
2047
|
+
this._sessionsState = { step: "idle" };
|
|
2048
|
+
this._sessionsStateListeners = /* @__PURE__ */ new Set();
|
|
1930
2049
|
this._walletBalanceState = { step: "idle" };
|
|
1931
2050
|
this._walletBalanceStateListeners = /* @__PURE__ */ new Set();
|
|
2051
|
+
this._enabledAssetsState = { step: "idle" };
|
|
2052
|
+
this._enabledAssetsStateListeners = /* @__PURE__ */ new Set();
|
|
1932
2053
|
this._authState = { step: "idle" };
|
|
1933
2054
|
this._authStateListeners = /* @__PURE__ */ new Set();
|
|
1934
2055
|
this._networkState = { step: "idle" };
|
|
@@ -1944,6 +2065,8 @@ var PollarClient = class {
|
|
|
1944
2065
|
this._storageDegradeListeners = /* @__PURE__ */ new Set();
|
|
1945
2066
|
this._walletAdapter = null;
|
|
1946
2067
|
this._loginController = null;
|
|
2068
|
+
/** Aborts an in-flight `/auth/session/resume` on destroy() or re-trigger. */
|
|
2069
|
+
this._resumeController = null;
|
|
1947
2070
|
this.apiKey = config.apiKey;
|
|
1948
2071
|
this.id = randomUUID();
|
|
1949
2072
|
this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
|
|
@@ -1956,6 +2079,8 @@ var PollarClient = class {
|
|
|
1956
2079
|
this._keyManager = config.keyManager ?? defaultKeyManager(this._storage, config.apiKey);
|
|
1957
2080
|
this._walletAdapterResolver = config.walletAdapter ?? null;
|
|
1958
2081
|
this._walletResolverTimeoutMs = config.walletResolverTimeoutMs ?? 5e3;
|
|
2082
|
+
this._passkey = config.passkey ?? null;
|
|
2083
|
+
this._passkeySign = config.passkeySign ?? null;
|
|
1959
2084
|
this._deviceLabel = config.deviceLabel;
|
|
1960
2085
|
this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
|
|
1961
2086
|
this._maxIdleMs = config.maxIdleMs;
|
|
@@ -1969,7 +2094,9 @@ var PollarClient = class {
|
|
|
1969
2094
|
this._initialized = Promise.resolve();
|
|
1970
2095
|
return;
|
|
1971
2096
|
}
|
|
1972
|
-
console.info(
|
|
2097
|
+
console.info(
|
|
2098
|
+
`[PollarClient] Initialized v${POLLAR_CORE_VERSION} \u2014 endpoint: ${this.basePath}, network: ${this._networkState.network}`
|
|
2099
|
+
);
|
|
1973
2100
|
this._initialized = this._initialize();
|
|
1974
2101
|
}
|
|
1975
2102
|
/**
|
|
@@ -2007,7 +2134,11 @@ var PollarClient = class {
|
|
|
2007
2134
|
}
|
|
2008
2135
|
await this._restoreSession();
|
|
2009
2136
|
this._visibilityUnsubscribe = this._visibilityProvider.onChange((visible) => {
|
|
2010
|
-
if (visible)
|
|
2137
|
+
if (!visible) return;
|
|
2138
|
+
void this._maybeProactiveRefresh();
|
|
2139
|
+
if (this._authState.step === "authenticated" && !this._authState.verified) {
|
|
2140
|
+
void this._resume();
|
|
2141
|
+
}
|
|
2011
2142
|
});
|
|
2012
2143
|
}
|
|
2013
2144
|
/** Detach the cross-tab storage listener and abort any in-flight login. */
|
|
@@ -2018,6 +2149,8 @@ var PollarClient = class {
|
|
|
2018
2149
|
}
|
|
2019
2150
|
this._loginController?.abort();
|
|
2020
2151
|
this._loginController = null;
|
|
2152
|
+
this._resumeController?.abort();
|
|
2153
|
+
this._resumeController = null;
|
|
2021
2154
|
this._clearRefreshTimer();
|
|
2022
2155
|
if (this._visibilityUnsubscribe) {
|
|
2023
2156
|
this._visibilityUnsubscribe();
|
|
@@ -2032,7 +2165,9 @@ var PollarClient = class {
|
|
|
2032
2165
|
request.headers.set("x-pollar-api-key", self.apiKey);
|
|
2033
2166
|
self._lastRequestAt = Date.now();
|
|
2034
2167
|
await self._initialized;
|
|
2035
|
-
|
|
2168
|
+
const cacheMethod = request.method.toUpperCase();
|
|
2169
|
+
const cacheBodyAllowed = cacheMethod !== "GET" && cacheMethod !== "HEAD";
|
|
2170
|
+
if (cacheBodyAllowed && request.body != null) {
|
|
2036
2171
|
try {
|
|
2037
2172
|
self._requestBodyCache.set(request, await request.clone().arrayBuffer());
|
|
2038
2173
|
} catch (err) {
|
|
@@ -2119,7 +2254,9 @@ var PollarClient = class {
|
|
|
2119
2254
|
}
|
|
2120
2255
|
}
|
|
2121
2256
|
}
|
|
2122
|
-
const
|
|
2257
|
+
const retryMethod = originalRequest.method.toUpperCase();
|
|
2258
|
+
const retryBodyAllowed = retryMethod !== "GET" && retryMethod !== "HEAD";
|
|
2259
|
+
const cachedBody = retryBodyAllowed ? this._requestBodyCache.get(originalRequest) : void 0;
|
|
2123
2260
|
const retried = new Request(originalRequest.url, {
|
|
2124
2261
|
method: originalRequest.method,
|
|
2125
2262
|
headers,
|
|
@@ -2163,19 +2300,26 @@ var PollarClient = class {
|
|
|
2163
2300
|
throw err;
|
|
2164
2301
|
}
|
|
2165
2302
|
if (error || !data) {
|
|
2166
|
-
console.
|
|
2303
|
+
console.error("[PollarClient] /auth/refresh returned error", { error });
|
|
2167
2304
|
await this._clearSession();
|
|
2168
2305
|
throw new Error("Refresh failed");
|
|
2169
2306
|
}
|
|
2170
2307
|
const successData = data;
|
|
2171
2308
|
if (!successData.success || !successData.content?.token) {
|
|
2172
|
-
console.
|
|
2309
|
+
console.error("[PollarClient] /auth/refresh response malformed", {
|
|
2310
|
+
success: successData.success,
|
|
2311
|
+
hasToken: !!successData.content?.token
|
|
2312
|
+
});
|
|
2173
2313
|
await this._clearSession();
|
|
2174
2314
|
throw new Error("Refresh response malformed");
|
|
2175
2315
|
}
|
|
2176
2316
|
const newToken = successData.content.token;
|
|
2177
2317
|
if (typeof newToken.accessToken !== "string" || typeof newToken.refreshToken !== "string" || typeof newToken.expiresAt !== "number") {
|
|
2178
|
-
console.
|
|
2318
|
+
console.error("[PollarClient] /auth/refresh token shape invalid", {
|
|
2319
|
+
accessToken: typeof newToken.accessToken,
|
|
2320
|
+
refreshToken: typeof newToken.refreshToken,
|
|
2321
|
+
expiresAt: typeof newToken.expiresAt
|
|
2322
|
+
});
|
|
2179
2323
|
await this._clearSession();
|
|
2180
2324
|
throw new Error("Refresh response token shape invalid");
|
|
2181
2325
|
}
|
|
@@ -2369,6 +2513,19 @@ var PollarClient = class {
|
|
|
2369
2513
|
const controller = this._newController();
|
|
2370
2514
|
loginWallet(type, this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
|
|
2371
2515
|
}
|
|
2516
|
+
/**
|
|
2517
|
+
* "Smart Wallet" login: runs the passkey (WebAuthn) ceremony and, for a new
|
|
2518
|
+
* user, creates a sponsored smart-account C-address. Requires the `passkey`
|
|
2519
|
+
* ceremony to be configured (e.g. via `@pollar/react`).
|
|
2520
|
+
*/
|
|
2521
|
+
loginSmartWallet() {
|
|
2522
|
+
if (!isClientRuntime) {
|
|
2523
|
+
warnServerSide("loginSmartWallet");
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
const controller = this._newController();
|
|
2527
|
+
loginSmartWallet(this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
|
|
2528
|
+
}
|
|
2372
2529
|
// ─── Cancel ───────────────────────────────────────────────────────────────
|
|
2373
2530
|
cancelLogin() {
|
|
2374
2531
|
this._loginController?.abort();
|
|
@@ -2431,6 +2588,29 @@ var PollarClient = class {
|
|
|
2431
2588
|
}
|
|
2432
2589
|
return data.content.sessions;
|
|
2433
2590
|
}
|
|
2591
|
+
getSessionsState() {
|
|
2592
|
+
return this._sessionsState;
|
|
2593
|
+
}
|
|
2594
|
+
onSessionsStateChange(cb) {
|
|
2595
|
+
this._sessionsStateListeners.add(cb);
|
|
2596
|
+
cb(this._sessionsState);
|
|
2597
|
+
return () => this._sessionsStateListeners.delete(cb);
|
|
2598
|
+
}
|
|
2599
|
+
/**
|
|
2600
|
+
* Fire-and-forget variant of {@link listSessions} that drives the observable
|
|
2601
|
+
* `SessionsState` store instead of returning the array. UI layers subscribe
|
|
2602
|
+
* via `onSessionsStateChange` and stay pure readers — mirrors `fetchTxHistory`.
|
|
2603
|
+
*/
|
|
2604
|
+
async fetchSessions() {
|
|
2605
|
+
this._setSessionsState({ step: "loading" });
|
|
2606
|
+
try {
|
|
2607
|
+
const sessions = await this.listSessions();
|
|
2608
|
+
this._setSessionsState({ step: "loaded", sessions });
|
|
2609
|
+
} catch (err) {
|
|
2610
|
+
const message = err instanceof Error ? err.message : "Failed to load sessions";
|
|
2611
|
+
this._setSessionsState({ step: "error", message });
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2434
2614
|
/**
|
|
2435
2615
|
* Revoke a specific refresh-token family (a single device session). Use
|
|
2436
2616
|
* `listSessions` to enumerate the familyIds. Revoking the current session
|
|
@@ -2507,16 +2687,19 @@ var PollarClient = class {
|
|
|
2507
2687
|
cb(this._walletBalanceState);
|
|
2508
2688
|
return () => this._walletBalanceStateListeners.delete(cb);
|
|
2509
2689
|
}
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2690
|
+
/**
|
|
2691
|
+
* Refreshes the balances of the authenticated user's OWN wallet. The wallet
|
|
2692
|
+
* and network are resolved server-side from the session — no arguments. Drives
|
|
2693
|
+
* `walletBalanceState`. For an arbitrary wallet, use {@link getWalletBalance}.
|
|
2694
|
+
*/
|
|
2695
|
+
async refreshBalance() {
|
|
2696
|
+
if (!this._session?.wallet?.address) {
|
|
2513
2697
|
this._setWalletBalanceState({ step: "error", message: "No wallet connected" });
|
|
2514
2698
|
return;
|
|
2515
2699
|
}
|
|
2516
2700
|
this._setWalletBalanceState({ step: "loading" });
|
|
2517
2701
|
try {
|
|
2518
|
-
const
|
|
2519
|
-
const { data, error } = await this._api.GET("/wallet/balance", { params: { query: { publicKey: pk, network } } });
|
|
2702
|
+
const { data, error } = await this._api.GET("/wallet/balance");
|
|
2520
2703
|
if (!error && data?.success && data.content) {
|
|
2521
2704
|
this._setWalletBalanceState({ step: "loaded", data: data.content });
|
|
2522
2705
|
} else {
|
|
@@ -2526,6 +2709,53 @@ var PollarClient = class {
|
|
|
2526
2709
|
this._setWalletBalanceState({ step: "error", message: "Failed to load balance" });
|
|
2527
2710
|
}
|
|
2528
2711
|
}
|
|
2712
|
+
/**
|
|
2713
|
+
* General-purpose balance lookup for ANY wallet on ANY network — not scoped
|
|
2714
|
+
* to this application. Enumerates the account's real on-chain holdings via
|
|
2715
|
+
* Horizon (server-side) and returns the data directly (no reactive state).
|
|
2716
|
+
* `network` defaults to the client's current network.
|
|
2717
|
+
*/
|
|
2718
|
+
async getWalletBalance(publicKey, network) {
|
|
2719
|
+
const { data, error } = await this._api.GET("/wallet/{publicKey}/balance", {
|
|
2720
|
+
params: { path: { publicKey }, query: { network: network ?? this.getNetwork() } }
|
|
2721
|
+
});
|
|
2722
|
+
if (error || !data?.success || !data.content) {
|
|
2723
|
+
throw new Error("[PollarClient] Failed to load wallet balance");
|
|
2724
|
+
}
|
|
2725
|
+
return data.content;
|
|
2726
|
+
}
|
|
2727
|
+
// ─── Enabled assets ───────────────────────────────────────────────────────
|
|
2728
|
+
getEnabledAssetsState() {
|
|
2729
|
+
return this._enabledAssetsState;
|
|
2730
|
+
}
|
|
2731
|
+
onEnabledAssetsStateChange(cb) {
|
|
2732
|
+
this._enabledAssetsStateListeners.add(cb);
|
|
2733
|
+
cb(this._enabledAssetsState);
|
|
2734
|
+
return () => this._enabledAssetsStateListeners.delete(cb);
|
|
2735
|
+
}
|
|
2736
|
+
/**
|
|
2737
|
+
* Loads the application's enabled assets paired with the authenticated
|
|
2738
|
+
* wallet's on-chain trustline state — so the SDK knows which trustlines still
|
|
2739
|
+
* need to be added. Wallet and network are resolved server-side from the
|
|
2740
|
+
* session. Drives `enabledAssetsState`; mirrors {@link refreshBalance}.
|
|
2741
|
+
*/
|
|
2742
|
+
async refreshAssets() {
|
|
2743
|
+
if (!this._session?.wallet?.address) {
|
|
2744
|
+
this._setEnabledAssetsState({ step: "error", message: "No wallet connected" });
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
this._setEnabledAssetsState({ step: "loading" });
|
|
2748
|
+
try {
|
|
2749
|
+
const { data, error } = await this._api.GET("/wallet/assets");
|
|
2750
|
+
if (!error && data?.success && data.content) {
|
|
2751
|
+
this._setEnabledAssetsState({ step: "loaded", data: data.content });
|
|
2752
|
+
} else {
|
|
2753
|
+
this._setEnabledAssetsState({ step: "error", message: "Failed to load assets" });
|
|
2754
|
+
}
|
|
2755
|
+
} catch {
|
|
2756
|
+
this._setEnabledAssetsState({ step: "error", message: "Failed to load assets" });
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2529
2759
|
// ─── Transactions ─────────────────────────────────────────────────────────
|
|
2530
2760
|
/**
|
|
2531
2761
|
* Builds an unsigned XDR. Drives `_setTransactionState` for modal-style UIs
|
|
@@ -2533,14 +2763,14 @@ var PollarClient = class {
|
|
|
2533
2763
|
* inspect the result without subscribing to state changes.
|
|
2534
2764
|
*/
|
|
2535
2765
|
async buildTx(operation, params, options) {
|
|
2536
|
-
if (!this._session?.wallet?.
|
|
2766
|
+
if (!this._session?.wallet?.address) {
|
|
2537
2767
|
const details = "No wallet connected";
|
|
2538
2768
|
this._setTransactionState({ step: "error", phase: "building", details });
|
|
2539
2769
|
return { status: "error", details };
|
|
2540
2770
|
}
|
|
2541
2771
|
const body = {
|
|
2542
2772
|
network: this.getNetwork(),
|
|
2543
|
-
|
|
2773
|
+
address: this._session.wallet.address,
|
|
2544
2774
|
operation,
|
|
2545
2775
|
params,
|
|
2546
2776
|
options: options ?? {}
|
|
@@ -2581,7 +2811,7 @@ var PollarClient = class {
|
|
|
2581
2811
|
const buildData = this._currentBuildData();
|
|
2582
2812
|
this._setTransactionState({ step: "signing", ...buildData && { buildData } });
|
|
2583
2813
|
if (this._walletAdapter) {
|
|
2584
|
-
const accountToSign = this._session?.wallet?.
|
|
2814
|
+
const accountToSign = this._session?.wallet?.address;
|
|
2585
2815
|
const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
|
|
2586
2816
|
try {
|
|
2587
2817
|
const { signedTxXdr } = await this._walletAdapter.signTransaction(unsignedXdr, signOpts);
|
|
@@ -2602,10 +2832,10 @@ var PollarClient = class {
|
|
|
2602
2832
|
return { status: "error", ...details && { details } };
|
|
2603
2833
|
}
|
|
2604
2834
|
}
|
|
2605
|
-
const
|
|
2835
|
+
const address = this._session?.wallet?.address ?? "";
|
|
2606
2836
|
try {
|
|
2607
2837
|
const { data, error } = await this._api.POST("/tx/sign", {
|
|
2608
|
-
body: { network: this.getNetwork(),
|
|
2838
|
+
body: { network: this.getNetwork(), address, unsignedXdr }
|
|
2609
2839
|
});
|
|
2610
2840
|
if (!error && data?.success && data.content?.signedXdr) {
|
|
2611
2841
|
const { signedXdr, idempotencyKey } = data.content;
|
|
@@ -2658,12 +2888,12 @@ var PollarClient = class {
|
|
|
2658
2888
|
const buildData = this._currentBuildData();
|
|
2659
2889
|
const outcomeExtra = buildData ? { buildData } : {};
|
|
2660
2890
|
this._setTransactionState({ step: "submitting", signedXdr, ...buildData && { buildData } });
|
|
2661
|
-
const
|
|
2891
|
+
const address = this._session?.wallet?.address ?? "";
|
|
2662
2892
|
try {
|
|
2663
2893
|
const { data, error } = await this._api.POST("/tx/submit", {
|
|
2664
2894
|
body: {
|
|
2665
2895
|
network: this.getNetwork(),
|
|
2666
|
-
|
|
2896
|
+
address,
|
|
2667
2897
|
signedXdr,
|
|
2668
2898
|
...opts?.submissionToken && { idempotencyKey: opts.submissionToken }
|
|
2669
2899
|
}
|
|
@@ -2723,6 +2953,19 @@ var PollarClient = class {
|
|
|
2723
2953
|
* `success` (ledger-confirmed), or `error[phase: 'signing-submitting']`.
|
|
2724
2954
|
*/
|
|
2725
2955
|
async signAndSubmitTx(unsignedXdr) {
|
|
2956
|
+
if (this._session?.wallet?.type === "smart") {
|
|
2957
|
+
const buildData2 = this._currentBuildData();
|
|
2958
|
+
if (!buildData2?.smart) {
|
|
2959
|
+
const details = "no prepared smart transaction; call buildTx first";
|
|
2960
|
+
this._setTransactionState({ step: "error", phase: "signing", details });
|
|
2961
|
+
return { status: "error", details };
|
|
2962
|
+
}
|
|
2963
|
+
return this._signSubmitSmart(buildData2);
|
|
2964
|
+
}
|
|
2965
|
+
if (!unsignedXdr) {
|
|
2966
|
+
this._setTransactionState({ step: "error", phase: "signing", details: "missing unsigned transaction" });
|
|
2967
|
+
return { status: "error", details: "missing unsigned transaction" };
|
|
2968
|
+
}
|
|
2726
2969
|
if (this._walletAdapter) {
|
|
2727
2970
|
const signed = await this.signTx(unsignedXdr);
|
|
2728
2971
|
if (signed.status === "error") {
|
|
@@ -2740,7 +2983,7 @@ var PollarClient = class {
|
|
|
2740
2983
|
this._setTransactionState({ step: "signing-submitting", ...buildData && { buildData } });
|
|
2741
2984
|
const body = {
|
|
2742
2985
|
network: this.getNetwork(),
|
|
2743
|
-
|
|
2986
|
+
address: this._session?.wallet?.address ?? "",
|
|
2744
2987
|
unsignedXdr
|
|
2745
2988
|
};
|
|
2746
2989
|
try {
|
|
@@ -2809,14 +3052,20 @@ var PollarClient = class {
|
|
|
2809
3052
|
* `signTx`, and `submitTx` separately instead.
|
|
2810
3053
|
*/
|
|
2811
3054
|
async buildAndSignAndSubmitTx(operation, params, options) {
|
|
3055
|
+
if (this._session?.wallet?.type === "smart") {
|
|
3056
|
+
return this._runSmartTx(operation, params, options);
|
|
3057
|
+
}
|
|
2812
3058
|
if (this._walletAdapter) {
|
|
2813
3059
|
const built = await this.buildTx(operation, params, options);
|
|
2814
3060
|
if (built.status === "error") {
|
|
2815
3061
|
return { status: "error", ...built.details && { details: built.details } };
|
|
2816
3062
|
}
|
|
3063
|
+
if (!built.buildData.unsignedXdr) {
|
|
3064
|
+
return { status: "error", details: "build returned no unsigned transaction" };
|
|
3065
|
+
}
|
|
2817
3066
|
return this.signAndSubmitTx(built.buildData.unsignedXdr);
|
|
2818
3067
|
}
|
|
2819
|
-
if (!this._session?.wallet?.
|
|
3068
|
+
if (!this._session?.wallet?.address) {
|
|
2820
3069
|
this._setTransactionState({ step: "error", phase: "building-signing-submitting", details: "No wallet connected" });
|
|
2821
3070
|
return { status: "error", details: "No wallet connected" };
|
|
2822
3071
|
}
|
|
@@ -2825,7 +3074,7 @@ var PollarClient = class {
|
|
|
2825
3074
|
const { data, error } = await this._api.POST("/tx/build-sign-submit", {
|
|
2826
3075
|
body: {
|
|
2827
3076
|
network: this.getNetwork(),
|
|
2828
|
-
|
|
3077
|
+
address: this._session.wallet.address,
|
|
2829
3078
|
operation,
|
|
2830
3079
|
params,
|
|
2831
3080
|
options: options ?? {}
|
|
@@ -2869,6 +3118,113 @@ var PollarClient = class {
|
|
|
2869
3118
|
async runTx(operation, params, options) {
|
|
2870
3119
|
return this.buildAndSignAndSubmitTx(operation, params, options);
|
|
2871
3120
|
}
|
|
3121
|
+
/**
|
|
3122
|
+
* Smart-wallet (passkey / C-address) transaction: build (server prepares the
|
|
3123
|
+
* SAC transfer + returns the auth digest) → sign the digest with the passkey
|
|
3124
|
+
* → submit (server assembles the signed auth entry and broadcasts; the
|
|
3125
|
+
* sponsor pays the fee). State machine: building → built → signing →
|
|
3126
|
+
* submitting → success.
|
|
3127
|
+
*/
|
|
3128
|
+
async _runSmartTx(operation, params, options) {
|
|
3129
|
+
const address = this._session?.wallet?.address;
|
|
3130
|
+
if (!address) {
|
|
3131
|
+
this._setTransactionState({ step: "error", phase: "building", details: "No wallet connected" });
|
|
3132
|
+
return { status: "error", details: "No wallet connected" };
|
|
3133
|
+
}
|
|
3134
|
+
if (!this._passkeySign) {
|
|
3135
|
+
const details = "Passkey signer not configured";
|
|
3136
|
+
this._setTransactionState({ step: "error", phase: "signing", details });
|
|
3137
|
+
return { status: "error", details };
|
|
3138
|
+
}
|
|
3139
|
+
this._setTransactionState({ step: "building" });
|
|
3140
|
+
let buildData;
|
|
3141
|
+
try {
|
|
3142
|
+
const body = {
|
|
3143
|
+
network: this.getNetwork(),
|
|
3144
|
+
address,
|
|
3145
|
+
operation,
|
|
3146
|
+
params,
|
|
3147
|
+
options: options ?? {}
|
|
3148
|
+
};
|
|
3149
|
+
const { data, error } = await this._api.POST("/tx/build", { body });
|
|
3150
|
+
if (error || !data?.success || !data.content?.smart) {
|
|
3151
|
+
const details = error?.details ?? "Failed to build transaction";
|
|
3152
|
+
this._setTransactionState({ step: "error", phase: "building", details });
|
|
3153
|
+
return { status: "error", details };
|
|
3154
|
+
}
|
|
3155
|
+
buildData = data.content;
|
|
3156
|
+
} catch (err) {
|
|
3157
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
3158
|
+
this._setTransactionState({ step: "error", phase: "building", ...details && { details } });
|
|
3159
|
+
return { status: "error", ...details && { details } };
|
|
3160
|
+
}
|
|
3161
|
+
this._setTransactionState({ step: "built", buildData });
|
|
3162
|
+
return this._signSubmitSmart(buildData);
|
|
3163
|
+
}
|
|
3164
|
+
/**
|
|
3165
|
+
* Steps 2–3 of the smart-wallet flow: sign the prepared auth digest with the
|
|
3166
|
+
* passkey, then submit. Shared by `_runSmartTx` (atomic) and `signAndSubmitTx`
|
|
3167
|
+
* (split flow, when a smart build is already on the state machine).
|
|
3168
|
+
*/
|
|
3169
|
+
async _signSubmitSmart(buildData) {
|
|
3170
|
+
const address = this._session?.wallet?.address;
|
|
3171
|
+
const smart = buildData.smart;
|
|
3172
|
+
if (!address || !smart) {
|
|
3173
|
+
const details = "no prepared smart transaction";
|
|
3174
|
+
this._setTransactionState({ step: "error", phase: "signing", buildData, details });
|
|
3175
|
+
return { status: "error", buildData, details };
|
|
3176
|
+
}
|
|
3177
|
+
if (!this._passkeySign) {
|
|
3178
|
+
const details = "Passkey signer not configured";
|
|
3179
|
+
this._setTransactionState({ step: "error", phase: "signing", buildData, details });
|
|
3180
|
+
return { status: "error", buildData, details };
|
|
3181
|
+
}
|
|
3182
|
+
this._setTransactionState({ step: "signing", buildData });
|
|
3183
|
+
let assertion;
|
|
3184
|
+
try {
|
|
3185
|
+
assertion = await this._passkeySign({ credentialId: smart.credentialId, challenge: smart.digest });
|
|
3186
|
+
} catch (err) {
|
|
3187
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
3188
|
+
this._setTransactionState({ step: "error", phase: "signing", buildData, ...details && { details } });
|
|
3189
|
+
return { status: "error", buildData, ...details && { details } };
|
|
3190
|
+
}
|
|
3191
|
+
this._setTransactionState({ step: "submitting", buildData });
|
|
3192
|
+
const outcomeExtra = { buildData };
|
|
3193
|
+
try {
|
|
3194
|
+
const { data, error } = await this._api.POST("/tx/submit", {
|
|
3195
|
+
body: {
|
|
3196
|
+
network: this.getNetwork(),
|
|
3197
|
+
address,
|
|
3198
|
+
smart: { entryXdr: smart.entryXdr, funcXdr: smart.funcXdr, assertion }
|
|
3199
|
+
}
|
|
3200
|
+
});
|
|
3201
|
+
if (!error && data?.success && data.content) {
|
|
3202
|
+
const { hash, status: backendStatus, resultCode } = data.content;
|
|
3203
|
+
if (backendStatus === "SUCCESS") {
|
|
3204
|
+
this._setTransactionState({ step: "success", hash, buildData });
|
|
3205
|
+
return { status: "success", hash, ...outcomeExtra };
|
|
3206
|
+
}
|
|
3207
|
+
if (backendStatus === "PENDING") {
|
|
3208
|
+
this._setTransactionState({ step: "submitted", hash, buildData });
|
|
3209
|
+
return { status: "pending", hash, ...outcomeExtra };
|
|
3210
|
+
}
|
|
3211
|
+
this._setTransactionState({
|
|
3212
|
+
step: "error",
|
|
3213
|
+
phase: "submitting",
|
|
3214
|
+
buildData,
|
|
3215
|
+
...resultCode && { details: resultCode }
|
|
3216
|
+
});
|
|
3217
|
+
return { status: "error", hash, ...outcomeExtra, ...resultCode && { details: resultCode, resultCode } };
|
|
3218
|
+
}
|
|
3219
|
+
const details = error?.details;
|
|
3220
|
+
this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
|
|
3221
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
3222
|
+
} catch (err) {
|
|
3223
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
3224
|
+
this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
|
|
3225
|
+
return { status: "error", ...outcomeExtra, ...details && { details } };
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
2872
3228
|
// ─── App config ───────────────────────────────────────────────────────────
|
|
2873
3229
|
async getAppConfig() {
|
|
2874
3230
|
try {
|
|
@@ -2922,10 +3278,18 @@ var PollarClient = class {
|
|
|
2922
3278
|
this._txHistoryState = next;
|
|
2923
3279
|
for (const cb of this._txHistoryStateListeners) cb(next);
|
|
2924
3280
|
}
|
|
3281
|
+
_setSessionsState(next) {
|
|
3282
|
+
this._sessionsState = next;
|
|
3283
|
+
for (const cb of this._sessionsStateListeners) cb(next);
|
|
3284
|
+
}
|
|
2925
3285
|
_setWalletBalanceState(next) {
|
|
2926
3286
|
this._walletBalanceState = next;
|
|
2927
3287
|
for (const cb of this._walletBalanceStateListeners) cb(next);
|
|
2928
3288
|
}
|
|
3289
|
+
_setEnabledAssetsState(next) {
|
|
3290
|
+
this._enabledAssetsState = next;
|
|
3291
|
+
for (const cb of this._enabledAssetsStateListeners) cb(next);
|
|
3292
|
+
}
|
|
2929
3293
|
// ─── Private ──────────────────────────────────────────────────────────────
|
|
2930
3294
|
_newController() {
|
|
2931
3295
|
this._loginController?.abort();
|
|
@@ -2950,6 +3314,7 @@ var PollarClient = class {
|
|
|
2950
3314
|
this._walletAdapter = adapter;
|
|
2951
3315
|
await writeWalletType(this._storage, this.apiKeyHash, id);
|
|
2952
3316
|
},
|
|
3317
|
+
...this._passkey ? { passkey: this._passkey } : {},
|
|
2953
3318
|
...this._deviceLabel ? { deviceLabel: this._deviceLabel } : {}
|
|
2954
3319
|
};
|
|
2955
3320
|
}
|
|
@@ -2979,7 +3344,7 @@ var PollarClient = class {
|
|
|
2979
3344
|
}
|
|
2980
3345
|
}
|
|
2981
3346
|
if (id === "freighter" /* FREIGHTER */) return new FreighterAdapter();
|
|
2982
|
-
if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter();
|
|
3347
|
+
if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter(this.getNetwork() === "mainnet" ? "public" : "testnet");
|
|
2983
3348
|
throw new Error(
|
|
2984
3349
|
`[PollarClient] No wallet adapter configured for "${id}". Pass a walletAdapter resolver in PollarClientConfig.`
|
|
2985
3350
|
);
|
|
@@ -3020,21 +3385,66 @@ var PollarClient = class {
|
|
|
3020
3385
|
}
|
|
3021
3386
|
}
|
|
3022
3387
|
console.info("[PollarClient] Session restored from storage");
|
|
3023
|
-
this._setAuthState({ step: "authenticated", session: this._session });
|
|
3388
|
+
this._setAuthState({ step: "authenticated", session: this._session, verified: false });
|
|
3024
3389
|
this._scheduleNextRefresh();
|
|
3390
|
+
void this._resume();
|
|
3025
3391
|
} else {
|
|
3026
3392
|
console.info("[PollarClient] No session in storage");
|
|
3027
3393
|
}
|
|
3028
3394
|
}
|
|
3395
|
+
/**
|
|
3396
|
+
* Validate the restored session against the server and repopulate the
|
|
3397
|
+
* in-memory profile (PII is never persisted, so it's null after a cold
|
|
3398
|
+
* reload). Goes through the normal authed client, so it coalesces with any
|
|
3399
|
+
* in-flight refresh (onRequest awaits `_refreshPromise`) and, being a GET,
|
|
3400
|
+
* is auto-retried after a 401-triggered refresh.
|
|
3401
|
+
*
|
|
3402
|
+
* - 200 → store profile, mark the session `verified`.
|
|
3403
|
+
* - 401 → the refresh-on-401 path already ran; if the family was
|
|
3404
|
+
* revoked, refresh failed and `_clearSession()` took us to
|
|
3405
|
+
* idle. Nothing to do here — don't double-handle.
|
|
3406
|
+
* - network error → stay optimistic (do NOT log out); revalidated later on
|
|
3407
|
+
* `visibilitychange` or first use.
|
|
3408
|
+
*/
|
|
3409
|
+
async _resume() {
|
|
3410
|
+
if (!this._session) return;
|
|
3411
|
+
this._resumeController?.abort();
|
|
3412
|
+
const controller = new AbortController();
|
|
3413
|
+
this._resumeController = controller;
|
|
3414
|
+
try {
|
|
3415
|
+
const { data, error } = await this._api.GET("/auth/session/resume", { signal: controller.signal });
|
|
3416
|
+
if (error || !data) return;
|
|
3417
|
+
const content = data.content;
|
|
3418
|
+
if (!content || !this._session) return;
|
|
3419
|
+
this._profile = { ...content };
|
|
3420
|
+
this._setAuthState({ step: "authenticated", session: this._session, verified: true });
|
|
3421
|
+
} catch (err) {
|
|
3422
|
+
if (err?.name === "AbortError") return;
|
|
3423
|
+
console.warn("[PollarClient] resume failed (network); will retry", err);
|
|
3424
|
+
} finally {
|
|
3425
|
+
if (this._resumeController === controller) this._resumeController = null;
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3029
3428
|
async _storeSession(session) {
|
|
3030
3429
|
console.info("[PollarClient] Session stored");
|
|
3430
|
+
const w = session.wallet;
|
|
3031
3431
|
const persisted = {
|
|
3032
3432
|
clientSessionId: session.clientSessionId,
|
|
3033
3433
|
userId: session.userId ?? null,
|
|
3034
3434
|
status: session.status,
|
|
3035
3435
|
token: session.token,
|
|
3036
3436
|
user: session.user,
|
|
3037
|
-
|
|
3437
|
+
// The wire response still carries the legacy `publicKey` alias (kept for
|
|
3438
|
+
// older SDKs); the persisted session standardizes on `address` only.
|
|
3439
|
+
wallet: {
|
|
3440
|
+
type: w.type,
|
|
3441
|
+
address: w.address ?? w.publicKey ?? null,
|
|
3442
|
+
...w.existsOnStellar !== void 0 ? { existsOnStellar: w.existsOnStellar } : {},
|
|
3443
|
+
...w.createdAt !== void 0 ? { createdAt: w.createdAt } : {},
|
|
3444
|
+
...w.linkedAt !== void 0 ? { linkedAt: w.linkedAt } : {},
|
|
3445
|
+
...w.network !== void 0 ? { network: w.network } : {},
|
|
3446
|
+
...w.deployTxHash !== void 0 ? { deployTxHash: w.deployTxHash } : {}
|
|
3447
|
+
}
|
|
3038
3448
|
};
|
|
3039
3449
|
this._session = persisted;
|
|
3040
3450
|
if (session.data) {
|
|
@@ -3047,7 +3457,7 @@ var PollarClient = class {
|
|
|
3047
3457
|
};
|
|
3048
3458
|
}
|
|
3049
3459
|
await writeStorage(this._storage, this.apiKeyHash, persisted);
|
|
3050
|
-
this._setAuthState({ step: "authenticated", session: persisted });
|
|
3460
|
+
this._setAuthState({ step: "authenticated", session: persisted, verified: true });
|
|
3051
3461
|
this._scheduleNextRefresh();
|
|
3052
3462
|
}
|
|
3053
3463
|
async _clearSession() {
|
|
@@ -3133,6 +3543,7 @@ _setDefaultKeyManagerFactory((_storage, apiKey) => new WebCryptoKeyManager(apiKe
|
|
|
3133
3543
|
exports.AUTH_ERROR_CODES = AUTH_ERROR_CODES;
|
|
3134
3544
|
exports.AlbedoAdapter = AlbedoAdapter;
|
|
3135
3545
|
exports.FreighterAdapter = FreighterAdapter;
|
|
3546
|
+
exports.POLLAR_CORE_VERSION = POLLAR_CORE_VERSION;
|
|
3136
3547
|
exports.PollarClient = PollarClient;
|
|
3137
3548
|
exports.StellarClient = StellarClient;
|
|
3138
3549
|
exports.WalletType = WalletType;
|