@cavos/kit 0.0.3 → 0.0.5

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.
@@ -5,7 +5,7 @@ import { PublicKey, TransactionInstruction, SystemProgram, SYSVAR_INSTRUCTIONS_P
5
5
  import { hkdf } from '@noble/hashes/hkdf';
6
6
  import { pbkdf2 } from '@noble/hashes/pbkdf2';
7
7
  import { randomBytes } from '@noble/hashes/utils';
8
- import { rpc, hash as hash$1, xdr, Address, StrKey, nativeToScVal, Operation, scValToNative, Account, TransactionBuilder, BASE_FEE } from '@stellar/stellar-sdk';
8
+ import { Horizon, TransactionBuilder, BASE_FEE, Operation, Asset, Account, Keypair } from '@stellar/stellar-sdk';
9
9
 
10
10
  // src/crypto/encoding.ts
11
11
  var U128_MASK = (1n << 128n) - 1n;
@@ -23,8 +23,8 @@ function bytesToHex(bytes) {
23
23
  return s;
24
24
  }
25
25
  function hexToBytes(hex) {
26
- const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
27
- const padded = clean.length % 2 ? "0" + clean : clean;
26
+ const clean2 = hex.startsWith("0x") ? hex.slice(2) : hex;
27
+ const padded = clean2.length % 2 ? "0" + clean2 : clean2;
28
28
  const out = new Uint8Array(padded.length / 2);
29
29
  for (let i = 0; i < out.length; i++) out[i] = parseInt(padded.slice(i * 2, i * 2 + 2), 16);
30
30
  return out;
@@ -176,7 +176,7 @@ var CAVOS_PAYMASTER_URL = {
176
176
  mainnet: "https://paymaster.cavos.xyz"
177
177
  };
178
178
  var DEVICE_ACCOUNT_CLASS_HASH = {
179
- sepolia: "0x25cbc5423e8ee895febb0ef2c3945b408da44d0039d915fbdd681fe6b6ba66b",
179
+ sepolia: "0x765f000b7021577aa0d6c206fb3d8ac73571686b3e76b9dc9b6d59a372e8c2c",
180
180
  mainnet: "0x1840aded59e8a0d2b440a134cb9079a7fc11b06c77f58ed189ab436a034ca6a"
181
181
  };
182
182
  var StarknetAdapter = class {
@@ -184,21 +184,34 @@ var StarknetAdapter = class {
184
184
  this.opts = opts;
185
185
  this.chain = "starknet";
186
186
  }
187
- computeAddress({ addressSeed, initialSigner, salt }) {
187
+ /**
188
+ * Deterministic address = f(addressSeed) ONLY. The device pubkey is NOT
189
+ * part of the derivation — anti-squatting is the integrator's responsibility
190
+ * (keep `appSalt` secret; deploy on first login). This makes the address
191
+ * recomputable by the user from (userId, appSalt) alone, even after losing
192
+ * every device.
193
+ *
194
+ * `initialSigner` in `ComputeAddressParams` is IGNORED on Starknet (kept in
195
+ * the shared type for Solana/Stellar, which still include it).
196
+ */
197
+ computeAddress({ addressSeed, salt }) {
188
198
  return hash.calculateContractAddressFromHash(
189
199
  num.toHex(salt ?? addressSeed),
190
200
  this.opts.classHash,
191
- this.constructorCalldata(addressSeed, initialSigner),
201
+ this.constructorCalldata(addressSeed),
192
202
  0
193
203
  // deployerAddress 0 => deterministic counterfactual address
194
204
  );
195
205
  }
196
- /** Single UDC deploy; the constructor registers the first device signer, so
197
- * the account is ready the moment it is deployed (fits the paymaster's
198
- * deploy + execute_from_outside bundle). */
206
+ /**
207
+ * UDC deploy call. The constructor takes ONLY the seed no device pubkey
208
+ * so the account is born with no signers. The caller MUST follow up with
209
+ * `buildInitialize` in the same multicall (or a separate tx) to register the
210
+ * first device signer; otherwise the account is unusable.
211
+ */
199
212
  buildDeploy(params) {
200
213
  const salt = params.salt ?? params.addressSeed;
201
- const calldata = this.constructorCalldata(params.addressSeed, params.initialSigner);
214
+ const calldata = this.constructorCalldata(params.addressSeed);
202
215
  return [
203
216
  {
204
217
  contractAddress: UDC_ADDRESS,
@@ -214,9 +227,22 @@ var StarknetAdapter = class {
214
227
  }
215
228
  ];
216
229
  }
217
- /** Constructor calldata: [address_seed, pub_x_low, pub_x_high, pub_y_low, pub_y_high]. */
218
- constructorCalldata(addressSeed, initialSigner) {
219
- return [num.toHex(addressSeed), ...pubkeyCalldata(initialSigner)];
230
+ /** Constructor calldata: [address_seed]. Device pubkey is registered post-deploy via initialize. */
231
+ constructorCalldata(addressSeed) {
232
+ return [num.toHex(addressSeed)];
233
+ }
234
+ /**
235
+ * `initialize` call: registers the first device signer. Callable only while
236
+ * the account has no signers (one-shot). In production this is bundled with
237
+ * the UDC deploy in a single sponsored multicall — see `connectStarknet`.
238
+ * Anti-squatting is NOT enforced on-chain.
239
+ */
240
+ buildInitialize(accountAddress, devicePubkey) {
241
+ return {
242
+ contractAddress: accountAddress,
243
+ entrypoint: "initialize",
244
+ calldata: pubkeyCalldata(devicePubkey)
245
+ };
220
246
  }
221
247
  buildAddSigner(accountAddress, signer) {
222
248
  return { contractAddress: accountAddress, entrypoint: "add_signer", calldata: pubkeyCalldata(signer) };
@@ -254,6 +280,17 @@ var StarknetAdapter = class {
254
280
  });
255
281
  return BigInt(res[0] ?? 0) !== 0n;
256
282
  }
283
+ /** True if the account has at least one passkey registered as an approver.
284
+ * Lets the UI decide whether to offer passkey approval before prompting. */
285
+ async hasPasskeyApprover(accountAddress) {
286
+ if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
287
+ const res = await this.opts.provider.callContract({
288
+ contractAddress: accountAddress,
289
+ entrypoint: "get_approver_count",
290
+ calldata: []
291
+ });
292
+ return BigInt(res[0] ?? 0) > 0n;
293
+ }
257
294
  async getPasskeyNonce(accountAddress) {
258
295
  if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
259
296
  const res = await this.opts.provider.callContract({
@@ -439,26 +476,30 @@ var SolanaAdapter = class {
439
476
  this.chain = "solana";
440
477
  this.programId = new PublicKey(opts.programId ?? DEVICE_ACCOUNT_PROGRAM_ID);
441
478
  }
442
- /** Deterministic account address: PDA of [seed, address_seed, initial_signer_x]. */
443
- computeAddress(addressSeed, initialSigner) {
444
- return this.pda(addressSeed, compressedPubkey(initialSigner)).toBase58();
479
+ /**
480
+ * Deterministic account address: PDA of [ACCOUNT_SEED, addressSeed] — the
481
+ * device pubkey is NOT part of the seeds, so the address is recomputable
482
+ * from (userId, appSalt) alone. Anti-squatting is the integrator's
483
+ * responsibility (keep `appSalt` secret; deploy on first login).
484
+ */
485
+ computeAddress(addressSeed) {
486
+ return this.pda(addressSeed).toBase58();
445
487
  }
446
- pda(addressSeed, initialCompressed) {
488
+ pda(addressSeed) {
447
489
  const [pda] = PublicKey.findProgramAddressSync(
448
- [
449
- Buffer.from(ACCOUNT_SEED),
450
- Buffer.from(addressSeed),
451
- Buffer.from(initialCompressed.slice(1, 33))
452
- // x-coordinate
453
- ],
490
+ [Buffer.from(ACCOUNT_SEED), Buffer.from(addressSeed)],
454
491
  this.programId
455
492
  );
456
493
  return pda;
457
494
  }
458
- /** `initialize` instruction creating the account with its first device signer. */
495
+ /**
496
+ * `initialize` instruction: creates the account PDA and registers the first
497
+ * device signer. No attestation is required — anti-squatting is NOT enforced
498
+ * on-chain.
499
+ */
459
500
  buildInitialize(addressSeed, payer, initialSigner) {
460
501
  const initialCompressed = compressedPubkey(initialSigner);
461
- const account = this.pda(addressSeed, initialCompressed);
502
+ const account = this.pda(addressSeed);
462
503
  const data = Buffer.concat([
463
504
  anchorDiscriminator("initialize"),
464
505
  Buffer.from(addressSeed),
@@ -466,7 +507,7 @@ var SolanaAdapter = class {
466
507
  Buffer.from(initialCompressed)
467
508
  // [u8;33]
468
509
  ]);
469
- return new TransactionInstruction({
510
+ const programIx = new TransactionInstruction({
470
511
  programId: this.programId,
471
512
  keys: [
472
513
  { pubkey: account, isSigner: false, isWritable: true },
@@ -475,6 +516,7 @@ var SolanaAdapter = class {
475
516
  ],
476
517
  data
477
518
  });
519
+ return [programIx];
478
520
  }
479
521
  /** `[precompile, add_signer]` bundle, authorized by an existing device signer. */
480
522
  async buildAddSigner(account, newSigner) {
@@ -590,6 +632,11 @@ var SolanaAdapter = class {
590
632
  return [precompileIx, ix];
591
633
  }
592
634
  /** Read whether `passkey` is a registered approver. */
635
+ /** True if the account has at least one passkey registered as an approver. */
636
+ async hasPasskeyApprover(account) {
637
+ const approvers = await this.fetchApprovers(new PublicKey(account));
638
+ return approvers.length > 0;
639
+ }
593
640
  async isApprover(account, passkey) {
594
641
  const approvers = await this.fetchApprovers(new PublicKey(account));
595
642
  const target = Buffer.from(compressedPubkey(passkey)).toString("hex");
@@ -808,8 +855,8 @@ function u64le(n) {
808
855
  new DataView(b.buffer, b.byteOffset, 8).setBigUint64(0, BigInt(n), true);
809
856
  return b;
810
857
  }
811
- function readU64le(buf, offset) {
812
- return new DataView(buf.buffer, buf.byteOffset, buf.length).getBigUint64(
858
+ function readU64le(buf3, offset) {
859
+ return new DataView(buf3.buffer, buf3.byteOffset, buf3.length).getBigUint64(
813
860
  offset,
814
861
  true
815
862
  );
@@ -868,11 +915,11 @@ var SolanaRelayer = class {
868
915
  async send(instructions) {
869
916
  const feePayer = await this.getFeePayer();
870
917
  const { blockhash } = await this.opts.connection.getLatestBlockhash("confirmed");
871
- const tx2 = new Transaction();
872
- tx2.feePayer = feePayer;
873
- tx2.recentBlockhash = blockhash;
874
- tx2.add(...instructions);
875
- const serialized = tx2.serialize({ requireAllSignatures: false, verifySignatures: false }).toString("base64");
918
+ const tx3 = new Transaction();
919
+ tx3.feePayer = feePayer;
920
+ tx3.recentBlockhash = blockhash;
921
+ tx3.add(...instructions);
922
+ const serialized = tx3.serialize({ requireAllSignatures: false, verifySignatures: false }).toString("base64");
876
923
  const res = await fetch(`${this.opts.baseUrl}/api/solana/relay`, {
877
924
  method: "POST",
878
925
  headers: { "Content-Type": "application/json" },
@@ -1280,6 +1327,8 @@ var CavosSolana = class _CavosSolana {
1280
1327
  this.feePayer = feePayer;
1281
1328
  /** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
1282
1329
  this.chain = "solana";
1330
+ /** True when this connect just created a brand-new account (first sign-up). */
1331
+ this.isNewAccount = false;
1283
1332
  }
1284
1333
  get publicKey() {
1285
1334
  return this.devicePubkey;
@@ -1314,23 +1363,23 @@ var CavosSolana = class _CavosSolana {
1314
1363
  opts.feePayer
1315
1364
  );
1316
1365
  }
1317
- const address = adapter.computeAddress(addressSeed, devicePubkey);
1366
+ const address = adapter.computeAddress(addressSeed);
1318
1367
  const deployed = await connection.getAccountInfo(new PublicKey(address)) !== null;
1319
1368
  if (!deployed) {
1320
1369
  if (relayer) {
1321
1370
  const payer = await relayer.getFeePayer();
1322
- const ix = adapter.buildInitialize(addressSeed, payer.toBase58(), devicePubkey);
1323
- await relayer.send([ix]);
1371
+ const ixs = adapter.buildInitialize(addressSeed, payer.toBase58(), devicePubkey);
1372
+ await relayer.send(ixs);
1324
1373
  } else if (opts.feePayer) {
1325
- const ix = adapter.buildInitialize(addressSeed, opts.feePayer.publicKey.toBase58(), devicePubkey);
1326
- await sendAndConfirmTransaction(connection, new Transaction().add(ix), [opts.feePayer]);
1374
+ const ixs = adapter.buildInitialize(addressSeed, opts.feePayer.publicKey.toBase58(), devicePubkey);
1375
+ await sendAndConfirmTransaction(connection, new Transaction().add(...ixs), [opts.feePayer]);
1327
1376
  } else {
1328
1377
  throw new Error("kit/solana: a relayer (appId) or feePayer is required to initialize a new account");
1329
1378
  }
1330
1379
  }
1331
1380
  await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
1332
1381
  const isSigner = await adapter.isAuthorizedSigner(address, devicePubkey);
1333
- return new _CavosSolana(
1382
+ const wallet = new _CavosSolana(
1334
1383
  identity,
1335
1384
  address,
1336
1385
  isSigner ? "ready" : "needs-device-approval",
@@ -1340,6 +1389,8 @@ var CavosSolana = class _CavosSolana {
1340
1389
  relayer,
1341
1390
  opts.feePayer
1342
1391
  );
1392
+ wallet.isNewAccount = !deployed && isSigner;
1393
+ return wallet;
1343
1394
  }
1344
1395
  /** Authorize an additional device signer (device-signed via precompile). */
1345
1396
  async addSigner(pubkey) {
@@ -1366,6 +1417,16 @@ var CavosSolana = class _CavosSolana {
1366
1417
  const transactionHash = await this.send(ixs);
1367
1418
  return { transactionHash };
1368
1419
  }
1420
+ /** True if this account already has a passkey enrolled as an approver, so a
1421
+ * new device can be approved with the passkey instead of the email flow. */
1422
+ async hasPasskey() {
1423
+ return this.adapter.hasPasskeyApprover(this.address);
1424
+ }
1425
+ /** Re-read (from chain) whether THIS device is now an authorized signer.
1426
+ * Used to poll for readiness after a passkey approval before it's indexed. */
1427
+ async isReady() {
1428
+ return this.adapter.isAuthorizedSigner(this.address, this.devicePubkey);
1429
+ }
1369
1430
  /**
1370
1431
  * From a fresh browser (status `needs-device-approval`), approve adding THIS
1371
1432
  * device with the user's synced passkey. Gasless via the relayer — the bundle
@@ -1410,12 +1471,12 @@ var CavosSolana = class _CavosSolana {
1410
1471
  return { transactionHash: await this.send(ixs) };
1411
1472
  }
1412
1473
  /** Move `amount` lamports out of the account to `destination` (device-signed). */
1413
- async execute(amount, destination) {
1474
+ async execute(amount, destination, opts) {
1414
1475
  if (this.status !== "ready") {
1415
1476
  throw new Error("kit/solana: this device is not yet an authorized signer of the wallet");
1416
1477
  }
1417
1478
  const ixs = await this.adapter.buildExecuteTransfer(this.address, destination, amount);
1418
- return this.send(ixs);
1479
+ return this.send(ixs, opts);
1419
1480
  }
1420
1481
  /**
1421
1482
  * Run arbitrary CPI `instructions` with the account PDA as signer (device-
@@ -1425,14 +1486,16 @@ var CavosSolana = class _CavosSolana {
1425
1486
  *
1426
1487
  * What the relayer will sponsor is constrained by the app's Solana program
1427
1488
  * allowlist (configured in the dashboard) — programs outside the allowlist are
1428
- * rejected before co-signing.
1489
+ * rejected before co-signing. Pass `{ sponsored: false }` to bypass the relayer
1490
+ * and pay the fee from a configured `feePayer` (e.g. for allowlisted programs
1491
+ * the relayer rejects, or to test the device signature end-to-end).
1429
1492
  */
1430
- async executeInstructions(instructions) {
1493
+ async executeInstructions(instructions, opts) {
1431
1494
  if (this.status !== "ready") {
1432
1495
  throw new Error("kit/solana: this device is not yet an authorized signer of the wallet");
1433
1496
  }
1434
1497
  const ixs = await this.adapter.buildExecute(this.address, instructions);
1435
- return this.send(ixs);
1498
+ return this.send(ixs, opts);
1436
1499
  }
1437
1500
  /**
1438
1501
  * Register the backup signer derived from `code` as an authorized signer of this
@@ -1511,302 +1574,982 @@ var CavosSolana = class _CavosSolana {
1511
1574
  opts.feePayer
1512
1575
  );
1513
1576
  }
1514
- async send(ixs) {
1515
- if (this.relayer) return this.relayer.send(ixs);
1577
+ /**
1578
+ * Submit a built instruction bundle. Sponsored by default (relayer pays the
1579
+ * fee); pass `{ sponsored: false }` to self-fund via the configured `feePayer`.
1580
+ * The device signature is embedded inside the secp256r1 precompile instruction,
1581
+ * NOT applied as a Solana tx signature — so switching only changes who pays,
1582
+ * never the signing identity.
1583
+ */
1584
+ async send(ixs, opts) {
1585
+ const sponsored = opts?.sponsored !== false;
1586
+ if (sponsored && this.relayer) return this.relayer.send(ixs);
1516
1587
  if (this.feePayer) {
1517
1588
  return sendAndConfirmTransaction(this.connection, new Transaction().add(...ixs), [this.feePayer]);
1518
1589
  }
1519
- throw new Error("kit/solana: no relayer or feePayer configured to submit transactions");
1590
+ throw new Error(
1591
+ `kit/solana: cannot ${sponsored ? "sponsor" : "self-fund"} \u2014 no ${sponsored ? "relayer" : "feePayer"} configured`
1592
+ );
1520
1593
  }
1521
1594
  };
1522
1595
  var defaultRegistry = new InMemoryWalletRegistry();
1523
1596
 
1524
1597
  // src/chains/stellar/constants.ts
1525
- var FACTORY_CONTRACT_ID = {
1526
- // Re-deployed 2026-07-01 with the passkey-approval device-account wasm (batched
1527
- // multi-chain challenge). The factory pins the wasm hash immutably, so a new
1528
- // wasm needs a new factory → new account addresses; testnet has no prod wallets.
1529
- "stellar-testnet": "CBCJIODXIEBOXXD66KCUCF7ZDYJARKI4ZIVQOVWPULOBH5XGNCDP6W3I",
1530
- // Set once the factory is deployed to mainnet (its address differs — network id
1531
- // is part of contract-address derivation).
1532
- "stellar-mainnet": ""
1533
- };
1534
- var DEVICE_ACCOUNT_WASM_HASH = {
1535
- "stellar-testnet": "2671b085578e59a385ef5a5664e42f0450322fe3249539f588e1263ed5a31dce",
1536
- "stellar-mainnet": ""
1537
- };
1538
1598
  var STELLAR_NETWORKS = {
1539
1599
  "stellar-testnet": {
1540
- rpcUrl: "https://soroban-testnet.stellar.org",
1541
1600
  passphrase: "Test SDF Network ; September 2015"
1542
1601
  },
1543
1602
  "stellar-mainnet": {
1544
- rpcUrl: "https://soroban-rpc.mainnet.stellar.gateway.fm",
1545
1603
  passphrase: "Public Global Stellar Network ; September 2015"
1546
1604
  }
1547
1605
  };
1548
- var NATIVE_SAC_ID = {
1549
- "stellar-testnet": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
1550
- "stellar-mainnet": "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA"
1606
+ var HORIZON_URL = {
1607
+ "stellar-testnet": "https://horizon-testnet.stellar.org",
1608
+ "stellar-mainnet": "https://horizon.stellar.org"
1551
1609
  };
1552
- var SECP256R1_N2 = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
1610
+ var XLM_DECIMALS = 7;
1611
+
1612
+ // node_modules/@noble/ciphers/esm/utils.js
1613
+ function isBytes(a) {
1614
+ return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array";
1615
+ }
1616
+ function abytes(b, ...lengths) {
1617
+ if (!isBytes(b))
1618
+ throw new Error("Uint8Array expected");
1619
+ if (lengths.length > 0 && !lengths.includes(b.length))
1620
+ throw new Error("Uint8Array expected of length " + lengths + ", got length=" + b.length);
1621
+ }
1622
+ function aexists(instance, checkFinished = true) {
1623
+ if (instance.destroyed)
1624
+ throw new Error("Hash instance has been destroyed");
1625
+ if (checkFinished && instance.finished)
1626
+ throw new Error("Hash#digest() has already been called");
1627
+ }
1628
+ function aoutput(out, instance) {
1629
+ abytes(out);
1630
+ const min = instance.outputLen;
1631
+ if (out.length < min) {
1632
+ throw new Error("digestInto() expects output buffer of length at least " + min);
1633
+ }
1634
+ }
1635
+ function u8(arr) {
1636
+ return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
1637
+ }
1638
+ function u32(arr) {
1639
+ return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
1640
+ }
1641
+ function clean(...arrays) {
1642
+ for (let i = 0; i < arrays.length; i++) {
1643
+ arrays[i].fill(0);
1644
+ }
1645
+ }
1646
+ function createView(arr) {
1647
+ return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
1648
+ }
1649
+ var isLE = /* @__PURE__ */ (() => new Uint8Array(new Uint32Array([287454020]).buffer)[0] === 68)();
1650
+ function utf8ToBytes(str) {
1651
+ if (typeof str !== "string")
1652
+ throw new Error("string expected");
1653
+ return new Uint8Array(new TextEncoder().encode(str));
1654
+ }
1655
+ function toBytes(data) {
1656
+ if (typeof data === "string")
1657
+ data = utf8ToBytes(data);
1658
+ else if (isBytes(data))
1659
+ data = copyBytes(data);
1660
+ else
1661
+ throw new Error("Uint8Array expected, got " + typeof data);
1662
+ return data;
1663
+ }
1664
+ function equalBytes(a, b) {
1665
+ if (a.length !== b.length)
1666
+ return false;
1667
+ let diff = 0;
1668
+ for (let i = 0; i < a.length; i++)
1669
+ diff |= a[i] ^ b[i];
1670
+ return diff === 0;
1671
+ }
1672
+ var wrapCipher = /* @__NO_SIDE_EFFECTS__ */ (params, constructor) => {
1673
+ function wrappedCipher(key, ...args) {
1674
+ abytes(key);
1675
+ if (!isLE)
1676
+ throw new Error("Non little-endian hardware is not yet supported");
1677
+ if (params.nonceLength !== void 0) {
1678
+ const nonce = args[0];
1679
+ if (!nonce)
1680
+ throw new Error("nonce / iv required");
1681
+ if (params.varSizeNonce)
1682
+ abytes(nonce);
1683
+ else
1684
+ abytes(nonce, params.nonceLength);
1685
+ }
1686
+ const tagl = params.tagLength;
1687
+ if (tagl && args[1] !== void 0) {
1688
+ abytes(args[1]);
1689
+ }
1690
+ const cipher = constructor(key, ...args);
1691
+ const checkOutput = (fnLength, output) => {
1692
+ if (output !== void 0) {
1693
+ if (fnLength !== 2)
1694
+ throw new Error("cipher output not supported");
1695
+ abytes(output);
1696
+ }
1697
+ };
1698
+ let called = false;
1699
+ const wrCipher = {
1700
+ encrypt(data, output) {
1701
+ if (called)
1702
+ throw new Error("cannot encrypt() twice with same key + nonce");
1703
+ called = true;
1704
+ abytes(data);
1705
+ checkOutput(cipher.encrypt.length, output);
1706
+ return cipher.encrypt(data, output);
1707
+ },
1708
+ decrypt(data, output) {
1709
+ abytes(data);
1710
+ if (tagl && data.length < tagl)
1711
+ throw new Error("invalid ciphertext length: smaller than tagLength=" + tagl);
1712
+ checkOutput(cipher.decrypt.length, output);
1713
+ return cipher.decrypt(data, output);
1714
+ }
1715
+ };
1716
+ return wrCipher;
1717
+ }
1718
+ Object.assign(wrappedCipher, params);
1719
+ return wrappedCipher;
1720
+ };
1721
+ function getOutput(expectedLength, out, onlyAligned = true) {
1722
+ if (out === void 0)
1723
+ return new Uint8Array(expectedLength);
1724
+ if (out.length !== expectedLength)
1725
+ throw new Error("invalid output length, expected " + expectedLength + ", got: " + out.length);
1726
+ if (onlyAligned && !isAligned32(out))
1727
+ throw new Error("invalid output, must be aligned");
1728
+ return out;
1729
+ }
1730
+ function setBigUint64(view, byteOffset, value, isLE2) {
1731
+ if (typeof view.setBigUint64 === "function")
1732
+ return view.setBigUint64(byteOffset, value, isLE2);
1733
+ const _32n = BigInt(32);
1734
+ const _u32_max = BigInt(4294967295);
1735
+ const wh = Number(value >> _32n & _u32_max);
1736
+ const wl = Number(value & _u32_max);
1737
+ const h = 0;
1738
+ const l = 4;
1739
+ view.setUint32(byteOffset + h, wh, isLE2);
1740
+ view.setUint32(byteOffset + l, wl, isLE2);
1741
+ }
1742
+ function u64Lengths(dataLength, aadLength, isLE2) {
1743
+ const num5 = new Uint8Array(16);
1744
+ const view = createView(num5);
1745
+ setBigUint64(view, 0, BigInt(aadLength), isLE2);
1746
+ setBigUint64(view, 8, BigInt(dataLength), isLE2);
1747
+ return num5;
1748
+ }
1749
+ function isAligned32(bytes) {
1750
+ return bytes.byteOffset % 4 === 0;
1751
+ }
1752
+ function copyBytes(bytes) {
1753
+ return Uint8Array.from(bytes);
1754
+ }
1755
+
1756
+ // node_modules/@noble/ciphers/esm/_polyval.js
1757
+ var BLOCK_SIZE = 16;
1758
+ var ZEROS16 = /* @__PURE__ */ new Uint8Array(16);
1759
+ var ZEROS32 = u32(ZEROS16);
1760
+ var POLY = 225;
1761
+ var mul2 = (s0, s1, s2, s3) => {
1762
+ const hiBit = s3 & 1;
1763
+ return {
1764
+ s3: s2 << 31 | s3 >>> 1,
1765
+ s2: s1 << 31 | s2 >>> 1,
1766
+ s1: s0 << 31 | s1 >>> 1,
1767
+ s0: s0 >>> 1 ^ POLY << 24 & -(hiBit & 1)
1768
+ // reduce % poly
1769
+ };
1770
+ };
1771
+ var swapLE = (n) => (n >>> 0 & 255) << 24 | (n >>> 8 & 255) << 16 | (n >>> 16 & 255) << 8 | n >>> 24 & 255 | 0;
1772
+ function _toGHASHKey(k) {
1773
+ k.reverse();
1774
+ const hiBit = k[15] & 1;
1775
+ let carry = 0;
1776
+ for (let i = 0; i < k.length; i++) {
1777
+ const t = k[i];
1778
+ k[i] = t >>> 1 | carry;
1779
+ carry = (t & 1) << 7;
1780
+ }
1781
+ k[0] ^= -hiBit & 225;
1782
+ return k;
1783
+ }
1784
+ var estimateWindow = (bytes) => {
1785
+ if (bytes > 64 * 1024)
1786
+ return 8;
1787
+ if (bytes > 1024)
1788
+ return 4;
1789
+ return 2;
1790
+ };
1791
+ var GHASH = class {
1792
+ // We select bits per window adaptively based on expectedLength
1793
+ constructor(key, expectedLength) {
1794
+ this.blockLen = BLOCK_SIZE;
1795
+ this.outputLen = BLOCK_SIZE;
1796
+ this.s0 = 0;
1797
+ this.s1 = 0;
1798
+ this.s2 = 0;
1799
+ this.s3 = 0;
1800
+ this.finished = false;
1801
+ key = toBytes(key);
1802
+ abytes(key, 16);
1803
+ const kView = createView(key);
1804
+ let k0 = kView.getUint32(0, false);
1805
+ let k1 = kView.getUint32(4, false);
1806
+ let k2 = kView.getUint32(8, false);
1807
+ let k3 = kView.getUint32(12, false);
1808
+ const doubles = [];
1809
+ for (let i = 0; i < 128; i++) {
1810
+ doubles.push({ s0: swapLE(k0), s1: swapLE(k1), s2: swapLE(k2), s3: swapLE(k3) });
1811
+ ({ s0: k0, s1: k1, s2: k2, s3: k3 } = mul2(k0, k1, k2, k3));
1812
+ }
1813
+ const W = estimateWindow(expectedLength || 1024);
1814
+ if (![1, 2, 4, 8].includes(W))
1815
+ throw new Error("ghash: invalid window size, expected 2, 4 or 8");
1816
+ this.W = W;
1817
+ const bits = 128;
1818
+ const windows = bits / W;
1819
+ const windowSize = this.windowSize = 2 ** W;
1820
+ const items = [];
1821
+ for (let w = 0; w < windows; w++) {
1822
+ for (let byte = 0; byte < windowSize; byte++) {
1823
+ let s0 = 0, s1 = 0, s2 = 0, s3 = 0;
1824
+ for (let j = 0; j < W; j++) {
1825
+ const bit = byte >>> W - j - 1 & 1;
1826
+ if (!bit)
1827
+ continue;
1828
+ const { s0: d0, s1: d1, s2: d2, s3: d3 } = doubles[W * w + j];
1829
+ s0 ^= d0, s1 ^= d1, s2 ^= d2, s3 ^= d3;
1830
+ }
1831
+ items.push({ s0, s1, s2, s3 });
1832
+ }
1833
+ }
1834
+ this.t = items;
1835
+ }
1836
+ _updateBlock(s0, s1, s2, s3) {
1837
+ s0 ^= this.s0, s1 ^= this.s1, s2 ^= this.s2, s3 ^= this.s3;
1838
+ const { W, t, windowSize } = this;
1839
+ let o0 = 0, o1 = 0, o2 = 0, o3 = 0;
1840
+ const mask = (1 << W) - 1;
1841
+ let w = 0;
1842
+ for (const num5 of [s0, s1, s2, s3]) {
1843
+ for (let bytePos = 0; bytePos < 4; bytePos++) {
1844
+ const byte = num5 >>> 8 * bytePos & 255;
1845
+ for (let bitPos = 8 / W - 1; bitPos >= 0; bitPos--) {
1846
+ const bit = byte >>> W * bitPos & mask;
1847
+ const { s0: e0, s1: e1, s2: e2, s3: e3 } = t[w * windowSize + bit];
1848
+ o0 ^= e0, o1 ^= e1, o2 ^= e2, o3 ^= e3;
1849
+ w += 1;
1850
+ }
1851
+ }
1852
+ }
1853
+ this.s0 = o0;
1854
+ this.s1 = o1;
1855
+ this.s2 = o2;
1856
+ this.s3 = o3;
1857
+ }
1858
+ update(data) {
1859
+ aexists(this);
1860
+ data = toBytes(data);
1861
+ abytes(data);
1862
+ const b32 = u32(data);
1863
+ const blocks = Math.floor(data.length / BLOCK_SIZE);
1864
+ const left = data.length % BLOCK_SIZE;
1865
+ for (let i = 0; i < blocks; i++) {
1866
+ this._updateBlock(b32[i * 4 + 0], b32[i * 4 + 1], b32[i * 4 + 2], b32[i * 4 + 3]);
1867
+ }
1868
+ if (left) {
1869
+ ZEROS16.set(data.subarray(blocks * BLOCK_SIZE));
1870
+ this._updateBlock(ZEROS32[0], ZEROS32[1], ZEROS32[2], ZEROS32[3]);
1871
+ clean(ZEROS32);
1872
+ }
1873
+ return this;
1874
+ }
1875
+ destroy() {
1876
+ const { t } = this;
1877
+ for (const elm of t) {
1878
+ elm.s0 = 0, elm.s1 = 0, elm.s2 = 0, elm.s3 = 0;
1879
+ }
1880
+ }
1881
+ digestInto(out) {
1882
+ aexists(this);
1883
+ aoutput(out, this);
1884
+ this.finished = true;
1885
+ const { s0, s1, s2, s3 } = this;
1886
+ const o32 = u32(out);
1887
+ o32[0] = s0;
1888
+ o32[1] = s1;
1889
+ o32[2] = s2;
1890
+ o32[3] = s3;
1891
+ return out;
1892
+ }
1893
+ digest() {
1894
+ const res = new Uint8Array(BLOCK_SIZE);
1895
+ this.digestInto(res);
1896
+ this.destroy();
1897
+ return res;
1898
+ }
1899
+ };
1900
+ var Polyval = class extends GHASH {
1901
+ constructor(key, expectedLength) {
1902
+ key = toBytes(key);
1903
+ abytes(key);
1904
+ const ghKey = _toGHASHKey(copyBytes(key));
1905
+ super(ghKey, expectedLength);
1906
+ clean(ghKey);
1907
+ }
1908
+ update(data) {
1909
+ data = toBytes(data);
1910
+ aexists(this);
1911
+ const b32 = u32(data);
1912
+ const left = data.length % BLOCK_SIZE;
1913
+ const blocks = Math.floor(data.length / BLOCK_SIZE);
1914
+ for (let i = 0; i < blocks; i++) {
1915
+ this._updateBlock(swapLE(b32[i * 4 + 3]), swapLE(b32[i * 4 + 2]), swapLE(b32[i * 4 + 1]), swapLE(b32[i * 4 + 0]));
1916
+ }
1917
+ if (left) {
1918
+ ZEROS16.set(data.subarray(blocks * BLOCK_SIZE));
1919
+ this._updateBlock(swapLE(ZEROS32[3]), swapLE(ZEROS32[2]), swapLE(ZEROS32[1]), swapLE(ZEROS32[0]));
1920
+ clean(ZEROS32);
1921
+ }
1922
+ return this;
1923
+ }
1924
+ digestInto(out) {
1925
+ aexists(this);
1926
+ aoutput(out, this);
1927
+ this.finished = true;
1928
+ const { s0, s1, s2, s3 } = this;
1929
+ const o32 = u32(out);
1930
+ o32[0] = s0;
1931
+ o32[1] = s1;
1932
+ o32[2] = s2;
1933
+ o32[3] = s3;
1934
+ return out.reverse();
1935
+ }
1936
+ };
1937
+ function wrapConstructorWithKey(hashCons) {
1938
+ const hashC = (msg, key) => hashCons(key, msg.length).update(toBytes(msg)).digest();
1939
+ const tmp = hashCons(new Uint8Array(16), 0);
1940
+ hashC.outputLen = tmp.outputLen;
1941
+ hashC.blockLen = tmp.blockLen;
1942
+ hashC.create = (key, expectedLength) => hashCons(key, expectedLength);
1943
+ return hashC;
1944
+ }
1945
+ var ghash = wrapConstructorWithKey((key, expectedLength) => new GHASH(key, expectedLength));
1946
+ wrapConstructorWithKey((key, expectedLength) => new Polyval(key, expectedLength));
1947
+
1948
+ // node_modules/@noble/ciphers/esm/aes.js
1949
+ var BLOCK_SIZE2 = 16;
1950
+ var BLOCK_SIZE32 = 4;
1951
+ var EMPTY_BLOCK = /* @__PURE__ */ new Uint8Array(BLOCK_SIZE2);
1952
+ var POLY2 = 283;
1953
+ function mul22(n) {
1954
+ return n << 1 ^ POLY2 & -(n >> 7);
1955
+ }
1956
+ function mul(a, b) {
1957
+ let res = 0;
1958
+ for (; b > 0; b >>= 1) {
1959
+ res ^= a & -(b & 1);
1960
+ a = mul22(a);
1961
+ }
1962
+ return res;
1963
+ }
1964
+ var sbox = /* @__PURE__ */ (() => {
1965
+ const t = new Uint8Array(256);
1966
+ for (let i = 0, x = 1; i < 256; i++, x ^= mul22(x))
1967
+ t[i] = x;
1968
+ const box = new Uint8Array(256);
1969
+ box[0] = 99;
1970
+ for (let i = 0; i < 255; i++) {
1971
+ let x = t[255 - i];
1972
+ x |= x << 8;
1973
+ box[t[i]] = (x ^ x >> 4 ^ x >> 5 ^ x >> 6 ^ x >> 7 ^ 99) & 255;
1974
+ }
1975
+ clean(t);
1976
+ return box;
1977
+ })();
1978
+ var rotr32_8 = (n) => n << 24 | n >>> 8;
1979
+ var rotl32_8 = (n) => n << 8 | n >>> 24;
1980
+ function genTtable(sbox2, fn) {
1981
+ if (sbox2.length !== 256)
1982
+ throw new Error("Wrong sbox length");
1983
+ const T0 = new Uint32Array(256).map((_, j) => fn(sbox2[j]));
1984
+ const T1 = T0.map(rotl32_8);
1985
+ const T2 = T1.map(rotl32_8);
1986
+ const T3 = T2.map(rotl32_8);
1987
+ const T01 = new Uint32Array(256 * 256);
1988
+ const T23 = new Uint32Array(256 * 256);
1989
+ const sbox22 = new Uint16Array(256 * 256);
1990
+ for (let i = 0; i < 256; i++) {
1991
+ for (let j = 0; j < 256; j++) {
1992
+ const idx = i * 256 + j;
1993
+ T01[idx] = T0[i] ^ T1[j];
1994
+ T23[idx] = T2[i] ^ T3[j];
1995
+ sbox22[idx] = sbox2[i] << 8 | sbox2[j];
1996
+ }
1997
+ }
1998
+ return { sbox: sbox2, sbox2: sbox22, T0, T1, T2, T3, T01, T23 };
1999
+ }
2000
+ var tableEncoding = /* @__PURE__ */ genTtable(sbox, (s) => mul(s, 3) << 24 | s << 16 | s << 8 | mul(s, 2));
2001
+ var xPowers = /* @__PURE__ */ (() => {
2002
+ const p = new Uint8Array(16);
2003
+ for (let i = 0, x = 1; i < 16; i++, x = mul22(x))
2004
+ p[i] = x;
2005
+ return p;
2006
+ })();
2007
+ function expandKeyLE(key) {
2008
+ abytes(key);
2009
+ const len = key.length;
2010
+ if (![16, 24, 32].includes(len))
2011
+ throw new Error("aes: invalid key size, should be 16, 24 or 32, got " + len);
2012
+ const { sbox2 } = tableEncoding;
2013
+ const toClean = [];
2014
+ if (!isAligned32(key))
2015
+ toClean.push(key = copyBytes(key));
2016
+ const k32 = u32(key);
2017
+ const Nk = k32.length;
2018
+ const subByte = (n) => applySbox(sbox2, n, n, n, n);
2019
+ const xk = new Uint32Array(len + 28);
2020
+ xk.set(k32);
2021
+ for (let i = Nk; i < xk.length; i++) {
2022
+ let t = xk[i - 1];
2023
+ if (i % Nk === 0)
2024
+ t = subByte(rotr32_8(t)) ^ xPowers[i / Nk - 1];
2025
+ else if (Nk > 6 && i % Nk === 4)
2026
+ t = subByte(t);
2027
+ xk[i] = xk[i - Nk] ^ t;
2028
+ }
2029
+ clean(...toClean);
2030
+ return xk;
2031
+ }
2032
+ function apply0123(T01, T23, s0, s1, s2, s3) {
2033
+ return T01[s0 << 8 & 65280 | s1 >>> 8 & 255] ^ T23[s2 >>> 8 & 65280 | s3 >>> 24 & 255];
2034
+ }
2035
+ function applySbox(sbox2, s0, s1, s2, s3) {
2036
+ return sbox2[s0 & 255 | s1 & 65280] | sbox2[s2 >>> 16 & 255 | s3 >>> 16 & 65280] << 16;
2037
+ }
2038
+ function encrypt(xk, s0, s1, s2, s3) {
2039
+ const { sbox2, T01, T23 } = tableEncoding;
2040
+ let k = 0;
2041
+ s0 ^= xk[k++], s1 ^= xk[k++], s2 ^= xk[k++], s3 ^= xk[k++];
2042
+ const rounds = xk.length / 4 - 2;
2043
+ for (let i = 0; i < rounds; i++) {
2044
+ const t02 = xk[k++] ^ apply0123(T01, T23, s0, s1, s2, s3);
2045
+ const t12 = xk[k++] ^ apply0123(T01, T23, s1, s2, s3, s0);
2046
+ const t22 = xk[k++] ^ apply0123(T01, T23, s2, s3, s0, s1);
2047
+ const t32 = xk[k++] ^ apply0123(T01, T23, s3, s0, s1, s2);
2048
+ s0 = t02, s1 = t12, s2 = t22, s3 = t32;
2049
+ }
2050
+ const t0 = xk[k++] ^ applySbox(sbox2, s0, s1, s2, s3);
2051
+ const t1 = xk[k++] ^ applySbox(sbox2, s1, s2, s3, s0);
2052
+ const t2 = xk[k++] ^ applySbox(sbox2, s2, s3, s0, s1);
2053
+ const t3 = xk[k++] ^ applySbox(sbox2, s3, s0, s1, s2);
2054
+ return { s0: t0, s1: t1, s2: t2, s3: t3 };
2055
+ }
2056
+ function ctr32(xk, isLE2, nonce, src, dst) {
2057
+ abytes(nonce, BLOCK_SIZE2);
2058
+ abytes(src);
2059
+ dst = getOutput(src.length, dst);
2060
+ const ctr = nonce;
2061
+ const c32 = u32(ctr);
2062
+ const view = createView(ctr);
2063
+ const src32 = u32(src);
2064
+ const dst32 = u32(dst);
2065
+ const ctrPos = 12;
2066
+ const srcLen = src.length;
2067
+ let ctrNum = view.getUint32(ctrPos, isLE2);
2068
+ let { s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]);
2069
+ for (let i = 0; i + 4 <= src32.length; i += 4) {
2070
+ dst32[i + 0] = src32[i + 0] ^ s0;
2071
+ dst32[i + 1] = src32[i + 1] ^ s1;
2072
+ dst32[i + 2] = src32[i + 2] ^ s2;
2073
+ dst32[i + 3] = src32[i + 3] ^ s3;
2074
+ ctrNum = ctrNum + 1 >>> 0;
2075
+ view.setUint32(ctrPos, ctrNum, isLE2);
2076
+ ({ s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]));
2077
+ }
2078
+ const start = BLOCK_SIZE2 * Math.floor(src32.length / BLOCK_SIZE32);
2079
+ if (start < srcLen) {
2080
+ const b32 = new Uint32Array([s0, s1, s2, s3]);
2081
+ const buf3 = u8(b32);
2082
+ for (let i = start, pos = 0; i < srcLen; i++, pos++)
2083
+ dst[i] = src[i] ^ buf3[pos];
2084
+ clean(b32);
2085
+ }
2086
+ return dst;
2087
+ }
2088
+ function computeTag(fn, isLE2, key, data, AAD) {
2089
+ const aadLength = AAD ? AAD.length : 0;
2090
+ const h = fn.create(key, data.length + aadLength);
2091
+ if (AAD)
2092
+ h.update(AAD);
2093
+ const num5 = u64Lengths(8 * data.length, 8 * aadLength, isLE2);
2094
+ h.update(data);
2095
+ h.update(num5);
2096
+ const res = h.digest();
2097
+ clean(num5);
2098
+ return res;
2099
+ }
2100
+ var gcm = /* @__PURE__ */ wrapCipher({ blockSize: 16, nonceLength: 12, tagLength: 16, varSizeNonce: true }, function aesgcm(key, nonce, AAD) {
2101
+ if (nonce.length < 8)
2102
+ throw new Error("aes/gcm: invalid nonce length");
2103
+ const tagLength = 16;
2104
+ function _computeTag(authKey, tagMask, data) {
2105
+ const tag = computeTag(ghash, false, authKey, data, AAD);
2106
+ for (let i = 0; i < tagMask.length; i++)
2107
+ tag[i] ^= tagMask[i];
2108
+ return tag;
2109
+ }
2110
+ function deriveKeys() {
2111
+ const xk = expandKeyLE(key);
2112
+ const authKey = EMPTY_BLOCK.slice();
2113
+ const counter = EMPTY_BLOCK.slice();
2114
+ ctr32(xk, false, counter, counter, authKey);
2115
+ if (nonce.length === 12) {
2116
+ counter.set(nonce);
2117
+ } else {
2118
+ const nonceLen = EMPTY_BLOCK.slice();
2119
+ const view = createView(nonceLen);
2120
+ setBigUint64(view, 8, BigInt(nonce.length * 8), false);
2121
+ const g = ghash.create(authKey).update(nonce).update(nonceLen);
2122
+ g.digestInto(counter);
2123
+ g.destroy();
2124
+ }
2125
+ const tagMask = ctr32(xk, false, counter, EMPTY_BLOCK);
2126
+ return { xk, authKey, counter, tagMask };
2127
+ }
2128
+ return {
2129
+ encrypt(plaintext) {
2130
+ const { xk, authKey, counter, tagMask } = deriveKeys();
2131
+ const out = new Uint8Array(plaintext.length + tagLength);
2132
+ const toClean = [xk, authKey, counter, tagMask];
2133
+ if (!isAligned32(plaintext))
2134
+ toClean.push(plaintext = copyBytes(plaintext));
2135
+ ctr32(xk, false, counter, plaintext, out.subarray(0, plaintext.length));
2136
+ const tag = _computeTag(authKey, tagMask, out.subarray(0, out.length - tagLength));
2137
+ toClean.push(tag);
2138
+ out.set(tag, plaintext.length);
2139
+ clean(...toClean);
2140
+ return out;
2141
+ },
2142
+ decrypt(ciphertext) {
2143
+ const { xk, authKey, counter, tagMask } = deriveKeys();
2144
+ const toClean = [xk, authKey, tagMask, counter];
2145
+ if (!isAligned32(ciphertext))
2146
+ toClean.push(ciphertext = copyBytes(ciphertext));
2147
+ const data = ciphertext.subarray(0, -tagLength);
2148
+ const passedTag = ciphertext.subarray(-tagLength);
2149
+ const tag = _computeTag(authKey, tagMask, data);
2150
+ toClean.push(tag);
2151
+ if (!equalBytes(tag, passedTag))
2152
+ throw new Error("aes/gcm: invalid ghash tag");
2153
+ const out = ctr32(xk, false, counter, data);
2154
+ clean(...toClean);
2155
+ return out;
2156
+ }
2157
+ };
2158
+ });
2159
+ var NONCE_LEN = 12;
2160
+ var RECOVERY_INFO = "cavos-stellar-dek-recovery";
2161
+ var PASSKEY_INFO = "cavos-stellar-dek-passkey";
2162
+ var ECIES_INFO = "cavos-stellar-dek-ecies";
2163
+ var RECOVERY_PBKDF2_ITERATIONS = 21e4;
2164
+ var RECOVERY_KDF_SALT = "cavos-stellar-recovery-v1";
2165
+ function generateDEK() {
2166
+ return randomBytes(32);
2167
+ }
2168
+ function sealControlSeed(controlSeed, dek) {
2169
+ const nonce = randomBytes(NONCE_LEN);
2170
+ const ct = gcm(dek, nonce).encrypt(controlSeed);
2171
+ return concat(nonce, ct);
2172
+ }
2173
+ function openControlSeed(sealed, dek) {
2174
+ const { nonce, ct } = splitNonce(sealed);
2175
+ return gcm(dek, nonce).decrypt(ct);
2176
+ }
2177
+ function wrapDEK(dek, kek) {
2178
+ const nonce = randomBytes(NONCE_LEN);
2179
+ return concat(nonce, gcm(kek, nonce).encrypt(dek));
2180
+ }
2181
+ function unwrapDEK(wrapped, kek) {
2182
+ const { nonce, ct } = splitNonce(wrapped);
2183
+ return gcm(kek, nonce).decrypt(ct);
2184
+ }
2185
+ function deriveRecoveryKEK(code) {
2186
+ const normalised = code.trim().replace(/\s+/g, " ").toLowerCase();
2187
+ if (!normalised) throw new Error("kit/stellar: recovery code is empty");
2188
+ const stretched = pbkdf2(sha256, new TextEncoder().encode(normalised), new TextEncoder().encode(RECOVERY_KDF_SALT), {
2189
+ c: RECOVERY_PBKDF2_ITERATIONS,
2190
+ dkLen: 32
2191
+ });
2192
+ return hkdf(sha256, stretched, void 0, RECOVERY_INFO, 32);
2193
+ }
2194
+ function derivePasskeyKEK(prfOutput) {
2195
+ if (prfOutput.length < 32) throw new Error("kit/stellar: passkey PRF output too short");
2196
+ return hkdf(sha256, prfOutput, void 0, PASSKEY_INFO, 32);
2197
+ }
2198
+ function eciesWrapDEK(dek, recipientPubSec1) {
2199
+ const ephPriv = p256.utils.randomPrivateKey();
2200
+ const ephPubCompressed = p256.getPublicKey(ephPriv, true);
2201
+ const kek = eciesKEK(p256.getSharedSecret(ephPriv, recipientPubSec1, false), ephPubCompressed);
2202
+ const wrapped = wrapDEK(dek, kek);
2203
+ return concat(ephPubCompressed, wrapped);
2204
+ }
2205
+ function eciesUnwrapDEK(blob, recipientPrivScalar) {
2206
+ const ephPubCompressed = blob.subarray(0, 33);
2207
+ const wrapped = blob.subarray(33);
2208
+ const kek = eciesKEK(p256.getSharedSecret(recipientPrivScalar, ephPubCompressed, false), ephPubCompressed);
2209
+ return unwrapDEK(wrapped, kek);
2210
+ }
2211
+ function eciesKEK(sharedUncompressed, ephPubCompressed) {
2212
+ return eciesKEKFromX(sharedUncompressed.subarray(1, 33), ephPubCompressed);
2213
+ }
2214
+ function eciesKEKFromX(sharedX, ephPubCompressed) {
2215
+ return hkdf(sha256, sharedX, ephPubCompressed, ECIES_INFO, 32);
2216
+ }
2217
+ var DATA_ENTRY_MAX = 64;
2218
+ function chunkTo64(blob) {
2219
+ if (blob.length <= DATA_ENTRY_MAX) return [blob];
2220
+ const chunks = [];
2221
+ for (let i = 0; i < blob.length; i += DATA_ENTRY_MAX) {
2222
+ chunks.push(blob.subarray(i, i + DATA_ENTRY_MAX));
2223
+ }
2224
+ return chunks;
2225
+ }
2226
+ function unchunk(chunks) {
2227
+ const total = chunks.reduce((n, c) => n + c.length, 0);
2228
+ const out = new Uint8Array(total);
2229
+ let off = 0;
2230
+ for (const c of chunks) {
2231
+ out.set(c, off);
2232
+ off += c.length;
2233
+ }
2234
+ return out;
2235
+ }
2236
+ function concat(a, b) {
2237
+ const out = new Uint8Array(a.length + b.length);
2238
+ out.set(a, 0);
2239
+ out.set(b, a.length);
2240
+ return out;
2241
+ }
2242
+ function splitNonce(blob) {
2243
+ return { nonce: blob.subarray(0, NONCE_LEN), ct: blob.subarray(NONCE_LEN) };
2244
+ }
2245
+
2246
+ // src/chains/stellar/datamap.ts
2247
+ var CT_BASE = "cv:ct";
2248
+ var PASSKEY_BASE = "cv:wp";
2249
+ var RECOVERY_BASE = "cv:wr";
2250
+ var VERSION_KEY = "cv:v";
2251
+ var ENVELOPE_VERSION = 1;
2252
+ function deviceBase(slot) {
2253
+ return `cv:wd:${slot}`;
2254
+ }
2255
+ function toDataEntries(env) {
2256
+ const out = {};
2257
+ writeChunked(out, CT_BASE, env.ct);
2258
+ for (const [slot, blob] of Object.entries(env.deviceWraps)) {
2259
+ writeChunked(out, deviceBase(slot), blob);
2260
+ }
2261
+ if (env.passkeyWrap) writeChunked(out, PASSKEY_BASE, env.passkeyWrap);
2262
+ if (env.recoveryWrap) writeChunked(out, RECOVERY_BASE, env.recoveryWrap);
2263
+ out[VERSION_KEY] = Uint8Array.of(ENVELOPE_VERSION);
2264
+ return out;
2265
+ }
2266
+ function fromDataEntries(entries) {
2267
+ const ct = readChunked(entries, CT_BASE);
2268
+ if (!ct) throw new Error("kit/stellar: account has no control-seed ciphertext (cv:ct)");
2269
+ const deviceWraps = {};
2270
+ const seenSlots = /* @__PURE__ */ new Set();
2271
+ for (const key of Object.keys(entries)) {
2272
+ const m = key.match(/^cv:wd:([^/]+)\/\d+$/);
2273
+ if (m) seenSlots.add(m[1]);
2274
+ }
2275
+ for (const slot of seenSlots) {
2276
+ const blob = readChunked(entries, deviceBase(slot));
2277
+ if (blob) deviceWraps[slot] = blob;
2278
+ }
2279
+ return {
2280
+ ct,
2281
+ deviceWraps,
2282
+ passkeyWrap: readChunked(entries, PASSKEY_BASE),
2283
+ recoveryWrap: readChunked(entries, RECOVERY_BASE)
2284
+ };
2285
+ }
2286
+ function deviceWrapEntries(slot, blob) {
2287
+ const out = {};
2288
+ writeChunked(out, deviceBase(slot), blob);
2289
+ return out;
2290
+ }
2291
+ function writeChunked(out, base, blob) {
2292
+ const chunks = chunkTo64(blob);
2293
+ chunks.forEach((chunk, i) => {
2294
+ out[`${base}/${i}`] = chunk;
2295
+ });
2296
+ }
2297
+ function readChunked(entries, base) {
2298
+ const chunks = [];
2299
+ for (let i = 0; ; i++) {
2300
+ const chunk = entries[`${base}/${i}`];
2301
+ if (!chunk) break;
2302
+ chunks.push(chunk);
2303
+ }
2304
+ return chunks.length ? unchunk(chunks) : void 0;
2305
+ }
2306
+ var TX_TIMEOUT = 180;
1553
2307
  var StellarAdapter = class {
1554
2308
  constructor(opts) {
1555
2309
  this.chain = "stellar";
1556
2310
  this.network = opts.network;
1557
2311
  this.passphrase = STELLAR_NETWORKS[opts.network].passphrase;
1558
- this.rpcUrl = opts.rpcUrl ?? STELLAR_NETWORKS[opts.network].rpcUrl;
1559
- this.factoryId = opts.factoryId ?? FACTORY_CONTRACT_ID[opts.network];
1560
- if (!this.factoryId) {
1561
- throw new Error(`kit/stellar: no factory contract id configured for ${opts.network}`);
1562
- }
1563
- this.signer = opts.signer;
2312
+ this.horizonUrl = opts.horizonUrl ?? HORIZON_URL[opts.network];
1564
2313
  }
1565
2314
  server() {
1566
2315
  if (!this._server) {
1567
- this._server = new rpc.Server(this.rpcUrl, {
1568
- allowHttp: this.rpcUrl.startsWith("http://")
2316
+ this._server = new Horizon.Server(this.horizonUrl, {
2317
+ allowHttp: this.horizonUrl.startsWith("http://")
1569
2318
  });
1570
2319
  }
1571
2320
  return this._server;
1572
2321
  }
1573
- networkId() {
1574
- return hash$1(Buffer.from(this.passphrase));
2322
+ /** Whether the classic account already exists on-chain. */
2323
+ async isDeployed(address) {
2324
+ try {
2325
+ await this.server().loadAccount(address);
2326
+ return true;
2327
+ } catch (e) {
2328
+ if (isNotFound(e)) return false;
2329
+ throw e;
2330
+ }
2331
+ }
2332
+ /** Read the account's `MANAGE_DATA` entries as raw bytes (name → value). */
2333
+ async loadDataEntries(address) {
2334
+ const account = await this.server().loadAccount(address);
2335
+ const out = {};
2336
+ for (const [name, b64] of Object.entries(account.data_attr ?? {})) {
2337
+ out[name] = new Uint8Array(Buffer.from(b64, "base64"));
2338
+ }
2339
+ return out;
2340
+ }
2341
+ /** Native XLM balance in stroops. Returns 0n if the account doesn't exist. */
2342
+ async balance(address) {
2343
+ try {
2344
+ const account = await this.server().loadAccount(address);
2345
+ const native = account.balances.find((b) => b.asset_type === "native");
2346
+ return native ? toStroops(native.balance) : 0n;
2347
+ } catch (e) {
2348
+ if (isNotFound(e)) return 0n;
2349
+ throw e;
2350
+ }
1575
2351
  }
1576
2352
  /**
1577
- * Deterministic account address for `(addressSeed, initialSigner)` computed
1578
- * off-chain, byte-identical to the factory's on-chain `account_address`.
1579
- * `contractId = sha256(HashIdPreimage(networkId, factory, salt))` with
1580
- * `salt = sha256(addressSeed || sec1(initialSigner))`.
2353
+ * Build the account-creation transaction (source = funder, the relayer or a
2354
+ * self-funded keypair):
2355
+ * 1. `createAccount` funds the deterministic `G…` master address,
2356
+ * 2. `manageData` writes the control-key envelope entries (authorized by the
2357
+ * still-weight-1 master),
2358
+ * 3. `setOptions` adds the control signer (weight 1), sets all thresholds to
2359
+ * 1, and zeroes the master weight — after this the master can never sign.
2360
+ *
2361
+ * The returned tx must be signed by BOTH the master keypair (for the account's
2362
+ * own ops) and the funder (source + fee). Sponsorship of reserves is layered on
2363
+ * in Phase 3.
1581
2364
  */
1582
- computeAddress(addressSeed, initialSigner) {
1583
- const salt = this.accountSalt(addressSeed, initialSigner);
1584
- const preimage = xdr.HashIdPreimage.envelopeTypeContractId(
1585
- new xdr.HashIdPreimageContractId({
1586
- networkId: this.networkId(),
1587
- contractIdPreimage: xdr.ContractIdPreimage.contractIdPreimageFromAddress(
1588
- new xdr.ContractIdPreimageFromAddress({
1589
- address: new Address(this.factoryId).toScAddress(),
1590
- salt
1591
- })
1592
- )
2365
+ async buildCreateTx(params) {
2366
+ const funderAccount = await this.server().loadAccount(params.funder);
2367
+ const builder = new TransactionBuilder(funderAccount, {
2368
+ fee: BASE_FEE,
2369
+ networkPassphrase: this.passphrase
2370
+ });
2371
+ builder.addOperation(
2372
+ Operation.createAccount({
2373
+ destination: params.masterAddress,
2374
+ startingBalance: fromStroops(params.startingBalance)
1593
2375
  })
1594
2376
  );
1595
- return StrKey.encodeContract(hash$1(preimage.toXDR()));
1596
- }
1597
- /** `salt = sha256(addressSeed(32) || sec1(initialSigner)(65))` — matches the factory. */
1598
- accountSalt(addressSeed, initialSigner) {
1599
- return hash$1(Buffer.concat([Buffer.from(addressSeed), Buffer.from(sec1Pubkey(initialSigner))]));
1600
- }
1601
- /** Host function: `factory.deploy(address_seed, initial_signer)`. */
1602
- buildDeploy(addressSeed, initialSigner) {
1603
- return invokeFunc(this.factoryId, "deploy", [
1604
- bytesScVal(addressSeed),
1605
- bytesScVal(sec1Pubkey(initialSigner))
1606
- ]);
1607
- }
1608
- /** Host function: `account.add_signer(new_signer)` (requires device auth). */
1609
- buildAddSigner(accountAddress, signer) {
1610
- return invokeFunc(accountAddress, "add_signer", [bytesScVal(sec1Pubkey(signer))]);
1611
- }
1612
- /** Host function: `account.remove_signer(signer)` (requires device auth). */
1613
- buildRemoveSigner(accountAddress, signer) {
1614
- return invokeFunc(accountAddress, "remove_signer", [bytesScVal(sec1Pubkey(signer))]);
1615
- }
1616
- /** Host function: `account.add_approver(passkey)` (requires device auth). */
1617
- buildAddApprover(accountAddress, passkey) {
1618
- return invokeFunc(accountAddress, "add_approver", [bytesScVal(sec1Pubkey(passkey))]);
1619
- }
1620
- /** Host function: `account.remove_approver(passkey)` (requires device auth). */
1621
- buildRemoveApprover(accountAddress, passkey) {
1622
- return invokeFunc(accountAddress, "remove_approver", [bytesScVal(sec1Pubkey(passkey))]);
1623
- }
1624
- /** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
1625
- * `sha256(sec1(new_signer) || nonce_be8)`. The batch challenge the passkey signs
1626
- * is `sha256(concat(leaves))` across chains. */
1627
- passkeyLeaf(newSigner, nonce) {
1628
- const msg = new Uint8Array(65 + 8);
1629
- msg.set(sec1Pubkey(newSigner), 0);
1630
- const n = new Uint8Array(8);
1631
- let v = nonce;
1632
- for (let i = 7; i >= 0; i--) {
1633
- n[i] = Number(v & 0xffn);
1634
- v >>= 8n;
1635
- }
1636
- msg.set(n, 65);
1637
- return sha256(msg);
1638
- }
1639
- /** Host function: passkey-authorized `add_signer_via_passkey` (no device auth —
1640
- * authorized by the embedded WebAuthn assertion, so any relayer can submit).
1641
- * `leaves`/`leafIndex` place this chain's leaf in the multi-chain batch. */
1642
- buildAddSignerViaPasskey(accountAddress, newSigner, passkey, nonce, leaves, leafIndex, assertion) {
1643
- const sig = encodeLowSSignature2({ r: assertion.r, s: assertion.s});
1644
- const leavesScVal = xdr.ScVal.scvVec(leaves.map((l) => bytesScVal(l)));
1645
- return invokeFunc(accountAddress, "add_signer_via_passkey", [
1646
- bytesScVal(sec1Pubkey(newSigner)),
1647
- bytesScVal(sec1Pubkey(passkey)),
1648
- nativeToScVal(nonce, { type: "u64" }),
1649
- leavesScVal,
1650
- nativeToScVal(leafIndex, { type: "u32" }),
1651
- bytesScVal(assertion.authenticatorData),
1652
- bytesScVal(assertion.clientDataJSON),
1653
- nativeToScVal(assertion.challengeOffset, { type: "u32" }),
1654
- bytesScVal(sig)
1655
- ]);
1656
- }
1657
- /** Read whether `passkey` is a registered approver (read-only simulation). */
1658
- async isApprover(accountAddress, passkey, readSource) {
1659
- if (!await this.isDeployed(accountAddress)) return false;
1660
- const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1661
- const src = new Account3(readSource, "0");
1662
- const op = Operation.invokeHostFunction({
1663
- func: invokeFunc(accountAddress, "is_approver", [bytesScVal(sec1Pubkey(passkey))]),
1664
- auth: []
1665
- });
1666
- const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
1667
- const sim = await this.server().simulateTransaction(tx2);
1668
- if (rpc.Api.isSimulationError(sim)) {
1669
- throw new Error(`kit/stellar: is_approver simulation failed: ${sim.error}`);
1670
- }
1671
- if (!sim.result?.retval) return false;
1672
- return scValToNative(sim.result.retval) === true;
1673
- }
1674
- /** Read the current passkey-approval nonce (read-only simulation). */
1675
- async passkeyNonce(accountAddress, readSource) {
1676
- if (!await this.isDeployed(accountAddress)) return 0n;
1677
- const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1678
- const src = new Account3(readSource, "0");
1679
- const op = Operation.invokeHostFunction({
1680
- func: invokeFunc(accountAddress, "passkey_nonce", []),
1681
- auth: []
1682
- });
1683
- const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
1684
- const sim = await this.server().simulateTransaction(tx2);
1685
- if (rpc.Api.isSimulationError(sim)) {
1686
- throw new Error(`kit/stellar: passkey_nonce simulation failed: ${sim.error}`);
1687
- }
1688
- if (!sim.result?.retval) return 0n;
1689
- return BigInt(scValToNative(sim.result.retval));
1690
- }
1691
- /** Host function: SEP-41 `token.transfer(from=account, to, amount)` (device auth). */
1692
- buildTransfer(tokenId, accountAddress, destination, amount) {
1693
- return invokeFunc(tokenId, "transfer", [
1694
- new Address(accountAddress).toScVal(),
1695
- new Address(destination).toScVal(),
1696
- nativeToScVal(amount, { type: "i128" })
1697
- ]);
2377
+ for (const [name, value] of Object.entries(toDataEntries(params.envelope))) {
2378
+ builder.addOperation(
2379
+ Operation.manageData({ name, value: Buffer.from(value), source: params.masterAddress })
2380
+ );
2381
+ }
2382
+ builder.addOperation(
2383
+ Operation.setOptions({
2384
+ source: params.masterAddress,
2385
+ masterWeight: 0,
2386
+ lowThreshold: 1,
2387
+ medThreshold: 1,
2388
+ highThreshold: 1,
2389
+ signer: { ed25519PublicKey: params.controlAddress, weight: 1 }
2390
+ })
2391
+ );
2392
+ return builder.setTimeout(TX_TIMEOUT).build();
1698
2393
  }
1699
2394
  /**
1700
- * Sign a Soroban authorization entry with the silent device key, producing the
1701
- * `Vec<DeviceSignature>` the account's `__check_auth` verifies. The device
1702
- * signs `sha256(preimage)` (WebCrypto hashes once more internally), which is
1703
- * exactly what the contract recomputes. Mutates + returns the entry.
2395
+ * Build a **sponsored** account-creation transaction whose source is the
2396
+ * relayer. Wraps the account setup in `beginSponsoringFutureReserves` /
2397
+ * `endSponsoringFutureReserves`, so the relayer (not the user) pays every
2398
+ * reserve the account is created with a 0 starting balance and holds no
2399
+ * locked XLM of the user's. Ops:
2400
+ * 0. beginSponsoringFutureReserves(G) source = relayer
2401
+ * 1. createAccount(G, 0) source = relayer
2402
+ * 2. manageData(cv:… envelope) source = G (master-signed)
2403
+ * 3. setOptions(control signer, master → 0) source = G
2404
+ * 4. endSponsoringFutureReserves() source = G
2405
+ *
2406
+ * Signed by the master (for the G ops, while it's still weight 1); the relayer
2407
+ * co-signs (source + fee + sponsorship) before submitting.
1704
2408
  */
1705
- async signAuthEntry(entry, validUntilLedger) {
1706
- const addrCreds = entry.credentials().address();
1707
- addrCreds.signatureExpirationLedger(validUntilLedger);
1708
- const preimage = xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
1709
- new xdr.HashIdPreimageSorobanAuthorization({
1710
- networkId: this.networkId(),
1711
- nonce: addrCreds.nonce(),
1712
- signatureExpirationLedger: validUntilLedger,
1713
- invocation: entry.rootInvocation()
2409
+ async buildSponsoredCreateTx(params) {
2410
+ const relayerAccount = await this.server().loadAccount(params.relayer);
2411
+ const builder = new TransactionBuilder(relayerAccount, {
2412
+ fee: BASE_FEE,
2413
+ networkPassphrase: this.passphrase
2414
+ });
2415
+ builder.addOperation(
2416
+ Operation.beginSponsoringFutureReserves({ sponsoredId: params.masterAddress, source: params.relayer })
2417
+ );
2418
+ builder.addOperation(
2419
+ Operation.createAccount({ destination: params.masterAddress, startingBalance: "0", source: params.relayer })
2420
+ );
2421
+ for (const [name, value] of Object.entries(toDataEntries(params.envelope))) {
2422
+ builder.addOperation(
2423
+ Operation.manageData({ name, value: Buffer.from(value), source: params.masterAddress })
2424
+ );
2425
+ }
2426
+ builder.addOperation(
2427
+ Operation.setOptions({
2428
+ source: params.masterAddress,
2429
+ masterWeight: 0,
2430
+ lowThreshold: 1,
2431
+ medThreshold: 1,
2432
+ highThreshold: 1,
2433
+ signer: { ed25519PublicKey: params.controlAddress, weight: 1 }
1714
2434
  })
1715
2435
  );
1716
- const payload = hash$1(preimage.toXDR());
1717
- const sig = await this.signer.sign(new Uint8Array(payload));
1718
- const pubkey = await this.signer.getPublicKey();
1719
- addrCreds.signature(deviceSignatureScVal(pubkey, sig));
1720
- return entry;
2436
+ builder.addOperation(Operation.endSponsoringFutureReserves({ source: params.masterAddress }));
2437
+ return builder.setTimeout(TX_TIMEOUT).build();
1721
2438
  }
1722
2439
  /**
1723
- * Read a SEP-41 token balance of `account` via a read-only simulation of
1724
- * `token.balance(account)`. Returns 0 when the account isn't deployed or holds
1725
- * none. `readSource` is any funded G-account (used only for the simulation).
2440
+ * Build a classic native-XLM payment as an *inner* transaction whose source is
2441
+ * the account itself (`G…`), signed by the control key. Wrap it in a fee-bump
2442
+ * (see `wrapFeeBump`) so the relayer pays the fee gasless.
1726
2443
  */
1727
- async readBalance(tokenId, account, readSource) {
1728
- if (!await this.isDeployed(account)) return 0n;
1729
- const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1730
- const src = new Account3(readSource, "0");
1731
- const op = Operation.invokeHostFunction({
1732
- func: invokeFunc(tokenId, "balance", [new Address(account).toScVal()]),
1733
- auth: []
1734
- });
1735
- const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
1736
- const sim = await this.server().simulateTransaction(tx2);
1737
- if (rpc.Api.isSimulationError(sim) || !sim.result?.retval) return 0n;
1738
- return BigInt(scValToNative(sim.result.retval));
2444
+ async buildPaymentTx(params) {
2445
+ const source = await this.server().loadAccount(params.from);
2446
+ return new TransactionBuilder(source, { fee: BASE_FEE, networkPassphrase: this.passphrase }).addOperation(
2447
+ Operation.payment({
2448
+ destination: params.to,
2449
+ asset: Asset.native(),
2450
+ amount: fromStroops(params.amount)
2451
+ })
2452
+ ).setTimeout(TX_TIMEOUT).build();
1739
2453
  }
1740
- /** Whether the account contract instance exists on-chain (is deployed). */
1741
- async isDeployed(accountAddress) {
1742
- try {
1743
- const res = await this.server().getContractData(
1744
- accountAddress,
1745
- xdr.ScVal.scvLedgerKeyContractInstance(),
1746
- rpc.Durability.Persistent
1747
- );
1748
- return !!res;
1749
- } catch {
1750
- return false;
2454
+ /**
2455
+ * Build a data-entry write (e.g. re-wrapping the DEK for a newly approved
2456
+ * device) as an inner tx sourced by the account, signed by the control key.
2457
+ */
2458
+ async buildDataTx(params) {
2459
+ const source = await this.server().loadAccount(params.account);
2460
+ const builder = new TransactionBuilder(source, { fee: BASE_FEE, networkPassphrase: this.passphrase });
2461
+ for (const [name, value] of Object.entries(params.entries)) {
2462
+ builder.addOperation(Operation.manageData({ name, value: Buffer.from(value) }));
1751
2463
  }
2464
+ return builder.setTimeout(TX_TIMEOUT).build();
1752
2465
  }
1753
2466
  /**
1754
- * Read whether `signer` is a currently-authorized signer of the account, via a
1755
- * read-only simulation of `account.is_authorized(signer)`. `readSource` is any
1756
- * funded G-account (used only for the simulation's source/sequence).
2467
+ * Build a **sponsored** data-entry write whose source is the relayer, so the
2468
+ * relayer (not the 0-balance account) pays the reserve of any NEW subentry.
2469
+ * Adding a factor (passkey / recovery) or a device slot creates a data entry
2470
+ * that needs ~0.5 XLM of reserve; a sponsored account holds no XLM, so — like
2471
+ * account creation — the write must be wrapped in begin/endSponsoringFuture
2472
+ * reserves with the relayer as sponsor. Ops:
2473
+ * 0. beginSponsoringFutureReserves(account) source = relayer
2474
+ * 1..n. manageData(cv:…) source = account (control-signed)
2475
+ * last. endSponsoringFutureReserves() source = account
2476
+ *
2477
+ * Signed by the control key (account ops) + the relayer (source + fee + sponsor).
1757
2478
  */
1758
- async isAuthorizedSigner(accountAddress, signer, readSource) {
1759
- if (!await this.isDeployed(accountAddress)) return false;
1760
- const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1761
- const src = new Account3(readSource, "0");
1762
- const op = Operation.invokeHostFunction({
1763
- func: invokeFunc(accountAddress, "is_authorized", [bytesScVal(sec1Pubkey(signer))]),
1764
- auth: []
2479
+ async buildSponsoredDataTx(params) {
2480
+ const relayerAccount = await this.server().loadAccount(params.relayer);
2481
+ const builder = new TransactionBuilder(relayerAccount, {
2482
+ fee: BASE_FEE,
2483
+ networkPassphrase: this.passphrase
1765
2484
  });
1766
- const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
1767
- const sim = await this.server().simulateTransaction(tx2);
1768
- if (rpc.Api.isSimulationError(sim)) {
1769
- throw new Error(`kit/stellar: is_authorized simulation failed: ${sim.error}`);
2485
+ builder.addOperation(
2486
+ Operation.beginSponsoringFutureReserves({ sponsoredId: params.account, source: params.relayer })
2487
+ );
2488
+ for (const [name, value] of Object.entries(params.entries)) {
2489
+ builder.addOperation(Operation.manageData({ name, value: Buffer.from(value), source: params.account }));
1770
2490
  }
1771
- if (!sim.result?.retval) return false;
1772
- return scValToNative(sim.result.retval) === true;
2491
+ builder.addOperation(Operation.endSponsoringFutureReserves({ source: params.account }));
2492
+ return builder.setTimeout(TX_TIMEOUT).build();
2493
+ }
2494
+ /** Wrap a control-signed inner tx in a fee-bump whose fee source is `feeSource`
2495
+ * (the relayer). The inner tx pays nothing; the relayer pays all fees. */
2496
+ wrapFeeBump(inner, feeSource) {
2497
+ return TransactionBuilder.buildFeeBumpTransaction(feeSource, BASE_FEE, inner, this.passphrase);
2498
+ }
2499
+ /** Submit a fully-signed classic transaction and return its hash. Throws with
2500
+ * the Horizon result codes on failure. */
2501
+ async submit(tx3) {
2502
+ try {
2503
+ const res = await this.server().submitTransaction(tx3);
2504
+ return res.hash;
2505
+ } catch (e) {
2506
+ throw new Error(`kit/stellar: submit failed: ${horizonError(e)}`);
2507
+ }
2508
+ }
2509
+ /** Build an `Account` handle for a known address + sequence (avoids a Horizon
2510
+ * round-trip when the caller already has the sequence). */
2511
+ accountAt(address, sequence) {
2512
+ return new Account(address, sequence);
1773
2513
  }
1774
2514
  };
1775
- function sec1Pubkey(pk) {
1776
- const out = new Uint8Array(65);
1777
- out[0] = 4;
1778
- out.set(bigIntTo32Bytes(pk.x), 1);
1779
- out.set(bigIntTo32Bytes(pk.y), 33);
1780
- return out;
2515
+ function isNotFound(e) {
2516
+ const status = e?.response?.status ?? e?.status;
2517
+ return status === 404;
1781
2518
  }
1782
- function encodeLowSSignature2(sig) {
1783
- const lowS2 = sig.s > SECP256R1_N2 / 2n ? SECP256R1_N2 - sig.s : sig.s;
1784
- const out = new Uint8Array(64);
1785
- out.set(bigIntTo32Bytes(sig.r), 0);
1786
- out.set(bigIntTo32Bytes(lowS2), 32);
1787
- return out;
2519
+ function horizonError(e) {
2520
+ const codes = e?.response?.data?.extras?.result_codes;
2521
+ return codes ? JSON.stringify(codes) : String(e?.message ?? e);
1788
2522
  }
1789
- function deviceSignatureScVal(pubkey, sig) {
1790
- const element = nativeToScVal(
1791
- {
1792
- public_key: Buffer.from(sec1Pubkey(pubkey)),
1793
- signature: Buffer.from(encodeLowSSignature2(sig))
1794
- },
1795
- { type: { public_key: ["symbol", "bytes"], signature: ["symbol", "bytes"] } }
1796
- );
1797
- return xdr.ScVal.scvVec([element]);
1798
- }
1799
- function invokeFunc(contractId, method, args) {
1800
- return xdr.HostFunction.hostFunctionTypeInvokeContract(
1801
- new xdr.InvokeContractArgs({
1802
- contractAddress: new Address(contractId).toScAddress(),
1803
- functionName: method,
1804
- args
1805
- })
1806
- );
2523
+ function toStroops(amount) {
2524
+ const [whole, frac = ""] = amount.split(".");
2525
+ const fracPadded = (frac + "0000000").slice(0, 7);
2526
+ return BigInt(whole) * 10000000n + BigInt(fracPadded);
2527
+ }
2528
+ function fromStroops(stroops) {
2529
+ const neg = stroops < 0n;
2530
+ const abs = neg ? -stroops : stroops;
2531
+ const whole = abs / 10000000n;
2532
+ const frac = (abs % 10000000n).toString().padStart(7, "0");
2533
+ return `${neg ? "-" : ""}${whole}.${frac}`;
2534
+ }
2535
+ var MASTER_HKDF_INFO = "cavos-stellar-master";
2536
+ function deriveStellarMasterSeed(identity) {
2537
+ const ikm = deriveAddressSeedStellar(identity);
2538
+ return hkdf(sha256, ikm, void 0, MASTER_HKDF_INFO, 32);
2539
+ }
2540
+ function deriveStellarMasterKeypair(identity) {
2541
+ return Keypair.fromRawEd25519Seed(Buffer.from(deriveStellarMasterSeed(identity)));
2542
+ }
2543
+ function deriveStellarAddress(identity) {
2544
+ return deriveStellarMasterKeypair(identity).publicKey();
1807
2545
  }
1808
- function bytesScVal(bytes) {
1809
- return xdr.ScVal.scvBytes(Buffer.from(bytes));
2546
+ function generateControlKey() {
2547
+ const seed = randomBytes(32);
2548
+ return { keypair: Keypair.fromRawEd25519Seed(Buffer.from(seed)), seed };
2549
+ }
2550
+ function controlKeypairFromSeed(seed) {
2551
+ if (seed.length !== 32) throw new Error("kit/stellar: control seed must be 32 bytes");
2552
+ return Keypair.fromRawEd25519Seed(Buffer.from(seed));
1810
2553
  }
1811
2554
 
1812
2555
  // src/chains/stellar/StellarRelayer.ts
@@ -1814,7 +2557,7 @@ var StellarRelayer = class {
1814
2557
  constructor(opts) {
1815
2558
  this.opts = opts;
1816
2559
  }
1817
- /** The relayer's source/fee-payer G-account (fetched + cached from the backend). */
2560
+ /** The relayer's source/fee-payer/sponsor G-account (fetched + cached). */
1818
2561
  async getSource() {
1819
2562
  if (this.source) return this.source;
1820
2563
  const res = await fetch(`${this.opts.baseUrl}/api/stellar/relay?network=${this.opts.network}`);
@@ -1823,17 +2566,16 @@ var StellarRelayer = class {
1823
2566
  this.source = fee_payer;
1824
2567
  return this.source;
1825
2568
  }
1826
- /**
1827
- * POST the assembled, device-authorized transaction XDR to the relayer to sign
1828
- * the envelope + submit. Returns the confirmed transaction hash.
1829
- */
1830
- async submit(transactionXdr) {
2569
+ /** POST a (partially) signed transaction XDR for the relayer to co-sign + submit.
2570
+ * `kind` selects the validation gate. Returns the confirmed transaction hash. */
2571
+ async submit(kind, transactionXdr) {
1831
2572
  const res = await fetch(`${this.opts.baseUrl}/api/stellar/relay`, {
1832
2573
  method: "POST",
1833
2574
  headers: { "Content-Type": "application/json" },
1834
2575
  body: JSON.stringify({
1835
2576
  app_id: this.opts.appId,
1836
2577
  network: this.opts.network,
2578
+ kind,
1837
2579
  transaction: transactionXdr
1838
2580
  })
1839
2581
  });
@@ -1841,294 +2583,413 @@ var StellarRelayer = class {
1841
2583
  const detail = await res.text().catch(() => "");
1842
2584
  throw new Error(`kit/stellar: relay failed (${res.status}) ${detail}`);
1843
2585
  }
1844
- const { hash: hash6 } = await res.json();
1845
- return hash6;
2586
+ const { hash: hash5 } = await res.json();
2587
+ return hash5;
1846
2588
  }
1847
2589
  };
2590
+
2591
+ // src/chains/stellar/CavosStellar.ts
2592
+ var DEFAULT_STARTING_BALANCE = 50000000n;
1848
2593
  var CavosStellar = class _CavosStellar {
1849
- constructor(identity, address, status, network, adapter, devicePubkey, relayer, sourceKeypair) {
2594
+ constructor(identity, address, status, network, adapter, deviceKey, control, dek, relayer) {
1850
2595
  this.identity = identity;
1851
2596
  this.address = address;
1852
- this.status = status;
1853
2597
  this.network = network;
1854
2598
  this.adapter = adapter;
1855
- this.devicePubkey = devicePubkey;
2599
+ this.deviceKey = deviceKey;
2600
+ this.control = control;
2601
+ this.dek = dek;
1856
2602
  this.relayer = relayer;
1857
- this.sourceKeypair = sourceKeypair;
1858
- /** Discriminant for the `CavosWallet` union narrows `execute()` per chain. */
2603
+ // Discriminant for the `CavosWallet` union. Classic `G…` IS the Stellar chain
2604
+ // now (the Soroban `C…` path was removed), so this is "stellar".
1859
2605
  this.chain = "stellar";
2606
+ this.isNewAccount = false;
2607
+ this.statusValue = status;
1860
2608
  }
1861
- get publicKey() {
1862
- return this.devicePubkey;
2609
+ get status() {
2610
+ return this.statusValue;
1863
2611
  }
1864
2612
  static async connect(opts) {
1865
2613
  const identity = opts.identity ?? await opts.auth?.authenticate();
1866
2614
  if (!identity) throw new Error("kit/stellar: connect requires `identity` or `auth`");
1867
- const signer = opts.createSigner ? await opts.createSigner(`${identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${identity.userId}:${opts.appSalt}` });
1868
- const devicePubkey = await signer.getPublicKey();
1869
- const adapter = new StellarAdapter({
1870
- network: opts.network,
1871
- rpcUrl: opts.rpcUrl,
1872
- factoryId: opts.factoryId,
1873
- signer
1874
- });
1875
- const addressSeed = deriveAddressSeedStellar({ userId: identity.userId, appSalt: opts.appSalt });
2615
+ const adapter = new StellarAdapter({ network: opts.network, horizonUrl: opts.horizonUrl });
2616
+ const master = deriveStellarMasterKeypair({ userId: identity.userId, appSalt: opts.appSalt });
2617
+ const address = master.publicKey();
2618
+ const startingBalance = opts.startingBalance ?? DEFAULT_STARTING_BALANCE;
1876
2619
  const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
1877
- const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
1878
2620
  const relayer = opts.relayer ?? (opts.appId ? new StellarRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : void 0);
1879
- const build = (address2, status) => new _CavosStellar(identity, address2, status, opts.network, adapter, devicePubkey, relayer, opts.sourceKeypair);
1880
- const self = build("", "needs-device-approval");
1881
- const readSource = await self.resolveSource();
1882
- const existing = await registry.lookup(identity.userId);
1883
- if (existing) {
1884
- const isSigner2 = await adapter.isAuthorizedSigner(existing.address, devicePubkey, readSource);
1885
- return build(existing.address, isSigner2 ? "ready" : "needs-device-approval");
2621
+ const build = (status, unlocked) => new _CavosStellar(
2622
+ identity,
2623
+ address,
2624
+ status,
2625
+ opts.network,
2626
+ adapter,
2627
+ opts.deviceKey,
2628
+ unlocked?.control,
2629
+ unlocked?.dek,
2630
+ relayer
2631
+ );
2632
+ if (await adapter.isDeployed(address)) {
2633
+ const unlocked = await unlockViaDevice(adapter, address, opts.deviceKey);
2634
+ return build(unlocked ? "ready" : "needs-device-approval", unlocked ?? void 0);
1886
2635
  }
1887
- const address = adapter.computeAddress(addressSeed, devicePubkey);
1888
- if (!await adapter.isDeployed(address)) {
1889
- const func = adapter.buildDeploy(addressSeed, devicePubkey);
1890
- await self.submitHostFunction(func, void 0);
2636
+ if (!relayer && !opts.sourceKeypair) {
2637
+ throw new Error("kit/stellar: a relayer (appId) or sourceKeypair is required to create the account");
2638
+ }
2639
+ const { keypair: control, seed: controlSeed } = generateControlKey();
2640
+ const dek = generateDEK();
2641
+ const envelope = {
2642
+ ct: sealControlSeed(controlSeed, dek),
2643
+ deviceWraps: { [opts.deviceKey.slotId()]: eciesWrapDEK(dek, opts.deviceKey.publicKeySec1()) }
2644
+ };
2645
+ if (relayer) {
2646
+ const relayerSource = await relayer.getSource();
2647
+ const tx3 = await adapter.buildSponsoredCreateTx({
2648
+ relayer: relayerSource,
2649
+ masterAddress: address,
2650
+ controlAddress: control.publicKey(),
2651
+ envelope
2652
+ });
2653
+ tx3.sign(master);
2654
+ await relayer.submit("create", tx3.toXDR());
2655
+ } else {
2656
+ const funder = opts.sourceKeypair;
2657
+ const tx3 = await adapter.buildCreateTx({
2658
+ funder: funder.publicKey(),
2659
+ masterAddress: address,
2660
+ controlAddress: control.publicKey(),
2661
+ envelope,
2662
+ startingBalance
2663
+ });
2664
+ tx3.sign(master, funder);
2665
+ await adapter.submit(tx3);
2666
+ }
2667
+ const wallet = build("ready", { control, dek });
2668
+ wallet.isNewAccount = true;
2669
+ return wallet;
2670
+ }
2671
+ /** Native XLM balance of the account, in stroops. */
2672
+ async balance() {
2673
+ return this.adapter.balance(this.address);
2674
+ }
2675
+ /** True if the account has a passkey factor enrolled (`cv:wp`), so a new device
2676
+ * can be approved with the passkey instead of a recovery code. Mirrors the
2677
+ * other chains' `hasPasskey()` for the React provider. */
2678
+ async hasPasskey() {
2679
+ try {
2680
+ const env = fromDataEntries(await this.adapter.loadDataEntries(this.address));
2681
+ return !!env.passkeyWrap;
2682
+ } catch {
2683
+ return false;
1891
2684
  }
1892
- await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
1893
- const isSigner = await adapter.isAuthorizedSigner(address, devicePubkey, readSource);
1894
- return build(address, isSigner ? "ready" : "needs-device-approval");
1895
2685
  }
1896
- /** Authorize an additional device signer (device-signed via `__check_auth`). */
1897
- async addSigner(pubkey) {
1898
- const func = this.adapter.buildAddSigner(this.address, pubkey);
1899
- return this.submitHostFunction(func, this.address);
2686
+ /** Whether the control key is unlocked on this device (status ready). Classic
2687
+ * approvals land synchronously via Horizon, so this reflects state immediately
2688
+ * (no indexing delay to poll for). */
2689
+ async isReady() {
2690
+ return this.statusValue === "ready";
1900
2691
  }
1901
2692
  /**
1902
- * Enroll a passkey as an approver (2FA-style step-up). Device-signed + gasless;
1903
- * requires a ready device. Idempotent. Returns the passkey pubkey + tx hash.
2693
+ * Move `amount` stroops of native XLM to `destination`, signed by the control
2694
+ * key. Sponsored by default (the relayer fee-bumps and pays the fee); pass
2695
+ * `{ sponsored: false }` to submit directly — the account pays its own (tiny)
2696
+ * fee from its XLM balance. The control key signs identically in both modes;
2697
+ * only the fee payer differs.
1904
2698
  */
1905
- async enrollPasskey(passkey, params) {
1906
- const enrolled = await passkey.enroll(params);
1907
- const { transactionHash } = await this.addApprover(enrolled.publicKey);
1908
- return { publicKey: enrolled.publicKey, transactionHash };
1909
- }
1910
- /** Register an already-enrolled passkey pubkey as an approver (gasless).
1911
- * Idempotent. Lets one passkey be registered across chains without re-prompting. */
1912
- async addApprover(pubkey) {
1913
- if (this.status !== "ready") {
1914
- throw new Error("kit/stellar: addApprover requires a ready, authorized device");
1915
- }
1916
- const readSource = await this.resolveSource();
1917
- if (await this.adapter.isApprover(this.address, pubkey, readSource)) return {};
1918
- const func = this.adapter.buildAddApprover(this.address, pubkey);
1919
- const transactionHash = await this.submitHostFunction(func, this.address);
1920
- return { transactionHash };
2699
+ async execute(amount, destination, opts) {
2700
+ const control = this.requireControl();
2701
+ const inner = await this.adapter.buildPaymentTx({ from: this.address, to: destination, amount });
2702
+ return this.submitInner(inner, control, opts);
1921
2703
  }
1922
2704
  /**
1923
- * From a fresh browser (status `needs-device-approval`), approve adding THIS
1924
- * device using the user's synced passkey. Gasless via the relayer the call
1925
- * carries the WebAuthn assertion, so no device signature is needed. Returns the
1926
- * tx hash. No trip back to an already-authorized device.
2705
+ * Enroll a passkey as an unlock factor: wrap the DEK under the passkey's PRF
2706
+ * output and write the `cv:wp` entry. This is the synced anchor used to approve
2707
+ * a new device or recover it survives device loss. Idempotent-ish: writing it
2708
+ * again just overwrites the wrap of the same DEK. Requires a ready device.
1927
2709
  */
1928
- async approveThisDeviceWithPasskey(passkey) {
1929
- if (this.status === "ready") {
1930
- throw new Error("kit/stellar: this device is already an authorized signer");
1931
- }
1932
- const { leaf, nonce } = await this.passkeyLeafForThisDevice();
1933
- const leaves = [leaf];
1934
- const assertion = await passkey.assert(batchChallenge(leaves));
1935
- const { transactionHash } = await this.submitPasskeyApproval(assertion, leaves, 0, nonce);
1936
- return transactionHash;
2710
+ async enrollPasskey(prfOutput) {
2711
+ const { control, dek } = this.requireUnlocked();
2712
+ const wrap = wrapDEK(dek, derivePasskeyKEK(prfOutput));
2713
+ return this.writeFactor(PASSKEY_BASE, wrap, control);
1937
2714
  }
1938
- /** This device's leaf + passkey nonce for a (possibly multi-chain) batch. */
1939
- async passkeyLeafForThisDevice() {
1940
- const readSource = await this.resolveSource();
1941
- const nonce = await this.adapter.passkeyNonce(this.address, readSource);
1942
- return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
2715
+ /**
2716
+ * Set up a recovery code as an unlock factor: wrap the DEK under the code's KEK
2717
+ * and write the `cv:wr` entry. Optional in v1 — the integrating app decides when
2718
+ * to surface it. The code never leaves the device; only the wrap goes on-chain.
2719
+ * Requires a ready device.
2720
+ */
2721
+ async setupRecovery(code) {
2722
+ const { control, dek } = this.requireUnlocked();
2723
+ const wrap = wrapDEK(dek, deriveRecoveryKEK(code));
2724
+ return this.writeFactor(RECOVERY_BASE, wrap, control);
1943
2725
  }
1944
- /** Submit `add_signer_via_passkey` given a shared assertion + batch position.
1945
- * No device auth entry authorized purely by the passkey assertion. */
1946
- async submitPasskeyApproval(assertion, leaves, leafIndex, nonce) {
1947
- const readSource = await this.resolveSource();
1948
- const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
1949
- const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
1950
- let approver = null;
1951
- for (const cand of candidates) {
1952
- if (await this.adapter.isApprover(this.address, cand.publicKey, readSource)) {
1953
- approver = cand.publicKey;
1954
- break;
1955
- }
1956
- }
1957
- if (!approver) throw new Error("kit/stellar: this passkey is not a registered approver");
1958
- const func = this.adapter.buildAddSignerViaPasskey(
1959
- this.address,
1960
- this.devicePubkey,
1961
- approver,
1962
- nonce,
1963
- leaves,
1964
- leafIndex,
1965
- assertion
2726
+ /**
2727
+ * From a new browser/device (`needs-device-approval`), approve THIS device using
2728
+ * the user's synced passkey: unlock the DEK via the passkey factor, then wrap it
2729
+ * to this device's slot so future sessions unlock silently. Flips status to
2730
+ * `ready`. No trip back to an already-authorized device.
2731
+ */
2732
+ async approveThisDeviceWithPasskey(prfOutput) {
2733
+ return this.approveThisDevice(
2734
+ await unlockViaPasskey(this.adapter, this.address, prfOutput),
2735
+ "passkey"
1966
2736
  );
1967
- return { transactionHash: await this.submitHostFunction(func, void 0) };
1968
2737
  }
1969
- /** Move `amount` stroops of native XLM to `destination` (device-signed). */
1970
- async execute(amount, destination) {
1971
- return this.executeTransfer(NATIVE_SAC_ID[this.network], amount, destination);
2738
+ /** Approve THIS device using the recovery code (same as the passkey path, for
2739
+ * the backup factor). */
2740
+ async approveThisDeviceWithRecovery(code) {
2741
+ return this.approveThisDevice(
2742
+ await unlockViaRecovery(this.adapter, this.address, code),
2743
+ "recovery code"
2744
+ );
1972
2745
  }
1973
- /** Read this account's balance of `tokenId` (defaults to native XLM), in stroops. */
1974
- async balance(tokenId = NATIVE_SAC_ID[this.network]) {
1975
- const readSource = await this.resolveSource();
1976
- return this.adapter.readBalance(tokenId, this.address, readSource);
2746
+ /** The control key's public G address (the weight-1 real signer), for display. */
2747
+ get controlAddress() {
2748
+ return this.control?.publicKey();
1977
2749
  }
1978
- /** Transfer `amount` of any SEP-41 token out of the account (device-signed). */
1979
- async executeTransfer(tokenId, amount, destination) {
1980
- if (this.status !== "ready") {
1981
- throw new Error("kit/stellar: this device is not yet an authorized signer of the wallet");
2750
+ // --- internals ----------------------------------------------------------
2751
+ async approveThisDevice(unlocked, factor) {
2752
+ if (this.statusValue === "ready") {
2753
+ throw new Error("kit/stellar: this device is already authorized");
1982
2754
  }
1983
- const func = this.adapter.buildTransfer(tokenId, this.address, destination, amount);
1984
- return this.submitHostFunction(func, this.address);
2755
+ if (!unlocked) {
2756
+ throw new Error(`kit/stellar: could not unlock the account with the ${factor} \u2014 wrong factor or not enrolled`);
2757
+ }
2758
+ const slot = this.deviceKey.slotId();
2759
+ const wrap = eciesWrapDEK(unlocked.dek, this.deviceKey.publicKeySec1());
2760
+ const hash5 = await this.submitDataWrite(deviceWrapEntries(slot, wrap), unlocked.control);
2761
+ this.control = unlocked.control;
2762
+ this.dek = unlocked.dek;
2763
+ this.statusValue = "ready";
2764
+ return hash5;
2765
+ }
2766
+ /** Write a single-factor wrap (passkey/recovery) into the account data entries,
2767
+ * signed by the control key. Overwrites cleanly if the base already existed and
2768
+ * the new blob has the same chunk count. */
2769
+ async writeFactor(base, wrap, control) {
2770
+ const entries = {};
2771
+ chunkTo64(wrap).forEach((chunk, i) => {
2772
+ entries[`${base}/${i}`] = chunk;
2773
+ });
2774
+ return this.submitDataWrite(entries, control);
1985
2775
  }
1986
2776
  /**
1987
- * Register the backup signer derived from `code` as an authorized signer of
1988
- * this account (device-signed). Idempotent. The code never leaves the device
1989
- * only the derived public key travels on-chain. Mirrors the other chains.
2777
+ * Sign an inner (account-sourced) payment tx with the control key and submit it:
2778
+ * - sponsored (default) with a relayer, wrap in a fee-bump (relayer pays
2779
+ * the fee) and POST; falls back to self-funded if no relayer;
2780
+ * - `{ sponsored: false }` → submit directly (the account pays its own fee).
2781
+ * Payments add no subentries, so no reserve sponsorship is needed here.
1990
2782
  */
1991
- async setupRecovery(code) {
1992
- if (this.status !== "ready") {
1993
- throw new Error("kit/stellar: setupRecovery requires a ready, registered device");
2783
+ async submitInner(inner, control, opts) {
2784
+ inner.sign(control);
2785
+ const sponsored = opts?.sponsored !== false;
2786
+ if (sponsored && this.relayer) {
2787
+ const feeSource = await this.relayer.getSource();
2788
+ const bump = this.adapter.wrapFeeBump(inner, feeSource);
2789
+ return this.relayer.submit("fee-bump", bump.toXDR());
1994
2790
  }
1995
- const { publicKey: backupPubkey } = deriveBackupKey(code);
1996
- const readSource = await this.resolveSource();
1997
- if (await this.adapter.isAuthorizedSigner(this.address, backupPubkey, readSource)) return void 0;
1998
- return this.addSigner(backupPubkey);
2791
+ return this.adapter.submit(inner);
1999
2792
  }
2000
2793
  /**
2001
- * Recover an account after losing every device signer: derive the backup key
2002
- * from `code`, use it (not the new device) to authorize `add_signer(newDevice)`,
2003
- * and return a ready handle bound to the new device. The address is unchanged.
2794
+ * Write data entries (add a factor / device slot) which create NEW subentries
2795
+ * that each need ~0.5 XLM of reserve. A relayer-sponsored account holds no XLM,
2796
+ * so the write must be sponsored by the relayer (source + sponsor), exactly like
2797
+ * account creation — a plain fee-bump would fail with `op_low_reserve`.
2798
+ * - sponsored (default) → with a relayer, build a sponsored write (relayer
2799
+ * source + begin/end sponsoring), control-sign the account ops, relay
2800
+ * co-signs + submits; falls back to self-funded if no relayer;
2801
+ * - `{ sponsored: false }` → the account writes directly (it must hold its
2802
+ * own reserve for the new subentries).
2004
2803
  */
2005
- static async recover(opts) {
2006
- const signer = opts.createSigner ? await opts.createSigner(`${opts.identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${opts.identity.userId}:${opts.appSalt}` });
2007
- const devicePubkey = await signer.getPublicKey();
2008
- const backup = BackupSigner.fromCode(opts.code);
2009
- const backupAdapter = new StellarAdapter({
2010
- network: opts.network,
2011
- rpcUrl: opts.rpcUrl,
2012
- factoryId: opts.factoryId,
2013
- signer: backup
2014
- });
2015
- const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
2016
- const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
2017
- const existing = await registry.lookup(opts.identity.userId);
2018
- if (!existing) {
2019
- throw new Error("kit/stellar: no account found for this identity \u2014 nothing to recover");
2804
+ async submitDataWrite(entries, control, opts) {
2805
+ const sponsored = opts?.sponsored !== false;
2806
+ if (sponsored && this.relayer) {
2807
+ const relayerSource = await this.relayer.getSource();
2808
+ const tx4 = await this.adapter.buildSponsoredDataTx({
2809
+ relayer: relayerSource,
2810
+ account: this.address,
2811
+ entries
2812
+ });
2813
+ tx4.sign(control);
2814
+ return this.relayer.submit("sponsored-data", tx4.toXDR());
2020
2815
  }
2021
- const relayer = opts.relayer ?? (opts.appId ? new StellarRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : void 0);
2022
- const backupHandle = new _CavosStellar(
2023
- opts.identity,
2024
- existing.address,
2025
- "ready",
2026
- opts.network,
2027
- backupAdapter,
2028
- devicePubkey,
2029
- relayer,
2030
- opts.sourceKeypair
2031
- );
2032
- const readSource = await backupHandle.resolveSource();
2033
- if (!await backupAdapter.isAuthorizedSigner(existing.address, devicePubkey, readSource)) {
2034
- await backupHandle.addSigner(devicePubkey);
2816
+ const tx3 = await this.adapter.buildDataTx({ account: this.address, entries });
2817
+ tx3.sign(control);
2818
+ return this.adapter.submit(tx3);
2819
+ }
2820
+ requireControl() {
2821
+ if (this.statusValue !== "ready" || !this.control) {
2822
+ throw new Error("kit/stellar: control key not unlocked on this device (needs approval)");
2035
2823
  }
2036
- const adapter = new StellarAdapter({
2037
- network: opts.network,
2038
- rpcUrl: opts.rpcUrl,
2039
- factoryId: opts.factoryId,
2040
- signer
2041
- });
2042
- return new _CavosStellar(
2043
- opts.identity,
2044
- existing.address,
2045
- "ready",
2046
- opts.network,
2047
- adapter,
2048
- devicePubkey,
2049
- relayer,
2050
- opts.sourceKeypair
2824
+ return this.control;
2825
+ }
2826
+ requireUnlocked() {
2827
+ const control = this.requireControl();
2828
+ if (!this.dek) throw new Error("kit/stellar: DEK unavailable on this device");
2829
+ return { control, dek: this.dek };
2830
+ }
2831
+ };
2832
+ async function unlockViaDevice(adapter, address, deviceKey) {
2833
+ const env = await loadEnvelope(adapter, address);
2834
+ const wrap = env.deviceWraps[deviceKey.slotId()];
2835
+ if (!wrap) return null;
2836
+ try {
2837
+ const dek = await deviceKey.unwrap(wrap);
2838
+ return openControl(env, dek);
2839
+ } catch {
2840
+ return null;
2841
+ }
2842
+ }
2843
+ async function unlockViaPasskey(adapter, address, prfOutput) {
2844
+ const env = await loadEnvelope(adapter, address);
2845
+ if (!env.passkeyWrap) return null;
2846
+ try {
2847
+ const dek = unwrapDEK(env.passkeyWrap, derivePasskeyKEK(prfOutput));
2848
+ return openControl(env, dek);
2849
+ } catch {
2850
+ return null;
2851
+ }
2852
+ }
2853
+ async function unlockViaRecovery(adapter, address, code) {
2854
+ const env = await loadEnvelope(adapter, address);
2855
+ if (!env.recoveryWrap) return null;
2856
+ try {
2857
+ const dek = unwrapDEK(env.recoveryWrap, deriveRecoveryKEK(code));
2858
+ return openControl(env, dek);
2859
+ } catch {
2860
+ return null;
2861
+ }
2862
+ }
2863
+ async function loadEnvelope(adapter, address) {
2864
+ return fromDataEntries(await adapter.loadDataEntries(address));
2865
+ }
2866
+ function openControl(env, dek) {
2867
+ const controlSeed = openControlSeed(env.ct, dek);
2868
+ return { control: controlKeypairFromSeed(controlSeed), dek };
2869
+ }
2870
+ var LocalDeviceUnwrapKey = class _LocalDeviceUnwrapKey {
2871
+ constructor(scalar) {
2872
+ this.scalar = scalar;
2873
+ }
2874
+ /** Generate a fresh device unwrap key. */
2875
+ static generate() {
2876
+ return new _LocalDeviceUnwrapKey(p256.utils.randomPrivateKey());
2877
+ }
2878
+ /** Rebuild from a persisted 32-byte scalar. */
2879
+ static fromScalar(scalar) {
2880
+ if (scalar.length !== 32) throw new Error("kit/stellar: device unwrap scalar must be 32 bytes");
2881
+ return new _LocalDeviceUnwrapKey(scalar);
2882
+ }
2883
+ /** The raw scalar, for the caller to persist in secure storage. */
2884
+ export() {
2885
+ return this.scalar;
2886
+ }
2887
+ publicKeySec1() {
2888
+ return p256.getPublicKey(this.scalar, false);
2889
+ }
2890
+ slotId() {
2891
+ return deviceSlotId(this.publicKeySec1());
2892
+ }
2893
+ async unwrap(blob) {
2894
+ return eciesUnwrapDEK(blob, this.scalar);
2895
+ }
2896
+ };
2897
+ function deviceSlotId(publicKeySec1) {
2898
+ const h = sha256(publicKeySec1);
2899
+ let s = "";
2900
+ for (const b of h.subarray(0, 4)) s += b.toString(16).padStart(2, "0");
2901
+ return s;
2902
+ }
2903
+ var IDB_NAME2 = "cavos-kit-stellar";
2904
+ var IDB_STORE2 = "unwrap-keys";
2905
+ var WebCryptoDeviceUnwrapKey = class _WebCryptoDeviceUnwrapKey {
2906
+ constructor(privateKey, publicRaw, keyId) {
2907
+ this.privateKey = privateKey;
2908
+ this.publicRaw = publicRaw;
2909
+ this.keyId = keyId;
2910
+ }
2911
+ /** Create a fresh device unwrap key and persist it. */
2912
+ static async create(opts) {
2913
+ assertSecureContext2();
2914
+ const pair = await crypto.subtle.generateKey(
2915
+ { name: "ECDH", namedCurve: "P-256" },
2916
+ false,
2917
+ // private key is NON-extractable
2918
+ ["deriveBits"]
2051
2919
  );
2920
+ const publicRaw = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
2921
+ await idbPut2(opts.keyId, { privateKey: pair.privateKey, publicRaw });
2922
+ return new _WebCryptoDeviceUnwrapKey(pair.privateKey, publicRaw, opts.keyId);
2052
2923
  }
2053
- /** The transaction source/fee-payer G-address (relayer or self-funded). */
2054
- async resolveSource() {
2055
- if (this.relayer) return this.relayer.getSource();
2056
- if (this.sourceKeypair) return this.sourceKeypair.publicKey();
2057
- throw new Error("kit/stellar: a relayer (appId) or sourceKeypair is required");
2924
+ /** Load an existing device unwrap key, or null if none exists yet. */
2925
+ static async load(opts) {
2926
+ const rec = await idbGet2(opts.keyId);
2927
+ if (!rec) return null;
2928
+ return new _WebCryptoDeviceUnwrapKey(rec.privateKey, rec.publicRaw, opts.keyId);
2058
2929
  }
2059
- /**
2060
- * Build simulate → device-sign auth → assemble → submit an invoke-contract
2061
- * host function. `authAccount` is the account whose `__check_auth` must sign the
2062
- * operation's Soroban auth entry (undefined for a plain factory deploy).
2063
- */
2064
- async submitHostFunction(func, authAccount) {
2065
- const server = this.adapter.server();
2066
- const sourceAddr = await this.resolveSource();
2067
- const simSource = new Account(sourceAddr, "0");
2068
- const unsignedOp = Operation.invokeHostFunction({ func, auth: [] });
2069
- const simTx = new TransactionBuilder(simSource, {
2070
- fee: BASE_FEE,
2071
- networkPassphrase: this.adapter.passphrase
2072
- }).addOperation(unsignedOp).setTimeout(180).build();
2073
- const sim = await server.simulateTransaction(simTx);
2074
- if (rpc.Api.isSimulationError(sim)) {
2075
- throw new Error(`kit/stellar: simulation failed: ${sim.error}`);
2076
- }
2077
- const validUntil = (await server.getLatestLedger()).sequence + 100;
2078
- const entries = sim.result?.auth ?? [];
2079
- const signedAuth = [];
2080
- for (const entry of entries) {
2081
- if (authAccount && isAddressCredentialFor(entry, authAccount)) {
2082
- signedAuth.push(await this.adapter.signAuthEntry(entry, validUntil));
2083
- } else {
2084
- signedAuth.push(entry);
2085
- }
2086
- }
2087
- const account = await server.getAccount(sourceAddr);
2088
- const finalOp = Operation.invokeHostFunction({ func, auth: signedAuth });
2089
- const built = new TransactionBuilder(account, {
2090
- fee: BASE_FEE,
2091
- networkPassphrase: this.adapter.passphrase
2092
- }).addOperation(finalOp).setTimeout(180).build();
2093
- const authSim = await server.simulateTransaction(built);
2094
- if (rpc.Api.isSimulationError(authSim)) {
2095
- throw new Error(`kit/stellar: auth simulation failed: ${authSim.error}`);
2096
- }
2097
- const assembled = rpc.assembleTransaction(built, authSim).build();
2098
- if (this.relayer) {
2099
- return this.relayer.submit(assembled.toXDR());
2100
- }
2101
- if (this.sourceKeypair) {
2102
- assembled.sign(this.sourceKeypair);
2103
- return this.sendAndConfirm(assembled);
2104
- }
2105
- throw new Error("kit/stellar: no relayer or sourceKeypair configured to submit");
2106
- }
2107
- /** Submit a signed tx via RPC and poll to confirmation. Returns the hash. */
2108
- async sendAndConfirm(tx2) {
2109
- const server = this.adapter.server();
2110
- const sent = await server.sendTransaction(tx2);
2111
- if (sent.status === "ERROR") {
2112
- throw new Error(`kit/stellar: submit rejected: ${JSON.stringify(sent.errorResult)}`);
2113
- }
2114
- const hash6 = sent.hash;
2115
- for (let i = 0; i < 30; i++) {
2116
- const got = await server.getTransaction(hash6);
2117
- if (got.status === rpc.Api.GetTransactionStatus.SUCCESS) return hash6;
2118
- if (got.status === rpc.Api.GetTransactionStatus.FAILED) {
2119
- throw new Error(`kit/stellar: tx ${hash6} failed`);
2120
- }
2121
- await new Promise((r) => setTimeout(r, 1e3));
2122
- }
2123
- throw new Error(`kit/stellar: tx ${hash6} not confirmed in time`);
2930
+ /** Load the device unwrap key, creating one on first use. */
2931
+ static async loadOrCreate(opts) {
2932
+ return await _WebCryptoDeviceUnwrapKey.load(opts) ?? await _WebCryptoDeviceUnwrapKey.create(opts);
2933
+ }
2934
+ publicKeySec1() {
2935
+ return this.publicRaw;
2936
+ }
2937
+ slotId() {
2938
+ return deviceSlotId(this.publicRaw);
2939
+ }
2940
+ async unwrap(blob) {
2941
+ const ephPubCompressed = blob.subarray(0, 33);
2942
+ const wrapped = blob.subarray(33);
2943
+ const ephUncompressed = p256.ProjectivePoint.fromHex(ephPubCompressed).toRawBytes(false);
2944
+ const ephKey = await crypto.subtle.importKey(
2945
+ "raw",
2946
+ ephUncompressed,
2947
+ { name: "ECDH", namedCurve: "P-256" },
2948
+ false,
2949
+ []
2950
+ );
2951
+ const sharedX = new Uint8Array(
2952
+ await crypto.subtle.deriveBits({ name: "ECDH", public: ephKey }, this.privateKey, 256)
2953
+ );
2954
+ const kek = eciesKEKFromX(sharedX, ephPubCompressed);
2955
+ return unwrapDEK(wrapped, kek);
2124
2956
  }
2125
2957
  };
2126
- function isAddressCredentialFor(entry, accountAddress) {
2127
- const creds = entry.credentials();
2128
- if (creds.switch() !== xdr.SorobanCredentialsType.sorobanCredentialsAddress()) return false;
2129
- return Address.fromScAddress(creds.address().address()).toString() === accountAddress;
2958
+ function assertSecureContext2() {
2959
+ const ok = typeof crypto !== "undefined" && typeof crypto.subtle !== "undefined" && (typeof window === "undefined" || window.isSecureContext);
2960
+ if (!ok) {
2961
+ throw new Error(
2962
+ "Cavos: WebCrypto is unavailable. Device keys require a secure context \u2014 use HTTPS, or http://localhost."
2963
+ );
2964
+ }
2965
+ }
2966
+ function openDb2() {
2967
+ return new Promise((resolve, reject) => {
2968
+ const req = indexedDB.open(IDB_NAME2, 1);
2969
+ req.onupgradeneeded = () => req.result.createObjectStore(IDB_STORE2);
2970
+ req.onsuccess = () => resolve(req.result);
2971
+ req.onerror = () => reject(req.error);
2972
+ });
2973
+ }
2974
+ async function idbPut2(keyId, value) {
2975
+ const db = await openDb2();
2976
+ await tx2(db, "readwrite", (store) => store.put(value, keyId));
2977
+ db.close();
2978
+ }
2979
+ async function idbGet2(keyId) {
2980
+ const db = await openDb2();
2981
+ const result = await tx2(db, "readonly", (store) => store.get(keyId));
2982
+ db.close();
2983
+ return result ?? null;
2984
+ }
2985
+ function tx2(db, mode, run) {
2986
+ return new Promise((resolve, reject) => {
2987
+ const store = db.transaction(IDB_STORE2, mode).objectStore(IDB_STORE2);
2988
+ const req = run(store);
2989
+ req.onsuccess = () => resolve(req.result);
2990
+ req.onerror = () => reject(req.error);
2991
+ });
2130
2992
  }
2131
- var defaultRegistry2 = new InMemoryWalletRegistry();
2132
2993
 
2133
2994
  // src/recovery/HttpRecoveryClient.ts
2134
2995
  function toHex2(n) {
@@ -2226,6 +3087,9 @@ var Cavos = class _Cavos {
2226
3087
  this.chain = "starknet";
2227
3088
  /** Request id of the pending device-addition, when status is needs-device-approval. */
2228
3089
  this.pendingRequestId = null;
3090
+ /** True when this connect just created & deployed a brand-new account (first
3091
+ * sign-up), so the UI can offer a one-time "secure your account" step. */
3092
+ this.isNewAccount = false;
2229
3093
  }
2230
3094
  /**
2231
3095
  * Unified entry point. Pick a `chain` and an `network` environment; the kit
@@ -2255,17 +3119,16 @@ var Cavos = class _Cavos {
2255
3119
  });
2256
3120
  }
2257
3121
  if (opts.chain === "stellar") {
3122
+ const identity = opts.identity ?? (opts.auth ? await opts.auth.authenticate() : void 0);
3123
+ if (!identity) throw new Error("kit: Stellar connect requires `identity` or `auth`");
3124
+ const deviceKey = opts.stellarDeviceKey ?? await WebCryptoDeviceUnwrapKey.loadOrCreate({ keyId: `${identity.userId}:${opts.appSalt}` });
2258
3125
  return CavosStellar.connect({
2259
3126
  network: STELLAR_ENV[opts.network],
2260
- ...opts.auth ? { auth: opts.auth } : {},
2261
- ...opts.identity ? { identity: opts.identity } : {},
3127
+ identity,
2262
3128
  appSalt: opts.appSalt,
3129
+ deviceKey,
2263
3130
  ...opts.appId ? { appId: opts.appId } : {},
2264
3131
  ...opts.backendUrl ? { backendUrl: opts.backendUrl } : {},
2265
- ...opts.registry ? { registry: opts.registry } : {},
2266
- ...opts.rpcUrl ? { rpcUrl: opts.rpcUrl } : {},
2267
- ...opts.factoryId ? { factoryId: opts.factoryId } : {},
2268
- ...opts.createSigner ? { createSigner: opts.createSigner } : {},
2269
3132
  ...opts.stellarRelayer ? { relayer: opts.stellarRelayer } : {},
2270
3133
  ...opts.stellarSourceKeypair ? { sourceKeypair: opts.stellarSourceKeypair } : {}
2271
3134
  });
@@ -2315,13 +3178,13 @@ var Cavos = class _Cavos {
2315
3178
  cairoVersion: "1"
2316
3179
  });
2317
3180
  const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
2318
- const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry3);
3181
+ const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
2319
3182
  const recovery = opts.recovery ?? (opts.appId ? new HttpRecoveryClient({ baseUrl: backendUrl, appId: opts.appId }) : null);
2320
3183
  const existing = await registry.lookup(identity.userId);
2321
3184
  if (existing) {
2322
3185
  const account2 = makeAccount(existing.address);
2323
3186
  const isSigner2 = await adapter.isAuthorizedSigner(existing.address, devicePubkey);
2324
- const cavos = new _Cavos(
3187
+ const cavos2 = new _Cavos(
2325
3188
  identity,
2326
3189
  existing.address,
2327
3190
  isSigner2 ? "ready" : "needs-device-approval",
@@ -2335,7 +3198,7 @@ var Cavos = class _Cavos {
2335
3198
  const fresh = dedup && Date.now() - dedup.requestedAt < DEVICE_REQUEST_DEDUP_MS;
2336
3199
  try {
2337
3200
  if (fresh) {
2338
- cavos.pendingRequestId = dedup.requestId;
3201
+ cavos2.pendingRequestId = dedup.requestId;
2339
3202
  } else {
2340
3203
  const { requestId } = await recovery.requestDeviceAddition({
2341
3204
  userId: identity.userId,
@@ -2343,16 +3206,16 @@ var Cavos = class _Cavos {
2343
3206
  newSigner: devicePubkey,
2344
3207
  ...identity.email ? { email: identity.email } : {}
2345
3208
  });
2346
- cavos.pendingRequestId = requestId;
3209
+ cavos2.pendingRequestId = requestId;
2347
3210
  lastDeviceRequest.set(identity.userId, { requestId, requestedAt: Date.now() });
2348
3211
  }
2349
3212
  } catch (e) {
2350
3213
  console.warn("[Cavos] requestDeviceAddition failed:", e);
2351
3214
  }
2352
3215
  }
2353
- return cavos;
3216
+ return cavos2;
2354
3217
  }
2355
- const address = adapter.computeAddress({ addressSeed, initialSigner: devicePubkey });
3218
+ const address = adapter.computeAddress({ addressSeed });
2356
3219
  const account = makeAccount(address);
2357
3220
  const alreadyDeployed = await isDeployed(provider, address);
2358
3221
  if (!alreadyDeployed) {
@@ -2360,10 +3223,11 @@ var Cavos = class _Cavos {
2360
3223
  address,
2361
3224
  class_hash: classHash,
2362
3225
  salt: num.toHex(addressSeed),
2363
- calldata: adapter.constructorCalldata(addressSeed, devicePubkey),
3226
+ calldata: adapter.constructorCalldata(addressSeed),
2364
3227
  version: 1
2365
3228
  };
2366
- const deployRes = await account.executePaymasterTransaction([], {
3229
+ const initCall = adapter.buildInitialize(address, devicePubkey);
3230
+ const deployRes = await account.executePaymasterTransaction([initCall], {
2367
3231
  feeMode: { mode: "sponsored" },
2368
3232
  deploymentData
2369
3233
  });
@@ -2381,7 +3245,7 @@ var Cavos = class _Cavos {
2381
3245
  console.warn("[Cavos] isAuthorizedSigner read failed:", e);
2382
3246
  isSigner = !alreadyDeployed;
2383
3247
  }
2384
- return new _Cavos(
3248
+ const cavos = new _Cavos(
2385
3249
  identity,
2386
3250
  address,
2387
3251
  isSigner ? "ready" : "needs-device-approval",
@@ -2390,24 +3254,33 @@ var Cavos = class _Cavos {
2390
3254
  devicePubkey,
2391
3255
  paymasterConfig
2392
3256
  );
3257
+ cavos.isNewAccount = !alreadyDeployed && isSigner;
3258
+ return cavos;
2393
3259
  }
2394
3260
  /** This device's public key (e.g. to request addition to an existing wallet). */
2395
3261
  get publicKey() {
2396
3262
  return this.devicePubkey;
2397
3263
  }
2398
3264
  /** Execute a sponsored (gasless) multicall, signed silently by the device. */
2399
- async execute(calls) {
3265
+ async execute(calls, opts) {
2400
3266
  if (this.status !== "ready") {
2401
3267
  throw new Error("kit: this device is not yet an authorized signer of the wallet");
2402
3268
  }
3269
+ if (opts?.sponsored === false) {
3270
+ const res2 = await this.account.execute(calls);
3271
+ return { transactionHash: res2.transaction_hash };
3272
+ }
2403
3273
  const res = await this.account.executePaymasterTransaction(calls, {
2404
3274
  feeMode: { mode: "sponsored" }
2405
3275
  });
2406
3276
  return { transactionHash: res.transaction_hash };
2407
3277
  }
2408
- /** Authorize an additional device signer (sponsored). Self-submitted. */
2409
- async addSigner(pubkey) {
2410
- return this.execute([this.adapter.buildAddSigner(this.address, pubkey)]);
3278
+ /**
3279
+ * Authorize an additional device signer. Sponsored by default; pass
3280
+ * `{ sponsored: false }` to pay the fee from the account's own ETH balance.
3281
+ */
3282
+ async addSigner(pubkey, opts) {
3283
+ return this.execute([this.adapter.buildAddSigner(this.address, pubkey)], opts);
2411
3284
  }
2412
3285
  /**
2413
3286
  * Enroll a passkey as an APPROVER so the user can later add devices from any
@@ -2416,27 +3289,45 @@ var Cavos = class _Cavos {
2416
3289
  * approver. Call this whenever the app decides to prompt "turn on device
2417
3290
  * approvals". Returns the passkey's public key + the enrollment tx hash.
2418
3291
  */
2419
- async enrollPasskey(passkey, params) {
3292
+ async enrollPasskey(passkey, params, opts) {
2420
3293
  const enrolled = await passkey.enroll(params);
2421
- const { transactionHash } = await this.addApprover(enrolled.publicKey);
3294
+ const { transactionHash } = await this.addApprover(enrolled.publicKey, opts);
2422
3295
  return { publicKey: enrolled.publicKey, transactionHash };
2423
3296
  }
2424
3297
  /**
2425
- * Register an ALREADY-enrolled passkey public key as an approver (gasless,
2426
- * device-signed). Idempotent. Use this to register ONE passkey across multiple
2427
- * chains without re-prompting `passkey.enroll()` on each: enroll once, then
2428
- * call `addApprover(pubkey)` on each chain's wallet.
3298
+ * Register an ALREADY-enrolled passkey public key as an approver (gasless by
3299
+ * default, device-signed). Idempotent. Use this to register ONE passkey across
3300
+ * multiple chains without re-prompting `passkey.enroll()` on each: enroll once,
3301
+ * then call `addApprover(pubkey)` on each chain's wallet. Pass
3302
+ * `{ sponsored: false }` to pay the fee from the account's own balance.
2429
3303
  */
2430
- async addApprover(pubkey) {
3304
+ async addApprover(pubkey, opts) {
2431
3305
  if (this.status !== "ready") {
2432
3306
  throw new Error("kit: addApprover requires a ready, authorized device");
2433
3307
  }
2434
3308
  if (await this.adapter.isApprover(this.address, pubkey)) return {};
2435
- const { transactionHash } = await this.execute([
2436
- this.adapter.buildAddApprover(this.address, pubkey)
2437
- ]);
3309
+ const { transactionHash } = await this.execute(
3310
+ [this.adapter.buildAddApprover(this.address, pubkey)],
3311
+ opts
3312
+ );
3313
+ try {
3314
+ await this.account.waitForTransaction(transactionHash);
3315
+ } catch (e) {
3316
+ console.warn("[Cavos] add_approver receipt wait failed:", e);
3317
+ }
2438
3318
  return { transactionHash };
2439
3319
  }
3320
+ /** True if this account already has a passkey enrolled as an approver, so a
3321
+ * new device can be approved with the passkey instead of the email flow. */
3322
+ async hasPasskey() {
3323
+ return this.adapter.hasPasskeyApprover(this.address);
3324
+ }
3325
+ /** Re-read (from chain) whether THIS device is now an authorized signer.
3326
+ * Cheap and side-effect free — used to poll for readiness after a passkey /
3327
+ * device approval submits, before the new signer is indexed. */
3328
+ async isReady() {
3329
+ return this.adapter.isAuthorizedSigner(this.address, this.devicePubkey);
3330
+ }
2440
3331
  /**
2441
3332
  * From a brand-new browser (status `needs-device-approval`), use the user's
2442
3333
  * synced passkey to authorize adding THIS device — no trip back to an already-
@@ -2505,11 +3396,11 @@ var Cavos = class _Cavos {
2505
3396
  * add_signer (gasless). Returns the transaction hash (or undefined when the
2506
3397
  * backup was already set up).
2507
3398
  */
2508
- async setupRecovery(code) {
3399
+ async setupRecovery(code, opts) {
2509
3400
  const { publicKey: backupPubkey } = deriveBackupKey(code);
2510
3401
  const already = await this.adapter.isAuthorizedSigner(this.address, backupPubkey);
2511
3402
  if (already) return void 0;
2512
- return this.addSigner(backupPubkey);
3403
+ return this.addSigner(backupPubkey, opts);
2513
3404
  }
2514
3405
  /**
2515
3406
  * Recover an account after losing every device signer. Derives the backup key
@@ -2536,11 +3427,11 @@ var Cavos = class _Cavos {
2536
3427
  const backup = BackupSigner.fromCode(opts.code);
2537
3428
  const backupAdapter = new StarknetAdapter({ classHash, signer: backup, provider });
2538
3429
  const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
2539
- const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network }) : defaultRegistry3);
2540
- const existing = await registry.lookup(opts.identity.userId);
2541
- if (!existing) {
3430
+ const address = opts.address ?? await lookupAddress(opts, backendUrl, network);
3431
+ if (!address) {
2542
3432
  throw new Error("kit: no account found for this identity \u2014 nothing to recover");
2543
3433
  }
3434
+ const existing = { address };
2544
3435
  const backupAccount = new Account$1({
2545
3436
  provider,
2546
3437
  address: existing.address,
@@ -2571,7 +3462,7 @@ var Cavos = class _Cavos {
2571
3462
  return new _Cavos(opts.identity, existing.address, "ready", account, adapter, devicePubkey);
2572
3463
  }
2573
3464
  };
2574
- var defaultRegistry3 = new InMemoryWalletRegistry();
3465
+ var defaultRegistry2 = new InMemoryWalletRegistry();
2575
3466
  var DEVICE_REQUEST_DEDUP_MS = 5 * 60 * 1e3;
2576
3467
  var lastDeviceRequest = /* @__PURE__ */ new Map();
2577
3468
  async function isDeployed(provider, address) {
@@ -2582,23 +3473,26 @@ async function isDeployed(provider, address) {
2582
3473
  return false;
2583
3474
  }
2584
3475
  }
3476
+ async function lookupAddress(opts, backendUrl, network) {
3477
+ const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network }) : defaultRegistry2);
3478
+ const existing = await registry.lookup(opts.identity.userId);
3479
+ return existing?.address ?? null;
3480
+ }
2585
3481
  async function approveDeviceEverywhere(wallets, passkey) {
2586
3482
  const targets = wallets.filter((w) => w.status === "needs-device-approval");
2587
3483
  if (targets.length === 0) return [];
2588
3484
  const infos = await Promise.all(targets.map((w) => w.passkeyLeafForThisDevice()));
2589
3485
  const leaves = infos.map((i) => i.leaf);
2590
3486
  const assertion = await passkey.assert(batchChallenge(leaves));
2591
- const out = [];
2592
- for (let i = 0; i < targets.length; i++) {
2593
- const { transactionHash } = await targets[i].submitPasskeyApproval(
2594
- assertion,
2595
- leaves,
2596
- i,
2597
- infos[i].nonce
2598
- );
2599
- out.push({ chain: targets[i].chain, transactionHash });
2600
- }
2601
- return out;
3487
+ const settled = await Promise.allSettled(
3488
+ targets.map((w, i) => w.submitPasskeyApproval(assertion, leaves, i, infos[i].nonce))
3489
+ );
3490
+ return settled.map(
3491
+ (r, i) => r.status === "fulfilled" ? { chain: targets[i].chain, transactionHash: r.value.transactionHash } : {
3492
+ chain: targets[i].chain,
3493
+ error: r.reason instanceof Error ? r.reason.message : String(r.reason)
3494
+ }
3495
+ );
2602
3496
  }
2603
3497
  async function paymasterExecuteDirect(paymaster, userAddress, call) {
2604
3498
  const body = {
@@ -2771,7 +3665,191 @@ function bytesToChunks(bytes) {
2771
3665
  for (const b of bytes.subarray(0, 31)) w = w << 8n | BigInt(b);
2772
3666
  return w;
2773
3667
  }
3668
+ var PRF_SALT = sha256(new TextEncoder().encode("cavos-stellar-prf-v1"));
3669
+ var PasskeyPrf = class {
3670
+ constructor(opts = {}) {
3671
+ if (typeof window === "undefined" || !navigator.credentials) {
3672
+ throw new Error("kit/passkey-prf: WebAuthn is only available in a browser");
3673
+ }
3674
+ this.rpId = opts.rpId ?? window.location.hostname;
3675
+ this.rpName = opts.rpName ?? this.rpId;
3676
+ if (isIpAddress(this.rpId)) {
3677
+ throw new Error(
3678
+ `kit/passkey-prf: passkeys can't use an IP address as the domain ("${this.rpId}"). Use http://localhost, a real HTTPS domain, or a tunnel \u2014 or pass an explicit \`rpId\`.`
3679
+ );
3680
+ }
3681
+ }
3682
+ /** True if this platform advertises a usable passkey (platform authenticator). */
3683
+ static async isSupported() {
3684
+ if (typeof window === "undefined" || !window.PublicKeyCredential) return false;
3685
+ try {
3686
+ return await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
3687
+ } catch {
3688
+ return false;
3689
+ }
3690
+ }
3691
+ /**
3692
+ * Create a new synced, discoverable passkey with the PRF extension enabled, then
3693
+ * immediately read its PRF secret. Returns the credential id and the 32-byte
3694
+ * secret so the caller can enroll the passkey factor in one step. Some
3695
+ * authenticators don't return PRF results on create — in that case `secret` is
3696
+ * undefined and the caller should follow up with `getSecret()`.
3697
+ */
3698
+ async enroll(params) {
3699
+ const cred = await navigator.credentials.create({
3700
+ publicKey: {
3701
+ challenge: buf(crypto.getRandomValues(new Uint8Array(32))),
3702
+ rp: { id: this.rpId, name: this.rpName },
3703
+ user: {
3704
+ id: buf(userHandle(params.userId)),
3705
+ name: params.userName,
3706
+ displayName: params.displayName ?? params.userName
3707
+ },
3708
+ pubKeyCredParams: [{ type: "public-key", alg: -7 }],
3709
+ // ES256 (P-256)
3710
+ authenticatorSelection: {
3711
+ residentKey: "required",
3712
+ requireResidentKey: true,
3713
+ userVerification: "required"
3714
+ },
3715
+ attestation: "none",
3716
+ extensions: { prf: { eval: { first: buf(PRF_SALT) } } }
3717
+ }
3718
+ });
3719
+ if (!cred) throw new Error("kit/passkey-prf: enrollment cancelled");
3720
+ return { credentialId: new Uint8Array(cred.rawId), secret: readPrf(cred) };
3721
+ }
3722
+ /**
3723
+ * Get the passkey's 32-byte PRF secret. Uses discoverable credentials (no
3724
+ * `allowCredentials`), so it works from a brand-new browser — the OS shows the
3725
+ * synced passkey picker. Throws if the authenticator doesn't support PRF.
3726
+ */
3727
+ async getSecret() {
3728
+ const cred = await navigator.credentials.get({
3729
+ publicKey: {
3730
+ challenge: buf(crypto.getRandomValues(new Uint8Array(32))),
3731
+ rpId: this.rpId,
3732
+ allowCredentials: [],
3733
+ userVerification: "required",
3734
+ extensions: { prf: { eval: { first: buf(PRF_SALT) } } }
3735
+ }
3736
+ });
3737
+ if (!cred) throw new Error("kit/passkey-prf: assertion cancelled");
3738
+ const secret = readPrf(cred);
3739
+ if (!secret) {
3740
+ throw new Error(
3741
+ "kit/passkey-prf: this authenticator did not return a PRF result \u2014 PRF is unsupported here"
3742
+ );
3743
+ }
3744
+ return secret;
3745
+ }
3746
+ };
3747
+ function readPrf(cred) {
3748
+ const results = cred.getClientExtensionResults().prf?.results?.first;
3749
+ return results ? new Uint8Array(results) : void 0;
3750
+ }
3751
+ function isIpAddress(host) {
3752
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true;
3753
+ if (host.includes(":")) return true;
3754
+ return false;
3755
+ }
3756
+ function userHandle(userId) {
3757
+ const bytes = new TextEncoder().encode(userId);
3758
+ return bytes.length <= 64 ? bytes : sha256(bytes);
3759
+ }
3760
+ function buf(bytes) {
3761
+ return bytes.slice();
3762
+ }
3763
+ var PasskeySigner = class {
3764
+ constructor(opts = {}) {
3765
+ if (typeof window === "undefined" || !navigator.credentials) {
3766
+ throw new Error("kit/passkey: WebAuthn is only available in a browser");
3767
+ }
3768
+ this.rpId = opts.rpId ?? window.location.hostname;
3769
+ this.rpName = opts.rpName ?? this.rpId;
3770
+ if (isIpAddress2(this.rpId)) {
3771
+ throw new Error(
3772
+ `kit/passkey: passkeys can't use an IP address as the domain ("${this.rpId}"). Use http://localhost, a real HTTPS domain, or a tunnel (cloudflared/ngrok) \u2014 or pass an explicit \`rpId\`. (The silent device key works over an IP; passkeys don't.)`
3773
+ );
3774
+ }
3775
+ }
3776
+ /** True if this platform advertises a usable passkey (platform authenticator). */
3777
+ static async isSupported() {
3778
+ if (typeof window === "undefined" || !window.PublicKeyCredential) return false;
3779
+ try {
3780
+ return await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
3781
+ } catch {
3782
+ return false;
3783
+ }
3784
+ }
3785
+ /** Create a new synced passkey and return its P-256 public key. */
3786
+ async enroll(params) {
3787
+ const challenge = crypto.getRandomValues(new Uint8Array(32));
3788
+ const cred = await navigator.credentials.create({
3789
+ publicKey: {
3790
+ challenge: buf2(challenge),
3791
+ rp: { id: this.rpId, name: this.rpName },
3792
+ user: {
3793
+ id: buf2(userHandle2(params.userId)),
3794
+ name: params.userName,
3795
+ displayName: params.displayName ?? params.userName
3796
+ },
3797
+ pubKeyCredParams: [{ type: "public-key", alg: -7 }],
3798
+ // ES256 (P-256)
3799
+ authenticatorSelection: {
3800
+ residentKey: "required",
3801
+ requireResidentKey: true,
3802
+ userVerification: "preferred"
3803
+ },
3804
+ attestation: "none"
3805
+ }
3806
+ });
3807
+ if (!cred) throw new Error("kit/passkey: enrollment cancelled");
3808
+ const response = cred.response;
3809
+ const spki = new Uint8Array(response.getPublicKey());
3810
+ return { publicKey: spkiToPublicKey(spki), credentialId: new Uint8Array(cred.rawId) };
3811
+ }
3812
+ /**
3813
+ * Produce a WebAuthn assertion over `challenge` (a 32-byte value the caller
3814
+ * derives from the signer being added + the on-chain nonce). Uses discoverable
3815
+ * credentials — no `allowCredentials` — so it works on a brand-new browser.
3816
+ */
3817
+ async assert(challenge) {
3818
+ const cred = await navigator.credentials.get({
3819
+ publicKey: {
3820
+ challenge: buf2(challenge),
3821
+ rpId: this.rpId,
3822
+ allowCredentials: [],
3823
+ userVerification: "preferred"
3824
+ }
3825
+ });
3826
+ if (!cred) throw new Error("kit/passkey: assertion cancelled");
3827
+ const response = cred.response;
3828
+ const authenticatorData = new Uint8Array(response.authenticatorData);
3829
+ const clientDataJSON = new Uint8Array(response.clientDataJSON);
3830
+ const { r, s } = derToRs(new Uint8Array(response.signature));
3831
+ const challengeOffset = challengeOffsetOf(clientDataJSON, base64urlEncode(challenge));
3832
+ return { authenticatorData, clientDataJSON, r, s, challengeOffset };
3833
+ }
3834
+ };
3835
+ function isIpAddress2(host) {
3836
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true;
3837
+ if (host.includes(":")) return true;
3838
+ return false;
3839
+ }
3840
+ function userHandle2(userId) {
3841
+ const bytes = new TextEncoder().encode(userId);
3842
+ return bytes.length <= 64 ? bytes : sha256(bytes);
3843
+ }
3844
+ function buf2(bytes) {
3845
+ return bytes.slice();
3846
+ }
3847
+ /*! Bundled license information:
3848
+
3849
+ @noble/ciphers/esm/utils.js:
3850
+ (*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) *)
3851
+ */
2774
3852
 
2775
- export { BackupSigner, Cavos, CavosAuth, CavosSolana, CavosStellar, DEVICE_ACCOUNT_CLASS_HASH, DEVICE_ACCOUNT_PROGRAM_ID, DEVICE_ACCOUNT_WASM_HASH, FACTORY_CONTRACT_ID, HttpRecoveryClient, HttpWalletRegistry, InMemoryWalletRegistry, NATIVE_SAC_ID, SECP256R1_PROGRAM_ID, SOLANA_NETWORKS, STARKNET_NETWORKS, STELLAR_NETWORKS, SolanaAdapter, SolanaRelayer, StarknetAdapter, StarknetDeviceSigner, StellarAdapter, StellarRelayer, UDC_ADDRESS, WebCryptoSigner, anchorDiscriminator, approveDeviceEverywhere, base64urlEncode, batchChallenge, bigIntTo32Bytes, buildSecp256r1Instruction, bytesToBigInt, bytesToHex, challengeOffsetOf, compressedPubkey, derToRs, deriveAddressSeed, deriveAddressSeedSolana, deriveAddressSeedStellar, deriveBackupKey, deviceSignatureScVal, encodeLowSSignature, encodeLowSSignature2, generateRecoveryCode, hexToBytes, lowS, recoverCandidatePublicKeys, recoverYParity, sec1Pubkey, serializeInstructions, signatureToFelts, spkiToPublicKey, u256ToFelts, webauthnDigest };
2776
- //# sourceMappingURL=chunk-BNGLH3Q3.mjs.map
2777
- //# sourceMappingURL=chunk-BNGLH3Q3.mjs.map
3853
+ export { BackupSigner, Cavos, CavosAuth, CavosSolana, CavosStellar, DEVICE_ACCOUNT_CLASS_HASH, DEVICE_ACCOUNT_PROGRAM_ID, HORIZON_URL, HttpRecoveryClient, HttpWalletRegistry, InMemoryWalletRegistry, LocalDeviceUnwrapKey, PasskeyPrf, PasskeySigner, SECP256R1_PROGRAM_ID, SOLANA_NETWORKS, STARKNET_NETWORKS, STELLAR_NETWORKS, SolanaAdapter, SolanaRelayer, StarknetAdapter, StarknetDeviceSigner, StellarAdapter, StellarRelayer, UDC_ADDRESS, WebCryptoDeviceUnwrapKey, WebCryptoSigner, XLM_DECIMALS, anchorDiscriminator, approveDeviceEverywhere, base64urlEncode, batchChallenge, bigIntTo32Bytes, buildSecp256r1Instruction, bytesToBigInt, bytesToHex, compressedPubkey, deriveAddressSeed, deriveAddressSeedSolana, deriveAddressSeedStellar, deriveBackupKey, derivePasskeyKEK, deriveRecoveryKEK, deriveStellarAddress, deriveStellarMasterKeypair, deviceSlotId, eciesUnwrapDEK, eciesWrapDEK, encodeLowSSignature, fromDataEntries, generateControlKey, generateDEK, generateRecoveryCode, hexToBytes, lowS, openControlSeed, recoverCandidatePublicKeys, recoverYParity, sealControlSeed, serializeInstructions, signatureToFelts, toDataEntries, u256ToFelts, unwrapDEK, webauthnDigest, wrapDEK };
3854
+ //# sourceMappingURL=chunk-M5BGBODC.mjs.map
3855
+ //# sourceMappingURL=chunk-M5BGBODC.mjs.map