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