@cavos/kit 0.0.1 → 0.0.2

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.
@@ -1,6 +1,7 @@
1
1
  import { p256 } from '@noble/curves/p256';
2
2
  import { sha256 } from '@noble/hashes/sha256';
3
3
  import { hash, num, Signer, RpcProvider, PaymasterRpc, Account } from 'starknet';
4
+ import { PublicKey, TransactionInstruction, SystemProgram, SYSVAR_INSTRUCTIONS_PUBKEY, Transaction, Connection, sendAndConfirmTransaction } from '@solana/web3.js';
4
5
  import { hkdf } from '@noble/hashes/hkdf';
5
6
  import { pbkdf2 } from '@noble/hashes/pbkdf2';
6
7
  import { randomBytes } from '@noble/hashes/utils';
@@ -162,8 +163,8 @@ var CAVOS_PAYMASTER_URL = {
162
163
  mainnet: "https://paymaster.cavos.xyz"
163
164
  };
164
165
  var DEVICE_ACCOUNT_CLASS_HASH = {
165
- sepolia: "0x6c3c3426667b6d4adda18a0b8d8cc34c495a1ace7276c4470068ad4c324876d",
166
- mainnet: ""
166
+ sepolia: "0x1840aded59e8a0d2b440a134cb9079a7fc11b06c77f58ed189ab436a034ca6a",
167
+ mainnet: "0x1840aded59e8a0d2b440a134cb9079a7fc11b06c77f58ed189ab436a034ca6a"
167
168
  };
