@cavos/kit 0.0.2 → 0.0.3

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.
@@ -8,6 +8,7 @@ var web3_js = require('@solana/web3.js');
8
8
  var hkdf = require('@noble/hashes/hkdf');
9
9
  var pbkdf2 = require('@noble/hashes/pbkdf2');
10
10
  var utils = require('@noble/hashes/utils');
11
+ var stellarSdk = require('@stellar/stellar-sdk');
11
12
  var jsxRuntime = require('react/jsx-runtime');
12
13
 
13
14
  // src/react/CavosProvider.tsx
@@ -22,6 +23,18 @@ function bytesToBigInt(bytes) {
22
23
  for (const b of bytes) out = out << 8n | BigInt(b);
23
24
  return out;
24
25
  }
26
+ function bytesToByteArrayCalldata(bytes) {
27
+ const CHUNK = 31;
28
+ const fullCount = Math.floor(bytes.length / CHUNK);
29
+ const out = [String(fullCount)];
30
+ for (let i = 0; i < fullCount; i++) {
31
+ out.push("0x" + bytesToBigInt(bytes.subarray(i * CHUNK, i * CHUNK + CHUNK)).toString(16));
32
+ }
33
+ const rem = bytes.subarray(fullCount * CHUNK);
34
+ out.push("0x" + (rem.length ? bytesToBigInt(rem).toString(16) : "0"));
35
+ out.push(String(rem.length));
36
+ return out;
37
+ }
25
38
  function bigIntTo32Bytes(value) {
26
39
  const out = new Uint8Array(32);
27
40
  let v = value;
@@ -159,7 +172,7 @@ var CAVOS_PAYMASTER_URL = {
159
172
  mainnet: "https://paymaster.cavos.xyz"
160
173
  };
161
174
  var DEVICE_ACCOUNT_CLASS_HASH = {
162
- sepolia: "0x1840aded59e8a0d2b440a134cb9079a7fc11b06c77f58ed189ab436a034ca6a",
175
+ sepolia: "0x25cbc5423e8ee895febb0ef2c3945b408da44d0039d915fbdd681fe6b6ba66b",
163
176
  mainnet: "0x1840aded59e8a0d2b440a134cb9079a7fc11b06c77f58ed189ab436a034ca6a"
164
177
  };
165
178
 
@@ -223,6 +236,73 @@ var StarknetAdapter = class {
223
236
  const sig = await this.opts.signer.sign(bigIntTo32Bytes(txHash));
224
237
  return signatureToFelts(sig).map((f) => starknet.num.toHex(f));
225
238
  }
239
+ // --- passkey approvers ---
240
+ buildAddApprover(accountAddress, passkey) {
241
+ return { contractAddress: accountAddress, entrypoint: "add_approver", calldata: pubkeyCalldata(passkey) };
242
+ }
243
+ buildRemoveApprover(accountAddress, passkey) {
244
+ return { contractAddress: accountAddress, entrypoint: "remove_approver", calldata: pubkeyCalldata(passkey) };
245
+ }
246
+ async isApprover(accountAddress, passkey) {
247
+ if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
248
+ const res = await this.opts.provider.callContract({
249
+ contractAddress: accountAddress,
250
+ entrypoint: "is_approver",
251
+ calldata: pubkeyCalldata(passkey)
252
+ });
253
+ return BigInt(res[0] ?? 0) !== 0n;
254
+ }
255
+ async getPasskeyNonce(accountAddress) {
256
+ if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
257
+ const res = await this.opts.provider.callContract({
258
+ contractAddress: accountAddress,
259
+ entrypoint: "get_passkey_nonce",
260
+ calldata: []
261
+ });
262
+ return BigInt(res[0] ?? 0);
263
+ }
264
+ /** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
265
+ * `sha256(new_x || new_y || nonce)` (coords 32B BE, nonce 16B BE). The batch
266
+ * challenge the passkey signs is `sha256(concat(leaves))` across chains. */
267
+ passkeyLeaf(newSigner, nonce) {
268
+ const msg = new Uint8Array(32 + 32 + 16);
269
+ msg.set(bigIntTo32Bytes(newSigner.x), 0);
270
+ msg.set(bigIntTo32Bytes(newSigner.y), 32);
271
+ msg.set(bigIntTo32Bytes(nonce).subarray(16), 64);
272
+ return sha256.sha256(msg);
273
+ }
274
+ /** Passkey-authorized `add_signer` call. `leaves`/`leafIndex` place this chain's
275
+ * leaf in the multi-chain batch (single chain → `[leaf]`, index 0). `yParity`
276
+ * matches the raw `(r, s)` — the contract normalizes high-S internally. */
277
+ buildAddSignerViaPasskey(accountAddress, newSigner, nonce, leaves, leafIndex, assertion, yParity) {
278
+ const [rl, rh] = u256ToFelts(assertion.r);
279
+ const [sl, sh] = u256ToFelts(assertion.s);
280
+ const leavesCalldata = [String(leaves.length)];
281
+ for (const leaf of leaves) {
282
+ const [lo, hi] = u256ToFelts(bytesToBigInt(leaf));
283
+ leavesCalldata.push(starknet.num.toHex(lo), starknet.num.toHex(hi));
284
+ }
285
+ return {
286
+ contractAddress: accountAddress,
287
+ entrypoint: "add_signer_via_passkey",
288
+ calldata: [
289
+ ...pubkeyCalldata(newSigner),
290
+ // new_x, new_y (u256 pairs)
291
+ starknet.num.toHex(nonce),
292
+ ...leavesCalldata,
293
+ // Array<u256> leaves
294
+ String(leafIndex),
295
+ ...bytesToByteArrayCalldata(assertion.authenticatorData),
296
+ ...bytesToByteArrayCalldata(assertion.clientDataJSON),
297
+ String(assertion.challengeOffset),
298
+ starknet.num.toHex(rl),
299
+ starknet.num.toHex(rh),
300
+ starknet.num.toHex(sl),
301
+ starknet.num.toHex(sh),
302
+ yParity ? "0x1" : "0x0"
303
+ ]
304
+ };
305
+ }
226
306
  };