168
169
  var StarknetAdapter = class {
169
170
  constructor(opts) {
@@ -310,76 +311,384 @@ var HttpWalletRegistry = class {
310
311
  async addDevice(params) {
311
312
  }
312
313
  };
314
+ function deriveAddressSeed({ userId, appSalt }) {
315
+ const h = hash.computePoseidonHashOnElements([feltFromString(userId), feltFromString(appSalt)]);
316
+ return BigInt(h);
317
+ }
318
+ function deriveAddressSeedSolana({ userId, appSalt }) {
319
+ return sha256(new TextEncoder().encode(`cavos:solana:v1:${userId}:${appSalt}`));
320
+ }
321
+ function feltFromString(s) {
322
+ const bytes = new TextEncoder().encode(s);
323
+ const chunks = [];
324
+ for (let i = 0; i < bytes.length; i += 31) {
325
+ let w = 0n;
326
+ for (const b of bytes.subarray(i, i + 31)) w = w << 8n | BigInt(b);
327
+ chunks.push(w);
328
+ }
329
+ if (chunks.length === 0) return 0n;
330
+ if (chunks.length === 1) return chunks[0];
331
+ return BigInt(hash.computePoseidonHashOnElements(chunks));
332
+ }
313
333
 
314
- // src/recovery/HttpRecoveryClient.ts
315
- function toHex2(n) {
316
- return "0x" + n.toString(16);
334
+ // src/chains/solana/constants.ts
335
+ var DEVICE_ACCOUNT_PROGRAM_ID = "FHnoYNfYAmFrwt18gcBGG7G1S5q3RAbCBvrV2D29izNJ";
336
+ var SECP256R1_PROGRAM_ID = "Secp256r1SigVerify1111111111111111111111111";
337
+ var ACCOUNT_SEED = "cavos-account";
338
+ var DOMAIN_ADD = "cavos:add_signer:v1";
339
+ var DOMAIN_REMOVE = "cavos:remove_signer:v1";
340
+ var DOMAIN_TRANSFER = "cavos:transfer:v1";
341
+ var DOMAIN_EXECUTE = "cavos:execute:v1";
342
+ var SECP256R1_N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
343
+ var SOLANA_NETWORKS = {
344
+ "solana-devnet": "https://api.devnet.solana.com",
345
+ "solana-mainnet": "https://api.mainnet-beta.solana.com",
346
+ "solana-localnet": "http://127.0.0.1:8899"
347
+ };
348
+ var COMPRESSED_PUBKEY_SIZE = 33;
349
+ var SIGNATURE_SIZE = 64;
350
+ var CURRENT_IX = 65535;
351
+ var SolanaAdapter = class {
352
+ constructor(opts = {}) {
353
+ this.opts = opts;
354
+ this.chain = "solana";
355
+ this.programId = new PublicKey(opts.programId ?? DEVICE_ACCOUNT_PROGRAM_ID);
356
+ }
357
+ /** Deterministic account address: PDA of [seed, address_seed, initial_signer_x]. */
358
+ computeAddress(addressSeed, initialSigner) {
359
+ return this.pda(addressSeed, compressedPubkey(initialSigner)).toBase58();
360
+ }
361
+ pda(addressSeed, initialCompressed) {
362
+ const [pda] = PublicKey.findProgramAddressSync(
363
+ [
364
+ Buffer.from(ACCOUNT_SEED),
365
+ Buffer.from(addressSeed),
366
+ Buffer.from(initialCompressed.slice(1, 33))
367
+ // x-coordinate
368
+ ],
369
+ this.programId
370
+ );
371
+ return pda;
372
+ }
373
+ /** `initialize` instruction creating the account with its first device signer. */
374
+ buildInitialize(addressSeed, payer, initialSigner) {
375
+ const initialCompressed = compressedPubkey(initialSigner);
376
+ const account = this.pda(addressSeed, initialCompressed);
377
+ const data = Buffer.concat([
378
+ anchorDiscriminator("initialize"),
379
+ Buffer.from(addressSeed),
380
+ // [u8;32]
381
+ Buffer.from(initialCompressed)
382
+ // [u8;33]
383
+ ]);
384
+ return new TransactionInstruction({
385
+ programId: this.programId,
386
+ keys: [
387
+ { pubkey: account, isSigner: false, isWritable: true },
388
+ { pubkey: new PublicKey(payer), isSigner: true, isWritable: true },
389
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }
390
+ ],
391
+ data
392
+ });
393
+ }
394
+ /** `[precompile, add_signer]` bundle, authorized by an existing device signer. */
395
+ async buildAddSigner(account, newSigner) {
396
+ const accountPk = new PublicKey(account);
397
+ const newCompressed = compressedPubkey(newSigner);
398
+ const nonce = await this.fetchNonce(accountPk);
399
+ const message = concatBytes(
400
+ Buffer.from(DOMAIN_ADD),
401
+ accountPk.toBuffer(),
402
+ newCompressed,
403
+ u64le(nonce)
404
+ );
405
+ const { precompileIx } = await this.signToPrecompile(message);
406
+ const ix = new TransactionInstruction({
407
+ programId: this.programId,
408
+ keys: this.guardedKeys(accountPk),
409
+ data: Buffer.concat([anchorDiscriminator("add_signer"), Buffer.from(newCompressed)])
410
+ });
411
+ return [precompileIx, ix];
412
+ }
413
+ /** `[precompile, remove_signer]` bundle, authorized by an existing device signer. */
414
+ async buildRemoveSigner(account, signer) {
415
+ const accountPk = new PublicKey(account);
416
+ const compressed = compressedPubkey(signer);
417
+ const nonce = await this.fetchNonce(accountPk);
418
+ const message = concatBytes(
419
+ Buffer.from(DOMAIN_REMOVE),
420
+ accountPk.toBuffer(),
421
+ compressed,
422
+ u64le(nonce)
423
+ );
424
+ const { precompileIx } = await this.signToPrecompile(message);
425
+ const ix = new TransactionInstruction({
426
+ programId: this.programId,
427
+ keys: this.guardedKeys(accountPk),
428
+ data: Buffer.concat([anchorDiscriminator("remove_signer"), Buffer.from(compressed)])
429
+ });
430
+ return [precompileIx, ix];
431
+ }
432
+ /** `[precompile, execute_transfer]` bundle moving lamports out of the account. */
433
+ async buildExecuteTransfer(account, destination, amount) {
434
+ const accountPk = new PublicKey(account);
435
+ const destPk = new PublicKey(destination);
436
+ const nonce = await this.fetchNonce(accountPk);
437
+ const message = concatBytes(
438
+ Buffer.from(DOMAIN_TRANSFER),
439
+ accountPk.toBuffer(),
440
+ destPk.toBuffer(),
441
+ u64le(amount),
442
+ u64le(nonce)
443
+ );
444
+ const { precompileIx } = await this.signToPrecompile(message);
445
+ const ix = new TransactionInstruction({
446
+ programId: this.programId,
447
+ keys: [
448
+ { pubkey: accountPk, isSigner: false, isWritable: true },
449
+ { pubkey: destPk, isSigner: false, isWritable: true },
450
+ { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false }
451
+ ],
452
+ data: Buffer.concat([anchorDiscriminator("execute_transfer"), u64le(amount)])
453
+ });
454
+ return [precompileIx, ix];
455
+ }
456
+ /**
457
+ * `[precompile, execute]` bundle running arbitrary CPI instructions with the
458
+ * account PDA as signer. The device key signs over
459
+ * `DOMAIN_EXECUTE || account || sha256(canonical(instructions)) || nonce`, so
460
+ * the signature commits to the EXACT instruction set the program will invoke —
461
+ * no account/data substitution is possible after signing.
462
+ *
463
+ * The instructions' accounts are passed to the program via `remaining_accounts`
464
+ * (flattened, in order); the program enforces an exact, ordered mapping.
465
+ */
466
+ async buildExecute(account, instructions) {
467
+ if (instructions.length === 0) throw new Error("kit/solana: execute requires at least one instruction");
468
+ const accountPk = new PublicKey(account);
469
+ const nonce = await this.fetchNonce(accountPk);
470
+ const blob = serializeInstructions(instructions);
471
+ const ixsHash = sha256(blob);
472
+ const message = concatBytes(
473
+ Buffer.from(DOMAIN_EXECUTE),
474
+ accountPk.toBuffer(),
475
+ Buffer.from(ixsHash),
476
+ u64le(nonce)
477
+ );
478
+ const { precompileIx } = await this.signToPrecompile(message);
479
+ const blobLen = Buffer.alloc(4);
480
+ new DataView(blobLen.buffer).setUint32(0, blob.length, true);
481
+ const data = Buffer.concat([anchorDiscriminator("execute"), blobLen, blob]);
482
+ const remainingAccounts = [];
483
+ for (const ix2 of instructions) {
484
+ for (const acc of ix2.accounts) {
485
+ remainingAccounts.push({
486
+ pubkey: new PublicKey(acc.pubkey),
487
+ isSigner: false,
488
+ // signer flags are part of the signed InstructionData
489
+ isWritable: acc.isWritable
490
+ });
491
+ }
492
+ remainingAccounts.push({
493
+ pubkey: new PublicKey(ix2.programId),
494
+ isSigner: false,
495
+ isWritable: false
496
+ });
497
+ }
498
+ const ix = new TransactionInstruction({
499
+ programId: this.programId,
500
+ keys: [
501
+ { pubkey: accountPk, isSigner: false, isWritable: true },
502
+ { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false },
503
+ ...remainingAccounts
504
+ ],
505
+ data
506
+ });
507
+ return [precompileIx, ix];
508
+ }
509
+ /** Read whether `signer` is currently an authorized signer of `account`. */
510
+ async isAuthorizedSigner(account, signer) {
511
+ const signers = await this.fetchSigners(new PublicKey(account));
512
+ const target = Buffer.from(compressedPubkey(signer)).toString("hex");
513
+ return signers.some((s) => Buffer.from(s).toString("hex") === target);
514
+ }
515
+ guardedKeys(account) {
516
+ return [
517
+ { pubkey: account, isSigner: false, isWritable: true },
518
+ { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false }
519
+ ];
520
+ }
521
+ /** Sign `message` with the device key and build the matching precompile ix. */
522
+ async signToPrecompile(message) {
523
+ if (!this.opts.signer) throw new Error("kit/solana: signer required to authorize");
524
+ const pubkey = await this.opts.signer.getPublicKey();
525
+ const sig = await this.opts.signer.sign(message);
526
+ const signature = encodeLowSSignature(sig.r, sig.s);
527
+ const precompileIx = buildSecp256r1Instruction(
528
+ compressedPubkey(pubkey),
529
+ signature,
530
+ message
531
+ );
532
+ return { precompileIx };
533
+ }
534
+ async fetchNonce(account) {
535
+ const info = await this.requireConnection().getAccountInfo(account);
536
+ if (!info) return 0n;
537
+ return readU64le(info.data, 41);
538
+ }
539
+ async fetchSigners(account) {
540
+ const info = await this.requireConnection().getAccountInfo(account);
541
+ if (!info) return [];
542
+ const d = info.data;
543
+ const lenOffset = 8 + 32 + 1 + 8 + COMPRESSED_PUBKEY_SIZE;
544
+ const count = d.readUInt32LE(lenOffset);
545
+ const out = [];
546
+ let off = lenOffset + 4;
547
+ for (let i = 0; i < count; i++) {
548
+ out.push(Uint8Array.from(d.subarray(off, off + COMPRESSED_PUBKEY_SIZE)));
549
+ off += COMPRESSED_PUBKEY_SIZE;
550
+ }
551
+ return out;
552
+ }
553
+ requireConnection() {
554
+ if (!this.opts.connection) throw new Error("kit/solana: connection required for reads");
555
+ return this.opts.connection;
556
+ }
557
+ };
558
+ function compressedPubkey(pk) {
559
+ const out = new Uint8Array(COMPRESSED_PUBKEY_SIZE);
560
+ out[0] = pk.y % 2n === 0n ? 2 : 3;
561
+ out.set(bigIntTo32Bytes(pk.x), 1);
562
+ return out;
317
563
  }
318
- function fromHex2(s) {
319
- return BigInt(s);
564
+ function encodeLowSSignature(r, s) {
565
+ const lowS = s > SECP256R1_N / 2n ? SECP256R1_N - s : s;
566
+ const out = new Uint8Array(SIGNATURE_SIZE);
567
+ out.set(bigIntTo32Bytes(r), 0);
568
+ out.set(bigIntTo32Bytes(lowS), 32);
569
+ return out;
320
570
  }
321
- function deviceLabel() {
322
- if (typeof navigator !== "undefined") {
323
- return navigator.userAgent || "a new device";
571
+ function buildSecp256r1Instruction(compressed, signature, message) {
572
+ const headerLen = 2;
573
+ const offsetsLen = 14;
574
+ const pubkeyOffset = headerLen + offsetsLen;
575
+ const sigOffset = pubkeyOffset + COMPRESSED_PUBKEY_SIZE;
576
+ const msgOffset = sigOffset + SIGNATURE_SIZE;
577
+ const data = Buffer.alloc(msgOffset + message.length);
578
+ data.writeUInt8(1, 0);
579
+ data.writeUInt8(0, 1);
580
+ let o = headerLen;
581
+ data.writeUInt16LE(sigOffset, o);
582
+ o += 2;
583
+ data.writeUInt16LE(CURRENT_IX, o);
584
+ o += 2;
585
+ data.writeUInt16LE(pubkeyOffset, o);
586
+ o += 2;
587
+ data.writeUInt16LE(CURRENT_IX, o);
588
+ o += 2;
589
+ data.writeUInt16LE(msgOffset, o);
590
+ o += 2;
591
+ data.writeUInt16LE(message.length, o);
592
+ o += 2;
593
+ data.writeUInt16LE(CURRENT_IX, o);
594
+ o += 2;
595
+ Buffer.from(compressed).copy(data, pubkeyOffset);
596
+ Buffer.from(signature).copy(data, sigOffset);
597
+ Buffer.from(message).copy(data, msgOffset);
598
+ return new TransactionInstruction({
599
+ keys: [],
600
+ programId: new PublicKey(SECP256R1_PROGRAM_ID),
601
+ data
602
+ });
603
+ }
604
+ function anchorDiscriminator(name) {
605
+ return Buffer.from(sha256(`global:${name}`).slice(0, 8));
606
+ }
607
+ function u64le(n) {
608
+ const b = Buffer.alloc(8);
609
+ new DataView(b.buffer, b.byteOffset, 8).setBigUint64(0, BigInt(n), true);
610
+ return b;
611
+ }
612
+ function readU64le(buf, offset) {
613
+ return new DataView(buf.buffer, buf.byteOffset, buf.length).getBigUint64(
614
+ offset,
615
+ true
616
+ );
617
+ }
618
+ function concatBytes(...parts) {
619
+ const total = parts.reduce((n, p) => n + p.length, 0);
620
+ const out = new Uint8Array(total);
621
+ let off = 0;
622
+ for (const p of parts) {
623
+ out.set(p, off);
624
+ off += p.length;
324
625
  }
325
- return "a new device";
626
+ return out;
326
627
  }
327
- var HttpRecoveryClient = class {
628
+ function serializeInstruction(ix) {
629
+ const programId = new PublicKey(ix.programId).toBuffer();
630
+ const accounts = serializeAccounts(ix.accounts);
631
+ const data = serializeVecU8(ix.data);
632
+ return Buffer.concat([programId, accounts, data]);
633
+ }
634
+ function serializeAccounts(metas) {
635
+ const len = Buffer.alloc(4);
636
+ new DataView(len.buffer).setUint32(0, metas.length, true);
637
+ const parts = metas.map(serializeAccountMeta);
638
+ return Buffer.concat([len, ...parts]);
639
+ }
640
+ function serializeAccountMeta(meta) {
641
+ const pubkey = new PublicKey(meta.pubkey).toBuffer();
642
+ return Buffer.concat([pubkey, Buffer.from([meta.isSigner ? 1 : 0, meta.isWritable ? 1 : 0])]);
643
+ }
644
+ function serializeVecU8(data) {
645
+ const len = Buffer.alloc(4);
646
+ new DataView(len.buffer).setUint32(0, data.length, true);
647
+ return Buffer.concat([len, Buffer.from(data)]);
648
+ }
649
+ function serializeInstructions(instructions) {
650
+ return Buffer.concat(instructions.map(serializeInstruction));
651
+ }
652
+ var SolanaRelayer = class {
328
653
  constructor(opts) {
329
654
  this.opts = opts;
330
655
  }
331
- async requestDeviceAddition(params) {
332
- const res = await fetch(new URL("/api/devices/request", this.opts.baseUrl), {
656
+ /** The relayer's fee-payer pubkey (fetched + cached from the backend). */
657
+ async getFeePayer() {
658
+ if (this.feePayer) return this.feePayer;
659
+ const res = await fetch(`${this.opts.baseUrl}/api/solana/relay?network=${this.opts.network}`);
660
+ if (!res.ok) throw new Error(`kit/solana: relayer fee-payer lookup failed (${res.status})`);
661
+ const { fee_payer } = await res.json();
662
+ this.feePayer = new PublicKey(fee_payer);
663
+ return this.feePayer;
664
+ }
665
+ /**
666
+ * Build a tx with the relayer as fee payer, serialize it unsigned, and POST it
667
+ * to the relayer to co-sign + submit. Returns the confirmed signature.
668
+ */
669
+ async send(instructions) {
670
+ const feePayer = await this.getFeePayer();
671
+ const { blockhash } = await this.opts.connection.getLatestBlockhash("confirmed");
672
+ const tx2 = new Transaction();
673
+ tx2.feePayer = feePayer;
674
+ tx2.recentBlockhash = blockhash;
675
+ tx2.add(...instructions);
676
+ const serialized = tx2.serialize({ requireAllSignatures: false, verifySignatures: false }).toString("base64");
677
+ const res = await fetch(`${this.opts.baseUrl}/api/solana/relay`, {
333
678
  method: "POST",
334
679
  headers: { "Content-Type": "application/json" },
335
680
  body: JSON.stringify({
336
681
  app_id: this.opts.appId,
337
- wallet_address: params.accountAddress,
338
- new_pub_x: toHex2(params.newSigner.x),
339
- new_pub_y: toHex2(params.newSigner.y),
340
- device_label: params.deviceLabel ?? deviceLabel(),
341
- ...params.email ? { email: params.email } : {}
682
+ network: this.opts.network,
683
+ transaction: serialized
342
684
  })
343
685
  });
344
686
  if (!res.ok) {
345
- const t = await res.text().catch(() => "");
346
- throw new Error(`requestDeviceAddition failed: ${res.status} ${t}`);
347
- }
348
- const data = await res.json();
349
- return { requestId: data.request_id };
350
- }
351
- async getPendingRequest(requestId) {
352
- const url = new URL("/api/devices/request", this.opts.baseUrl);
353
- url.searchParams.set("id", requestId);
354
- const res = await fetch(url, { headers: { "Content-Type": "application/json" } });
355
- if (!res.ok) throw new Error(`getPendingRequest failed: ${res.status}`);
356
- const data = await res.json();
357
- if (!data.found) return null;
358
- const status = data.status;
359
- return {
360
- requestId: data.request_id,
361
- appId: data.app_id,
362
- userId: "",
363
- // the approving device already knows its own identity
364
- accountAddress: data.wallet_address,
365
- newSigner: { x: fromHex2(data.new_pub_x), y: fromHex2(data.new_pub_y) },
366
- createdAt: data.created_at,
367
- status
368
- };
369
- }
370
- async confirmDeviceAddition(params) {
371
- const res = await fetch(
372
- new URL(`/api/devices/request/${params.requestId}/confirm`, this.opts.baseUrl),
373
- {
374
- method: "POST",
375
- headers: { "Content-Type": "application/json" },
376
- body: JSON.stringify({ tx_hash: params.txHash })
377
- }
378
- );
379
- if (!res.ok) {
380
- const t = await res.text().catch(() => "");
381
- throw new Error(`confirmDeviceAddition failed: ${res.status} ${t}`);
687
+ const detail = await res.text().catch(() => "");
688
+ throw new Error(`kit/solana: relay failed (${res.status}) ${detail}`);
382
689
  }
690
+ const { signature } = await res.json();
691
+ return signature;
383
692
  }
384
693
  };
385
694
  var BACKUP_KDF_SALT = "cavos-recovery-v1";
@@ -689,22 +998,275 @@ var WORDLIST = [
689
998
  "beach",
690
999
  "dusk"
691
1000
  ];
692
- function deriveAddressSeed({ userId, appSalt }) {
693
- const h = hash.computePoseidonHashOnElements([feltFromString(userId), feltFromString(appSalt)]);
694
- return BigInt(h);
1001
+ var CavosSolana = class _CavosSolana {
1002
+ constructor(identity, address, status, connection, adapter, devicePubkey, relayer, feePayer) {
1003
+ this.identity = identity;
1004
+ this.address = address;
1005
+ this.status = status;
1006
+ this.connection = connection;
1007
+ this.adapter = adapter;
1008
+ this.devicePubkey = devicePubkey;
1009
+ this.relayer = relayer;
1010
+ this.feePayer = feePayer;
1011
+ /** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
1012
+ this.chain = "solana";
1013
+ }
1014
+ get publicKey() {
1015
+ return this.devicePubkey;
1016
+ }
1017
+ static async connect(opts) {
1018
+ const identity = opts.identity ?? await opts.auth?.authenticate();
1019
+ if (!identity) throw new Error("kit/solana: connect requires `identity` or `auth`");
1020
+ if (opts.network === "solana-mainnet" && !opts.rpcUrl) {
1021
+ console.warn(
1022
+ "[cavos] Using the public mainnet-beta RPC. Pass `rpcUrl` with your own provider (Helius/Triton/QuickNode) for production \u2014 the public endpoint is rate-limited."
1023
+ );
1024
+ }
1025
+ const connection = new Connection(opts.rpcUrl ?? SOLANA_NETWORKS[opts.network], "confirmed");
1026
+ const signer = opts.createSigner ? await opts.createSigner(`${identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${identity.userId}:${opts.appSalt}` });
1027
+ const devicePubkey = await signer.getPublicKey();
1028
+ const adapter = new SolanaAdapter({ programId: opts.programId, connection, signer });
1029
+ const addressSeed = deriveAddressSeedSolana({ userId: identity.userId, appSalt: opts.appSalt });
1030
+ const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
1031
+ const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry);
1032
+ const relayer = opts.relayer ?? (opts.appId ? new SolanaRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network, connection }) : void 0);
1033
+ const existing = await registry.lookup(identity.userId);
1034
+ if (existing) {
1035
+ const isSigner2 = await adapter.isAuthorizedSigner(existing.address, devicePubkey);
1036
+ return new _CavosSolana(
1037
+ identity,
1038
+ existing.address,
1039
+ isSigner2 ? "ready" : "needs-device-approval",
1040
+ connection,
1041
+ adapter,
1042
+ devicePubkey,
1043
+ relayer,
1044
+ opts.feePayer
1045
+ );
1046
+ }
1047
+ const address = adapter.computeAddress(addressSeed, devicePubkey);
1048
+ const deployed = await connection.getAccountInfo(new PublicKey(address)) !== null;
1049
+ if (!deployed) {
1050
+ if (relayer) {
1051
+ const payer = await relayer.getFeePayer();
1052
+ const ix = adapter.buildInitialize(addressSeed, payer.toBase58(), devicePubkey);
1053
+ await relayer.send([ix]);
1054
+ } else if (opts.feePayer) {
1055
+ const ix = adapter.buildInitialize(addressSeed, opts.feePayer.publicKey.toBase58(), devicePubkey);
1056
+ await sendAndConfirmTransaction(connection, new Transaction().add(ix), [opts.feePayer]);
1057
+ } else {
1058
+ throw new Error("kit/solana: a relayer (appId) or feePayer is required to initialize a new account");
1059
+ }
1060
+ }
1061
+ await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
1062
+ const isSigner = await adapter.isAuthorizedSigner(address, devicePubkey);
1063
+ return new _CavosSolana(
1064
+ identity,
1065
+ address,
1066
+ isSigner ? "ready" : "needs-device-approval",
1067
+ connection,
1068
+ adapter,
1069
+ devicePubkey,
1070
+ relayer,
1071
+ opts.feePayer
1072
+ );
1073
+ }
1074
+ /** Authorize an additional device signer (device-signed via precompile). */
1075
+ async addSigner(pubkey) {
1076
+ const ixs = await this.adapter.buildAddSigner(this.address, pubkey);
1077
+ return this.send(ixs);
1078
+ }
1079
+ /** Move `amount` lamports out of the account to `destination` (device-signed). */
1080
+ async execute(amount, destination) {
1081
+ if (this.status !== "ready") {
1082
+ throw new Error("kit/solana: this device is not yet an authorized signer of the wallet");
1083
+ }
1084
+ const ixs = await this.adapter.buildExecuteTransfer(this.address, destination, amount);
1085
+ return this.send(ixs);
1086
+ }
1087
+ /**
1088
+ * Run arbitrary CPI `instructions` with the account PDA as signer (device-
1089
+ * signed). The signature commits to sha256 of the canonical Borsh
1090
+ * serialization of the instructions, so it binds exactly the operations the
1091
+ * program will invoke. Unlocks SPL transfers, swaps, staking, etc.
1092
+ *
1093
+ * What the relayer will sponsor is constrained by the app's Solana program
1094
+ * allowlist (configured in the dashboard) — programs outside the allowlist are
1095
+ * rejected before co-signing.
1096
+ */
1097
+ async executeInstructions(instructions) {
1098
+ if (this.status !== "ready") {
1099
+ throw new Error("kit/solana: this device is not yet an authorized signer of the wallet");
1100
+ }
1101
+ const ixs = await this.adapter.buildExecute(this.address, instructions);
1102
+ return this.send(ixs);
1103
+ }
1104
+ /**
1105
+ * Register the backup signer derived from `code` as an authorized signer of this
1106
+ * account (device-signed via precompile). Idempotent: returns without a tx if
1107
+ * the backup signer is already registered. The code never leaves the device —
1108
+ * only the derived public key travels on-chain.
1109
+ *
1110
+ * Self-custodial: anyone who can re-derive the backup key from the code (i.e.
1111
+ * the rightful owner) can later recover the account with `CavosSolana.recover`.
1112
+ * Run this once, on a registered device, and have the user store the code.
1113
+ */
1114
+ async setupRecovery(code) {
1115
+ if (this.status !== "ready") {
1116
+ throw new Error("kit/solana: setupRecovery requires a ready, registered device");
1117
+ }
1118
+ const { publicKey: backupPubkey } = deriveBackupKey(code);
1119
+ const already = await this.adapter.isAuthorizedSigner(this.address, backupPubkey);
1120
+ if (already) return void 0;
1121
+ return this.addSigner(backupPubkey);
1122
+ }
1123
+ /**
1124
+ * Recover an account after losing every device signer. Derives the backup key
1125
+ * from `code`, uses it (not the new device key) to sign an `add_signer` for the
1126
+ * new device, and returns a ready CavosSolana bound to the new device. The
1127
+ * account address is unchanged.
1128
+ *
1129
+ * Self-custodial: only someone holding the code (i.e. the rightful owner) can
1130
+ * re-derive the backup key. The backend never sees the code.
1131
+ *
1132
+ * This mirrors `Cavos.recover` (Starknet): the backup key is just another
1133
+ * authorized signer, so recovery is an `add_signer(newDevice)` bundle signed by
1134
+ * the backup key. The on-chain program needs no recovery-specific entrypoint.
1135
+ */
1136
+ static async recover(opts) {
1137
+ if (opts.network === "solana-mainnet" && !opts.rpcUrl) {
1138
+ console.warn(
1139
+ "[cavos] Using the public mainnet-beta RPC. Pass `rpcUrl` with your own provider (Helius/Triton/QuickNode) for production \u2014 the public endpoint is rate-limited."
1140
+ );
1141
+ }
1142
+ const connection = new Connection(opts.rpcUrl ?? SOLANA_NETWORKS[opts.network], "confirmed");
1143
+ const signer = opts.createSigner ? await opts.createSigner(`${opts.identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${opts.identity.userId}:${opts.appSalt}` });
1144
+ const devicePubkey = await signer.getPublicKey();
1145
+ const backup = BackupSigner.fromCode(opts.code);
1146
+ const backupAdapter = new SolanaAdapter({
1147
+ programId: opts.programId,
1148
+ connection,
1149
+ signer: backup
1150
+ });
1151
+ const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
1152
+ const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry);
1153
+ const existing = await registry.lookup(opts.identity.userId);
1154
+ if (!existing) {
1155
+ throw new Error("kit/solana: no account found for this identity \u2014 nothing to recover");
1156
+ }
1157
+ const relayer = opts.relayer ?? (opts.appId ? new SolanaRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network, connection }) : void 0);
1158
+ const alreadyAuthed = await backupAdapter.isAuthorizedSigner(existing.address, devicePubkey);
1159
+ if (!alreadyAuthed) {
1160
+ const ixs = await backupAdapter.buildAddSigner(existing.address, devicePubkey);
1161
+ if (relayer) {
1162
+ await relayer.send(ixs);
1163
+ } else if (opts.feePayer) {
1164
+ await sendAndConfirmTransaction(connection, new Transaction().add(...ixs), [opts.feePayer]);
1165
+ } else {
1166
+ throw new Error("kit/solana: a relayer (appId) or feePayer is required to recover");
1167
+ }
1168
+ }
1169
+ const adapter = new SolanaAdapter({ programId: opts.programId, connection, signer });
1170
+ return new _CavosSolana(
1171
+ opts.identity,
1172
+ existing.address,
1173
+ "ready",
1174
+ connection,
1175
+ adapter,
1176
+ devicePubkey,
1177
+ relayer,
1178
+ opts.feePayer
1179
+ );
1180
+ }
1181
+ async send(ixs) {
1182
+ if (this.relayer) return this.relayer.send(ixs);
1183
+ if (this.feePayer) {
1184
+ return sendAndConfirmTransaction(this.connection, new Transaction().add(...ixs), [this.feePayer]);
1185
+ }
1186
+ throw new Error("kit/solana: no relayer or feePayer configured to submit transactions");
1187
+ }
1188
+ };
1189
+ var defaultRegistry = new InMemoryWalletRegistry();
1190
+
1191
+ // src/recovery/HttpRecoveryClient.ts
1192
+ function toHex2(n) {
1193
+ return "0x" + n.toString(16);
695
1194
  }
696
- function feltFromString(s) {
697
- const bytes = new TextEncoder().encode(s);
698
- const chunks = [];
699
- for (let i = 0; i < bytes.length; i += 31) {
700
- let w = 0n;
701
- for (const b of bytes.subarray(i, i + 31)) w = w << 8n | BigInt(b);
702
- chunks.push(w);
1195
+ function fromHex2(s) {
1196
+ return BigInt(s);
1197
+ }
1198
+ function deviceLabel() {
1199
+ if (typeof navigator !== "undefined") {
1200
+ return navigator.userAgent || "a new device";
703
1201
  }
704
- if (chunks.length === 0) return 0n;
705
- if (chunks.length === 1) return chunks[0];
706
- return BigInt(hash.computePoseidonHashOnElements(chunks));
1202
+ return "a new device";
707
1203
  }
1204
+ var HttpRecoveryClient = class {
1205
+ constructor(opts) {
1206
+ this.opts = opts;
1207
+ }
1208
+ async requestDeviceAddition(params) {
1209
+ const res = await fetch(new URL("/api/devices/request", this.opts.baseUrl), {
1210
+ method: "POST",
1211
+ headers: { "Content-Type": "application/json" },
1212
+ body: JSON.stringify({
1213
+ app_id: this.opts.appId,
1214
+ wallet_address: params.accountAddress,
1215
+ new_pub_x: toHex2(params.newSigner.x),
1216
+ new_pub_y: toHex2(params.newSigner.y),
1217
+ device_label: params.deviceLabel ?? deviceLabel(),
1218
+ ...params.email ? { email: params.email } : {}
1219
+ })
1220
+ });
1221
+ if (!res.ok) {
1222
+ const t = await res.text().catch(() => "");
1223
+ throw new Error(`requestDeviceAddition failed: ${res.status} ${t}`);
1224
+ }
1225
+ const data = await res.json();
1226
+ return { requestId: data.request_id };
1227
+ }
1228
+ async getPendingRequest(requestId) {
1229
+ const url = new URL("/api/devices/request", this.opts.baseUrl);
1230
+ url.searchParams.set("id", requestId);
1231
+ const res = await fetch(url, { headers: { "Content-Type": "application/json" } });
1232
+ if (!res.ok) throw new Error(`getPendingRequest failed: ${res.status}`);
1233
+ const data = await res.json();
1234
+ if (!data.found) return null;
1235
+ const status = data.status;
1236
+ return {
1237
+ requestId: data.request_id,
1238
+ appId: data.app_id,
1239
+ userId: "",
1240
+ // the approving device already knows its own identity
1241
+ accountAddress: data.wallet_address,
1242
+ newSigner: { x: fromHex2(data.new_pub_x), y: fromHex2(data.new_pub_y) },
1243
+ createdAt: data.created_at,
1244
+ status
1245
+ };
1246
+ }
1247
+ async confirmDeviceAddition(params) {
1248
+ const res = await fetch(
1249
+ new URL(`/api/devices/request/${params.requestId}/confirm`, this.opts.baseUrl),
1250
+ {
1251
+ method: "POST",
1252
+ headers: { "Content-Type": "application/json" },
1253
+ body: JSON.stringify({ tx_hash: params.txHash })
1254
+ }
1255
+ );
1256
+ if (!res.ok) {
1257
+ const t = await res.text().catch(() => "");
1258
+ throw new Error(`confirmDeviceAddition failed: ${res.status} ${t}`);
1259
+ }
1260
+ }
1261
+ };
1262
+ var STARKNET_ENV = {
1263
+ mainnet: "mainnet",
1264
+ testnet: "sepolia"
1265
+ };
1266
+ var SOLANA_ENV = {
1267
+ mainnet: "solana-mainnet",
1268
+ testnet: "solana-devnet"
1269
+ };
708
1270
  var Cavos = class _Cavos {
709
1271
  constructor(identity, address, status, account, adapter, devicePubkey) {
710
1272
  this.identity = identity;
@@ -713,10 +1275,58 @@ var Cavos = class _Cavos {
713
1275
  this.account = account;
714
1276
  this.adapter = adapter;
715
1277
  this.devicePubkey = devicePubkey;
1278
+ /** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
1279
+ this.chain = "starknet";
716
1280
  /** Request id of the pending device-addition, when status is needs-device-approval. */
717
1281
  this.pendingRequestId = null;
718
1282
  }
1283
+ /**
1284
+ * Unified entry point. Pick a `chain` and an `network` environment; the kit
1285
+ * resolves the concrete network (sepolia/devnet for testnet, mainnet for
1286
+ * mainnet) and returns a chain-native wallet. The result is a discriminated
1287
+ * union (`wallet.chain`), so `execute()` keeps each chain's native signature:
1288
+ *
1289
+ * const wallet = await Cavos.connect({ chain: "solana", network: "testnet", identity, appSalt, appId });
1290
+ * if (wallet.chain === "starknet") await wallet.execute(calls);
1291
+ * else await wallet.execute(amount, dest);
1292
+ */
719
1293
  static async connect(opts) {
1294
+ if (opts.chain === "solana") {
1295
+ return CavosSolana.connect({
1296
+ network: SOLANA_ENV[opts.network],
1297
+ ...opts.auth ? { auth: opts.auth } : {},
1298
+ ...opts.identity ? { identity: opts.identity } : {},
1299
+ appSalt: opts.appSalt,
1300
+ ...opts.appId ? { appId: opts.appId } : {},
1301
+ ...opts.backendUrl ? { backendUrl: opts.backendUrl } : {},
1302
+ ...opts.registry ? { registry: opts.registry } : {},
1303
+ ...opts.rpcUrl ? { rpcUrl: opts.rpcUrl } : {},
1304
+ ...opts.programId ? { programId: opts.programId } : {},
1305
+ ...opts.createSigner ? { createSigner: opts.createSigner } : {},
1306
+ ...opts.relayer ? { relayer: opts.relayer } : {},
1307
+ ...opts.feePayer ? { feePayer: opts.feePayer } : {}
1308
+ });
1309
+ }
1310
+ if (!opts.paymasterApiKey) {
1311
+ throw new Error("kit: `paymasterApiKey` is required for Starknet connections");
1312
+ }
1313
+ return _Cavos.connectStarknet({
1314
+ network: STARKNET_ENV[opts.network],
1315
+ auth: opts.auth,
1316
+ identity: opts.identity,
1317
+ appSalt: opts.appSalt,
1318
+ appId: opts.appId,
1319
+ backendUrl: opts.backendUrl,
1320
+ registry: opts.registry,
1321
+ recovery: opts.recovery,
1322
+ paymasterApiKey: opts.paymasterApiKey,
1323
+ paymasterUrl: opts.paymasterUrl,
1324
+ rpcUrl: opts.rpcUrl,
1325
+ classHash: opts.classHash,
1326
+ createSigner: opts.createSigner
1327
+ });
1328
+ }
1329
+ static async connectStarknet(opts) {
720
1330
  const identity = opts.identity ?? await opts.auth?.authenticate();
721
1331
  if (!identity) throw new Error("kit: connect requires `identity` or `auth`");
722
1332
  const classHash = opts.classHash ?? DEVICE_ACCOUNT_CLASS_HASH[opts.network];
@@ -740,7 +1350,7 @@ var Cavos = class _Cavos {
740
1350
  cairoVersion: "1"
741
1351
  });
742
1352
  const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
743
- const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry);
1353
+ const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
744
1354
  const recovery = opts.recovery ?? (opts.appId ? new HttpRecoveryClient({ baseUrl: backendUrl, appId: opts.appId }) : null);
745
1355
  const existing = await registry.lookup(identity.userId);
746
1356
  if (existing) {
@@ -858,13 +1468,14 @@ var Cavos = class _Cavos {
858
1468
  * re-derive the backup key. The backend never sees the code.
859
1469
  */
860
1470
  static async recover(opts) {
861
- const classHash = opts.classHash ?? DEVICE_ACCOUNT_CLASS_HASH[opts.network];
862
- if (!classHash) throw new Error(`kit: no DeviceAccount class hash for ${opts.network}`);
1471
+ const network = STARKNET_ENV[opts.network];
1472
+ const classHash = opts.classHash ?? DEVICE_ACCOUNT_CLASS_HASH[network];
1473
+ if (!classHash) throw new Error(`kit: no DeviceAccount class hash for ${network}`);
863
1474
  const provider = new RpcProvider({
864
- nodeUrl: opts.rpcUrl ?? STARKNET_NETWORKS[opts.network].rpcUrl
1475
+ nodeUrl: opts.rpcUrl ?? STARKNET_NETWORKS[network].rpcUrl
865
1476
  });
866
1477
  const paymaster = new PaymasterRpc({
867
- nodeUrl: opts.paymasterUrl ?? CAVOS_PAYMASTER_URL[opts.network],
1478
+ nodeUrl: opts.paymasterUrl ?? CAVOS_PAYMASTER_URL[network],
868
1479
  headers: { "x-paymaster-api-key": opts.paymasterApiKey }
869
1480
  });
870
1481
  const signer = opts.createSigner ? await opts.createSigner(`${opts.identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${opts.identity.userId}:${opts.appSalt}` });
@@ -872,7 +1483,7 @@ var Cavos = class _Cavos {
872
1483
  const backup = BackupSigner.fromCode(opts.code);
873
1484
  const backupAdapter = new StarknetAdapter({ classHash, signer: backup, provider });
874
1485
  const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
875
- const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry);
1486
+ const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network }) : defaultRegistry2);
876
1487
  const existing = await registry.lookup(opts.identity.userId);
877
1488
  if (!existing) {
878
1489
  throw new Error("kit: no account found for this identity \u2014 nothing to recover");
@@ -907,7 +1518,7 @@ var Cavos = class _Cavos {
907
1518
  return new _Cavos(opts.identity, existing.address, "ready", account, adapter, devicePubkey);
908
1519
  }
909
1520
  };
910
- var defaultRegistry = new InMemoryWalletRegistry();
1521
+ var defaultRegistry2 = new InMemoryWalletRegistry();
911
1522
  var DEVICE_REQUEST_DEDUP_MS = 5 * 60 * 1e3;
912
1523
  var lastDeviceRequest = /* @__PURE__ */ new Map();
913
1524
  async function isDeployed(provider, address) {
@@ -1056,6 +1667,6 @@ function bytesToChunks(bytes) {
1056
1667
  return w;
1057
1668
  }
1058
1669
 
1059
- export { BackupSigner, Cavos, CavosAuth, DEVICE_ACCOUNT_CLASS_HASH, HttpRecoveryClient, HttpWalletRegistry, InMemoryWalletRegistry, STARKNET_NETWORKS, StarknetAdapter, StarknetDeviceSigner, UDC_ADDRESS, WebCryptoSigner, bigIntTo32Bytes, bytesToBigInt, bytesToHex, deriveAddressSeed, deriveBackupKey, generateRecoveryCode, hexToBytes, recoverYParity, signatureToFelts, u256ToFelts };
1060
- //# sourceMappingURL=chunk-XWBX2ZIO.mjs.map
1061
- //# sourceMappingURL=chunk-XWBX2ZIO.mjs.map
1670
+ export { BackupSigner, Cavos, CavosAuth, CavosSolana, DEVICE_ACCOUNT_CLASS_HASH, DEVICE_ACCOUNT_PROGRAM_ID, HttpRecoveryClient, HttpWalletRegistry, InMemoryWalletRegistry, SECP256R1_PROGRAM_ID, SOLANA_NETWORKS, STARKNET_NETWORKS, SolanaAdapter, SolanaRelayer, StarknetAdapter, StarknetDeviceSigner, UDC_ADDRESS, WebCryptoSigner, anchorDiscriminator, bigIntTo32Bytes, buildSecp256r1Instruction, bytesToBigInt, bytesToHex, compressedPubkey, deriveAddressSeed, deriveAddressSeedSolana, deriveBackupKey, encodeLowSSignature, generateRecoveryCode, hexToBytes, recoverYParity, serializeInstructions, signatureToFelts, u256ToFelts };
1671
+ //# sourceMappingURL=chunk-PEUQQZB5.mjs.map
1672
+ //# sourceMappingURL=chunk-PEUQQZB5.mjs.map