227
307
  function pubkeyCalldata(pk) {
228
308
  const [xl, xh] = u256ToFelts(pk.x);
@@ -316,6 +396,9 @@ function deriveAddressSeed({ userId, appSalt }) {
316
396
  function deriveAddressSeedSolana({ userId, appSalt }) {
317
397
  return sha256.sha256(new TextEncoder().encode(`cavos:solana:v1:${userId}:${appSalt}`));
318
398
  }
399
+ function deriveAddressSeedStellar({ userId, appSalt }) {
400
+ return sha256.sha256(new TextEncoder().encode(`cavos:stellar:v1:${userId}:${appSalt}`));
401
+ }
319
402
  function feltFromString(s) {
320
403
  const bytes = new TextEncoder().encode(s);
321
404
  const chunks = [];
@@ -337,6 +420,8 @@ var DOMAIN_ADD = "cavos:add_signer:v1";
337
420
  var DOMAIN_REMOVE = "cavos:remove_signer:v1";
338
421
  var DOMAIN_TRANSFER = "cavos:transfer:v1";
339
422
  var DOMAIN_EXECUTE = "cavos:execute:v1";
423
+ var DOMAIN_ADD_APPROVER = "cavos:add_approver:v1";
424
+ var DOMAIN_REMOVE_APPROVER = "cavos:remove_approver:v1";
340
425
  var SECP256R1_N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
341
426
  var SOLANA_NETWORKS = {
342
427
  "solana-devnet": "https://api.devnet.solana.com",
@@ -429,6 +514,99 @@ var SolanaAdapter = class {
429
514
  });
430
515
  return [precompileIx, ix];
431
516
  }
517
+ /** `[precompile, add_approver]` bundle enrolling a passkey approver (device-signed). */
518
+ async buildAddApprover(account, passkey) {
519
+ const accountPk = new web3_js.PublicKey(account);
520
+ const compressed = compressedPubkey(passkey);
521
+ const nonce = await this.fetchNonce(accountPk);
522
+ const message = concatBytes(
523
+ Buffer.from(DOMAIN_ADD_APPROVER),
524
+ accountPk.toBuffer(),
525
+ compressed,
526
+ u64le(nonce)
527
+ );
528
+ const { precompileIx } = await this.signToPrecompile(message);
529
+ const ix = new web3_js.TransactionInstruction({
530
+ programId: this.programId,
531
+ keys: this.guardedKeys(accountPk),
532
+ data: Buffer.concat([anchorDiscriminator("add_approver"), Buffer.from(compressed)])
533
+ });
534
+ return [precompileIx, ix];
535
+ }
536
+ /** `[precompile, remove_approver]` bundle (device-signed). */
537
+ async buildRemoveApprover(account, passkey) {
538
+ const accountPk = new web3_js.PublicKey(account);
539
+ const compressed = compressedPubkey(passkey);
540
+ const nonce = await this.fetchNonce(accountPk);
541
+ const message = concatBytes(
542
+ Buffer.from(DOMAIN_REMOVE_APPROVER),
543
+ accountPk.toBuffer(),
544
+ compressed,
545
+ u64le(nonce)
546
+ );
547
+ const { precompileIx } = await this.signToPrecompile(message);
548
+ const ix = new web3_js.TransactionInstruction({
549
+ programId: this.programId,
550
+ keys: this.guardedKeys(accountPk),
551
+ data: Buffer.concat([anchorDiscriminator("remove_approver"), Buffer.from(compressed)])
552
+ });
553
+ return [precompileIx, ix];
554
+ }
555
+ /** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
556
+ * `sha256(compressed(new_signer) || passkey_nonce_le8)`. The batch challenge the
557
+ * passkey signs is `sha256(concat(leaves))` across chains. */
558
+ passkeyLeaf(newSigner, nonce) {
559
+ return sha256.sha256(concatBytes(compressedPubkey(newSigner), u64le(nonce)));
560
+ }
561
+ /**
562
+ * `[precompile(passkey), add_signer_via_passkey]` bundle. The precompile ix
563
+ * verifies the PASSKEY's WebAuthn assertion over `authData || sha256(clientDataJSON)`;
564
+ * the program ix binds the challenge to `newSigner` + the passkey nonce and adds
565
+ * the signer. No device signature — a gasless relayer can submit it.
566
+ */
567
+ buildAddSignerViaPasskey(account, newSigner, passkey, leaves, leafIndex, assertion) {
568
+ const accountPk = new web3_js.PublicKey(account);
569
+ const newCompressed = compressedPubkey(newSigner);
570
+ const passkeyCompressed = compressedPubkey(passkey);
571
+ const clientHash = sha256.sha256(assertion.clientDataJSON);
572
+ const message = concatBytes(assertion.authenticatorData, clientHash);
573
+ const signature = encodeLowSSignature(assertion.r, assertion.s);
574
+ const precompileIx = buildSecp256r1Instruction(passkeyCompressed, signature, message);
575
+ const leavesBlob = Buffer.concat([u32le(leaves.length), ...leaves.map((l) => Buffer.from(l))]);
576
+ const data = Buffer.concat([
577
+ anchorDiscriminator("add_signer_via_passkey"),
578
+ Buffer.from(newCompressed),
579
+ leavesBlob,
580
+ u32le(leafIndex),
581
+ serializeVecU8(assertion.authenticatorData),
582
+ serializeVecU8(assertion.clientDataJSON),
583
+ u32le(assertion.challengeOffset)
584
+ ]);
585
+ const ix = new web3_js.TransactionInstruction({
586
+ programId: this.programId,
587
+ keys: this.guardedKeys(accountPk),
588
+ data
589
+ });
590
+ return [precompileIx, ix];
591
+ }
592
+ /** Read whether `passkey` is a registered approver. */
593
+ async isApprover(account, passkey) {
594
+ const approvers = await this.fetchApprovers(new web3_js.PublicKey(account));
595
+ const target = Buffer.from(compressedPubkey(passkey)).toString("hex");
596
+ return approvers.some((a) => Buffer.from(a).toString("hex") === target);
597
+ }
598
+ /** Read the current passkey-approval nonce. */
599
+ async passkeyNonce(account) {
600
+ const info = await this.requireConnection().getAccountInfo(new web3_js.PublicKey(account));
601
+ if (!info) return 0n;
602
+ const d = info.data;
603
+ const signersLenOff = 8 + 32 + 1 + 8 + COMPRESSED_PUBKEY_SIZE;
604
+ const signerCount = d.readUInt32LE(signersLenOff);
605
+ const approversLenOff = signersLenOff + 4 + signerCount * COMPRESSED_PUBKEY_SIZE;
606
+ const approverCount = d.readUInt32LE(approversLenOff);
607
+ const passkeyNonceOff = approversLenOff + 4 + approverCount * COMPRESSED_PUBKEY_SIZE;
608
+ return readU64le(d, passkeyNonceOff);
609
+ }
432
610
  /** `[precompile, execute_transfer]` bundle moving lamports out of the account. */
433
611
  async buildExecuteTransfer(account, destination, amount) {
434
612
  const accountPk = new web3_js.PublicKey(account);
@@ -550,6 +728,22 @@ var SolanaAdapter = class {
550
728
  }
551
729
  return out;
552
730
  }
731
+ async fetchApprovers(account) {
732
+ const info = await this.requireConnection().getAccountInfo(account);
733
+ if (!info) return [];
734
+ const d = info.data;
735
+ const signersLenOff = 8 + 32 + 1 + 8 + COMPRESSED_PUBKEY_SIZE;
736
+ const signerCount = d.readUInt32LE(signersLenOff);
737
+ const approversLenOff = signersLenOff + 4 + signerCount * COMPRESSED_PUBKEY_SIZE;
738
+ const count = d.readUInt32LE(approversLenOff);
739
+ const out = [];
740
+ let off = approversLenOff + 4;
741
+ for (let i = 0; i < count; i++) {
742
+ out.push(Uint8Array.from(d.subarray(off, off + COMPRESSED_PUBKEY_SIZE)));
743
+ off += COMPRESSED_PUBKEY_SIZE;
744
+ }
745
+ return out;
746
+ }
553
747
  requireConnection() {
554
748
  if (!this.opts.connection) throw new Error("kit/solana: connection required for reads");
555
749
  return this.opts.connection;
@@ -604,6 +798,11 @@ function buildSecp256r1Instruction(compressed, signature, message) {
604
798
  function anchorDiscriminator(name) {
605
799
  return Buffer.from(sha256.sha256(`global:${name}`).slice(0, 8));
606
800
  }
801
+ function u32le(n) {
802
+ const b = Buffer.alloc(4);
803
+ b.writeUInt32LE(n);
804
+ return b;
805
+ }
607
806
  function u64le(n) {
608
807
  const b = Buffer.alloc(8);
609
808
  new DataView(b.buffer, b.byteOffset, 8).setBigUint64(0, BigInt(n), true);
@@ -998,6 +1197,34 @@ var WORDLIST = [
998
1197
  "beach",
999
1198
  "dusk"
1000
1199
  ];
1200
+ function batchChallenge(leaves) {
1201
+ const total = leaves.reduce((n, l) => n + l.length, 0);
1202
+ const cat = new Uint8Array(total);
1203
+ let o = 0;
1204
+ for (const l of leaves) {
1205
+ cat.set(l, o);
1206
+ o += l.length;
1207
+ }
1208
+ return sha256.sha256(cat);
1209
+ }
1210
+ function webauthnDigest(authenticatorData, clientDataJSON) {
1211
+ const clientHash = sha256.sha256(clientDataJSON);
1212
+ const msg = new Uint8Array(authenticatorData.length + clientHash.length);
1213
+ msg.set(authenticatorData, 0);
1214
+ msg.set(clientHash, authenticatorData.length);
1215
+ return sha256.sha256(msg);
1216
+ }
1217
+ function recoverCandidatePublicKeys(r, s, digest) {
1218
+ const out = [];
1219
+ for (const bit of [0, 1]) {
1220
+ try {
1221
+ const point = new p256.p256.Signature(r, s).addRecoveryBit(bit).recoverPublicKey(digest).toAffine();
1222
+ out.push({ publicKey: { x: point.x, y: point.y }, yParity: bit === 1 });
1223
+ } catch {
1224
+ }
1225
+ }
1226
+ return out;
1227
+ }
1001
1228
 
1002
1229
  // src/chains/solana/CavosSolana.ts
1003
1230
  var CavosSolana = class _CavosSolana {
@@ -1078,6 +1305,69 @@ var CavosSolana = class _CavosSolana {
1078
1305
  const ixs = await this.adapter.buildAddSigner(this.address, pubkey);
1079
1306
  return this.send(ixs);
1080
1307
  }
1308
+ /**
1309
+ * Enroll a passkey as an approver (2FA-style step-up). Device-signed + gasless;
1310
+ * requires a ready device. Idempotent. Returns the passkey pubkey + tx hash.
1311
+ */
1312
+ async enrollPasskey(passkey, params) {
1313
+ const enrolled = await passkey.enroll(params);
1314
+ const { transactionHash } = await this.addApprover(enrolled.publicKey);
1315
+ return { publicKey: enrolled.publicKey, transactionHash };
1316
+ }
1317
+ /** Register an already-enrolled passkey pubkey as an approver (gasless).
1318
+ * Idempotent. Lets one passkey be registered across chains without re-prompting. */
1319
+ async addApprover(pubkey) {
1320
+ if (this.status !== "ready") {
1321
+ throw new Error("kit/solana: addApprover requires a ready, authorized device");
1322
+ }
1323
+ if (await this.adapter.isApprover(this.address, pubkey)) return {};
1324
+ const ixs = await this.adapter.buildAddApprover(this.address, pubkey);
1325
+ const transactionHash = await this.send(ixs);
1326
+ return { transactionHash };
1327
+ }
1328
+ /**
1329
+ * From a fresh browser (status `needs-device-approval`), approve adding THIS
1330
+ * device with the user's synced passkey. Gasless via the relayer — the bundle
1331
+ * carries the passkey's WebAuthn assertion, so no device signature is needed.
1332
+ */
1333
+ async approveThisDeviceWithPasskey(passkey) {
1334
+ if (this.status === "ready") {
1335
+ throw new Error("kit/solana: this device is already an authorized signer");
1336
+ }
1337
+ const { leaf, nonce } = await this.passkeyLeafForThisDevice();
1338
+ const leaves = [leaf];
1339
+ const assertion = await passkey.assert(batchChallenge(leaves));
1340
+ const { transactionHash } = await this.submitPasskeyApproval(assertion, leaves, 0, nonce);
1341
+ return transactionHash;
1342
+ }
1343
+ /** This device's leaf + passkey nonce for a (possibly multi-chain) batch. */
1344
+ async passkeyLeafForThisDevice() {
1345
+ const nonce = await this.adapter.passkeyNonce(this.address);
1346
+ return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
1347
+ }
1348
+ /** Submit `add_signer_via_passkey` given a shared assertion + batch position.
1349
+ * Used by `approveThisDeviceWithPasskey` and `approveDeviceEverywhere`. */
1350
+ async submitPasskeyApproval(assertion, leaves, leafIndex, _nonce) {
1351
+ const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
1352
+ const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
1353
+ let approver = null;
1354
+ for (const cand of candidates) {
1355
+ if (await this.adapter.isApprover(this.address, cand.publicKey)) {
1356
+ approver = cand.publicKey;
1357
+ break;
1358
+ }
1359
+ }
1360
+ if (!approver) throw new Error("kit/solana: this passkey is not a registered approver");
1361
+ const ixs = this.adapter.buildAddSignerViaPasskey(
1362
+ this.address,
1363
+ this.devicePubkey,
1364
+ approver,
1365
+ leaves,
1366
+ leafIndex,
1367
+ assertion
1368
+ );
1369
+ return { transactionHash: await this.send(ixs) };
1370
+ }
1081
1371
  /** Move `amount` lamports out of the account to `destination` (device-signed). */
1082
1372
  async execute(amount, destination) {
1083
1373
  if (this.status !== "ready") {
@@ -1190,6 +1480,615 @@ var CavosSolana = class _CavosSolana {
1190
1480
  };
1191
1481
  var defaultRegistry = new InMemoryWalletRegistry();
1192
1482
 
1483
+ // src/chains/stellar/constants.ts
1484
+ var FACTORY_CONTRACT_ID = {
1485
+ // Re-deployed 2026-07-01 with the passkey-approval device-account wasm (batched
1486
+ // multi-chain challenge). The factory pins the wasm hash immutably, so a new
1487
+ // wasm needs a new factory → new account addresses; testnet has no prod wallets.
1488
+ "stellar-testnet": "CBCJIODXIEBOXXD66KCUCF7ZDYJARKI4ZIVQOVWPULOBH5XGNCDP6W3I",
1489
+ // Set once the factory is deployed to mainnet (its address differs — network id
1490
+ // is part of contract-address derivation).
1491
+ "stellar-mainnet": ""
1492
+ };
1493
+ var STELLAR_NETWORKS = {
1494
+ "stellar-testnet": {
1495
+ rpcUrl: "https://soroban-testnet.stellar.org",
1496
+ passphrase: "Test SDF Network ; September 2015"
1497
+ },
1498
+ "stellar-mainnet": {
1499
+ rpcUrl: "https://soroban-rpc.mainnet.stellar.gateway.fm",
1500
+ passphrase: "Public Global Stellar Network ; September 2015"
1501
+ }
1502
+ };
1503
+ var NATIVE_SAC_ID = {
1504
+ "stellar-testnet": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
1505
+ "stellar-mainnet": "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA"
1506
+ };
1507
+
1508
+ // src/chains/stellar/StellarAdapter.ts
1509
+ var SECP256R1_N2 = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
1510
+ var StellarAdapter = class {
1511
+ constructor(opts) {
1512
+ this.chain = "stellar";
1513
+ this.network = opts.network;
1514
+ this.passphrase = STELLAR_NETWORKS[opts.network].passphrase;
1515
+ this.rpcUrl = opts.rpcUrl ?? STELLAR_NETWORKS[opts.network].rpcUrl;
1516
+ this.factoryId = opts.factoryId ?? FACTORY_CONTRACT_ID[opts.network];
1517
+ if (!this.factoryId) {
1518
+ throw new Error(`kit/stellar: no factory contract id configured for ${opts.network}`);
1519
+ }
1520
+ this.signer = opts.signer;
1521
+ }
1522
+ server() {
1523
+ if (!this._server) {
1524
+ this._server = new stellarSdk.rpc.Server(this.rpcUrl, {
1525
+ allowHttp: this.rpcUrl.startsWith("http://")
1526
+ });
1527
+ }
1528
+ return this._server;
1529
+ }
1530
+ networkId() {
1531
+ return stellarSdk.hash(Buffer.from(this.passphrase));
1532
+ }
1533
+ /**
1534
+ * Deterministic account address for `(addressSeed, initialSigner)` — computed
1535
+ * off-chain, byte-identical to the factory's on-chain `account_address`.
1536
+ * `contractId = sha256(HashIdPreimage(networkId, factory, salt))` with
1537
+ * `salt = sha256(addressSeed || sec1(initialSigner))`.
1538
+ */
1539
+ computeAddress(addressSeed, initialSigner) {
1540
+ const salt = this.accountSalt(addressSeed, initialSigner);
1541
+ const preimage = stellarSdk.xdr.HashIdPreimage.envelopeTypeContractId(
1542
+ new stellarSdk.xdr.HashIdPreimageContractId({
1543
+ networkId: this.networkId(),
1544
+ contractIdPreimage: stellarSdk.xdr.ContractIdPreimage.contractIdPreimageFromAddress(
1545
+ new stellarSdk.xdr.ContractIdPreimageFromAddress({
1546
+ address: new stellarSdk.Address(this.factoryId).toScAddress(),
1547
+ salt
1548
+ })
1549
+ )
1550
+ })
1551
+ );
1552
+ return stellarSdk.StrKey.encodeContract(stellarSdk.hash(preimage.toXDR()));
1553
+ }
1554
+ /** `salt = sha256(addressSeed(32) || sec1(initialSigner)(65))` — matches the factory. */
1555
+ accountSalt(addressSeed, initialSigner) {
1556
+ return stellarSdk.hash(Buffer.concat([Buffer.from(addressSeed), Buffer.from(sec1Pubkey(initialSigner))]));
1557
+ }
1558
+ /** Host function: `factory.deploy(address_seed, initial_signer)`. */
1559
+ buildDeploy(addressSeed, initialSigner) {
1560
+ return invokeFunc(this.factoryId, "deploy", [
1561
+ bytesScVal(addressSeed),
1562
+ bytesScVal(sec1Pubkey(initialSigner))
1563
+ ]);
1564
+ }
1565
+ /** Host function: `account.add_signer(new_signer)` (requires device auth). */
1566
+ buildAddSigner(accountAddress, signer) {
1567
+ return invokeFunc(accountAddress, "add_signer", [bytesScVal(sec1Pubkey(signer))]);
1568
+ }
1569
+ /** Host function: `account.remove_signer(signer)` (requires device auth). */
1570
+ buildRemoveSigner(accountAddress, signer) {
1571
+ return invokeFunc(accountAddress, "remove_signer", [bytesScVal(sec1Pubkey(signer))]);
1572
+ }
1573
+ /** Host function: `account.add_approver(passkey)` (requires device auth). */
1574
+ buildAddApprover(accountAddress, passkey) {
1575
+ return invokeFunc(accountAddress, "add_approver", [bytesScVal(sec1Pubkey(passkey))]);
1576
+ }
1577
+ /** Host function: `account.remove_approver(passkey)` (requires device auth). */
1578
+ buildRemoveApprover(accountAddress, passkey) {
1579
+ return invokeFunc(accountAddress, "remove_approver", [bytesScVal(sec1Pubkey(passkey))]);
1580
+ }
1581
+ /** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
1582
+ * `sha256(sec1(new_signer) || nonce_be8)`. The batch challenge the passkey signs
1583
+ * is `sha256(concat(leaves))` across chains. */
1584
+ passkeyLeaf(newSigner, nonce) {
1585
+ const msg = new Uint8Array(65 + 8);
1586
+ msg.set(sec1Pubkey(newSigner), 0);
1587
+ const n = new Uint8Array(8);
1588
+ let v = nonce;
1589
+ for (let i = 7; i >= 0; i--) {
1590
+ n[i] = Number(v & 0xffn);
1591
+ v >>= 8n;
1592
+ }
1593
+ msg.set(n, 65);
1594
+ return sha256.sha256(msg);
1595
+ }
1596
+ /** Host function: passkey-authorized `add_signer_via_passkey` (no device auth —
1597
+ * authorized by the embedded WebAuthn assertion, so any relayer can submit).
1598
+ * `leaves`/`leafIndex` place this chain's leaf in the multi-chain batch. */
1599
+ buildAddSignerViaPasskey(accountAddress, newSigner, passkey, nonce, leaves, leafIndex, assertion) {
1600
+ const sig = encodeLowSSignature2({ r: assertion.r, s: assertion.s});
1601
+ const leavesScVal = stellarSdk.xdr.ScVal.scvVec(leaves.map((l) => bytesScVal(l)));
1602
+ return invokeFunc(accountAddress, "add_signer_via_passkey", [
1603
+ bytesScVal(sec1Pubkey(newSigner)),
1604
+ bytesScVal(sec1Pubkey(passkey)),
1605
+ stellarSdk.nativeToScVal(nonce, { type: "u64" }),
1606
+ leavesScVal,
1607
+ stellarSdk.nativeToScVal(leafIndex, { type: "u32" }),
1608
+ bytesScVal(assertion.authenticatorData),
1609
+ bytesScVal(assertion.clientDataJSON),
1610
+ stellarSdk.nativeToScVal(assertion.challengeOffset, { type: "u32" }),
1611
+ bytesScVal(sig)
1612
+ ]);
1613
+ }
1614
+ /** Read whether `passkey` is a registered approver (read-only simulation). */
1615
+ async isApprover(accountAddress, passkey, readSource) {
1616
+ if (!await this.isDeployed(accountAddress)) return false;
1617
+ const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1618
+ const src = new Account3(readSource, "0");
1619
+ const op = stellarSdk.Operation.invokeHostFunction({
1620
+ func: invokeFunc(accountAddress, "is_approver", [bytesScVal(sec1Pubkey(passkey))]),
1621
+ auth: []
1622
+ });
1623
+ const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
1624
+ const sim = await this.server().simulateTransaction(tx2);
1625
+ if (stellarSdk.rpc.Api.isSimulationError(sim)) {
1626
+ throw new Error(`kit/stellar: is_approver simulation failed: ${sim.error}`);
1627
+ }
1628
+ if (!sim.result?.retval) return false;
1629
+ return stellarSdk.scValToNative(sim.result.retval) === true;
1630
+ }
1631
+ /** Read the current passkey-approval nonce (read-only simulation). */
1632
+ async passkeyNonce(accountAddress, readSource) {
1633
+ if (!await this.isDeployed(accountAddress)) return 0n;
1634
+ const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1635
+ const src = new Account3(readSource, "0");
1636
+ const op = stellarSdk.Operation.invokeHostFunction({
1637
+ func: invokeFunc(accountAddress, "passkey_nonce", []),
1638
+ auth: []
1639
+ });
1640
+ const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
1641
+ const sim = await this.server().simulateTransaction(tx2);
1642
+ if (stellarSdk.rpc.Api.isSimulationError(sim)) {
1643
+ throw new Error(`kit/stellar: passkey_nonce simulation failed: ${sim.error}`);
1644
+ }
1645
+ if (!sim.result?.retval) return 0n;
1646
+ return BigInt(stellarSdk.scValToNative(sim.result.retval));
1647
+ }
1648
+ /** Host function: SEP-41 `token.transfer(from=account, to, amount)` (device auth). */
1649
+ buildTransfer(tokenId, accountAddress, destination, amount) {
1650
+ return invokeFunc(tokenId, "transfer", [
1651
+ new stellarSdk.Address(accountAddress).toScVal(),
1652
+ new stellarSdk.Address(destination).toScVal(),
1653
+ stellarSdk.nativeToScVal(amount, { type: "i128" })
1654
+ ]);
1655
+ }
1656
+ /**
1657
+ * Sign a Soroban authorization entry with the silent device key, producing the
1658
+ * `Vec<DeviceSignature>` the account's `__check_auth` verifies. The device
1659
+ * signs `sha256(preimage)` (WebCrypto hashes once more internally), which is
1660
+ * exactly what the contract recomputes. Mutates + returns the entry.
1661
+ */
1662
+ async signAuthEntry(entry, validUntilLedger) {
1663
+ const addrCreds = entry.credentials().address();
1664
+ addrCreds.signatureExpirationLedger(validUntilLedger);
1665
+ const preimage = stellarSdk.xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
1666
+ new stellarSdk.xdr.HashIdPreimageSorobanAuthorization({
1667
+ networkId: this.networkId(),
1668
+ nonce: addrCreds.nonce(),
1669
+ signatureExpirationLedger: validUntilLedger,
1670
+ invocation: entry.rootInvocation()
1671
+ })
1672
+ );
1673
+ const payload = stellarSdk.hash(preimage.toXDR());
1674
+ const sig = await this.signer.sign(new Uint8Array(payload));
1675
+ const pubkey = await this.signer.getPublicKey();
1676
+ addrCreds.signature(deviceSignatureScVal(pubkey, sig));
1677
+ return entry;
1678
+ }
1679
+ /**
1680
+ * Read a SEP-41 token balance of `account` via a read-only simulation of
1681
+ * `token.balance(account)`. Returns 0 when the account isn't deployed or holds
1682
+ * none. `readSource` is any funded G-account (used only for the simulation).
1683
+ */
1684
+ async readBalance(tokenId, account, readSource) {
1685
+ if (!await this.isDeployed(account)) return 0n;
1686
+ const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1687
+ const src = new Account3(readSource, "0");
1688
+ const op = stellarSdk.Operation.invokeHostFunction({
1689
+ func: invokeFunc(tokenId, "balance", [new stellarSdk.Address(account).toScVal()]),
1690
+ auth: []
1691
+ });
1692
+ const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
1693
+ const sim = await this.server().simulateTransaction(tx2);
1694
+ if (stellarSdk.rpc.Api.isSimulationError(sim) || !sim.result?.retval) return 0n;
1695
+ return BigInt(stellarSdk.scValToNative(sim.result.retval));
1696
+ }
1697
+ /** Whether the account contract instance exists on-chain (is deployed). */
1698
+ async isDeployed(accountAddress) {
1699
+ try {
1700
+ const res = await this.server().getContractData(
1701
+ accountAddress,
1702
+ stellarSdk.xdr.ScVal.scvLedgerKeyContractInstance(),
1703
+ stellarSdk.rpc.Durability.Persistent
1704
+ );
1705
+ return !!res;
1706
+ } catch {
1707
+ return false;
1708
+ }
1709
+ }
1710
+ /**
1711
+ * Read whether `signer` is a currently-authorized signer of the account, via a
1712
+ * read-only simulation of `account.is_authorized(signer)`. `readSource` is any
1713
+ * funded G-account (used only for the simulation's source/sequence).
1714
+ */
1715
+ async isAuthorizedSigner(accountAddress, signer, readSource) {
1716
+ if (!await this.isDeployed(accountAddress)) return false;
1717
+ const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1718
+ const src = new Account3(readSource, "0");
1719
+ const op = stellarSdk.Operation.invokeHostFunction({
1720
+ func: invokeFunc(accountAddress, "is_authorized", [bytesScVal(sec1Pubkey(signer))]),
1721
+ auth: []
1722
+ });
1723
+ const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
1724
+ const sim = await this.server().simulateTransaction(tx2);
1725
+ if (stellarSdk.rpc.Api.isSimulationError(sim)) {
1726
+ throw new Error(`kit/stellar: is_authorized simulation failed: ${sim.error}`);
1727
+ }
1728
+ if (!sim.result?.retval) return false;
1729
+ return stellarSdk.scValToNative(sim.result.retval) === true;
1730
+ }
1731
+ };
1732
+ function sec1Pubkey(pk) {
1733
+ const out = new Uint8Array(65);
1734
+ out[0] = 4;
1735
+ out.set(bigIntTo32Bytes(pk.x), 1);
1736
+ out.set(bigIntTo32Bytes(pk.y), 33);
1737
+ return out;
1738
+ }
1739
+ function encodeLowSSignature2(sig) {
1740
+ const lowS = sig.s > SECP256R1_N2 / 2n ? SECP256R1_N2 - sig.s : sig.s;
1741
+ const out = new Uint8Array(64);
1742
+ out.set(bigIntTo32Bytes(sig.r), 0);
1743
+ out.set(bigIntTo32Bytes(lowS), 32);
1744
+ return out;
1745
+ }
1746
+ function deviceSignatureScVal(pubkey, sig) {
1747
+ const element = stellarSdk.nativeToScVal(
1748
+ {
1749
+ public_key: Buffer.from(sec1Pubkey(pubkey)),
1750
+ signature: Buffer.from(encodeLowSSignature2(sig))
1751
+ },
1752
+ { type: { public_key: ["symbol", "bytes"], signature: ["symbol", "bytes"] } }
1753
+ );
1754
+ return stellarSdk.xdr.ScVal.scvVec([element]);
1755
+ }
1756
+ function invokeFunc(contractId, method, args) {
1757
+ return stellarSdk.xdr.HostFunction.hostFunctionTypeInvokeContract(
1758
+ new stellarSdk.xdr.InvokeContractArgs({
1759
+ contractAddress: new stellarSdk.Address(contractId).toScAddress(),
1760
+ functionName: method,
1761
+ args
1762
+ })
1763
+ );
1764
+ }
1765
+ function bytesScVal(bytes) {
1766
+ return stellarSdk.xdr.ScVal.scvBytes(Buffer.from(bytes));
1767
+ }
1768
+
1769
+ // src/chains/stellar/StellarRelayer.ts
1770
+ var StellarRelayer = class {
1771
+ constructor(opts) {
1772
+ this.opts = opts;
1773
+ }
1774
+ /** The relayer's source/fee-payer G-account (fetched + cached from the backend). */
1775
+ async getSource() {
1776
+ if (this.source) return this.source;
1777
+ const res = await fetch(`${this.opts.baseUrl}/api/stellar/relay?network=${this.opts.network}`);
1778
+ if (!res.ok) throw new Error(`kit/stellar: relayer source lookup failed (${res.status})`);
1779
+ const { fee_payer } = await res.json();
1780
+ this.source = fee_payer;
1781
+ return this.source;
1782
+ }
1783
+ /**
1784
+ * POST the assembled, device-authorized transaction XDR to the relayer to sign
1785
+ * the envelope + submit. Returns the confirmed transaction hash.
1786
+ */
1787
+ async submit(transactionXdr) {
1788
+ const res = await fetch(`${this.opts.baseUrl}/api/stellar/relay`, {
1789
+ method: "POST",
1790
+ headers: { "Content-Type": "application/json" },
1791
+ body: JSON.stringify({
1792
+ app_id: this.opts.appId,
1793
+ network: this.opts.network,
1794
+ transaction: transactionXdr
1795
+ })
1796
+ });
1797
+ if (!res.ok) {
1798
+ const detail = await res.text().catch(() => "");
1799
+ throw new Error(`kit/stellar: relay failed (${res.status}) ${detail}`);
1800
+ }
1801
+ const { hash: hash6 } = await res.json();
1802
+ return hash6;
1803
+ }
1804
+ };
1805
+
1806
+ // src/chains/stellar/CavosStellar.ts
1807
+ var CavosStellar = class _CavosStellar {
1808
+ constructor(identity, address, status, network, adapter, devicePubkey, relayer, sourceKeypair) {
1809
+ this.identity = identity;
1810
+ this.address = address;
1811
+ this.status = status;
1812
+ this.network = network;
1813
+ this.adapter = adapter;
1814
+ this.devicePubkey = devicePubkey;
1815
+ this.relayer = relayer;
1816
+ this.sourceKeypair = sourceKeypair;
1817
+ /** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
1818
+ this.chain = "stellar";
1819
+ }
1820
+ get publicKey() {
1821
+ return this.devicePubkey;
1822
+ }
1823
+ static async connect(opts) {
1824
+ const identity = opts.identity ?? await opts.auth?.authenticate();
1825
+ if (!identity) throw new Error("kit/stellar: connect requires `identity` or `auth`");
1826
+ const signer = opts.createSigner ? await opts.createSigner(`${identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${identity.userId}:${opts.appSalt}` });
1827
+ const devicePubkey = await signer.getPublicKey();
1828
+ const adapter = new StellarAdapter({
1829
+ network: opts.network,
1830
+ rpcUrl: opts.rpcUrl,
1831
+ factoryId: opts.factoryId,
1832
+ signer
1833
+ });
1834
+ const addressSeed = deriveAddressSeedStellar({ userId: identity.userId, appSalt: opts.appSalt });
1835
+ const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
1836
+ const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
1837
+ const relayer = opts.relayer ?? (opts.appId ? new StellarRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : void 0);
1838
+ const build = (address2, status) => new _CavosStellar(identity, address2, status, opts.network, adapter, devicePubkey, relayer, opts.sourceKeypair);
1839
+ const self = build("", "needs-device-approval");
1840
+ const readSource = await self.resolveSource();
1841
+ const existing = await registry.lookup(identity.userId);
1842
+ if (existing) {
1843
+ const isSigner2 = await adapter.isAuthorizedSigner(existing.address, devicePubkey, readSource);
1844
+ return build(existing.address, isSigner2 ? "ready" : "needs-device-approval");
1845
+ }
1846
+ const address = adapter.computeAddress(addressSeed, devicePubkey);
1847
+ if (!await adapter.isDeployed(address)) {
1848
+ const func = adapter.buildDeploy(addressSeed, devicePubkey);
1849
+ await self.submitHostFunction(func, void 0);
1850
+ }
1851
+ await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
1852
+ const isSigner = await adapter.isAuthorizedSigner(address, devicePubkey, readSource);
1853
+ return build(address, isSigner ? "ready" : "needs-device-approval");
1854
+ }
1855
+ /** Authorize an additional device signer (device-signed via `__check_auth`). */
1856
+ async addSigner(pubkey) {
1857
+ const func = this.adapter.buildAddSigner(this.address, pubkey);
1858
+ return this.submitHostFunction(func, this.address);
1859
+ }
1860
+ /**
1861
+ * Enroll a passkey as an approver (2FA-style step-up). Device-signed + gasless;
1862
+ * requires a ready device. Idempotent. Returns the passkey pubkey + tx hash.
1863
+ */
1864
+ async enrollPasskey(passkey, params) {
1865
+ const enrolled = await passkey.enroll(params);
1866
+ const { transactionHash } = await this.addApprover(enrolled.publicKey);
1867
+ return { publicKey: enrolled.publicKey, transactionHash };
1868
+ }
1869
+ /** Register an already-enrolled passkey pubkey as an approver (gasless).
1870
+ * Idempotent. Lets one passkey be registered across chains without re-prompting. */
1871
+ async addApprover(pubkey) {
1872
+ if (this.status !== "ready") {
1873
+ throw new Error("kit/stellar: addApprover requires a ready, authorized device");
1874
+ }
1875
+ const readSource = await this.resolveSource();
1876
+ if (await this.adapter.isApprover(this.address, pubkey, readSource)) return {};
1877
+ const func = this.adapter.buildAddApprover(this.address, pubkey);
1878
+ const transactionHash = await this.submitHostFunction(func, this.address);
1879
+ return { transactionHash };
1880
+ }
1881
+ /**
1882
+ * From a fresh browser (status `needs-device-approval`), approve adding THIS
1883
+ * device using the user's synced passkey. Gasless via the relayer — the call
1884
+ * carries the WebAuthn assertion, so no device signature is needed. Returns the
1885
+ * tx hash. No trip back to an already-authorized device.
1886
+ */
1887
+ async approveThisDeviceWithPasskey(passkey) {
1888
+ if (this.status === "ready") {
1889
+ throw new Error("kit/stellar: this device is already an authorized signer");
1890
+ }
1891
+ const { leaf, nonce } = await this.passkeyLeafForThisDevice();
1892
+ const leaves = [leaf];
1893
+ const assertion = await passkey.assert(batchChallenge(leaves));
1894
+ const { transactionHash } = await this.submitPasskeyApproval(assertion, leaves, 0, nonce);
1895
+ return transactionHash;
1896
+ }
1897
+ /** This device's leaf + passkey nonce for a (possibly multi-chain) batch. */
1898
+ async passkeyLeafForThisDevice() {
1899
+ const readSource = await this.resolveSource();
1900
+ const nonce = await this.adapter.passkeyNonce(this.address, readSource);
1901
+ return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
1902
+ }
1903
+ /** Submit `add_signer_via_passkey` given a shared assertion + batch position.
1904
+ * No device auth entry — authorized purely by the passkey assertion. */
1905
+ async submitPasskeyApproval(assertion, leaves, leafIndex, nonce) {
1906
+ const readSource = await this.resolveSource();
1907
+ const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
1908
+ const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
1909
+ let approver = null;
1910
+ for (const cand of candidates) {
1911
+ if (await this.adapter.isApprover(this.address, cand.publicKey, readSource)) {
1912
+ approver = cand.publicKey;
1913
+ break;
1914
+ }
1915
+ }
1916
+ if (!approver) throw new Error("kit/stellar: this passkey is not a registered approver");
1917
+ const func = this.adapter.buildAddSignerViaPasskey(
1918
+ this.address,
1919
+ this.devicePubkey,
1920
+ approver,
1921
+ nonce,
1922
+ leaves,
1923
+ leafIndex,
1924
+ assertion
1925
+ );
1926
+ return { transactionHash: await this.submitHostFunction(func, void 0) };
1927
+ }
1928
+ /** Move `amount` stroops of native XLM to `destination` (device-signed). */
1929
+ async execute(amount, destination) {
1930
+ return this.executeTransfer(NATIVE_SAC_ID[this.network], amount, destination);
1931
+ }
1932
+ /** Read this account's balance of `tokenId` (defaults to native XLM), in stroops. */
1933
+ async balance(tokenId = NATIVE_SAC_ID[this.network]) {
1934
+ const readSource = await this.resolveSource();
1935
+ return this.adapter.readBalance(tokenId, this.address, readSource);
1936
+ }
1937
+ /** Transfer `amount` of any SEP-41 token out of the account (device-signed). */
1938
+ async executeTransfer(tokenId, amount, destination) {
1939
+ if (this.status !== "ready") {
1940
+ throw new Error("kit/stellar: this device is not yet an authorized signer of the wallet");
1941
+ }
1942
+ const func = this.adapter.buildTransfer(tokenId, this.address, destination, amount);
1943
+ return this.submitHostFunction(func, this.address);
1944
+ }
1945
+ /**
1946
+ * Register the backup signer derived from `code` as an authorized signer of
1947
+ * this account (device-signed). Idempotent. The code never leaves the device —
1948
+ * only the derived public key travels on-chain. Mirrors the other chains.
1949
+ */
1950
+ async setupRecovery(code) {
1951
+ if (this.status !== "ready") {
1952
+ throw new Error("kit/stellar: setupRecovery requires a ready, registered device");
1953
+ }
1954
+ const { publicKey: backupPubkey } = deriveBackupKey(code);
1955
+ const readSource = await this.resolveSource();
1956
+ if (await this.adapter.isAuthorizedSigner(this.address, backupPubkey, readSource)) return void 0;
1957
+ return this.addSigner(backupPubkey);
1958
+ }
1959
+ /**
1960
+ * Recover an account after losing every device signer: derive the backup key
1961
+ * from `code`, use it (not the new device) to authorize `add_signer(newDevice)`,
1962
+ * and return a ready handle bound to the new device. The address is unchanged.
1963
+ */
1964
+ static async recover(opts) {
1965
+ const signer = opts.createSigner ? await opts.createSigner(`${opts.identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${opts.identity.userId}:${opts.appSalt}` });
1966
+ const devicePubkey = await signer.getPublicKey();
1967
+ const backup = BackupSigner.fromCode(opts.code);
1968
+ const backupAdapter = new StellarAdapter({
1969
+ network: opts.network,
1970
+ rpcUrl: opts.rpcUrl,
1971
+ factoryId: opts.factoryId,
1972
+ signer: backup
1973
+ });
1974
+ const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
1975
+ const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
1976
+ const existing = await registry.lookup(opts.identity.userId);
1977
+ if (!existing) {
1978
+ throw new Error("kit/stellar: no account found for this identity \u2014 nothing to recover");
1979
+ }
1980
+ const relayer = opts.relayer ?? (opts.appId ? new StellarRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : void 0);
1981
+ const backupHandle = new _CavosStellar(
1982
+ opts.identity,
1983
+ existing.address,
1984
+ "ready",
1985
+ opts.network,
1986
+ backupAdapter,
1987
+ devicePubkey,
1988
+ relayer,
1989
+ opts.sourceKeypair
1990
+ );
1991
+ const readSource = await backupHandle.resolveSource();
1992
+ if (!await backupAdapter.isAuthorizedSigner(existing.address, devicePubkey, readSource)) {
1993
+ await backupHandle.addSigner(devicePubkey);
1994
+ }
1995
+ const adapter = new StellarAdapter({
1996
+ network: opts.network,
1997
+ rpcUrl: opts.rpcUrl,
1998
+ factoryId: opts.factoryId,
1999
+ signer
2000
+ });
2001
+ return new _CavosStellar(
2002
+ opts.identity,
2003
+ existing.address,
2004
+ "ready",
2005
+ opts.network,
2006
+ adapter,
2007
+ devicePubkey,
2008
+ relayer,
2009
+ opts.sourceKeypair
2010
+ );
2011
+ }
2012
+ /** The transaction source/fee-payer G-address (relayer or self-funded). */
2013
+ async resolveSource() {
2014
+ if (this.relayer) return this.relayer.getSource();
2015
+ if (this.sourceKeypair) return this.sourceKeypair.publicKey();
2016
+ throw new Error("kit/stellar: a relayer (appId) or sourceKeypair is required");
2017
+ }
2018
+ /**
2019
+ * Build → simulate → device-sign auth → assemble → submit an invoke-contract
2020
+ * host function. `authAccount` is the account whose `__check_auth` must sign the
2021
+ * operation's Soroban auth entry (undefined for a plain factory deploy).
2022
+ */
2023
+ async submitHostFunction(func, authAccount) {
2024
+ const server = this.adapter.server();
2025
+ const sourceAddr = await this.resolveSource();
2026
+ const simSource = new stellarSdk.Account(sourceAddr, "0");
2027
+ const unsignedOp = stellarSdk.Operation.invokeHostFunction({ func, auth: [] });
2028
+ const simTx = new stellarSdk.TransactionBuilder(simSource, {
2029
+ fee: stellarSdk.BASE_FEE,
2030
+ networkPassphrase: this.adapter.passphrase
2031
+ }).addOperation(unsignedOp).setTimeout(180).build();
2032
+ const sim = await server.simulateTransaction(simTx);
2033
+ if (stellarSdk.rpc.Api.isSimulationError(sim)) {
2034
+ throw new Error(`kit/stellar: simulation failed: ${sim.error}`);
2035
+ }
2036
+ const validUntil = (await server.getLatestLedger()).sequence + 100;
2037
+ const entries = sim.result?.auth ?? [];
2038
+ const signedAuth = [];
2039
+ for (const entry of entries) {
2040
+ if (authAccount && isAddressCredentialFor(entry, authAccount)) {
2041
+ signedAuth.push(await this.adapter.signAuthEntry(entry, validUntil));
2042
+ } else {
2043
+ signedAuth.push(entry);
2044
+ }
2045
+ }
2046
+ const account = await server.getAccount(sourceAddr);
2047
+ const finalOp = stellarSdk.Operation.invokeHostFunction({ func, auth: signedAuth });
2048
+ const built = new stellarSdk.TransactionBuilder(account, {
2049
+ fee: stellarSdk.BASE_FEE,
2050
+ networkPassphrase: this.adapter.passphrase
2051
+ }).addOperation(finalOp).setTimeout(180).build();
2052
+ const authSim = await server.simulateTransaction(built);
2053
+ if (stellarSdk.rpc.Api.isSimulationError(authSim)) {
2054
+ throw new Error(`kit/stellar: auth simulation failed: ${authSim.error}`);
2055
+ }
2056
+ const assembled = stellarSdk.rpc.assembleTransaction(built, authSim).build();
2057
+ if (this.relayer) {
2058
+ return this.relayer.submit(assembled.toXDR());
2059
+ }
2060
+ if (this.sourceKeypair) {
2061
+ assembled.sign(this.sourceKeypair);
2062
+ return this.sendAndConfirm(assembled);
2063
+ }
2064
+ throw new Error("kit/stellar: no relayer or sourceKeypair configured to submit");
2065
+ }
2066
+ /** Submit a signed tx via RPC and poll to confirmation. Returns the hash. */
2067
+ async sendAndConfirm(tx2) {
2068
+ const server = this.adapter.server();
2069
+ const sent = await server.sendTransaction(tx2);
2070
+ if (sent.status === "ERROR") {
2071
+ throw new Error(`kit/stellar: submit rejected: ${JSON.stringify(sent.errorResult)}`);
2072
+ }
2073
+ const hash6 = sent.hash;
2074
+ for (let i = 0; i < 30; i++) {
2075
+ const got = await server.getTransaction(hash6);
2076
+ if (got.status === stellarSdk.rpc.Api.GetTransactionStatus.SUCCESS) return hash6;
2077
+ if (got.status === stellarSdk.rpc.Api.GetTransactionStatus.FAILED) {
2078
+ throw new Error(`kit/stellar: tx ${hash6} failed`);
2079
+ }
2080
+ await new Promise((r) => setTimeout(r, 1e3));
2081
+ }
2082
+ throw new Error(`kit/stellar: tx ${hash6} not confirmed in time`);
2083
+ }
2084
+ };
2085
+ function isAddressCredentialFor(entry, accountAddress) {
2086
+ const creds = entry.credentials();
2087
+ if (creds.switch() !== stellarSdk.xdr.SorobanCredentialsType.sorobanCredentialsAddress()) return false;
2088
+ return stellarSdk.Address.fromScAddress(creds.address().address()).toString() === accountAddress;
2089
+ }
2090
+ var defaultRegistry2 = new InMemoryWalletRegistry();
2091
+
1193
2092
  // src/recovery/HttpRecoveryClient.ts
1194
2093
  function toHex2(n) {
1195
2094
  return "0x" + n.toString(16);
@@ -1271,14 +2170,19 @@ var SOLANA_ENV = {
1271
2170
  mainnet: "solana-mainnet",
1272
2171
  testnet: "solana-devnet"
1273
2172
  };
2173
+ var STELLAR_ENV = {
2174
+ mainnet: "stellar-mainnet",
2175
+ testnet: "stellar-testnet"
2176
+ };
1274
2177
  var Cavos = class _Cavos {
1275
- constructor(identity, address, status, account, adapter, devicePubkey) {
2178
+ constructor(identity, address, status, account, adapter, devicePubkey, paymaster) {
1276
2179
  this.identity = identity;
1277
2180
  this.address = address;
1278
2181
  this.status = status;
1279
2182
  this.account = account;
1280
2183
  this.adapter = adapter;
1281
2184
  this.devicePubkey = devicePubkey;
2185
+ this.paymaster = paymaster;
1282
2186
  /** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
1283
2187
  this.chain = "starknet";
1284
2188
  /** Request id of the pending device-addition, when status is needs-device-approval. */
@@ -1311,6 +2215,22 @@ var Cavos = class _Cavos {
1311
2215
  ...opts.feePayer ? { feePayer: opts.feePayer } : {}
1312
2216
  });
1313
2217
  }
2218
+ if (opts.chain === "stellar") {
2219
+ return CavosStellar.connect({
2220
+ network: STELLAR_ENV[opts.network],
2221
+ ...opts.auth ? { auth: opts.auth } : {},
2222
+ ...opts.identity ? { identity: opts.identity } : {},
2223
+ appSalt: opts.appSalt,
2224
+ ...opts.appId ? { appId: opts.appId } : {},
2225
+ ...opts.backendUrl ? { backendUrl: opts.backendUrl } : {},
2226
+ ...opts.registry ? { registry: opts.registry } : {},
2227
+ ...opts.rpcUrl ? { rpcUrl: opts.rpcUrl } : {},
2228
+ ...opts.factoryId ? { factoryId: opts.factoryId } : {},
2229
+ ...opts.createSigner ? { createSigner: opts.createSigner } : {},
2230
+ ...opts.stellarRelayer ? { relayer: opts.stellarRelayer } : {},
2231
+ ...opts.stellarSourceKeypair ? { sourceKeypair: opts.stellarSourceKeypair } : {}
2232
+ });
2233
+ }
1314
2234
  if (!opts.paymasterApiKey) {
1315
2235
  throw new Error("kit: `paymasterApiKey` is required for Starknet connections");
1316
2236
  }
@@ -1338,8 +2258,10 @@ var Cavos = class _Cavos {
1338
2258
  const provider = new starknet.RpcProvider({
1339
2259
  nodeUrl: opts.rpcUrl ?? STARKNET_NETWORKS[opts.network].rpcUrl
1340
2260
  });
2261
+ const paymasterUrl = opts.paymasterUrl ?? CAVOS_PAYMASTER_URL[opts.network];
2262
+ const paymasterConfig = { url: paymasterUrl, apiKey: opts.paymasterApiKey };
1341
2263
  const paymaster = new starknet.PaymasterRpc({
1342
- nodeUrl: opts.paymasterUrl ?? CAVOS_PAYMASTER_URL[opts.network],
2264
+ nodeUrl: paymasterUrl,
1343
2265
  headers: { "x-paymaster-api-key": opts.paymasterApiKey }
1344
2266
  });
1345
2267
  const addressSeed = deriveAddressSeed({ userId: identity.userId, appSalt: opts.appSalt });
@@ -1354,7 +2276,7 @@ var Cavos = class _Cavos {
1354
2276
  cairoVersion: "1"
1355
2277
  });
1356
2278
  const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
1357
- const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
2279
+ const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry3);
1358
2280
  const recovery = opts.recovery ?? (opts.appId ? new HttpRecoveryClient({ baseUrl: backendUrl, appId: opts.appId }) : null);
1359
2281
  const existing = await registry.lookup(identity.userId);
1360
2282
  if (existing) {
@@ -1366,7 +2288,8 @@ var Cavos = class _Cavos {
1366
2288
  isSigner2 ? "ready" : "needs-device-approval",
1367
2289
  account2,
1368
2290
  adapter,
1369
- devicePubkey
2291
+ devicePubkey,
2292
+ paymasterConfig
1370
2293
  );
1371
2294
  if (!isSigner2 && recovery) {
1372
2295
  const dedup = lastDeviceRequest.get(identity.userId);
@@ -1425,7 +2348,8 @@ var Cavos = class _Cavos {
1425
2348
  isSigner ? "ready" : "needs-device-approval",
1426
2349
  account,
1427
2350
  adapter,
1428
- devicePubkey
2351
+ devicePubkey,
2352
+ paymasterConfig
1429
2353
  );
1430
2354
  }
1431
2355
  /** This device's public key (e.g. to request addition to an existing wallet). */
@@ -1446,6 +2370,92 @@ var Cavos = class _Cavos {
1446
2370
  async addSigner(pubkey) {
1447
2371
  return this.execute([this.adapter.buildAddSigner(this.address, pubkey)]);
1448
2372
  }
2373
+ /**
2374
+ * Enroll a passkey as an APPROVER so the user can later add devices from any
2375
+ * browser (2FA-style step-up). Requires a ready device (the enrollment call is
2376
+ * device-signed and gasless). Idempotent: a no-op if the passkey is already an
2377
+ * approver. Call this whenever the app decides to prompt "turn on device
2378
+ * approvals". Returns the passkey's public key + the enrollment tx hash.
2379
+ */
2380
+ async enrollPasskey(passkey, params) {
2381
+ const enrolled = await passkey.enroll(params);
2382
+ const { transactionHash } = await this.addApprover(enrolled.publicKey);
2383
+ return { publicKey: enrolled.publicKey, transactionHash };
2384
+ }
2385
+ /**
2386
+ * Register an ALREADY-enrolled passkey public key as an approver (gasless,
2387
+ * device-signed). Idempotent. Use this to register ONE passkey across multiple
2388
+ * chains without re-prompting `passkey.enroll()` on each: enroll once, then
2389
+ * call `addApprover(pubkey)` on each chain's wallet.
2390
+ */
2391
+ async addApprover(pubkey) {
2392
+ if (this.status !== "ready") {
2393
+ throw new Error("kit: addApprover requires a ready, authorized device");
2394
+ }
2395
+ if (await this.adapter.isApprover(this.address, pubkey)) return {};
2396
+ const { transactionHash } = await this.execute([
2397
+ this.adapter.buildAddApprover(this.address, pubkey)
2398
+ ]);
2399
+ return { transactionHash };
2400
+ }
2401
+ /**
2402
+ * From a brand-new browser (status `needs-device-approval`), use the user's
2403
+ * synced passkey to authorize adding THIS device — no trip back to an already-
2404
+ * authorized device.
2405
+ *
2406
+ * `add_signer_via_passkey` is a public external authorized by the embedded
2407
+ * WebAuthn assertion (no device signature), so by default we sponsor it through
2408
+ * the Cavos paymaster's `paymaster_executeDirectTransaction` (the forwarder's
2409
+ * `execute_sponsored` runs a generic call — it does NOT require SNIP-9). Pass a
2410
+ * custom `submit` to route it through your own relayer instead. Returns the tx.
2411
+ */
2412
+ async approveThisDeviceWithPasskey(opts) {
2413
+ if (this.status === "ready") {
2414
+ throw new Error("kit: this device is already an authorized signer");
2415
+ }
2416
+ const { leaf, nonce } = await this.passkeyLeafForThisDevice();
2417
+ const leaves = [leaf];
2418
+ const assertion = await opts.passkey.assert(batchChallenge(leaves));
2419
+ return this.submitPasskeyApproval(assertion, leaves, 0, nonce, opts.submit);
2420
+ }
2421
+ /** This device's leaf + the current passkey nonce, for a (possibly multi-chain)
2422
+ * passkey approval batch. See `approveDeviceEverywhere`. */
2423
+ async passkeyLeafForThisDevice() {
2424
+ const nonce = await this.adapter.getPasskeyNonce(this.address);
2425
+ return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
2426
+ }
2427
+ /** Submit `add_signer_via_passkey` given a (shared) assertion + this chain's
2428
+ * position in the batch. The assertion doesn't carry the passkey pubkey, so we
2429
+ * recover both candidates and pick the enrolled approver via the on-chain view
2430
+ * (no backend). Defaults to sponsoring through the paymaster. */
2431
+ async submitPasskeyApproval(assertion, leaves, leafIndex, nonce, submit) {
2432
+ const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
2433
+ const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
2434
+ let yParity = null;
2435
+ for (const cand of candidates) {
2436
+ if (await this.adapter.isApprover(this.address, cand.publicKey)) {
2437
+ yParity = cand.yParity;
2438
+ break;
2439
+ }
2440
+ }
2441
+ if (yParity === null) {
2442
+ throw new Error("kit: this passkey is not a registered approver of the wallet");
2443
+ }
2444
+ const call = this.adapter.buildAddSignerViaPasskey(
2445
+ this.address,
2446
+ this.devicePubkey,
2447
+ nonce,
2448
+ leaves,
2449
+ leafIndex,
2450
+ assertion,
2451
+ yParity
2452
+ );
2453
+ if (submit) return submit(call);
2454
+ if (!this.paymaster) {
2455
+ throw new Error("kit: no paymaster configured \u2014 pass a `submit` relayer to approveThisDeviceWithPasskey");
2456
+ }
2457
+ return paymasterExecuteDirect(this.paymaster, this.address, call);
2458
+ }
1449
2459
  /**
1450
2460
  * Register a self-custodial backup signer derived from `code`, so the account
1451
2461
  * can be recovered after the user loses every device. Idempotent: if the
@@ -1487,7 +2497,7 @@ var Cavos = class _Cavos {
1487
2497
  const backup = BackupSigner.fromCode(opts.code);
1488
2498
  const backupAdapter = new StarknetAdapter({ classHash, signer: backup, provider });
1489
2499
  const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
1490
- const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network }) : defaultRegistry2);
2500
+ const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network }) : defaultRegistry3);
1491
2501
  const existing = await registry.lookup(opts.identity.userId);
1492
2502
  if (!existing) {
1493
2503
  throw new Error("kit: no account found for this identity \u2014 nothing to recover");
@@ -1522,7 +2532,7 @@ var Cavos = class _Cavos {
1522
2532
  return new _Cavos(opts.identity, existing.address, "ready", account, adapter, devicePubkey);
1523
2533
  }
1524
2534
  };
1525
- var defaultRegistry2 = new InMemoryWalletRegistry();
2535
+ var defaultRegistry3 = new InMemoryWalletRegistry();
1526
2536
  var DEVICE_REQUEST_DEDUP_MS = 5 * 60 * 1e3;
1527
2537
  var lastDeviceRequest = /* @__PURE__ */ new Map();
1528
2538
  async function isDeployed(provider, address) {
@@ -1533,6 +2543,40 @@ async function isDeployed(provider, address) {
1533
2543
  return false;
1534
2544
  }
1535
2545
  }
2546
+ async function paymasterExecuteDirect(paymaster, userAddress, call) {
2547
+ const body = {
2548
+ jsonrpc: "2.0",
2549
+ id: 1,
2550
+ method: "paymaster_executeDirectTransaction",
2551
+ params: {
2552
+ transaction: {
2553
+ type: "invoke",
2554
+ invoke: {
2555
+ user_address: userAddress,
2556
+ execute_from_outside_call: {
2557
+ to: call.contractAddress,
2558
+ selector: starknet.hash.getSelectorFromName(call.entrypoint),
2559
+ calldata: call.calldata.map((c) => starknet.num.toHex(c))
2560
+ }
2561
+ }
2562
+ },
2563
+ parameters: { version: "0x1", fee_mode: { mode: "sponsored" } }
2564
+ }
2565
+ };
2566
+ const res = await fetch(paymaster.url, {
2567
+ method: "POST",
2568
+ headers: {
2569
+ "Content-Type": "application/json",
2570
+ ...paymaster.apiKey ? { "x-paymaster-api-key": paymaster.apiKey } : {}
2571
+ },
2572
+ body: JSON.stringify(body)
2573
+ });
2574
+ const json = await res.json();
2575
+ if (json.error) {
2576
+ throw new Error(`kit: paymaster passkey approval failed: ${JSON.stringify(json.error)}`);
2577
+ }
2578
+ return { transactionHash: json.result?.transaction_hash ?? json.result?.tracking_id };
2579
+ }
1536
2580
  var CavosAuth = class {
1537
2581
  constructor(opts = {}) {
1538
2582
  this.opts = opts;
@@ -2563,6 +3607,24 @@ function CavosProvider({ config, modal, children }) {
2563
3607
  },
2564
3608
  [cavos]
2565
3609
  );
3610
+ const enrollPasskey = react.useCallback(
3611
+ async (passkey, params) => {
3612
+ if (!cavos) throw new Error("Not logged in");
3613
+ return cavos.enrollPasskey(passkey, params);
3614
+ },
3615
+ [cavos]
3616
+ );
3617
+ const approveThisDeviceWithPasskey = react.useCallback(
3618
+ async (passkey, submit) => {
3619
+ if (!cavos) throw new Error("Not logged in");
3620
+ if (cavos.chain === "starknet") {
3621
+ return cavos.approveThisDeviceWithPasskey({ passkey, ...submit ? { submit } : {} });
3622
+ }
3623
+ const transactionHash = await cavos.approveThisDeviceWithPasskey(passkey);
3624
+ return { transactionHash };
3625
+ },
3626
+ [cavos]
3627
+ );
2566
3628
  const setupRecovery = react.useCallback(async () => {
2567
3629
  if (!cavos) throw new Error("Not logged in");
2568
3630
  const code = generateRecoveryCode();
@@ -2655,6 +3717,8 @@ function CavosProvider({ config, modal, children }) {
2655
3717
  handleCallback,
2656
3718
  execute,
2657
3719
  addSigner,
3720
+ enrollPasskey,
3721
+ approveThisDeviceWithPasskey,
2658
3722
  resendDeviceApproval,
2659
3723
  setupRecovery,
2660
3724
  recover,