@cavos/kit 0.0.2 → 0.0.4

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.
@@ -0,0 +1,2946 @@
1
+ import { p256 } from '@noble/curves/p256';
2
+ import { sha256 } from '@noble/hashes/sha256';
3
+ import { hash, num, Signer, RpcProvider, PaymasterRpc, Account as Account$1 } from 'starknet';
4
+ import { PublicKey, TransactionInstruction, SystemProgram, SYSVAR_INSTRUCTIONS_PUBKEY, Transaction, Connection, sendAndConfirmTransaction } from '@solana/web3.js';
5
+ import { hkdf } from '@noble/hashes/hkdf';
6
+ import { pbkdf2 } from '@noble/hashes/pbkdf2';
7
+ import { randomBytes } from '@noble/hashes/utils';
8
+ import { rpc, hash as hash$1, xdr, Address, StrKey, nativeToScVal, Operation, scValToNative, Account, TransactionBuilder, BASE_FEE } from '@stellar/stellar-sdk';
9
+
10
+ // src/crypto/encoding.ts
11
+ var U128_MASK = (1n << 128n) - 1n;
12
+ function u256ToFelts(value) {
13
+ return [value & U128_MASK, value >> 128n];
14
+ }
15
+ function bytesToBigInt(bytes) {
16
+ let out = 0n;
17
+ for (const b of bytes) out = out << 8n | BigInt(b);
18
+ return out;
19
+ }
20
+ function bytesToHex(bytes) {
21
+ let s = "0x";
22
+ for (const b of bytes) s += b.toString(16).padStart(2, "0");
23
+ return s;
24
+ }
25
+ function hexToBytes(hex) {
26
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
27
+ const padded = clean.length % 2 ? "0" + clean : clean;
28
+ const out = new Uint8Array(padded.length / 2);
29
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(padded.slice(i * 2, i * 2 + 2), 16);
30
+ return out;
31
+ }
32
+ function bytesToByteArrayCalldata(bytes) {
33
+ const CHUNK = 31;
34
+ const fullCount = Math.floor(bytes.length / CHUNK);
35
+ const out = [String(fullCount)];
36
+ for (let i = 0; i < fullCount; i++) {
37
+ out.push("0x" + bytesToBigInt(bytes.subarray(i * CHUNK, i * CHUNK + CHUNK)).toString(16));
38
+ }
39
+ const rem = bytes.subarray(fullCount * CHUNK);
40
+ out.push("0x" + (rem.length ? bytesToBigInt(rem).toString(16) : "0"));
41
+ out.push(String(rem.length));
42
+ return out;
43
+ }
44
+ function bigIntTo32Bytes(value) {
45
+ const out = new Uint8Array(32);
46
+ let v = value;
47
+ for (let i = 31; i >= 0; i--) {
48
+ out[i] = Number(v & 0xffn);
49
+ v >>= 8n;
50
+ }
51
+ return out;
52
+ }
53
+ function signatureToFelts(sig) {
54
+ const [rLow, rHigh] = u256ToFelts(sig.r);
55
+ const [sLow, sHigh] = u256ToFelts(sig.s);
56
+ return [rLow, rHigh, sLow, sHigh, sig.yParity ? 1n : 0n];
57
+ }
58
+ function recoverYParity(r, s, digest, pubkey) {
59
+ for (const bit of [0, 1]) {
60
+ try {
61
+ const candidate = new p256.Signature(r, s).addRecoveryBit(bit);
62
+ const point = candidate.recoverPublicKey(digest).toAffine();
63
+ if (point.x === pubkey.x && point.y === pubkey.y) {
64
+ return bit === 1;
65
+ }
66
+ } catch {
67
+ }
68
+ }
69
+ throw new Error("kit/signature: could not recover parity for the given pubkey");
70
+ }
71
+ var IDB_NAME = "cavos-kit";
72
+ var IDB_STORE = "device-keys";
73
+ var WebCryptoSigner = class _WebCryptoSigner {
74
+ constructor(privateKey, publicKey, keyId) {
75
+ this.privateKey = privateKey;
76
+ this.publicKey = publicKey;
77
+ this.keyId = keyId;
78
+ }
79
+ /** Create a fresh device key (first run on this device) and persist it. */
80
+ static async create(opts) {
81
+ assertSecureContext();
82
+ const pair = await crypto.subtle.generateKey(
83
+ { name: "ECDSA", namedCurve: "P-256" },
84
+ false,
85
+ // private key is NON-extractable
86
+ ["sign", "verify"]
87
+ );
88
+ const publicKey = await exportPublicKey(pair.publicKey);
89
+ await idbPut(opts.keyId, { privateKey: pair.privateKey, x: publicKey.x, y: publicKey.y });
90
+ return new _WebCryptoSigner(pair.privateKey, publicKey, opts.keyId);
91
+ }
92
+ /** Load an existing device key from storage, or null if none exists yet. */
93
+ static async load(opts) {
94
+ const rec = await idbGet(opts.keyId);
95
+ if (!rec) return null;
96
+ return new _WebCryptoSigner(rec.privateKey, { x: rec.x, y: rec.y }, opts.keyId);
97
+ }
98
+ /** Load the device key, creating one on first use. */
99
+ static async loadOrCreate(opts) {
100
+ return await _WebCryptoSigner.load(opts) ?? await _WebCryptoSigner.create(opts);
101
+ }
102
+ async getPublicKey() {
103
+ return this.publicKey;
104
+ }
105
+ async sign(txHash) {
106
+ const raw = new Uint8Array(
107
+ await crypto.subtle.sign(
108
+ { name: "ECDSA", hash: "SHA-256" },
109
+ this.privateKey,
110
+ txHash
111
+ )
112
+ );
113
+ const r = bytesToBigInt(raw.subarray(0, 32));
114
+ const s = bytesToBigInt(raw.subarray(32, 64));
115
+ const digest = sha256(txHash);
116
+ const yParity = recoverYParity(r, s, digest, this.publicKey);
117
+ return { r, s, yParity };
118
+ }
119
+ };
120
+ async function exportPublicKey(publicKey) {
121
+ const raw = new Uint8Array(await crypto.subtle.exportKey("raw", publicKey));
122
+ return { x: bytesToBigInt(raw.subarray(1, 33)), y: bytesToBigInt(raw.subarray(33, 65)) };
123
+ }
124
+ function assertSecureContext() {
125
+ const ok = typeof crypto !== "undefined" && typeof crypto.subtle !== "undefined" && (typeof window === "undefined" || window.isSecureContext);
126
+ if (!ok) {
127
+ throw new Error(
128
+ "Cavos: WebCrypto is unavailable. Device keys require a secure context \u2014 use HTTPS, or http://localhost. (For LAN/mobile dev testing, run `next dev --experimental-https`.)"
129
+ );
130
+ }
131
+ }
132
+ function openDb() {
133
+ return new Promise((resolve, reject) => {
134
+ const req = indexedDB.open(IDB_NAME, 1);
135
+ req.onupgradeneeded = () => req.result.createObjectStore(IDB_STORE);
136
+ req.onsuccess = () => resolve(req.result);
137
+ req.onerror = () => reject(req.error);
138
+ });
139
+ }
140
+ async function idbPut(keyId, value) {
141
+ const db = await openDb();
142
+ await tx(db, "readwrite", (store) => store.put(value, keyId));
143
+ db.close();
144
+ }
145
+ async function idbGet(keyId) {
146
+ const db = await openDb();
147
+ const result = await tx(db, "readonly", (store) => store.get(keyId));
148
+ db.close();
149
+ return result ?? null;
150
+ }
151
+ function tx(db, mode, run) {
152
+ return new Promise((resolve, reject) => {
153
+ const store = db.transaction(IDB_STORE, mode).objectStore(IDB_STORE);
154
+ const req = run(store);
155
+ req.onsuccess = () => resolve(req.result);
156
+ req.onerror = () => reject(req.error);
157
+ });
158
+ }
159
+
160
+ // src/chains/starknet/constants.ts
161
+ var STARKNET_NETWORKS = {
162
+ sepolia: {
163
+ chainId: "0x534e5f5345504f4c4941",
164
+ // SN_SEPOLIA
165
+ rpcUrl: "https://api.cartridge.gg/x/starknet/sepolia"
166
+ },
167
+ mainnet: {
168
+ chainId: "0x534e5f4d41494e",
169
+ // SN_MAIN
170
+ rpcUrl: "https://api.cartridge.gg/x/starknet/mainnet"
171
+ }
172
+ };
173
+ var UDC_ADDRESS = "0x041a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf";
174
+ var CAVOS_PAYMASTER_URL = {
175
+ sepolia: "https://sepolia-paymaster.cavos.xyz",
176
+ mainnet: "https://paymaster.cavos.xyz"
177
+ };
178
+ var DEVICE_ACCOUNT_CLASS_HASH = {
179
+ sepolia: "0x25cbc5423e8ee895febb0ef2c3945b408da44d0039d915fbdd681fe6b6ba66b",
180
+ mainnet: "0x1840aded59e8a0d2b440a134cb9079a7fc11b06c77f58ed189ab436a034ca6a"
181
+ };
182
+ var StarknetAdapter = class {
183
+ constructor(opts) {
184
+ this.opts = opts;
185
+ this.chain = "starknet";
186
+ }
187
+ computeAddress({ addressSeed, initialSigner, salt }) {
188
+ return hash.calculateContractAddressFromHash(
189
+ num.toHex(salt ?? addressSeed),
190
+ this.opts.classHash,
191
+ this.constructorCalldata(addressSeed, initialSigner),
192
+ 0
193
+ // deployerAddress 0 => deterministic counterfactual address
194
+ );
195
+ }
196
+ /** Single UDC deploy; the constructor registers the first device signer, so
197
+ * the account is ready the moment it is deployed (fits the paymaster's
198
+ * deploy + execute_from_outside bundle). */
199
+ buildDeploy(params) {
200
+ const salt = params.salt ?? params.addressSeed;
201
+ const calldata = this.constructorCalldata(params.addressSeed, params.initialSigner);
202
+ return [
203
+ {
204
+ contractAddress: UDC_ADDRESS,
205
+ entrypoint: "deployContract",
206
+ calldata: [
207
+ this.opts.classHash,
208
+ num.toHex(salt),
209
+ "0x0",
210
+ // unique = false -> deployer-independent address
211
+ num.toHex(calldata.length),
212
+ ...calldata
213
+ ]
214
+ }
215
+ ];
216
+ }
217
+ /** Constructor calldata: [address_seed, pub_x_low, pub_x_high, pub_y_low, pub_y_high]. */
218
+ constructorCalldata(addressSeed, initialSigner) {
219
+ return [num.toHex(addressSeed), ...pubkeyCalldata(initialSigner)];
220
+ }
221
+ buildAddSigner(accountAddress, signer) {
222
+ return { contractAddress: accountAddress, entrypoint: "add_signer", calldata: pubkeyCalldata(signer) };
223
+ }
224
+ buildRemoveSigner(accountAddress, signer) {
225
+ return { contractAddress: accountAddress, entrypoint: "remove_signer", calldata: pubkeyCalldata(signer) };
226
+ }
227
+ async isAuthorizedSigner(accountAddress, signer) {
228
+ if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
229
+ const res = await this.opts.provider.callContract({
230
+ contractAddress: accountAddress,
231
+ entrypoint: "is_authorized_signer",
232
+ calldata: pubkeyCalldata(signer)
233
+ });
234
+ return BigInt(res[0] ?? 0) !== 0n;
235
+ }
236
+ async buildSignature(txHash) {
237
+ if (!this.opts.signer) throw new Error("kit/starknet: signer required to sign");
238
+ const sig = await this.opts.signer.sign(bigIntTo32Bytes(txHash));
239
+ return signatureToFelts(sig).map((f) => num.toHex(f));
240
+ }
241
+ // --- passkey approvers ---
242
+ buildAddApprover(accountAddress, passkey) {
243
+ return { contractAddress: accountAddress, entrypoint: "add_approver", calldata: pubkeyCalldata(passkey) };
244
+ }
245
+ buildRemoveApprover(accountAddress, passkey) {
246
+ return { contractAddress: accountAddress, entrypoint: "remove_approver", calldata: pubkeyCalldata(passkey) };
247
+ }
248
+ async isApprover(accountAddress, passkey) {
249
+ if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
250
+ const res = await this.opts.provider.callContract({
251
+ contractAddress: accountAddress,
252
+ entrypoint: "is_approver",
253
+ calldata: pubkeyCalldata(passkey)
254
+ });
255
+ return BigInt(res[0] ?? 0) !== 0n;
256
+ }
257
+ /** True if the account has at least one passkey registered as an approver.
258
+ * Lets the UI decide whether to offer passkey approval before prompting. */
259
+ async hasPasskeyApprover(accountAddress) {
260
+ if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
261
+ const res = await this.opts.provider.callContract({
262
+ contractAddress: accountAddress,
263
+ entrypoint: "get_approver_count",
264
+ calldata: []
265
+ });
266
+ return BigInt(res[0] ?? 0) > 0n;
267
+ }
268
+ async getPasskeyNonce(accountAddress) {
269
+ if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
270
+ const res = await this.opts.provider.callContract({
271
+ contractAddress: accountAddress,
272
+ entrypoint: "get_passkey_nonce",
273
+ calldata: []
274
+ });
275
+ return BigInt(res[0] ?? 0);
276
+ }
277
+ /** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
278
+ * `sha256(new_x || new_y || nonce)` (coords 32B BE, nonce 16B BE). The batch
279
+ * challenge the passkey signs is `sha256(concat(leaves))` across chains. */
280
+ passkeyLeaf(newSigner, nonce) {
281
+ const msg = new Uint8Array(32 + 32 + 16);
282
+ msg.set(bigIntTo32Bytes(newSigner.x), 0);
283
+ msg.set(bigIntTo32Bytes(newSigner.y), 32);
284
+ msg.set(bigIntTo32Bytes(nonce).subarray(16), 64);
285
+ return sha256(msg);
286
+ }
287
+ /** Passkey-authorized `add_signer` call. `leaves`/`leafIndex` place this chain's
288
+ * leaf in the multi-chain batch (single chain → `[leaf]`, index 0). `yParity`
289
+ * matches the raw `(r, s)` — the contract normalizes high-S internally. */
290
+ buildAddSignerViaPasskey(accountAddress, newSigner, nonce, leaves, leafIndex, assertion, yParity) {
291
+ const [rl, rh] = u256ToFelts(assertion.r);
292
+ const [sl, sh] = u256ToFelts(assertion.s);
293
+ const leavesCalldata = [String(leaves.length)];
294
+ for (const leaf of leaves) {
295
+ const [lo, hi] = u256ToFelts(bytesToBigInt(leaf));
296
+ leavesCalldata.push(num.toHex(lo), num.toHex(hi));
297
+ }
298
+ return {
299
+ contractAddress: accountAddress,
300
+ entrypoint: "add_signer_via_passkey",
301
+ calldata: [
302
+ ...pubkeyCalldata(newSigner),
303
+ // new_x, new_y (u256 pairs)
304
+ num.toHex(nonce),
305
+ ...leavesCalldata,
306
+ // Array<u256> leaves
307
+ String(leafIndex),
308
+ ...bytesToByteArrayCalldata(assertion.authenticatorData),
309
+ ...bytesToByteArrayCalldata(assertion.clientDataJSON),
310
+ String(assertion.challengeOffset),
311
+ num.toHex(rl),
312
+ num.toHex(rh),
313
+ num.toHex(sl),
314
+ num.toHex(sh),
315
+ yParity ? "0x1" : "0x0"
316
+ ]
317
+ };
318
+ }
319
+ };
320
+ function pubkeyCalldata(pk) {
321
+ const [xl, xh] = u256ToFelts(pk.x);
322
+ const [yl, yh] = u256ToFelts(pk.y);
323
+ return [num.toHex(xl), num.toHex(xh), num.toHex(yl), num.toHex(yh)];
324
+ }
325
+ var StarknetDeviceSigner = class extends Signer {
326
+ constructor(device) {
327
+ super("0x1");
328
+ this.device = device;
329
+ }
330
+ /** Device accounts are not controlled by a single Stark pubkey. */
331
+ async getPubKey() {
332
+ return "0x0";
333
+ }
334
+ /** Sign the computed tx hash silently with the device signer. */
335
+ async signRaw(msgHash) {
336
+ const sig = await this.device.sign(bigIntTo32Bytes(BigInt(msgHash)));
337
+ return signatureToFelts(sig).map((f) => num.toHex(f));
338
+ }
339
+ };
340
+
341
+ // src/registry/WalletRegistry.ts
342
+ var InMemoryWalletRegistry = class {
343
+ constructor() {
344
+ this.wallets = /* @__PURE__ */ new Map();
345
+ }
346
+ async lookup(userId) {
347
+ return this.wallets.get(userId) ?? null;
348
+ }
349
+ async register(params) {
350
+ this.wallets.set(params.userId, { address: params.address, devices: [params.initialSigner] });
351
+ }
352
+ async addDevice(params) {
353
+ const w = this.wallets.get(params.userId);
354
+ if (w) w.devices = [...w.devices ?? [], params.signer];
355
+ }
356
+ };
357
+
358
+ // src/registry/HttpWalletRegistry.ts
359
+ function toHex(n) {
360
+ return "0x" + n.toString(16);
361
+ }
362
+ function fromHex(s) {
363
+ return BigInt(s);
364
+ }
365
+ var HttpWalletRegistry = class {
366
+ constructor(opts) {
367
+ this.opts = opts;
368
+ }
369
+ async lookup(userId) {
370
+ const url = new URL("/api/wallets", this.opts.baseUrl);
371
+ url.searchParams.set("app_id", this.opts.appId);
372
+ url.searchParams.set("user_social_id", userId);
373
+ url.searchParams.set("network", this.opts.network);
374
+ const res = await fetch(url, { headers: { "Content-Type": "application/json" } });
375
+ if (!res.ok) throw new Error(`registry lookup failed: ${res.status}`);
376
+ const data = await res.json();
377
+ if (!data.found || !data.address) return null;
378
+ const devices = Array.isArray(data.devices) ? data.devices.map((d) => ({
379
+ x: fromHex(d.pub_x),
380
+ y: fromHex(d.pub_y)
381
+ })) : void 0;
382
+ return { address: data.address, devices };
383
+ }
384
+ async register(params) {
385
+ const res = await fetch(new URL("/api/wallets", this.opts.baseUrl), {
386
+ method: "POST",
387
+ headers: { "Content-Type": "application/json" },
388
+ body: JSON.stringify({
389
+ app_id: this.opts.appId,
390
+ user_social_id: params.userId,
391
+ network: this.opts.network,
392
+ address: params.address,
393
+ // Device-signer wallets send their initial signer (no encrypted blob).
394
+ devices: [{ x: toHex(params.initialSigner.x), y: toHex(params.initialSigner.y) }]
395
+ })
396
+ });
397
+ if (!res.ok) {
398
+ const t = await res.text().catch(() => "");
399
+ throw new Error(`registry register failed: ${res.status} ${t}`);
400
+ }
401
+ }
402
+ async addDevice(params) {
403
+ }
404
+ };
405
+ function deriveAddressSeed({ userId, appSalt }) {
406
+ const h = hash.computePoseidonHashOnElements([feltFromString(userId), feltFromString(appSalt)]);
407
+ return BigInt(h);
408
+ }
409
+ function deriveAddressSeedSolana({ userId, appSalt }) {
410
+ return sha256(new TextEncoder().encode(`cavos:solana:v1:${userId}:${appSalt}`));
411
+ }
412
+ function deriveAddressSeedStellar({ userId, appSalt }) {
413
+ return sha256(new TextEncoder().encode(`cavos:stellar:v1:${userId}:${appSalt}`));
414
+ }
415
+ function feltFromString(s) {
416
+ const bytes = new TextEncoder().encode(s);
417
+ const chunks = [];
418
+ for (let i = 0; i < bytes.length; i += 31) {
419
+ let w = 0n;
420
+ for (const b of bytes.subarray(i, i + 31)) w = w << 8n | BigInt(b);
421
+ chunks.push(w);
422
+ }
423
+ if (chunks.length === 0) return 0n;
424
+ if (chunks.length === 1) return chunks[0];
425
+ return BigInt(hash.computePoseidonHashOnElements(chunks));
426
+ }
427
+
428
+ // src/chains/solana/constants.ts
429
+ var DEVICE_ACCOUNT_PROGRAM_ID = "FHnoYNfYAmFrwt18gcBGG7G1S5q3RAbCBvrV2D29izNJ";
430
+ var SECP256R1_PROGRAM_ID = "Secp256r1SigVerify1111111111111111111111111";
431
+ var ACCOUNT_SEED = "cavos-account";
432
+ var DOMAIN_ADD = "cavos:add_signer:v1";
433
+ var DOMAIN_REMOVE = "cavos:remove_signer:v1";
434
+ var DOMAIN_TRANSFER = "cavos:transfer:v1";
435
+ var DOMAIN_EXECUTE = "cavos:execute:v1";
436
+ var DOMAIN_ADD_APPROVER = "cavos:add_approver:v1";
437
+ var DOMAIN_REMOVE_APPROVER = "cavos:remove_approver:v1";
438
+ var SECP256R1_N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
439
+ var SOLANA_NETWORKS = {
440
+ "solana-devnet": "https://api.devnet.solana.com",
441
+ "solana-mainnet": "https://api.mainnet-beta.solana.com",
442
+ "solana-localnet": "http://127.0.0.1:8899"
443
+ };
444
+ var COMPRESSED_PUBKEY_SIZE = 33;
445
+ var SIGNATURE_SIZE = 64;
446
+ var CURRENT_IX = 65535;
447
+ var SolanaAdapter = class {
448
+ constructor(opts = {}) {
449
+ this.opts = opts;
450
+ this.chain = "solana";
451
+ this.programId = new PublicKey(opts.programId ?? DEVICE_ACCOUNT_PROGRAM_ID);
452
+ }
453
+ /** Deterministic account address: PDA of [seed, address_seed, initial_signer_x]. */
454
+ computeAddress(addressSeed, initialSigner) {
455
+ return this.pda(addressSeed, compressedPubkey(initialSigner)).toBase58();
456
+ }
457
+ pda(addressSeed, initialCompressed) {
458
+ const [pda] = PublicKey.findProgramAddressSync(
459
+ [
460
+ Buffer.from(ACCOUNT_SEED),
461
+ Buffer.from(addressSeed),
462
+ Buffer.from(initialCompressed.slice(1, 33))
463
+ // x-coordinate
464
+ ],
465
+ this.programId
466
+ );
467
+ return pda;
468
+ }
469
+ /** `initialize` instruction creating the account with its first device signer. */
470
+ buildInitialize(addressSeed, payer, initialSigner) {
471
+ const initialCompressed = compressedPubkey(initialSigner);
472
+ const account = this.pda(addressSeed, initialCompressed);
473
+ const data = Buffer.concat([
474
+ anchorDiscriminator("initialize"),
475
+ Buffer.from(addressSeed),
476
+ // [u8;32]
477
+ Buffer.from(initialCompressed)
478
+ // [u8;33]
479
+ ]);
480
+ return new TransactionInstruction({
481
+ programId: this.programId,
482
+ keys: [
483
+ { pubkey: account, isSigner: false, isWritable: true },
484
+ { pubkey: new PublicKey(payer), isSigner: true, isWritable: true },
485
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }
486
+ ],
487
+ data
488
+ });
489
+ }
490
+ /** `[precompile, add_signer]` bundle, authorized by an existing device signer. */
491
+ async buildAddSigner(account, newSigner) {
492
+ const accountPk = new PublicKey(account);
493
+ const newCompressed = compressedPubkey(newSigner);
494
+ const nonce = await this.fetchNonce(accountPk);
495
+ const message = concatBytes(
496
+ Buffer.from(DOMAIN_ADD),
497
+ accountPk.toBuffer(),
498
+ newCompressed,
499
+ u64le(nonce)
500
+ );
501
+ const { precompileIx } = await this.signToPrecompile(message);
502
+ const ix = new TransactionInstruction({
503
+ programId: this.programId,
504
+ keys: this.guardedKeys(accountPk),
505
+ data: Buffer.concat([anchorDiscriminator("add_signer"), Buffer.from(newCompressed)])
506
+ });
507
+ return [precompileIx, ix];
508
+ }
509
+ /** `[precompile, remove_signer]` bundle, authorized by an existing device signer. */
510
+ async buildRemoveSigner(account, signer) {
511
+ const accountPk = new PublicKey(account);
512
+ const compressed = compressedPubkey(signer);
513
+ const nonce = await this.fetchNonce(accountPk);
514
+ const message = concatBytes(
515
+ Buffer.from(DOMAIN_REMOVE),
516
+ accountPk.toBuffer(),
517
+ compressed,
518
+ u64le(nonce)
519
+ );
520
+ const { precompileIx } = await this.signToPrecompile(message);
521
+ const ix = new TransactionInstruction({
522
+ programId: this.programId,
523
+ keys: this.guardedKeys(accountPk),
524
+ data: Buffer.concat([anchorDiscriminator("remove_signer"), Buffer.from(compressed)])
525
+ });
526
+ return [precompileIx, ix];
527
+ }
528
+ /** `[precompile, add_approver]` bundle enrolling a passkey approver (device-signed). */
529
+ async buildAddApprover(account, passkey) {
530
+ const accountPk = new PublicKey(account);
531
+ const compressed = compressedPubkey(passkey);
532
+ const nonce = await this.fetchNonce(accountPk);
533
+ const message = concatBytes(
534
+ Buffer.from(DOMAIN_ADD_APPROVER),
535
+ accountPk.toBuffer(),
536
+ compressed,
537
+ u64le(nonce)
538
+ );
539
+ const { precompileIx } = await this.signToPrecompile(message);
540
+ const ix = new TransactionInstruction({
541
+ programId: this.programId,
542
+ keys: this.guardedKeys(accountPk),
543
+ data: Buffer.concat([anchorDiscriminator("add_approver"), Buffer.from(compressed)])
544
+ });
545
+ return [precompileIx, ix];
546
+ }
547
+ /** `[precompile, remove_approver]` bundle (device-signed). */
548
+ async buildRemoveApprover(account, passkey) {
549
+ const accountPk = new PublicKey(account);
550
+ const compressed = compressedPubkey(passkey);
551
+ const nonce = await this.fetchNonce(accountPk);
552
+ const message = concatBytes(
553
+ Buffer.from(DOMAIN_REMOVE_APPROVER),
554
+ accountPk.toBuffer(),
555
+ compressed,
556
+ u64le(nonce)
557
+ );
558
+ const { precompileIx } = await this.signToPrecompile(message);
559
+ const ix = new TransactionInstruction({
560
+ programId: this.programId,
561
+ keys: this.guardedKeys(accountPk),
562
+ data: Buffer.concat([anchorDiscriminator("remove_approver"), Buffer.from(compressed)])
563
+ });
564
+ return [precompileIx, ix];
565
+ }
566
+ /** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
567
+ * `sha256(compressed(new_signer) || passkey_nonce_le8)`. The batch challenge the
568
+ * passkey signs is `sha256(concat(leaves))` across chains. */
569
+ passkeyLeaf(newSigner, nonce) {
570
+ return sha256(concatBytes(compressedPubkey(newSigner), u64le(nonce)));
571
+ }
572
+ /**
573
+ * `[precompile(passkey), add_signer_via_passkey]` bundle. The precompile ix
574
+ * verifies the PASSKEY's WebAuthn assertion over `authData || sha256(clientDataJSON)`;
575
+ * the program ix binds the challenge to `newSigner` + the passkey nonce and adds
576
+ * the signer. No device signature — a gasless relayer can submit it.
577
+ */
578
+ buildAddSignerViaPasskey(account, newSigner, passkey, leaves, leafIndex, assertion) {
579
+ const accountPk = new PublicKey(account);
580
+ const newCompressed = compressedPubkey(newSigner);
581
+ const passkeyCompressed = compressedPubkey(passkey);
582
+ const clientHash = sha256(assertion.clientDataJSON);
583
+ const message = concatBytes(assertion.authenticatorData, clientHash);
584
+ const signature = encodeLowSSignature(assertion.r, assertion.s);
585
+ const precompileIx = buildSecp256r1Instruction(passkeyCompressed, signature, message);
586
+ const leavesBlob = Buffer.concat([u32le(leaves.length), ...leaves.map((l) => Buffer.from(l))]);
587
+ const data = Buffer.concat([
588
+ anchorDiscriminator("add_signer_via_passkey"),
589
+ Buffer.from(newCompressed),
590
+ leavesBlob,
591
+ u32le(leafIndex),
592
+ serializeVecU8(assertion.authenticatorData),
593
+ serializeVecU8(assertion.clientDataJSON),
594
+ u32le(assertion.challengeOffset)
595
+ ]);
596
+ const ix = new TransactionInstruction({
597
+ programId: this.programId,
598
+ keys: this.guardedKeys(accountPk),
599
+ data
600
+ });
601
+ return [precompileIx, ix];
602
+ }
603
+ /** Read whether `passkey` is a registered approver. */
604
+ /** True if the account has at least one passkey registered as an approver. */
605
+ async hasPasskeyApprover(account) {
606
+ const approvers = await this.fetchApprovers(new PublicKey(account));
607
+ return approvers.length > 0;
608
+ }
609
+ async isApprover(account, passkey) {
610
+ const approvers = await this.fetchApprovers(new PublicKey(account));
611
+ const target = Buffer.from(compressedPubkey(passkey)).toString("hex");
612
+ return approvers.some((a) => Buffer.from(a).toString("hex") === target);
613
+ }
614
+ /** Read the current passkey-approval nonce. */
615
+ async passkeyNonce(account) {
616
+ const info = await this.requireConnection().getAccountInfo(new PublicKey(account));
617
+ if (!info) return 0n;
618
+ const d = info.data;
619
+ const signersLenOff = 8 + 32 + 1 + 8 + COMPRESSED_PUBKEY_SIZE;
620
+ const signerCount = d.readUInt32LE(signersLenOff);
621
+ const approversLenOff = signersLenOff + 4 + signerCount * COMPRESSED_PUBKEY_SIZE;
622
+ const approverCount = d.readUInt32LE(approversLenOff);
623
+ const passkeyNonceOff = approversLenOff + 4 + approverCount * COMPRESSED_PUBKEY_SIZE;
624
+ return readU64le(d, passkeyNonceOff);
625
+ }
626
+ /** `[precompile, execute_transfer]` bundle moving lamports out of the account. */
627
+ async buildExecuteTransfer(account, destination, amount) {
628
+ const accountPk = new PublicKey(account);
629
+ const destPk = new PublicKey(destination);
630
+ const nonce = await this.fetchNonce(accountPk);
631
+ const message = concatBytes(
632
+ Buffer.from(DOMAIN_TRANSFER),
633
+ accountPk.toBuffer(),
634
+ destPk.toBuffer(),
635
+ u64le(amount),
636
+ u64le(nonce)
637
+ );
638
+ const { precompileIx } = await this.signToPrecompile(message);
639
+ const ix = new TransactionInstruction({
640
+ programId: this.programId,
641
+ keys: [
642
+ { pubkey: accountPk, isSigner: false, isWritable: true },
643
+ { pubkey: destPk, isSigner: false, isWritable: true },
644
+ { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false }
645
+ ],
646
+ data: Buffer.concat([anchorDiscriminator("execute_transfer"), u64le(amount)])
647
+ });
648
+ return [precompileIx, ix];
649
+ }
650
+ /**
651
+ * `[precompile, execute]` bundle running arbitrary CPI instructions with the
652
+ * account PDA as signer. The device key signs over
653
+ * `DOMAIN_EXECUTE || account || sha256(canonical(instructions)) || nonce`, so
654
+ * the signature commits to the EXACT instruction set the program will invoke —
655
+ * no account/data substitution is possible after signing.
656
+ *
657
+ * The instructions' accounts are passed to the program via `remaining_accounts`
658
+ * (flattened, in order); the program enforces an exact, ordered mapping.
659
+ */
660
+ async buildExecute(account, instructions) {
661
+ if (instructions.length === 0) throw new Error("kit/solana: execute requires at least one instruction");
662
+ const accountPk = new PublicKey(account);
663
+ const nonce = await this.fetchNonce(accountPk);
664
+ const blob = serializeInstructions(instructions);
665
+ const ixsHash = sha256(blob);
666
+ const message = concatBytes(
667
+ Buffer.from(DOMAIN_EXECUTE),
668
+ accountPk.toBuffer(),
669
+ Buffer.from(ixsHash),
670
+ u64le(nonce)
671
+ );
672
+ const { precompileIx } = await this.signToPrecompile(message);
673
+ const blobLen = Buffer.alloc(4);
674
+ new DataView(blobLen.buffer).setUint32(0, blob.length, true);
675
+ const data = Buffer.concat([anchorDiscriminator("execute"), blobLen, blob]);
676
+ const remainingAccounts = [];
677
+ for (const ix2 of instructions) {
678
+ for (const acc of ix2.accounts) {
679
+ remainingAccounts.push({
680
+ pubkey: new PublicKey(acc.pubkey),
681
+ isSigner: false,
682
+ // signer flags are part of the signed InstructionData
683
+ isWritable: acc.isWritable
684
+ });
685
+ }
686
+ remainingAccounts.push({
687
+ pubkey: new PublicKey(ix2.programId),
688
+ isSigner: false,
689
+ isWritable: false
690
+ });
691
+ }
692
+ const ix = new TransactionInstruction({
693
+ programId: this.programId,
694
+ keys: [
695
+ { pubkey: accountPk, isSigner: false, isWritable: true },
696
+ { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false },
697
+ ...remainingAccounts
698
+ ],
699
+ data
700
+ });
701
+ return [precompileIx, ix];
702
+ }
703
+ /** Read whether `signer` is currently an authorized signer of `account`. */
704
+ async isAuthorizedSigner(account, signer) {
705
+ const signers = await this.fetchSigners(new PublicKey(account));
706
+ const target = Buffer.from(compressedPubkey(signer)).toString("hex");
707
+ return signers.some((s) => Buffer.from(s).toString("hex") === target);
708
+ }
709
+ guardedKeys(account) {
710
+ return [
711
+ { pubkey: account, isSigner: false, isWritable: true },
712
+ { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false }
713
+ ];
714
+ }
715
+ /** Sign `message` with the device key and build the matching precompile ix. */
716
+ async signToPrecompile(message) {
717
+ if (!this.opts.signer) throw new Error("kit/solana: signer required to authorize");
718
+ const pubkey = await this.opts.signer.getPublicKey();
719
+ const sig = await this.opts.signer.sign(message);
720
+ const signature = encodeLowSSignature(sig.r, sig.s);
721
+ const precompileIx = buildSecp256r1Instruction(
722
+ compressedPubkey(pubkey),
723
+ signature,
724
+ message
725
+ );
726
+ return { precompileIx };
727
+ }
728
+ async fetchNonce(account) {
729
+ const info = await this.requireConnection().getAccountInfo(account);
730
+ if (!info) return 0n;
731
+ return readU64le(info.data, 41);
732
+ }
733
+ async fetchSigners(account) {
734
+ const info = await this.requireConnection().getAccountInfo(account);
735
+ if (!info) return [];
736
+ const d = info.data;
737
+ const lenOffset = 8 + 32 + 1 + 8 + COMPRESSED_PUBKEY_SIZE;
738
+ const count = d.readUInt32LE(lenOffset);
739
+ const out = [];
740
+ let off = lenOffset + 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
+ }
747
+ async fetchApprovers(account) {
748
+ const info = await this.requireConnection().getAccountInfo(account);
749
+ if (!info) return [];
750
+ const d = info.data;
751
+ const signersLenOff = 8 + 32 + 1 + 8 + COMPRESSED_PUBKEY_SIZE;
752
+ const signerCount = d.readUInt32LE(signersLenOff);
753
+ const approversLenOff = signersLenOff + 4 + signerCount * COMPRESSED_PUBKEY_SIZE;
754
+ const count = d.readUInt32LE(approversLenOff);
755
+ const out = [];
756
+ let off = approversLenOff + 4;
757
+ for (let i = 0; i < count; i++) {
758
+ out.push(Uint8Array.from(d.subarray(off, off + COMPRESSED_PUBKEY_SIZE)));
759
+ off += COMPRESSED_PUBKEY_SIZE;
760
+ }
761
+ return out;
762
+ }
763
+ requireConnection() {
764
+ if (!this.opts.connection) throw new Error("kit/solana: connection required for reads");
765
+ return this.opts.connection;
766
+ }
767
+ };
768
+ function compressedPubkey(pk) {
769
+ const out = new Uint8Array(COMPRESSED_PUBKEY_SIZE);
770
+ out[0] = pk.y % 2n === 0n ? 2 : 3;
771
+ out.set(bigIntTo32Bytes(pk.x), 1);
772
+ return out;
773
+ }
774
+ function encodeLowSSignature(r, s) {
775
+ const lowS2 = s > SECP256R1_N / 2n ? SECP256R1_N - s : s;
776
+ const out = new Uint8Array(SIGNATURE_SIZE);
777
+ out.set(bigIntTo32Bytes(r), 0);
778
+ out.set(bigIntTo32Bytes(lowS2), 32);
779
+ return out;
780
+ }
781
+ function buildSecp256r1Instruction(compressed, signature, message) {
782
+ const headerLen = 2;
783
+ const offsetsLen = 14;
784
+ const pubkeyOffset = headerLen + offsetsLen;
785
+ const sigOffset = pubkeyOffset + COMPRESSED_PUBKEY_SIZE;
786
+ const msgOffset = sigOffset + SIGNATURE_SIZE;
787
+ const data = Buffer.alloc(msgOffset + message.length);
788
+ data.writeUInt8(1, 0);
789
+ data.writeUInt8(0, 1);
790
+ let o = headerLen;
791
+ data.writeUInt16LE(sigOffset, o);
792
+ o += 2;
793
+ data.writeUInt16LE(CURRENT_IX, o);
794
+ o += 2;
795
+ data.writeUInt16LE(pubkeyOffset, o);
796
+ o += 2;
797
+ data.writeUInt16LE(CURRENT_IX, o);
798
+ o += 2;
799
+ data.writeUInt16LE(msgOffset, o);
800
+ o += 2;
801
+ data.writeUInt16LE(message.length, o);
802
+ o += 2;
803
+ data.writeUInt16LE(CURRENT_IX, o);
804
+ o += 2;
805
+ Buffer.from(compressed).copy(data, pubkeyOffset);
806
+ Buffer.from(signature).copy(data, sigOffset);
807
+ Buffer.from(message).copy(data, msgOffset);
808
+ return new TransactionInstruction({
809
+ keys: [],
810
+ programId: new PublicKey(SECP256R1_PROGRAM_ID),
811
+ data
812
+ });
813
+ }
814
+ function anchorDiscriminator(name) {
815
+ return Buffer.from(sha256(`global:${name}`).slice(0, 8));
816
+ }
817
+ function u32le(n) {
818
+ const b = Buffer.alloc(4);
819
+ b.writeUInt32LE(n);
820
+ return b;
821
+ }
822
+ function u64le(n) {
823
+ const b = Buffer.alloc(8);
824
+ new DataView(b.buffer, b.byteOffset, 8).setBigUint64(0, BigInt(n), true);
825
+ return b;
826
+ }
827
+ function readU64le(buf2, offset) {
828
+ return new DataView(buf2.buffer, buf2.byteOffset, buf2.length).getBigUint64(
829
+ offset,
830
+ true
831
+ );
832
+ }
833
+ function concatBytes(...parts) {
834
+ const total = parts.reduce((n, p) => n + p.length, 0);
835
+ const out = new Uint8Array(total);
836
+ let off = 0;
837
+ for (const p of parts) {
838
+ out.set(p, off);
839
+ off += p.length;
840
+ }
841
+ return out;
842
+ }
843
+ function serializeInstruction(ix) {
844
+ const programId = new PublicKey(ix.programId).toBuffer();
845
+ const accounts = serializeAccounts(ix.accounts);
846
+ const data = serializeVecU8(ix.data);
847
+ return Buffer.concat([programId, accounts, data]);
848
+ }
849
+ function serializeAccounts(metas) {
850
+ const len = Buffer.alloc(4);
851
+ new DataView(len.buffer).setUint32(0, metas.length, true);
852
+ const parts = metas.map(serializeAccountMeta);
853
+ return Buffer.concat([len, ...parts]);
854
+ }
855
+ function serializeAccountMeta(meta) {
856
+ const pubkey = new PublicKey(meta.pubkey).toBuffer();
857
+ return Buffer.concat([pubkey, Buffer.from([meta.isSigner ? 1 : 0, meta.isWritable ? 1 : 0])]);
858
+ }
859
+ function serializeVecU8(data) {
860
+ const len = Buffer.alloc(4);
861
+ new DataView(len.buffer).setUint32(0, data.length, true);
862
+ return Buffer.concat([len, Buffer.from(data)]);
863
+ }
864
+ function serializeInstructions(instructions) {
865
+ return Buffer.concat(instructions.map(serializeInstruction));
866
+ }
867
+ var SolanaRelayer = class {
868
+ constructor(opts) {
869
+ this.opts = opts;
870
+ }
871
+ /** The relayer's fee-payer pubkey (fetched + cached from the backend). */
872
+ async getFeePayer() {
873
+ if (this.feePayer) return this.feePayer;
874
+ const res = await fetch(`${this.opts.baseUrl}/api/solana/relay?network=${this.opts.network}`);
875
+ if (!res.ok) throw new Error(`kit/solana: relayer fee-payer lookup failed (${res.status})`);
876
+ const { fee_payer } = await res.json();
877
+ this.feePayer = new PublicKey(fee_payer);
878
+ return this.feePayer;
879
+ }
880
+ /**
881
+ * Build a tx with the relayer as fee payer, serialize it unsigned, and POST it
882
+ * to the relayer to co-sign + submit. Returns the confirmed signature.
883
+ */
884
+ async send(instructions) {
885
+ const feePayer = await this.getFeePayer();
886
+ const { blockhash } = await this.opts.connection.getLatestBlockhash("confirmed");
887
+ const tx2 = new Transaction();
888
+ tx2.feePayer = feePayer;
889
+ tx2.recentBlockhash = blockhash;
890
+ tx2.add(...instructions);
891
+ const serialized = tx2.serialize({ requireAllSignatures: false, verifySignatures: false }).toString("base64");
892
+ const res = await fetch(`${this.opts.baseUrl}/api/solana/relay`, {
893
+ method: "POST",
894
+ headers: { "Content-Type": "application/json" },
895
+ body: JSON.stringify({
896
+ app_id: this.opts.appId,
897
+ network: this.opts.network,
898
+ transaction: serialized
899
+ })
900
+ });
901
+ if (!res.ok) {
902
+ const detail = await res.text().catch(() => "");
903
+ throw new Error(`kit/solana: relay failed (${res.status}) ${detail}`);
904
+ }
905
+ const { signature } = await res.json();
906
+ return signature;
907
+ }
908
+ };
909
+ var BACKUP_KDF_SALT = "cavos-recovery-v1";
910
+ var BACKUP_PBKDF2_ITERATIONS = 21e4;
911
+ var BACKUP_HKDF_INFO = "cavos-backup-signer";
912
+ var CODE_WORDS = 16;
913
+ function generateRecoveryCode() {
914
+ const bytes = randomBytes(CODE_WORDS);
915
+ const words = [];
916
+ for (const b of bytes) words.push(WORDLIST[b]);
917
+ return words.join(" ");
918
+ }
919
+ function deriveBackupKey(code) {
920
+ const normalised = code.trim().replace(/\s+/g, " ").toLowerCase();
921
+ if (!normalised) throw new Error("kit: recovery code is empty");
922
+ const stretched = pbkdf2(
923
+ sha256,
924
+ new TextEncoder().encode(normalised),
925
+ new TextEncoder().encode(BACKUP_KDF_SALT),
926
+ { c: BACKUP_PBKDF2_ITERATIONS, dkLen: 32 }
927
+ );
928
+ const seed = hkdf(sha256, stretched, void 0, BACKUP_HKDF_INFO, 32);
929
+ const d = bytesToBigInt(seed) % p256.CURVE.n;
930
+ if (d === 0n) throw new Error("kit: derived backup key is zero (retry with a new code)");
931
+ const priv = bigIntTo32Bytes(d);
932
+ const pub = p256.getPublicKey(priv, false);
933
+ return {
934
+ privateKey: priv,
935
+ publicKey: { x: bytesToBigInt(pub.subarray(1, 33)), y: bytesToBigInt(pub.subarray(33, 65)) }
936
+ };
937
+ }
938
+ var BackupSigner = class _BackupSigner {
939
+ constructor(privateKey, publicKey) {
940
+ this.privateKey = privateKey;
941
+ this.publicKeyValue = publicKey;
942
+ }
943
+ /** Build a signer from a recovery code (derive + wrap in one step). */
944
+ static fromCode(code) {
945
+ const { privateKey, publicKey } = deriveBackupKey(code);
946
+ return new _BackupSigner(privateKey, publicKey);
947
+ }
948
+ async getPublicKey() {
949
+ return this.publicKeyValue;
950
+ }
951
+ async sign(txHash) {
952
+ const digest = sha256(txHash);
953
+ const sig = p256.sign(digest, this.privateKey);
954
+ const yParity = recoverYParity(sig.r, sig.s, digest, this.publicKeyValue);
955
+ return { r: sig.r, s: sig.s, yParity };
956
+ }
957
+ };
958
+ var WORDLIST = [
959
+ "able",
960
+ "acid",
961
+ "amber",
962
+ "apple",
963
+ "arch",
964
+ "arrow",
965
+ "ashen",
966
+ "atlas",
967
+ "axis",
968
+ "badge",
969
+ "baker",
970
+ "balm",
971
+ "banner",
972
+ "basin",
973
+ "beacon",
974
+ "bench",
975
+ "beryl",
976
+ "birch",
977
+ "blade",
978
+ "bloom",
979
+ "bluer",
980
+ "border",
981
+ "brave",
982
+ "brick",
983
+ "brook",
984
+ "cabin",
985
+ "candle",
986
+ "carbon",
987
+ "cargo",
988
+ "cedar",
989
+ "chalk",
990
+ "charm",
991
+ "chrome",
992
+ "cipher",
993
+ "clam",
994
+ "clasp",
995
+ "cliff",
996
+ "clock",
997
+ "cobia",
998
+ "comet",
999
+ "coral",
1000
+ "cotton",
1001
+ "coves",
1002
+ "crane",
1003
+ "crest",
1004
+ "crow",
1005
+ "crystal",
1006
+ "curio",
1007
+ "dawn",
1008
+ "delta",
1009
+ "denim",
1010
+ "depth",
1011
+ "dewy",
1012
+ "digger",
1013
+ "docks",
1014
+ "dover",
1015
+ "drift",
1016
+ "dunes",
1017
+ "eagle",
1018
+ "ember",
1019
+ "echo",
1020
+ "eden",
1021
+ "elite",
1022
+ "ethic",
1023
+ "fable",
1024
+ "falcon",
1025
+ "fawn",
1026
+ "feather",
1027
+ "fern",
1028
+ "fjord",
1029
+ "flame",
1030
+ "flint",
1031
+ "forest",
1032
+ "forge",
1033
+ "frost",
1034
+ "garnet",
1035
+ "gemini",
1036
+ "glade",
1037
+ "glider",
1038
+ "glow",
1039
+ "granite",
1040
+ "grove",
1041
+ "guppy",
1042
+ "harbor",
1043
+ "haven",
1044
+ "hazel",
1045
+ "helio",
1046
+ "heron",
1047
+ "hickory",
1048
+ "honey",
1049
+ "horizon",
1050
+ "ivory",
1051
+ "jade",
1052
+ "jasper",
1053
+ "kestrel",
1054
+ "knot",
1055
+ "lagoon",
1056
+ "lattice",
1057
+ "laurel",
1058
+ "lavender",
1059
+ "lemon",
1060
+ "linden",
1061
+ "loon",
1062
+ "luger",
1063
+ "lumen",
1064
+ "lunar",
1065
+ "mango",
1066
+ "maple",
1067
+ "marble",
1068
+ "marsh",
1069
+ "meadow",
1070
+ "mercy",
1071
+ "mistle",
1072
+ "monsoon",
1073
+ "morning",
1074
+ "moss",
1075
+ "nacre",
1076
+ "nectar",
1077
+ "needle",
1078
+ "nimbus",
1079
+ "nova",
1080
+ "ocean",
1081
+ "onyx",
1082
+ "orbit",
1083
+ "otter",
1084
+ "palm",
1085
+ "panda",
1086
+ "pansy",
1087
+ "papaya",
1088
+ "passage",
1089
+ "pebble",
1090
+ "pelican",
1091
+ "pepper",
1092
+ "petal",
1093
+ "piano",
1094
+ "pierce",
1095
+ "pilot",
1096
+ "pioneer",
1097
+ "platinum",
1098
+ "plume",
1099
+ "poplar",
1100
+ "porpoise",
1101
+ "prairie",
1102
+ "prism",
1103
+ "pulsar",
1104
+ "quartz",
1105
+ "quasar",
1106
+ "quill",
1107
+ "quiver",
1108
+ "raven",
1109
+ "reef",
1110
+ "relic",
1111
+ "ridge",
1112
+ "ripple",
1113
+ "robin",
1114
+ "rocket",
1115
+ "rouge",
1116
+ "ruby",
1117
+ "saffron",
1118
+ "sage",
1119
+ "sail",
1120
+ "salmon",
1121
+ "sapphire",
1122
+ "scarab",
1123
+ "shadow",
1124
+ "shale",
1125
+ "sienna",
1126
+ "silica",
1127
+ "silver",
1128
+ "skyline",
1129
+ "slate",
1130
+ "sonar",
1131
+ "spruce",
1132
+ "starling",
1133
+ "stone",
1134
+ "sugar",
1135
+ "summit",
1136
+ "sunset",
1137
+ "swan",
1138
+ "tangent",
1139
+ "tarragon",
1140
+ "temple",
1141
+ "thistle",
1142
+ "thrush",
1143
+ "tiger",
1144
+ "topaz",
1145
+ "tundra",
1146
+ "turtle",
1147
+ "umber",
1148
+ "union",
1149
+ "valley",
1150
+ "vapor",
1151
+ "vector",
1152
+ "velvet",
1153
+ "violet",
1154
+ "vortex",
1155
+ "walnut",
1156
+ "whale",
1157
+ "winter",
1158
+ "wisp",
1159
+ "wisteria",
1160
+ "xenon",
1161
+ "yarrow",
1162
+ "zephyr",
1163
+ "zinc",
1164
+ "zodiac",
1165
+ "anchor",
1166
+ "basil",
1167
+ "cider",
1168
+ "daisy",
1169
+ "elfin",
1170
+ "ferry",
1171
+ "gimlet",
1172
+ "halcyon",
1173
+ "indigo",
1174
+ "juniper",
1175
+ "kindle",
1176
+ "lilac",
1177
+ "mantis",
1178
+ "nylon",
1179
+ "oracle",
1180
+ "parch",
1181
+ "quokka",
1182
+ "ramble",
1183
+ "thatch",
1184
+ "ultra",
1185
+ "vivid",
1186
+ "xylo",
1187
+ "yodel",
1188
+ "zesty",
1189
+ "arbor",
1190
+ "bliss",
1191
+ "calyx",
1192
+ "dwindle",
1193
+ "folio",
1194
+ "globe",
1195
+ "hymn",
1196
+ "ionic",
1197
+ "jolly",
1198
+ "knack",
1199
+ "lyric",
1200
+ "myrtle",
1201
+ "noble",
1202
+ "plumb",
1203
+ "quaint",
1204
+ "rustic",
1205
+ "satin",
1206
+ "timber",
1207
+ "urge",
1208
+ "vault",
1209
+ "whimsy",
1210
+ "yearn",
1211
+ "zenith",
1212
+ "ash",
1213
+ "beach",
1214
+ "dusk"
1215
+ ];
1216
+ function base64urlEncode(bytes) {
1217
+ let bin = "";
1218
+ for (const b of bytes) bin += String.fromCharCode(b);
1219
+ const b64 = typeof btoa !== "undefined" ? btoa(bin) : Buffer.from(bytes).toString("base64");
1220
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1221
+ }
1222
+ function derToRs(der) {
1223
+ let i = 0;
1224
+ if (der[i++] !== 48) throw new Error("kit/webauthn: bad DER (no SEQUENCE)");
1225
+ if (der[i] & 128) i += 1 + (der[i] & 127);
1226
+ else i += 1;
1227
+ if (der[i++] !== 2) throw new Error("kit/webauthn: bad DER (no r INTEGER)");
1228
+ const rlen = der[i++];
1229
+ const r = bytesToBigInt(der.subarray(i, i + rlen));
1230
+ i += rlen;
1231
+ if (der[i++] !== 2) throw new Error("kit/webauthn: bad DER (no s INTEGER)");
1232
+ const slen = der[i++];
1233
+ const s = bytesToBigInt(der.subarray(i, i + slen));
1234
+ return { r, s };
1235
+ }
1236
+ function spkiToPublicKey(spki) {
1237
+ const idx = spki.lastIndexOf(4, spki.length - 65);
1238
+ const start = spki.length - 65;
1239
+ const prefix = spki[start];
1240
+ if (prefix !== 4) {
1241
+ if (idx < 0) throw new Error("kit/webauthn: no uncompressed EC point in SPKI");
1242
+ return { x: bytesToBigInt(spki.subarray(idx + 1, idx + 33)), y: bytesToBigInt(spki.subarray(idx + 33, idx + 65)) };
1243
+ }
1244
+ return {
1245
+ x: bytesToBigInt(spki.subarray(start + 1, start + 33)),
1246
+ y: bytesToBigInt(spki.subarray(start + 33, start + 65))
1247
+ };
1248
+ }
1249
+ function batchChallenge(leaves) {
1250
+ const total = leaves.reduce((n, l) => n + l.length, 0);
1251
+ const cat = new Uint8Array(total);
1252
+ let o = 0;
1253
+ for (const l of leaves) {
1254
+ cat.set(l, o);
1255
+ o += l.length;
1256
+ }
1257
+ return sha256(cat);
1258
+ }
1259
+ function webauthnDigest(authenticatorData, clientDataJSON) {
1260
+ const clientHash = sha256(clientDataJSON);
1261
+ const msg = new Uint8Array(authenticatorData.length + clientHash.length);
1262
+ msg.set(authenticatorData, 0);
1263
+ msg.set(clientHash, authenticatorData.length);
1264
+ return sha256(msg);
1265
+ }
1266
+ function recoverCandidatePublicKeys(r, s, digest) {
1267
+ const out = [];
1268
+ for (const bit of [0, 1]) {
1269
+ try {
1270
+ const point = new p256.Signature(r, s).addRecoveryBit(bit).recoverPublicKey(digest).toAffine();
1271
+ out.push({ publicKey: { x: point.x, y: point.y }, yParity: bit === 1 });
1272
+ } catch {
1273
+ }
1274
+ }
1275
+ return out;
1276
+ }
1277
+ function lowS(s) {
1278
+ const n = p256.CURVE.n;
1279
+ return s > n / 2n ? n - s : s;
1280
+ }
1281
+ function challengeOffsetOf(clientDataJSON, challengeB64) {
1282
+ const text = new TextDecoder().decode(clientDataJSON);
1283
+ const idx = text.indexOf(challengeB64);
1284
+ if (idx < 0) throw new Error("kit/webauthn: challenge not found in clientDataJSON");
1285
+ return idx;
1286
+ }
1287
+ var CavosSolana = class _CavosSolana {
1288
+ constructor(identity, address, status, connection, adapter, devicePubkey, relayer, feePayer) {
1289
+ this.identity = identity;
1290
+ this.address = address;
1291
+ this.status = status;
1292
+ this.connection = connection;
1293
+ this.adapter = adapter;
1294
+ this.devicePubkey = devicePubkey;
1295
+ this.relayer = relayer;
1296
+ this.feePayer = feePayer;
1297
+ /** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
1298
+ this.chain = "solana";
1299
+ /** True when this connect just created a brand-new account (first sign-up). */
1300
+ this.isNewAccount = false;
1301
+ }
1302
+ get publicKey() {
1303
+ return this.devicePubkey;
1304
+ }
1305
+ static async connect(opts) {
1306
+ const identity = opts.identity ?? await opts.auth?.authenticate();
1307
+ if (!identity) throw new Error("kit/solana: connect requires `identity` or `auth`");
1308
+ if (opts.network === "solana-mainnet" && !opts.rpcUrl) {
1309
+ console.warn(
1310
+ "[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."
1311
+ );
1312
+ }
1313
+ const connection = new Connection(opts.rpcUrl ?? SOLANA_NETWORKS[opts.network], "confirmed");
1314
+ const signer = opts.createSigner ? await opts.createSigner(`${identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${identity.userId}:${opts.appSalt}` });
1315
+ const devicePubkey = await signer.getPublicKey();
1316
+ const adapter = new SolanaAdapter({ programId: opts.programId, connection, signer });
1317
+ const addressSeed = deriveAddressSeedSolana({ userId: identity.userId, appSalt: opts.appSalt });
1318
+ const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
1319
+ const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry);
1320
+ const relayer = opts.relayer ?? (opts.appId ? new SolanaRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network, connection }) : void 0);
1321
+ const existing = await registry.lookup(identity.userId);
1322
+ if (existing) {
1323
+ const isSigner2 = await adapter.isAuthorizedSigner(existing.address, devicePubkey);
1324
+ return new _CavosSolana(
1325
+ identity,
1326
+ existing.address,
1327
+ isSigner2 ? "ready" : "needs-device-approval",
1328
+ connection,
1329
+ adapter,
1330
+ devicePubkey,
1331
+ relayer,
1332
+ opts.feePayer
1333
+ );
1334
+ }
1335
+ const address = adapter.computeAddress(addressSeed, devicePubkey);
1336
+ const deployed = await connection.getAccountInfo(new PublicKey(address)) !== null;
1337
+ if (!deployed) {
1338
+ if (relayer) {
1339
+ const payer = await relayer.getFeePayer();
1340
+ const ix = adapter.buildInitialize(addressSeed, payer.toBase58(), devicePubkey);
1341
+ await relayer.send([ix]);
1342
+ } else if (opts.feePayer) {
1343
+ const ix = adapter.buildInitialize(addressSeed, opts.feePayer.publicKey.toBase58(), devicePubkey);
1344
+ await sendAndConfirmTransaction(connection, new Transaction().add(ix), [opts.feePayer]);
1345
+ } else {
1346
+ throw new Error("kit/solana: a relayer (appId) or feePayer is required to initialize a new account");
1347
+ }
1348
+ }
1349
+ await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
1350
+ const isSigner = await adapter.isAuthorizedSigner(address, devicePubkey);
1351
+ const wallet = new _CavosSolana(
1352
+ identity,
1353
+ address,
1354
+ isSigner ? "ready" : "needs-device-approval",
1355
+ connection,
1356
+ adapter,
1357
+ devicePubkey,
1358
+ relayer,
1359
+ opts.feePayer
1360
+ );
1361
+ wallet.isNewAccount = !deployed && isSigner;
1362
+ return wallet;
1363
+ }
1364
+ /** Authorize an additional device signer (device-signed via precompile). */
1365
+ async addSigner(pubkey) {
1366
+ const ixs = await this.adapter.buildAddSigner(this.address, pubkey);
1367
+ return this.send(ixs);
1368
+ }
1369
+ /**
1370
+ * Enroll a passkey as an approver (2FA-style step-up). Device-signed + gasless;
1371
+ * requires a ready device. Idempotent. Returns the passkey pubkey + tx hash.
1372
+ */
1373
+ async enrollPasskey(passkey, params) {
1374
+ const enrolled = await passkey.enroll(params);
1375
+ const { transactionHash } = await this.addApprover(enrolled.publicKey);
1376
+ return { publicKey: enrolled.publicKey, transactionHash };
1377
+ }
1378
+ /** Register an already-enrolled passkey pubkey as an approver (gasless).
1379
+ * Idempotent. Lets one passkey be registered across chains without re-prompting. */
1380
+ async addApprover(pubkey) {
1381
+ if (this.status !== "ready") {
1382
+ throw new Error("kit/solana: addApprover requires a ready, authorized device");
1383
+ }
1384
+ if (await this.adapter.isApprover(this.address, pubkey)) return {};
1385
+ const ixs = await this.adapter.buildAddApprover(this.address, pubkey);
1386
+ const transactionHash = await this.send(ixs);
1387
+ return { transactionHash };
1388
+ }
1389
+ /** True if this account already has a passkey enrolled as an approver, so a
1390
+ * new device can be approved with the passkey instead of the email flow. */
1391
+ async hasPasskey() {
1392
+ return this.adapter.hasPasskeyApprover(this.address);
1393
+ }
1394
+ /** Re-read (from chain) whether THIS device is now an authorized signer.
1395
+ * Used to poll for readiness after a passkey approval before it's indexed. */
1396
+ async isReady() {
1397
+ return this.adapter.isAuthorizedSigner(this.address, this.devicePubkey);
1398
+ }
1399
+ /**
1400
+ * From a fresh browser (status `needs-device-approval`), approve adding THIS
1401
+ * device with the user's synced passkey. Gasless via the relayer — the bundle
1402
+ * carries the passkey's WebAuthn assertion, so no device signature is needed.
1403
+ */
1404
+ async approveThisDeviceWithPasskey(passkey) {
1405
+ if (this.status === "ready") {
1406
+ throw new Error("kit/solana: this device is already an authorized signer");
1407
+ }
1408
+ const { leaf, nonce } = await this.passkeyLeafForThisDevice();
1409
+ const leaves = [leaf];
1410
+ const assertion = await passkey.assert(batchChallenge(leaves));
1411
+ const { transactionHash } = await this.submitPasskeyApproval(assertion, leaves, 0, nonce);
1412
+ return transactionHash;
1413
+ }
1414
+ /** This device's leaf + passkey nonce for a (possibly multi-chain) batch. */
1415
+ async passkeyLeafForThisDevice() {
1416
+ const nonce = await this.adapter.passkeyNonce(this.address);
1417
+ return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
1418
+ }
1419
+ /** Submit `add_signer_via_passkey` given a shared assertion + batch position.
1420
+ * Used by `approveThisDeviceWithPasskey` and `approveDeviceEverywhere`. */
1421
+ async submitPasskeyApproval(assertion, leaves, leafIndex, _nonce) {
1422
+ const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
1423
+ const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
1424
+ let approver = null;
1425
+ for (const cand of candidates) {
1426
+ if (await this.adapter.isApprover(this.address, cand.publicKey)) {
1427
+ approver = cand.publicKey;
1428
+ break;
1429
+ }
1430
+ }
1431
+ if (!approver) throw new Error("kit/solana: this passkey is not a registered approver");
1432
+ const ixs = this.adapter.buildAddSignerViaPasskey(
1433
+ this.address,
1434
+ this.devicePubkey,
1435
+ approver,
1436
+ leaves,
1437
+ leafIndex,
1438
+ assertion
1439
+ );
1440
+ return { transactionHash: await this.send(ixs) };
1441
+ }
1442
+ /** Move `amount` lamports out of the account to `destination` (device-signed). */
1443
+ async execute(amount, destination) {
1444
+ if (this.status !== "ready") {
1445
+ throw new Error("kit/solana: this device is not yet an authorized signer of the wallet");
1446
+ }
1447
+ const ixs = await this.adapter.buildExecuteTransfer(this.address, destination, amount);
1448
+ return this.send(ixs);
1449
+ }
1450
+ /**
1451
+ * Run arbitrary CPI `instructions` with the account PDA as signer (device-
1452
+ * signed). The signature commits to sha256 of the canonical Borsh
1453
+ * serialization of the instructions, so it binds exactly the operations the
1454
+ * program will invoke. Unlocks SPL transfers, swaps, staking, etc.
1455
+ *
1456
+ * What the relayer will sponsor is constrained by the app's Solana program
1457
+ * allowlist (configured in the dashboard) — programs outside the allowlist are
1458
+ * rejected before co-signing.
1459
+ */
1460
+ async executeInstructions(instructions) {
1461
+ if (this.status !== "ready") {
1462
+ throw new Error("kit/solana: this device is not yet an authorized signer of the wallet");
1463
+ }
1464
+ const ixs = await this.adapter.buildExecute(this.address, instructions);
1465
+ return this.send(ixs);
1466
+ }
1467
+ /**
1468
+ * Register the backup signer derived from `code` as an authorized signer of this
1469
+ * account (device-signed via precompile). Idempotent: returns without a tx if
1470
+ * the backup signer is already registered. The code never leaves the device —
1471
+ * only the derived public key travels on-chain.
1472
+ *
1473
+ * Self-custodial: anyone who can re-derive the backup key from the code (i.e.
1474
+ * the rightful owner) can later recover the account with `CavosSolana.recover`.
1475
+ * Run this once, on a registered device, and have the user store the code.
1476
+ */
1477
+ async setupRecovery(code) {
1478
+ if (this.status !== "ready") {
1479
+ throw new Error("kit/solana: setupRecovery requires a ready, registered device");
1480
+ }
1481
+ const { publicKey: backupPubkey } = deriveBackupKey(code);
1482
+ const already = await this.adapter.isAuthorizedSigner(this.address, backupPubkey);
1483
+ if (already) return void 0;
1484
+ return this.addSigner(backupPubkey);
1485
+ }
1486
+ /**
1487
+ * Recover an account after losing every device signer. Derives the backup key
1488
+ * from `code`, uses it (not the new device key) to sign an `add_signer` for the
1489
+ * new device, and returns a ready CavosSolana bound to the new device. The
1490
+ * account address is unchanged.
1491
+ *
1492
+ * Self-custodial: only someone holding the code (i.e. the rightful owner) can
1493
+ * re-derive the backup key. The backend never sees the code.
1494
+ *
1495
+ * This mirrors `Cavos.recover` (Starknet): the backup key is just another
1496
+ * authorized signer, so recovery is an `add_signer(newDevice)` bundle signed by
1497
+ * the backup key. The on-chain program needs no recovery-specific entrypoint.
1498
+ */
1499
+ static async recover(opts) {
1500
+ if (opts.network === "solana-mainnet" && !opts.rpcUrl) {
1501
+ console.warn(
1502
+ "[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."
1503
+ );
1504
+ }
1505
+ const connection = new Connection(opts.rpcUrl ?? SOLANA_NETWORKS[opts.network], "confirmed");
1506
+ const signer = opts.createSigner ? await opts.createSigner(`${opts.identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${opts.identity.userId}:${opts.appSalt}` });
1507
+ const devicePubkey = await signer.getPublicKey();
1508
+ const backup = BackupSigner.fromCode(opts.code);
1509
+ const backupAdapter = new SolanaAdapter({
1510
+ programId: opts.programId,
1511
+ connection,
1512
+ signer: backup
1513
+ });
1514
+ const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
1515
+ const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry);
1516
+ const existing = await registry.lookup(opts.identity.userId);
1517
+ if (!existing) {
1518
+ throw new Error("kit/solana: no account found for this identity \u2014 nothing to recover");
1519
+ }
1520
+ const relayer = opts.relayer ?? (opts.appId ? new SolanaRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network, connection }) : void 0);
1521
+ const alreadyAuthed = await backupAdapter.isAuthorizedSigner(existing.address, devicePubkey);
1522
+ if (!alreadyAuthed) {
1523
+ const ixs = await backupAdapter.buildAddSigner(existing.address, devicePubkey);
1524
+ if (relayer) {
1525
+ await relayer.send(ixs);
1526
+ } else if (opts.feePayer) {
1527
+ await sendAndConfirmTransaction(connection, new Transaction().add(...ixs), [opts.feePayer]);
1528
+ } else {
1529
+ throw new Error("kit/solana: a relayer (appId) or feePayer is required to recover");
1530
+ }
1531
+ }
1532
+ const adapter = new SolanaAdapter({ programId: opts.programId, connection, signer });
1533
+ return new _CavosSolana(
1534
+ opts.identity,
1535
+ existing.address,
1536
+ "ready",
1537
+ connection,
1538
+ adapter,
1539
+ devicePubkey,
1540
+ relayer,
1541
+ opts.feePayer
1542
+ );
1543
+ }
1544
+ async send(ixs) {
1545
+ if (this.relayer) return this.relayer.send(ixs);
1546
+ if (this.feePayer) {
1547
+ return sendAndConfirmTransaction(this.connection, new Transaction().add(...ixs), [this.feePayer]);
1548
+ }
1549
+ throw new Error("kit/solana: no relayer or feePayer configured to submit transactions");
1550
+ }
1551
+ };
1552
+ var defaultRegistry = new InMemoryWalletRegistry();
1553
+
1554
+ // src/chains/stellar/constants.ts
1555
+ var FACTORY_CONTRACT_ID = {
1556
+ // Re-deployed 2026-07-01 with the passkey-approval device-account wasm (batched
1557
+ // multi-chain challenge). The factory pins the wasm hash immutably, so a new
1558
+ // wasm needs a new factory → new account addresses; testnet has no prod wallets.
1559
+ "stellar-testnet": "CBCJIODXIEBOXXD66KCUCF7ZDYJARKI4ZIVQOVWPULOBH5XGNCDP6W3I",
1560
+ // Set once the factory is deployed to mainnet (its address differs — network id
1561
+ // is part of contract-address derivation).
1562
+ "stellar-mainnet": ""
1563
+ };
1564
+ var DEVICE_ACCOUNT_WASM_HASH = {
1565
+ "stellar-testnet": "2671b085578e59a385ef5a5664e42f0450322fe3249539f588e1263ed5a31dce",
1566
+ "stellar-mainnet": ""
1567
+ };
1568
+ var STELLAR_NETWORKS = {
1569
+ "stellar-testnet": {
1570
+ rpcUrl: "https://soroban-testnet.stellar.org",
1571
+ passphrase: "Test SDF Network ; September 2015"
1572
+ },
1573
+ "stellar-mainnet": {
1574
+ rpcUrl: "https://soroban-rpc.mainnet.stellar.gateway.fm",
1575
+ passphrase: "Public Global Stellar Network ; September 2015"
1576
+ }
1577
+ };
1578
+ var NATIVE_SAC_ID = {
1579
+ "stellar-testnet": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
1580
+ "stellar-mainnet": "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA"
1581
+ };
1582
+ var SECP256R1_N2 = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
1583
+ var StellarAdapter = class {
1584
+ constructor(opts) {
1585
+ this.chain = "stellar";
1586
+ this.network = opts.network;
1587
+ this.passphrase = STELLAR_NETWORKS[opts.network].passphrase;
1588
+ this.rpcUrl = opts.rpcUrl ?? STELLAR_NETWORKS[opts.network].rpcUrl;
1589
+ this.factoryId = opts.factoryId ?? FACTORY_CONTRACT_ID[opts.network];
1590
+ if (!this.factoryId) {
1591
+ throw new Error(`kit/stellar: no factory contract id configured for ${opts.network}`);
1592
+ }
1593
+ this.signer = opts.signer;
1594
+ }
1595
+ server() {
1596
+ if (!this._server) {
1597
+ this._server = new rpc.Server(this.rpcUrl, {
1598
+ allowHttp: this.rpcUrl.startsWith("http://")
1599
+ });
1600
+ }
1601
+ return this._server;
1602
+ }
1603
+ networkId() {
1604
+ return hash$1(Buffer.from(this.passphrase));
1605
+ }
1606
+ /**
1607
+ * Deterministic account address for `(addressSeed, initialSigner)` — computed
1608
+ * off-chain, byte-identical to the factory's on-chain `account_address`.
1609
+ * `contractId = sha256(HashIdPreimage(networkId, factory, salt))` with
1610
+ * `salt = sha256(addressSeed || sec1(initialSigner))`.
1611
+ */
1612
+ computeAddress(addressSeed, initialSigner) {
1613
+ const salt = this.accountSalt(addressSeed, initialSigner);
1614
+ const preimage = xdr.HashIdPreimage.envelopeTypeContractId(
1615
+ new xdr.HashIdPreimageContractId({
1616
+ networkId: this.networkId(),
1617
+ contractIdPreimage: xdr.ContractIdPreimage.contractIdPreimageFromAddress(
1618
+ new xdr.ContractIdPreimageFromAddress({
1619
+ address: new Address(this.factoryId).toScAddress(),
1620
+ salt
1621
+ })
1622
+ )
1623
+ })
1624
+ );
1625
+ return StrKey.encodeContract(hash$1(preimage.toXDR()));
1626
+ }
1627
+ /** `salt = sha256(addressSeed(32) || sec1(initialSigner)(65))` — matches the factory. */
1628
+ accountSalt(addressSeed, initialSigner) {
1629
+ return hash$1(Buffer.concat([Buffer.from(addressSeed), Buffer.from(sec1Pubkey(initialSigner))]));
1630
+ }
1631
+ /** Host function: `factory.deploy(address_seed, initial_signer)`. */
1632
+ buildDeploy(addressSeed, initialSigner) {
1633
+ return invokeFunc(this.factoryId, "deploy", [
1634
+ bytesScVal(addressSeed),
1635
+ bytesScVal(sec1Pubkey(initialSigner))
1636
+ ]);
1637
+ }
1638
+ /** Host function: `account.add_signer(new_signer)` (requires device auth). */
1639
+ buildAddSigner(accountAddress, signer) {
1640
+ return invokeFunc(accountAddress, "add_signer", [bytesScVal(sec1Pubkey(signer))]);
1641
+ }
1642
+ /** Host function: `account.remove_signer(signer)` (requires device auth). */
1643
+ buildRemoveSigner(accountAddress, signer) {
1644
+ return invokeFunc(accountAddress, "remove_signer", [bytesScVal(sec1Pubkey(signer))]);
1645
+ }
1646
+ /** Host function: `account.add_approver(passkey)` (requires device auth). */
1647
+ buildAddApprover(accountAddress, passkey) {
1648
+ return invokeFunc(accountAddress, "add_approver", [bytesScVal(sec1Pubkey(passkey))]);
1649
+ }
1650
+ /** Host function: `account.remove_approver(passkey)` (requires device auth). */
1651
+ buildRemoveApprover(accountAddress, passkey) {
1652
+ return invokeFunc(accountAddress, "remove_approver", [bytesScVal(sec1Pubkey(passkey))]);
1653
+ }
1654
+ /** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
1655
+ * `sha256(sec1(new_signer) || nonce_be8)`. The batch challenge the passkey signs
1656
+ * is `sha256(concat(leaves))` across chains. */
1657
+ passkeyLeaf(newSigner, nonce) {
1658
+ const msg = new Uint8Array(65 + 8);
1659
+ msg.set(sec1Pubkey(newSigner), 0);
1660
+ const n = new Uint8Array(8);
1661
+ let v = nonce;
1662
+ for (let i = 7; i >= 0; i--) {
1663
+ n[i] = Number(v & 0xffn);
1664
+ v >>= 8n;
1665
+ }
1666
+ msg.set(n, 65);
1667
+ return sha256(msg);
1668
+ }
1669
+ /** Host function: passkey-authorized `add_signer_via_passkey` (no device auth —
1670
+ * authorized by the embedded WebAuthn assertion, so any relayer can submit).
1671
+ * `leaves`/`leafIndex` place this chain's leaf in the multi-chain batch. */
1672
+ buildAddSignerViaPasskey(accountAddress, newSigner, passkey, nonce, leaves, leafIndex, assertion) {
1673
+ const sig = encodeLowSSignature2({ r: assertion.r, s: assertion.s});
1674
+ const leavesScVal = xdr.ScVal.scvVec(leaves.map((l) => bytesScVal(l)));
1675
+ return invokeFunc(accountAddress, "add_signer_via_passkey", [
1676
+ bytesScVal(sec1Pubkey(newSigner)),
1677
+ bytesScVal(sec1Pubkey(passkey)),
1678
+ nativeToScVal(nonce, { type: "u64" }),
1679
+ leavesScVal,
1680
+ nativeToScVal(leafIndex, { type: "u32" }),
1681
+ bytesScVal(assertion.authenticatorData),
1682
+ bytesScVal(assertion.clientDataJSON),
1683
+ nativeToScVal(assertion.challengeOffset, { type: "u32" }),
1684
+ bytesScVal(sig)
1685
+ ]);
1686
+ }
1687
+ /** Read whether `passkey` is a registered approver (read-only simulation). */
1688
+ /** True if the account has at least one passkey registered as an approver.
1689
+ * Reads the contract's `approvers` view and checks the list is non-empty. */
1690
+ async hasPasskeyApprover(accountAddress, readSource) {
1691
+ if (!await this.isDeployed(accountAddress)) return false;
1692
+ const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1693
+ const src = new Account3(readSource, "0");
1694
+ const op = Operation.invokeHostFunction({
1695
+ func: invokeFunc(accountAddress, "approvers", []),
1696
+ auth: []
1697
+ });
1698
+ const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
1699
+ const sim = await this.server().simulateTransaction(tx2);
1700
+ if (rpc.Api.isSimulationError(sim)) {
1701
+ throw new Error(`kit/stellar: approvers simulation failed: ${sim.error}`);
1702
+ }
1703
+ if (!sim.result?.retval) return false;
1704
+ const list = scValToNative(sim.result.retval);
1705
+ return Array.isArray(list) && list.length > 0;
1706
+ }
1707
+ async isApprover(accountAddress, passkey, readSource) {
1708
+ if (!await this.isDeployed(accountAddress)) return false;
1709
+ const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1710
+ const src = new Account3(readSource, "0");
1711
+ const op = Operation.invokeHostFunction({
1712
+ func: invokeFunc(accountAddress, "is_approver", [bytesScVal(sec1Pubkey(passkey))]),
1713
+ auth: []
1714
+ });
1715
+ const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
1716
+ const sim = await this.server().simulateTransaction(tx2);
1717
+ if (rpc.Api.isSimulationError(sim)) {
1718
+ throw new Error(`kit/stellar: is_approver simulation failed: ${sim.error}`);
1719
+ }
1720
+ if (!sim.result?.retval) return false;
1721
+ return scValToNative(sim.result.retval) === true;
1722
+ }
1723
+ /** Read the current passkey-approval nonce (read-only simulation). */
1724
+ async passkeyNonce(accountAddress, readSource) {
1725
+ if (!await this.isDeployed(accountAddress)) return 0n;
1726
+ const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1727
+ const src = new Account3(readSource, "0");
1728
+ const op = Operation.invokeHostFunction({
1729
+ func: invokeFunc(accountAddress, "passkey_nonce", []),
1730
+ auth: []
1731
+ });
1732
+ const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
1733
+ const sim = await this.server().simulateTransaction(tx2);
1734
+ if (rpc.Api.isSimulationError(sim)) {
1735
+ throw new Error(`kit/stellar: passkey_nonce simulation failed: ${sim.error}`);
1736
+ }
1737
+ if (!sim.result?.retval) return 0n;
1738
+ return BigInt(scValToNative(sim.result.retval));
1739
+ }
1740
+ /** Host function: SEP-41 `token.transfer(from=account, to, amount)` (device auth). */
1741
+ buildTransfer(tokenId, accountAddress, destination, amount) {
1742
+ return invokeFunc(tokenId, "transfer", [
1743
+ new Address(accountAddress).toScVal(),
1744
+ new Address(destination).toScVal(),
1745
+ nativeToScVal(amount, { type: "i128" })
1746
+ ]);
1747
+ }
1748
+ /**
1749
+ * Sign a Soroban authorization entry with the silent device key, producing the
1750
+ * `Vec<DeviceSignature>` the account's `__check_auth` verifies. The device
1751
+ * signs `sha256(preimage)` (WebCrypto hashes once more internally), which is
1752
+ * exactly what the contract recomputes. Mutates + returns the entry.
1753
+ */
1754
+ async signAuthEntry(entry, validUntilLedger) {
1755
+ const addrCreds = entry.credentials().address();
1756
+ addrCreds.signatureExpirationLedger(validUntilLedger);
1757
+ const preimage = xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
1758
+ new xdr.HashIdPreimageSorobanAuthorization({
1759
+ networkId: this.networkId(),
1760
+ nonce: addrCreds.nonce(),
1761
+ signatureExpirationLedger: validUntilLedger,
1762
+ invocation: entry.rootInvocation()
1763
+ })
1764
+ );
1765
+ const payload = hash$1(preimage.toXDR());
1766
+ const sig = await this.signer.sign(new Uint8Array(payload));
1767
+ const pubkey = await this.signer.getPublicKey();
1768
+ addrCreds.signature(deviceSignatureScVal(pubkey, sig));
1769
+ return entry;
1770
+ }
1771
+ /**
1772
+ * Read a SEP-41 token balance of `account` via a read-only simulation of
1773
+ * `token.balance(account)`. Returns 0 when the account isn't deployed or holds
1774
+ * none. `readSource` is any funded G-account (used only for the simulation).
1775
+ */
1776
+ async readBalance(tokenId, account, readSource) {
1777
+ if (!await this.isDeployed(account)) return 0n;
1778
+ const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1779
+ const src = new Account3(readSource, "0");
1780
+ const op = Operation.invokeHostFunction({
1781
+ func: invokeFunc(tokenId, "balance", [new Address(account).toScVal()]),
1782
+ auth: []
1783
+ });
1784
+ const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
1785
+ const sim = await this.server().simulateTransaction(tx2);
1786
+ if (rpc.Api.isSimulationError(sim) || !sim.result?.retval) return 0n;
1787
+ return BigInt(scValToNative(sim.result.retval));
1788
+ }
1789
+ /** Whether the account contract instance exists on-chain (is deployed). */
1790
+ async isDeployed(accountAddress) {
1791
+ try {
1792
+ const res = await this.server().getContractData(
1793
+ accountAddress,
1794
+ xdr.ScVal.scvLedgerKeyContractInstance(),
1795
+ rpc.Durability.Persistent
1796
+ );
1797
+ return !!res;
1798
+ } catch {
1799
+ return false;
1800
+ }
1801
+ }
1802
+ /**
1803
+ * Read whether `signer` is a currently-authorized signer of the account, via a
1804
+ * read-only simulation of `account.is_authorized(signer)`. `readSource` is any
1805
+ * funded G-account (used only for the simulation's source/sequence).
1806
+ */
1807
+ async isAuthorizedSigner(accountAddress, signer, readSource) {
1808
+ if (!await this.isDeployed(accountAddress)) return false;
1809
+ const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1810
+ const src = new Account3(readSource, "0");
1811
+ const op = Operation.invokeHostFunction({
1812
+ func: invokeFunc(accountAddress, "is_authorized", [bytesScVal(sec1Pubkey(signer))]),
1813
+ auth: []
1814
+ });
1815
+ const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
1816
+ const sim = await this.server().simulateTransaction(tx2);
1817
+ if (rpc.Api.isSimulationError(sim)) {
1818
+ throw new Error(`kit/stellar: is_authorized simulation failed: ${sim.error}`);
1819
+ }
1820
+ if (!sim.result?.retval) return false;
1821
+ return scValToNative(sim.result.retval) === true;
1822
+ }
1823
+ };
1824
+ function sec1Pubkey(pk) {
1825
+ const out = new Uint8Array(65);
1826
+ out[0] = 4;
1827
+ out.set(bigIntTo32Bytes(pk.x), 1);
1828
+ out.set(bigIntTo32Bytes(pk.y), 33);
1829
+ return out;
1830
+ }
1831
+ function encodeLowSSignature2(sig) {
1832
+ const lowS2 = sig.s > SECP256R1_N2 / 2n ? SECP256R1_N2 - sig.s : sig.s;
1833
+ const out = new Uint8Array(64);
1834
+ out.set(bigIntTo32Bytes(sig.r), 0);
1835
+ out.set(bigIntTo32Bytes(lowS2), 32);
1836
+ return out;
1837
+ }
1838
+ function deviceSignatureScVal(pubkey, sig) {
1839
+ const element = nativeToScVal(
1840
+ {
1841
+ public_key: Buffer.from(sec1Pubkey(pubkey)),
1842
+ signature: Buffer.from(encodeLowSSignature2(sig))
1843
+ },
1844
+ { type: { public_key: ["symbol", "bytes"], signature: ["symbol", "bytes"] } }
1845
+ );
1846
+ return xdr.ScVal.scvVec([element]);
1847
+ }
1848
+ function invokeFunc(contractId, method, args) {
1849
+ return xdr.HostFunction.hostFunctionTypeInvokeContract(
1850
+ new xdr.InvokeContractArgs({
1851
+ contractAddress: new Address(contractId).toScAddress(),
1852
+ functionName: method,
1853
+ args
1854
+ })
1855
+ );
1856
+ }
1857
+ function bytesScVal(bytes) {
1858
+ return xdr.ScVal.scvBytes(Buffer.from(bytes));
1859
+ }
1860
+
1861
+ // src/chains/stellar/StellarRelayer.ts
1862
+ var StellarRelayer = class {
1863
+ constructor(opts) {
1864
+ this.opts = opts;
1865
+ }
1866
+ /** The relayer's source/fee-payer G-account (fetched + cached from the backend). */
1867
+ async getSource() {
1868
+ if (this.source) return this.source;
1869
+ const res = await fetch(`${this.opts.baseUrl}/api/stellar/relay?network=${this.opts.network}`);
1870
+ if (!res.ok) throw new Error(`kit/stellar: relayer source lookup failed (${res.status})`);
1871
+ const { fee_payer } = await res.json();
1872
+ this.source = fee_payer;
1873
+ return this.source;
1874
+ }
1875
+ /**
1876
+ * POST the assembled, device-authorized transaction XDR to the relayer to sign
1877
+ * the envelope + submit. Returns the confirmed transaction hash.
1878
+ */
1879
+ async submit(transactionXdr) {
1880
+ const res = await fetch(`${this.opts.baseUrl}/api/stellar/relay`, {
1881
+ method: "POST",
1882
+ headers: { "Content-Type": "application/json" },
1883
+ body: JSON.stringify({
1884
+ app_id: this.opts.appId,
1885
+ network: this.opts.network,
1886
+ transaction: transactionXdr
1887
+ })
1888
+ });
1889
+ if (!res.ok) {
1890
+ const detail = await res.text().catch(() => "");
1891
+ throw new Error(`kit/stellar: relay failed (${res.status}) ${detail}`);
1892
+ }
1893
+ const { hash: hash6 } = await res.json();
1894
+ return hash6;
1895
+ }
1896
+ };
1897
+ var CavosStellar = class _CavosStellar {
1898
+ constructor(identity, address, status, network, adapter, devicePubkey, relayer, sourceKeypair) {
1899
+ this.identity = identity;
1900
+ this.address = address;
1901
+ this.status = status;
1902
+ this.network = network;
1903
+ this.adapter = adapter;
1904
+ this.devicePubkey = devicePubkey;
1905
+ this.relayer = relayer;
1906
+ this.sourceKeypair = sourceKeypair;
1907
+ /** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
1908
+ this.chain = "stellar";
1909
+ /** True when this connect just created a brand-new account (first sign-up). */
1910
+ this.isNewAccount = false;
1911
+ }
1912
+ get publicKey() {
1913
+ return this.devicePubkey;
1914
+ }
1915
+ static async connect(opts) {
1916
+ const identity = opts.identity ?? await opts.auth?.authenticate();
1917
+ if (!identity) throw new Error("kit/stellar: connect requires `identity` or `auth`");
1918
+ const signer = opts.createSigner ? await opts.createSigner(`${identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${identity.userId}:${opts.appSalt}` });
1919
+ const devicePubkey = await signer.getPublicKey();
1920
+ const adapter = new StellarAdapter({
1921
+ network: opts.network,
1922
+ rpcUrl: opts.rpcUrl,
1923
+ factoryId: opts.factoryId,
1924
+ signer
1925
+ });
1926
+ const addressSeed = deriveAddressSeedStellar({ userId: identity.userId, appSalt: opts.appSalt });
1927
+ const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
1928
+ const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
1929
+ const relayer = opts.relayer ?? (opts.appId ? new StellarRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : void 0);
1930
+ const build = (address2, status) => new _CavosStellar(identity, address2, status, opts.network, adapter, devicePubkey, relayer, opts.sourceKeypair);
1931
+ const self = build("", "needs-device-approval");
1932
+ const readSource = await self.resolveSource();
1933
+ const existing = await registry.lookup(identity.userId);
1934
+ if (existing) {
1935
+ const isSigner2 = await adapter.isAuthorizedSigner(existing.address, devicePubkey, readSource);
1936
+ return build(existing.address, isSigner2 ? "ready" : "needs-device-approval");
1937
+ }
1938
+ const address = adapter.computeAddress(addressSeed, devicePubkey);
1939
+ const wasDeployed = await adapter.isDeployed(address);
1940
+ if (!wasDeployed) {
1941
+ const func = adapter.buildDeploy(addressSeed, devicePubkey);
1942
+ await self.submitHostFunction(func, void 0);
1943
+ }
1944
+ await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
1945
+ const isSigner = await adapter.isAuthorizedSigner(address, devicePubkey, readSource);
1946
+ const wallet = build(address, isSigner ? "ready" : "needs-device-approval");
1947
+ wallet.isNewAccount = !wasDeployed && isSigner;
1948
+ return wallet;
1949
+ }
1950
+ /** Authorize an additional device signer (device-signed via `__check_auth`). */
1951
+ async addSigner(pubkey) {
1952
+ const func = this.adapter.buildAddSigner(this.address, pubkey);
1953
+ return this.submitHostFunction(func, this.address);
1954
+ }
1955
+ /**
1956
+ * Enroll a passkey as an approver (2FA-style step-up). Device-signed + gasless;
1957
+ * requires a ready device. Idempotent. Returns the passkey pubkey + tx hash.
1958
+ */
1959
+ async enrollPasskey(passkey, params) {
1960
+ const enrolled = await passkey.enroll(params);
1961
+ const { transactionHash } = await this.addApprover(enrolled.publicKey);
1962
+ return { publicKey: enrolled.publicKey, transactionHash };
1963
+ }
1964
+ /** Register an already-enrolled passkey pubkey as an approver (gasless).
1965
+ * Idempotent. Lets one passkey be registered across chains without re-prompting. */
1966
+ async addApprover(pubkey) {
1967
+ if (this.status !== "ready") {
1968
+ throw new Error("kit/stellar: addApprover requires a ready, authorized device");
1969
+ }
1970
+ const readSource = await this.resolveSource();
1971
+ if (await this.adapter.isApprover(this.address, pubkey, readSource)) return {};
1972
+ const func = this.adapter.buildAddApprover(this.address, pubkey);
1973
+ const transactionHash = await this.submitHostFunction(func, this.address);
1974
+ return { transactionHash };
1975
+ }
1976
+ /** True if this account already has a passkey enrolled as an approver, so a
1977
+ * new device can be approved with the passkey instead of the email flow. */
1978
+ async hasPasskey() {
1979
+ const readSource = await this.resolveSource();
1980
+ return this.adapter.hasPasskeyApprover(this.address, readSource);
1981
+ }
1982
+ /** Re-read (from chain) whether THIS device is now an authorized signer.
1983
+ * Used to poll for readiness after a passkey approval before it's indexed. */
1984
+ async isReady() {
1985
+ const readSource = await this.resolveSource();
1986
+ return this.adapter.isAuthorizedSigner(this.address, this.devicePubkey, readSource);
1987
+ }
1988
+ /**
1989
+ * From a fresh browser (status `needs-device-approval`), approve adding THIS
1990
+ * device using the user's synced passkey. Gasless via the relayer — the call
1991
+ * carries the WebAuthn assertion, so no device signature is needed. Returns the
1992
+ * tx hash. No trip back to an already-authorized device.
1993
+ */
1994
+ async approveThisDeviceWithPasskey(passkey) {
1995
+ if (this.status === "ready") {
1996
+ throw new Error("kit/stellar: this device is already an authorized signer");
1997
+ }
1998
+ const { leaf, nonce } = await this.passkeyLeafForThisDevice();
1999
+ const leaves = [leaf];
2000
+ const assertion = await passkey.assert(batchChallenge(leaves));
2001
+ const { transactionHash } = await this.submitPasskeyApproval(assertion, leaves, 0, nonce);
2002
+ return transactionHash;
2003
+ }
2004
+ /** This device's leaf + passkey nonce for a (possibly multi-chain) batch. */
2005
+ async passkeyLeafForThisDevice() {
2006
+ const readSource = await this.resolveSource();
2007
+ const nonce = await this.adapter.passkeyNonce(this.address, readSource);
2008
+ return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
2009
+ }
2010
+ /** Submit `add_signer_via_passkey` given a shared assertion + batch position.
2011
+ * No device auth entry — authorized purely by the passkey assertion. */
2012
+ async submitPasskeyApproval(assertion, leaves, leafIndex, nonce) {
2013
+ const readSource = await this.resolveSource();
2014
+ const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
2015
+ const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
2016
+ let approver = null;
2017
+ for (const cand of candidates) {
2018
+ if (await this.adapter.isApprover(this.address, cand.publicKey, readSource)) {
2019
+ approver = cand.publicKey;
2020
+ break;
2021
+ }
2022
+ }
2023
+ if (!approver) throw new Error("kit/stellar: this passkey is not a registered approver");
2024
+ const func = this.adapter.buildAddSignerViaPasskey(
2025
+ this.address,
2026
+ this.devicePubkey,
2027
+ approver,
2028
+ nonce,
2029
+ leaves,
2030
+ leafIndex,
2031
+ assertion
2032
+ );
2033
+ return { transactionHash: await this.submitHostFunction(func, void 0) };
2034
+ }
2035
+ /** Move `amount` stroops of native XLM to `destination` (device-signed). */
2036
+ async execute(amount, destination) {
2037
+ return this.executeTransfer(NATIVE_SAC_ID[this.network], amount, destination);
2038
+ }
2039
+ /** Read this account's balance of `tokenId` (defaults to native XLM), in stroops. */
2040
+ async balance(tokenId = NATIVE_SAC_ID[this.network]) {
2041
+ const readSource = await this.resolveSource();
2042
+ return this.adapter.readBalance(tokenId, this.address, readSource);
2043
+ }
2044
+ /** Transfer `amount` of any SEP-41 token out of the account (device-signed). */
2045
+ async executeTransfer(tokenId, amount, destination) {
2046
+ if (this.status !== "ready") {
2047
+ throw new Error("kit/stellar: this device is not yet an authorized signer of the wallet");
2048
+ }
2049
+ const func = this.adapter.buildTransfer(tokenId, this.address, destination, amount);
2050
+ return this.submitHostFunction(func, this.address);
2051
+ }
2052
+ /**
2053
+ * Register the backup signer derived from `code` as an authorized signer of
2054
+ * this account (device-signed). Idempotent. The code never leaves the device —
2055
+ * only the derived public key travels on-chain. Mirrors the other chains.
2056
+ */
2057
+ async setupRecovery(code) {
2058
+ if (this.status !== "ready") {
2059
+ throw new Error("kit/stellar: setupRecovery requires a ready, registered device");
2060
+ }
2061
+ const { publicKey: backupPubkey } = deriveBackupKey(code);
2062
+ const readSource = await this.resolveSource();
2063
+ if (await this.adapter.isAuthorizedSigner(this.address, backupPubkey, readSource)) return void 0;
2064
+ return this.addSigner(backupPubkey);
2065
+ }
2066
+ /**
2067
+ * Recover an account after losing every device signer: derive the backup key
2068
+ * from `code`, use it (not the new device) to authorize `add_signer(newDevice)`,
2069
+ * and return a ready handle bound to the new device. The address is unchanged.
2070
+ */
2071
+ static async recover(opts) {
2072
+ const signer = opts.createSigner ? await opts.createSigner(`${opts.identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${opts.identity.userId}:${opts.appSalt}` });
2073
+ const devicePubkey = await signer.getPublicKey();
2074
+ const backup = BackupSigner.fromCode(opts.code);
2075
+ const backupAdapter = new StellarAdapter({
2076
+ network: opts.network,
2077
+ rpcUrl: opts.rpcUrl,
2078
+ factoryId: opts.factoryId,
2079
+ signer: backup
2080
+ });
2081
+ const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
2082
+ const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
2083
+ const existing = await registry.lookup(opts.identity.userId);
2084
+ if (!existing) {
2085
+ throw new Error("kit/stellar: no account found for this identity \u2014 nothing to recover");
2086
+ }
2087
+ const relayer = opts.relayer ?? (opts.appId ? new StellarRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : void 0);
2088
+ const backupHandle = new _CavosStellar(
2089
+ opts.identity,
2090
+ existing.address,
2091
+ "ready",
2092
+ opts.network,
2093
+ backupAdapter,
2094
+ devicePubkey,
2095
+ relayer,
2096
+ opts.sourceKeypair
2097
+ );
2098
+ const readSource = await backupHandle.resolveSource();
2099
+ if (!await backupAdapter.isAuthorizedSigner(existing.address, devicePubkey, readSource)) {
2100
+ await backupHandle.addSigner(devicePubkey);
2101
+ }
2102
+ const adapter = new StellarAdapter({
2103
+ network: opts.network,
2104
+ rpcUrl: opts.rpcUrl,
2105
+ factoryId: opts.factoryId,
2106
+ signer
2107
+ });
2108
+ return new _CavosStellar(
2109
+ opts.identity,
2110
+ existing.address,
2111
+ "ready",
2112
+ opts.network,
2113
+ adapter,
2114
+ devicePubkey,
2115
+ relayer,
2116
+ opts.sourceKeypair
2117
+ );
2118
+ }
2119
+ /** The transaction source/fee-payer G-address (relayer or self-funded). */
2120
+ async resolveSource() {
2121
+ if (this.relayer) return this.relayer.getSource();
2122
+ if (this.sourceKeypair) return this.sourceKeypair.publicKey();
2123
+ throw new Error("kit/stellar: a relayer (appId) or sourceKeypair is required");
2124
+ }
2125
+ /**
2126
+ * Build → simulate → device-sign auth → assemble → submit an invoke-contract
2127
+ * host function. `authAccount` is the account whose `__check_auth` must sign the
2128
+ * operation's Soroban auth entry (undefined for a plain factory deploy).
2129
+ */
2130
+ async submitHostFunction(func, authAccount) {
2131
+ const server = this.adapter.server();
2132
+ const sourceAddr = await this.resolveSource();
2133
+ const simSource = new Account(sourceAddr, "0");
2134
+ const unsignedOp = Operation.invokeHostFunction({ func, auth: [] });
2135
+ const simTx = new TransactionBuilder(simSource, {
2136
+ fee: BASE_FEE,
2137
+ networkPassphrase: this.adapter.passphrase
2138
+ }).addOperation(unsignedOp).setTimeout(180).build();
2139
+ const sim = await server.simulateTransaction(simTx);
2140
+ if (rpc.Api.isSimulationError(sim)) {
2141
+ throw new Error(`kit/stellar: simulation failed: ${sim.error}`);
2142
+ }
2143
+ const validUntil = (await server.getLatestLedger()).sequence + 100;
2144
+ const entries = sim.result?.auth ?? [];
2145
+ const signedAuth = [];
2146
+ for (const entry of entries) {
2147
+ if (authAccount && isAddressCredentialFor(entry, authAccount)) {
2148
+ signedAuth.push(await this.adapter.signAuthEntry(entry, validUntil));
2149
+ } else {
2150
+ signedAuth.push(entry);
2151
+ }
2152
+ }
2153
+ const account = await server.getAccount(sourceAddr);
2154
+ const finalOp = Operation.invokeHostFunction({ func, auth: signedAuth });
2155
+ const built = new TransactionBuilder(account, {
2156
+ fee: BASE_FEE,
2157
+ networkPassphrase: this.adapter.passphrase
2158
+ }).addOperation(finalOp).setTimeout(180).build();
2159
+ const authSim = await server.simulateTransaction(built);
2160
+ if (rpc.Api.isSimulationError(authSim)) {
2161
+ throw new Error(`kit/stellar: auth simulation failed: ${authSim.error}`);
2162
+ }
2163
+ const assembled = rpc.assembleTransaction(built, authSim).build();
2164
+ if (this.relayer) {
2165
+ return this.relayer.submit(assembled.toXDR());
2166
+ }
2167
+ if (this.sourceKeypair) {
2168
+ assembled.sign(this.sourceKeypair);
2169
+ return this.sendAndConfirm(assembled);
2170
+ }
2171
+ throw new Error("kit/stellar: no relayer or sourceKeypair configured to submit");
2172
+ }
2173
+ /** Submit a signed tx via RPC and poll to confirmation. Returns the hash. */
2174
+ async sendAndConfirm(tx2) {
2175
+ const server = this.adapter.server();
2176
+ const sent = await server.sendTransaction(tx2);
2177
+ if (sent.status === "ERROR") {
2178
+ throw new Error(`kit/stellar: submit rejected: ${JSON.stringify(sent.errorResult)}`);
2179
+ }
2180
+ const hash6 = sent.hash;
2181
+ for (let i = 0; i < 30; i++) {
2182
+ const got = await server.getTransaction(hash6);
2183
+ if (got.status === rpc.Api.GetTransactionStatus.SUCCESS) return hash6;
2184
+ if (got.status === rpc.Api.GetTransactionStatus.FAILED) {
2185
+ throw new Error(`kit/stellar: tx ${hash6} failed`);
2186
+ }
2187
+ await new Promise((r) => setTimeout(r, 1e3));
2188
+ }
2189
+ throw new Error(`kit/stellar: tx ${hash6} not confirmed in time`);
2190
+ }
2191
+ };
2192
+ function isAddressCredentialFor(entry, accountAddress) {
2193
+ const creds = entry.credentials();
2194
+ if (creds.switch() !== xdr.SorobanCredentialsType.sorobanCredentialsAddress()) return false;
2195
+ return Address.fromScAddress(creds.address().address()).toString() === accountAddress;
2196
+ }
2197
+ var defaultRegistry2 = new InMemoryWalletRegistry();
2198
+
2199
+ // src/recovery/HttpRecoveryClient.ts
2200
+ function toHex2(n) {
2201
+ return "0x" + n.toString(16);
2202
+ }
2203
+ function fromHex2(s) {
2204
+ return BigInt(s);
2205
+ }
2206
+ function deviceLabel() {
2207
+ if (typeof navigator !== "undefined") {
2208
+ return navigator.userAgent || "a new device";
2209
+ }
2210
+ return "a new device";
2211
+ }
2212
+ var HttpRecoveryClient = class {
2213
+ constructor(opts) {
2214
+ this.opts = opts;
2215
+ }
2216
+ async requestDeviceAddition(params) {
2217
+ const res = await fetch(new URL("/api/devices/request", this.opts.baseUrl), {
2218
+ method: "POST",
2219
+ headers: { "Content-Type": "application/json" },
2220
+ body: JSON.stringify({
2221
+ app_id: this.opts.appId,
2222
+ wallet_address: params.accountAddress,
2223
+ new_pub_x: toHex2(params.newSigner.x),
2224
+ new_pub_y: toHex2(params.newSigner.y),
2225
+ device_label: params.deviceLabel ?? deviceLabel(),
2226
+ ...params.email ? { email: params.email } : {}
2227
+ })
2228
+ });
2229
+ if (!res.ok) {
2230
+ const t = await res.text().catch(() => "");
2231
+ throw new Error(`requestDeviceAddition failed: ${res.status} ${t}`);
2232
+ }
2233
+ const data = await res.json();
2234
+ return { requestId: data.request_id };
2235
+ }
2236
+ async getPendingRequest(requestId) {
2237
+ const url = new URL("/api/devices/request", this.opts.baseUrl);
2238
+ url.searchParams.set("id", requestId);
2239
+ const res = await fetch(url, { headers: { "Content-Type": "application/json" } });
2240
+ if (!res.ok) throw new Error(`getPendingRequest failed: ${res.status}`);
2241
+ const data = await res.json();
2242
+ if (!data.found) return null;
2243
+ const status = data.status;
2244
+ return {
2245
+ requestId: data.request_id,
2246
+ appId: data.app_id,
2247
+ userId: "",
2248
+ // the approving device already knows its own identity
2249
+ accountAddress: data.wallet_address,
2250
+ newSigner: { x: fromHex2(data.new_pub_x), y: fromHex2(data.new_pub_y) },
2251
+ createdAt: data.created_at,
2252
+ status
2253
+ };
2254
+ }
2255
+ async confirmDeviceAddition(params) {
2256
+ const res = await fetch(
2257
+ new URL(`/api/devices/request/${params.requestId}/confirm`, this.opts.baseUrl),
2258
+ {
2259
+ method: "POST",
2260
+ headers: { "Content-Type": "application/json" },
2261
+ body: JSON.stringify({ tx_hash: params.txHash })
2262
+ }
2263
+ );
2264
+ if (!res.ok) {
2265
+ const t = await res.text().catch(() => "");
2266
+ throw new Error(`confirmDeviceAddition failed: ${res.status} ${t}`);
2267
+ }
2268
+ }
2269
+ };
2270
+ var STARKNET_ENV = {
2271
+ mainnet: "mainnet",
2272
+ testnet: "sepolia"
2273
+ };
2274
+ var SOLANA_ENV = {
2275
+ mainnet: "solana-mainnet",
2276
+ testnet: "solana-devnet"
2277
+ };
2278
+ var STELLAR_ENV = {
2279
+ mainnet: "stellar-mainnet",
2280
+ testnet: "stellar-testnet"
2281
+ };
2282
+ var Cavos = class _Cavos {
2283
+ constructor(identity, address, status, account, adapter, devicePubkey, paymaster) {
2284
+ this.identity = identity;
2285
+ this.address = address;
2286
+ this.status = status;
2287
+ this.account = account;
2288
+ this.adapter = adapter;
2289
+ this.devicePubkey = devicePubkey;
2290
+ this.paymaster = paymaster;
2291
+ /** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
2292
+ this.chain = "starknet";
2293
+ /** Request id of the pending device-addition, when status is needs-device-approval. */
2294
+ this.pendingRequestId = null;
2295
+ /** True when this connect just created & deployed a brand-new account (first
2296
+ * sign-up), so the UI can offer a one-time "secure your account" step. */
2297
+ this.isNewAccount = false;
2298
+ }
2299
+ /**
2300
+ * Unified entry point. Pick a `chain` and an `network` environment; the kit
2301
+ * resolves the concrete network (sepolia/devnet for testnet, mainnet for
2302
+ * mainnet) and returns a chain-native wallet. The result is a discriminated
2303
+ * union (`wallet.chain`), so `execute()` keeps each chain's native signature:
2304
+ *
2305
+ * const wallet = await Cavos.connect({ chain: "solana", network: "testnet", identity, appSalt, appId });
2306
+ * if (wallet.chain === "starknet") await wallet.execute(calls);
2307
+ * else await wallet.execute(amount, dest);
2308
+ */
2309
+ static async connect(opts) {
2310
+ if (opts.chain === "solana") {
2311
+ return CavosSolana.connect({
2312
+ network: SOLANA_ENV[opts.network],
2313
+ ...opts.auth ? { auth: opts.auth } : {},
2314
+ ...opts.identity ? { identity: opts.identity } : {},
2315
+ appSalt: opts.appSalt,
2316
+ ...opts.appId ? { appId: opts.appId } : {},
2317
+ ...opts.backendUrl ? { backendUrl: opts.backendUrl } : {},
2318
+ ...opts.registry ? { registry: opts.registry } : {},
2319
+ ...opts.rpcUrl ? { rpcUrl: opts.rpcUrl } : {},
2320
+ ...opts.programId ? { programId: opts.programId } : {},
2321
+ ...opts.createSigner ? { createSigner: opts.createSigner } : {},
2322
+ ...opts.relayer ? { relayer: opts.relayer } : {},
2323
+ ...opts.feePayer ? { feePayer: opts.feePayer } : {}
2324
+ });
2325
+ }
2326
+ if (opts.chain === "stellar") {
2327
+ return CavosStellar.connect({
2328
+ network: STELLAR_ENV[opts.network],
2329
+ ...opts.auth ? { auth: opts.auth } : {},
2330
+ ...opts.identity ? { identity: opts.identity } : {},
2331
+ appSalt: opts.appSalt,
2332
+ ...opts.appId ? { appId: opts.appId } : {},
2333
+ ...opts.backendUrl ? { backendUrl: opts.backendUrl } : {},
2334
+ ...opts.registry ? { registry: opts.registry } : {},
2335
+ ...opts.rpcUrl ? { rpcUrl: opts.rpcUrl } : {},
2336
+ ...opts.factoryId ? { factoryId: opts.factoryId } : {},
2337
+ ...opts.createSigner ? { createSigner: opts.createSigner } : {},
2338
+ ...opts.stellarRelayer ? { relayer: opts.stellarRelayer } : {},
2339
+ ...opts.stellarSourceKeypair ? { sourceKeypair: opts.stellarSourceKeypair } : {}
2340
+ });
2341
+ }
2342
+ if (!opts.paymasterApiKey) {
2343
+ throw new Error("kit: `paymasterApiKey` is required for Starknet connections");
2344
+ }
2345
+ return _Cavos.connectStarknet({
2346
+ network: STARKNET_ENV[opts.network],
2347
+ auth: opts.auth,
2348
+ identity: opts.identity,
2349
+ appSalt: opts.appSalt,
2350
+ appId: opts.appId,
2351
+ backendUrl: opts.backendUrl,
2352
+ registry: opts.registry,
2353
+ recovery: opts.recovery,
2354
+ paymasterApiKey: opts.paymasterApiKey,
2355
+ paymasterUrl: opts.paymasterUrl,
2356
+ rpcUrl: opts.rpcUrl,
2357
+ classHash: opts.classHash,
2358
+ createSigner: opts.createSigner
2359
+ });
2360
+ }
2361
+ static async connectStarknet(opts) {
2362
+ const identity = opts.identity ?? await opts.auth?.authenticate();
2363
+ if (!identity) throw new Error("kit: connect requires `identity` or `auth`");
2364
+ const classHash = opts.classHash ?? DEVICE_ACCOUNT_CLASS_HASH[opts.network];
2365
+ if (!classHash) throw new Error(`kit: no DeviceAccount class hash for ${opts.network}`);
2366
+ const provider = new RpcProvider({
2367
+ nodeUrl: opts.rpcUrl ?? STARKNET_NETWORKS[opts.network].rpcUrl
2368
+ });
2369
+ const paymasterUrl = opts.paymasterUrl ?? CAVOS_PAYMASTER_URL[opts.network];
2370
+ const paymasterConfig = { url: paymasterUrl, apiKey: opts.paymasterApiKey };
2371
+ const paymaster = new PaymasterRpc({
2372
+ nodeUrl: paymasterUrl,
2373
+ headers: { "x-paymaster-api-key": opts.paymasterApiKey }
2374
+ });
2375
+ const addressSeed = deriveAddressSeed({ userId: identity.userId, appSalt: opts.appSalt });
2376
+ const signer = opts.createSigner ? await opts.createSigner(`${identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${identity.userId}:${opts.appSalt}` });
2377
+ const devicePubkey = await signer.getPublicKey();
2378
+ const adapter = new StarknetAdapter({ classHash, signer, provider });
2379
+ const makeAccount = (address2) => new Account$1({
2380
+ provider,
2381
+ address: address2,
2382
+ signer: new StarknetDeviceSigner(signer),
2383
+ paymaster,
2384
+ cairoVersion: "1"
2385
+ });
2386
+ const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
2387
+ const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry3);
2388
+ const recovery = opts.recovery ?? (opts.appId ? new HttpRecoveryClient({ baseUrl: backendUrl, appId: opts.appId }) : null);
2389
+ const existing = await registry.lookup(identity.userId);
2390
+ if (existing) {
2391
+ const account2 = makeAccount(existing.address);
2392
+ const isSigner2 = await adapter.isAuthorizedSigner(existing.address, devicePubkey);
2393
+ const cavos2 = new _Cavos(
2394
+ identity,
2395
+ existing.address,
2396
+ isSigner2 ? "ready" : "needs-device-approval",
2397
+ account2,
2398
+ adapter,
2399
+ devicePubkey,
2400
+ paymasterConfig
2401
+ );
2402
+ if (!isSigner2 && recovery) {
2403
+ const dedup = lastDeviceRequest.get(identity.userId);
2404
+ const fresh = dedup && Date.now() - dedup.requestedAt < DEVICE_REQUEST_DEDUP_MS;
2405
+ try {
2406
+ if (fresh) {
2407
+ cavos2.pendingRequestId = dedup.requestId;
2408
+ } else {
2409
+ const { requestId } = await recovery.requestDeviceAddition({
2410
+ userId: identity.userId,
2411
+ accountAddress: existing.address,
2412
+ newSigner: devicePubkey,
2413
+ ...identity.email ? { email: identity.email } : {}
2414
+ });
2415
+ cavos2.pendingRequestId = requestId;
2416
+ lastDeviceRequest.set(identity.userId, { requestId, requestedAt: Date.now() });
2417
+ }
2418
+ } catch (e) {
2419
+ console.warn("[Cavos] requestDeviceAddition failed:", e);
2420
+ }
2421
+ }
2422
+ return cavos2;
2423
+ }
2424
+ const address = adapter.computeAddress({ addressSeed, initialSigner: devicePubkey });
2425
+ const account = makeAccount(address);
2426
+ const alreadyDeployed = await isDeployed(provider, address);
2427
+ if (!alreadyDeployed) {
2428
+ const deploymentData = {
2429
+ address,
2430
+ class_hash: classHash,
2431
+ salt: num.toHex(addressSeed),
2432
+ calldata: adapter.constructorCalldata(addressSeed, devicePubkey),
2433
+ version: 1
2434
+ };
2435
+ const deployRes = await account.executePaymasterTransaction([], {
2436
+ feeMode: { mode: "sponsored" },
2437
+ deploymentData
2438
+ });
2439
+ try {
2440
+ await provider.waitForTransaction(deployRes.transaction_hash);
2441
+ } catch (e) {
2442
+ console.warn("[Cavos] deploy receipt wait failed:", e);
2443
+ }
2444
+ }
2445
+ await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
2446
+ let isSigner;
2447
+ try {
2448
+ isSigner = await adapter.isAuthorizedSigner(address, devicePubkey);
2449
+ } catch (e) {
2450
+ console.warn("[Cavos] isAuthorizedSigner read failed:", e);
2451
+ isSigner = !alreadyDeployed;
2452
+ }
2453
+ const cavos = new _Cavos(
2454
+ identity,
2455
+ address,
2456
+ isSigner ? "ready" : "needs-device-approval",
2457
+ account,
2458
+ adapter,
2459
+ devicePubkey,
2460
+ paymasterConfig
2461
+ );
2462
+ cavos.isNewAccount = !alreadyDeployed && isSigner;
2463
+ return cavos;
2464
+ }
2465
+ /** This device's public key (e.g. to request addition to an existing wallet). */
2466
+ get publicKey() {
2467
+ return this.devicePubkey;
2468
+ }
2469
+ /** Execute a sponsored (gasless) multicall, signed silently by the device. */
2470
+ async execute(calls) {
2471
+ if (this.status !== "ready") {
2472
+ throw new Error("kit: this device is not yet an authorized signer of the wallet");
2473
+ }
2474
+ const res = await this.account.executePaymasterTransaction(calls, {
2475
+ feeMode: { mode: "sponsored" }
2476
+ });
2477
+ return { transactionHash: res.transaction_hash };
2478
+ }
2479
+ /** Authorize an additional device signer (sponsored). Self-submitted. */
2480
+ async addSigner(pubkey) {
2481
+ return this.execute([this.adapter.buildAddSigner(this.address, pubkey)]);
2482
+ }
2483
+ /**
2484
+ * Enroll a passkey as an APPROVER so the user can later add devices from any
2485
+ * browser (2FA-style step-up). Requires a ready device (the enrollment call is
2486
+ * device-signed and gasless). Idempotent: a no-op if the passkey is already an
2487
+ * approver. Call this whenever the app decides to prompt "turn on device
2488
+ * approvals". Returns the passkey's public key + the enrollment tx hash.
2489
+ */
2490
+ async enrollPasskey(passkey, params) {
2491
+ const enrolled = await passkey.enroll(params);
2492
+ const { transactionHash } = await this.addApprover(enrolled.publicKey);
2493
+ return { publicKey: enrolled.publicKey, transactionHash };
2494
+ }
2495
+ /**
2496
+ * Register an ALREADY-enrolled passkey public key as an approver (gasless,
2497
+ * device-signed). Idempotent. Use this to register ONE passkey across multiple
2498
+ * chains without re-prompting `passkey.enroll()` on each: enroll once, then
2499
+ * call `addApprover(pubkey)` on each chain's wallet.
2500
+ */
2501
+ async addApprover(pubkey) {
2502
+ if (this.status !== "ready") {
2503
+ throw new Error("kit: addApprover requires a ready, authorized device");
2504
+ }
2505
+ if (await this.adapter.isApprover(this.address, pubkey)) return {};
2506
+ const { transactionHash } = await this.execute([
2507
+ this.adapter.buildAddApprover(this.address, pubkey)
2508
+ ]);
2509
+ try {
2510
+ await this.account.waitForTransaction(transactionHash);
2511
+ } catch (e) {
2512
+ console.warn("[Cavos] add_approver receipt wait failed:", e);
2513
+ }
2514
+ return { transactionHash };
2515
+ }
2516
+ /** True if this account already has a passkey enrolled as an approver, so a
2517
+ * new device can be approved with the passkey instead of the email flow. */
2518
+ async hasPasskey() {
2519
+ return this.adapter.hasPasskeyApprover(this.address);
2520
+ }
2521
+ /** Re-read (from chain) whether THIS device is now an authorized signer.
2522
+ * Cheap and side-effect free — used to poll for readiness after a passkey /
2523
+ * device approval submits, before the new signer is indexed. */
2524
+ async isReady() {
2525
+ return this.adapter.isAuthorizedSigner(this.address, this.devicePubkey);
2526
+ }
2527
+ /**
2528
+ * From a brand-new browser (status `needs-device-approval`), use the user's
2529
+ * synced passkey to authorize adding THIS device — no trip back to an already-
2530
+ * authorized device.
2531
+ *
2532
+ * `add_signer_via_passkey` is a public external authorized by the embedded
2533
+ * WebAuthn assertion (no device signature), so by default we sponsor it through
2534
+ * the Cavos paymaster's `paymaster_executeDirectTransaction` (the forwarder's
2535
+ * `execute_sponsored` runs a generic call — it does NOT require SNIP-9). Pass a
2536
+ * custom `submit` to route it through your own relayer instead. Returns the tx.
2537
+ */
2538
+ async approveThisDeviceWithPasskey(opts) {
2539
+ if (this.status === "ready") {
2540
+ throw new Error("kit: this device is already an authorized signer");
2541
+ }
2542
+ const { leaf, nonce } = await this.passkeyLeafForThisDevice();
2543
+ const leaves = [leaf];
2544
+ const assertion = await opts.passkey.assert(batchChallenge(leaves));
2545
+ return this.submitPasskeyApproval(assertion, leaves, 0, nonce, opts.submit);
2546
+ }
2547
+ /** This device's leaf + the current passkey nonce, for a (possibly multi-chain)
2548
+ * passkey approval batch. See `approveDeviceEverywhere`. */
2549
+ async passkeyLeafForThisDevice() {
2550
+ const nonce = await this.adapter.getPasskeyNonce(this.address);
2551
+ return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
2552
+ }
2553
+ /** Submit `add_signer_via_passkey` given a (shared) assertion + this chain's
2554
+ * position in the batch. The assertion doesn't carry the passkey pubkey, so we
2555
+ * recover both candidates and pick the enrolled approver via the on-chain view
2556
+ * (no backend). Defaults to sponsoring through the paymaster. */
2557
+ async submitPasskeyApproval(assertion, leaves, leafIndex, nonce, submit) {
2558
+ const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
2559
+ const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
2560
+ let yParity = null;
2561
+ for (const cand of candidates) {
2562
+ if (await this.adapter.isApprover(this.address, cand.publicKey)) {
2563
+ yParity = cand.yParity;
2564
+ break;
2565
+ }
2566
+ }
2567
+ if (yParity === null) {
2568
+ throw new Error("kit: this passkey is not a registered approver of the wallet");
2569
+ }
2570
+ const call = this.adapter.buildAddSignerViaPasskey(
2571
+ this.address,
2572
+ this.devicePubkey,
2573
+ nonce,
2574
+ leaves,
2575
+ leafIndex,
2576
+ assertion,
2577
+ yParity
2578
+ );
2579
+ if (submit) return submit(call);
2580
+ if (!this.paymaster) {
2581
+ throw new Error("kit: no paymaster configured \u2014 pass a `submit` relayer to approveThisDeviceWithPasskey");
2582
+ }
2583
+ return paymasterExecuteDirect(this.paymaster, this.address, call);
2584
+ }
2585
+ /**
2586
+ * Register a self-custodial backup signer derived from `code`, so the account
2587
+ * can be recovered after the user loses every device. Idempotent: if the
2588
+ * derived backup key is already an authorised signer, this is a no-op.
2589
+ *
2590
+ * The code never leaves the device — only its deterministic public key is
2591
+ * added on-chain as an ordinary signer. Sponsor this like any other
2592
+ * add_signer (gasless). Returns the transaction hash (or undefined when the
2593
+ * backup was already set up).
2594
+ */
2595
+ async setupRecovery(code) {
2596
+ const { publicKey: backupPubkey } = deriveBackupKey(code);
2597
+ const already = await this.adapter.isAuthorizedSigner(this.address, backupPubkey);
2598
+ if (already) return void 0;
2599
+ return this.addSigner(backupPubkey);
2600
+ }
2601
+ /**
2602
+ * Recover an account after losing every device signer. Derives the backup key
2603
+ * from `code`, uses it (not the new device key) to sign an `add_signer` for
2604
+ * the new device, and returns a ready Cavos bound to the new device. The
2605
+ * account address is unchanged.
2606
+ *
2607
+ * Self-custodial: only someone holding the code (i.e. the rightful owner) can
2608
+ * re-derive the backup key. The backend never sees the code.
2609
+ */
2610
+ static async recover(opts) {
2611
+ const network = STARKNET_ENV[opts.network];
2612
+ const classHash = opts.classHash ?? DEVICE_ACCOUNT_CLASS_HASH[network];
2613
+ if (!classHash) throw new Error(`kit: no DeviceAccount class hash for ${network}`);
2614
+ const provider = new RpcProvider({
2615
+ nodeUrl: opts.rpcUrl ?? STARKNET_NETWORKS[network].rpcUrl
2616
+ });
2617
+ const paymaster = new PaymasterRpc({
2618
+ nodeUrl: opts.paymasterUrl ?? CAVOS_PAYMASTER_URL[network],
2619
+ headers: { "x-paymaster-api-key": opts.paymasterApiKey }
2620
+ });
2621
+ const signer = opts.createSigner ? await opts.createSigner(`${opts.identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${opts.identity.userId}:${opts.appSalt}` });
2622
+ const devicePubkey = await signer.getPublicKey();
2623
+ const backup = BackupSigner.fromCode(opts.code);
2624
+ const backupAdapter = new StarknetAdapter({ classHash, signer: backup, provider });
2625
+ const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
2626
+ const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network }) : defaultRegistry3);
2627
+ const existing = await registry.lookup(opts.identity.userId);
2628
+ if (!existing) {
2629
+ throw new Error("kit: no account found for this identity \u2014 nothing to recover");
2630
+ }
2631
+ const backupAccount = new Account$1({
2632
+ provider,
2633
+ address: existing.address,
2634
+ signer: new StarknetDeviceSigner(backup),
2635
+ paymaster,
2636
+ cairoVersion: "1"
2637
+ });
2638
+ const alreadyAuthed = await backupAdapter.isAuthorizedSigner(existing.address, devicePubkey);
2639
+ if (!alreadyAuthed) {
2640
+ const res = await backupAccount.executePaymasterTransaction(
2641
+ [backupAdapter.buildAddSigner(existing.address, devicePubkey)],
2642
+ { feeMode: { mode: "sponsored" } }
2643
+ );
2644
+ try {
2645
+ await provider.waitForTransaction(res.transaction_hash);
2646
+ } catch (e) {
2647
+ console.warn("[Cavos] recovery add_signer receipt wait failed:", e);
2648
+ }
2649
+ }
2650
+ const adapter = new StarknetAdapter({ classHash, signer, provider });
2651
+ const account = new Account$1({
2652
+ provider,
2653
+ address: existing.address,
2654
+ signer: new StarknetDeviceSigner(signer),
2655
+ paymaster,
2656
+ cairoVersion: "1"
2657
+ });
2658
+ return new _Cavos(opts.identity, existing.address, "ready", account, adapter, devicePubkey);
2659
+ }
2660
+ };
2661
+ var defaultRegistry3 = new InMemoryWalletRegistry();
2662
+ var DEVICE_REQUEST_DEDUP_MS = 5 * 60 * 1e3;
2663
+ var lastDeviceRequest = /* @__PURE__ */ new Map();
2664
+ async function isDeployed(provider, address) {
2665
+ try {
2666
+ const classHash = await provider.getClassHashAt(address);
2667
+ return !!classHash && classHash !== "0x0";
2668
+ } catch {
2669
+ return false;
2670
+ }
2671
+ }
2672
+ async function approveDeviceEverywhere(wallets, passkey) {
2673
+ const targets = wallets.filter((w) => w.status === "needs-device-approval");
2674
+ if (targets.length === 0) return [];
2675
+ const infos = await Promise.all(targets.map((w) => w.passkeyLeafForThisDevice()));
2676
+ const leaves = infos.map((i) => i.leaf);
2677
+ const assertion = await passkey.assert(batchChallenge(leaves));
2678
+ const settled = await Promise.allSettled(
2679
+ targets.map((w, i) => w.submitPasskeyApproval(assertion, leaves, i, infos[i].nonce))
2680
+ );
2681
+ return settled.map(
2682
+ (r, i) => r.status === "fulfilled" ? { chain: targets[i].chain, transactionHash: r.value.transactionHash } : {
2683
+ chain: targets[i].chain,
2684
+ error: r.reason instanceof Error ? r.reason.message : String(r.reason)
2685
+ }
2686
+ );
2687
+ }
2688
+ async function paymasterExecuteDirect(paymaster, userAddress, call) {
2689
+ const body = {
2690
+ jsonrpc: "2.0",
2691
+ id: 1,
2692
+ method: "paymaster_executeDirectTransaction",
2693
+ params: {
2694
+ transaction: {
2695
+ type: "invoke",
2696
+ invoke: {
2697
+ user_address: userAddress,
2698
+ execute_from_outside_call: {
2699
+ to: call.contractAddress,
2700
+ selector: hash.getSelectorFromName(call.entrypoint),
2701
+ calldata: call.calldata.map((c) => num.toHex(c))
2702
+ }
2703
+ }
2704
+ },
2705
+ parameters: { version: "0x1", fee_mode: { mode: "sponsored" } }
2706
+ }
2707
+ };
2708
+ const res = await fetch(paymaster.url, {
2709
+ method: "POST",
2710
+ headers: {
2711
+ "Content-Type": "application/json",
2712
+ ...paymaster.apiKey ? { "x-paymaster-api-key": paymaster.apiKey } : {}
2713
+ },
2714
+ body: JSON.stringify(body)
2715
+ });
2716
+ const json = await res.json();
2717
+ if (json.error) {
2718
+ throw new Error(`kit: paymaster passkey approval failed: ${JSON.stringify(json.error)}`);
2719
+ }
2720
+ return { transactionHash: json.result?.transaction_hash ?? json.result?.tracking_id };
2721
+ }
2722
+ var CavosAuth = class {
2723
+ constructor(opts = {}) {
2724
+ this.opts = opts;
2725
+ /** Most recent nonce sent to the backend (for the pending OAuth/OTP request). */
2726
+ this.pendingNonce = null;
2727
+ this.last = null;
2728
+ this.backendUrl = opts.backendUrl ?? "https://cavos.xyz";
2729
+ }
2730
+ /** Redirect URL for Google login (open it; user returns to your redirectUri). */
2731
+ async getGoogleOAuthUrl(redirectUri) {
2732
+ return this.oauthUrl("google", redirectUri);
2733
+ }
2734
+ /** Redirect URL for Apple login. */
2735
+ async getAppleOAuthUrl(redirectUri) {
2736
+ return this.oauthUrl("apple", redirectUri);
2737
+ }
2738
+ async oauthUrl(provider, redirectUri) {
2739
+ if (typeof window === "undefined") throw new Error("kit/auth: OAuth requires a browser");
2740
+ const params = new URLSearchParams({
2741
+ nonce: this.freshNonce(),
2742
+ redirect_uri: redirectUri ?? window.location.href,
2743
+ ...this.opts.appId ? { app_id: this.opts.appId } : {}
2744
+ });
2745
+ const { url } = await this.get(`/api/oauth/${provider}?${params}`);
2746
+ return url;
2747
+ }
2748
+ /**
2749
+ * Resolve the identity from an OAuth callback. The auth data is carried in the
2750
+ * `auth_data` (or `zk_auth_data`) query param on return. We only extract `sub`.
2751
+ */
2752
+ async handleCallback(authDataOrSearch) {
2753
+ const authData = extractAuthData(authDataOrSearch);
2754
+ return this.identityFromAuthData(authData, "oauth");
2755
+ }
2756
+ /** Send a one-time code to an email (Firebase OTP). */
2757
+ async sendOtp(email) {
2758
+ await this.post("/api/oauth/firebase/otp/request", {
2759
+ email,
2760
+ nonce: this.freshNonce(),
2761
+ ...this.opts.appId ? { app_id: this.opts.appId } : {}
2762
+ });
2763
+ }
2764
+ /** Send a passwordless magic-link sign-in email (Firebase). */
2765
+ async sendMagicLink(email) {
2766
+ await this.post("/api/oauth/firebase/magic-link", {
2767
+ email,
2768
+ nonce: this.freshNonce(),
2769
+ ...this.opts.appId ? { app_id: this.opts.appId } : {},
2770
+ ...typeof window !== "undefined" ? { redirect_uri: window.location.href } : {}
2771
+ });
2772
+ }
2773
+ /** Verify the OTP and resolve the identity. */
2774
+ async verifyOtp(email, code) {
2775
+ const res = await this.post("/api/oauth/firebase/otp/verify", {
2776
+ email,
2777
+ code,
2778
+ nonce: this.consumeNonce(),
2779
+ ...this.opts.appId ? { app_id: this.opts.appId } : {}
2780
+ });
2781
+ return this.identityFromAuthData(res.id_token ?? res.jwt ?? res.token ?? JSON.stringify(res), "otp", email);
2782
+ }
2783
+ /** AuthProvider: returns the identity resolved by the last login step. */
2784
+ async authenticate() {
2785
+ if (!this.last) throw new Error("kit/auth: no identity yet \u2014 complete a login first");
2786
+ return this.last;
2787
+ }
2788
+ // ── internals ──────────────────────────────────────────────────────────────
2789
+ /**
2790
+ * Build an `Identity` from whatever the backend returned. The Cavos backend
2791
+ * wraps the user id in a JWT (its `sub` claim); for the device model we only
2792
+ * need that stable id — the signature is never checked on-chain.
2793
+ */
2794
+ async identityFromAuthData(authData, provider, emailOverride) {
2795
+ let token = authData;
2796
+ try {
2797
+ const parsed = JSON.parse(authData);
2798
+ token = parsed.id_token ?? parsed.jwt ?? parsed.token ?? authData;
2799
+ } catch {
2800
+ }
2801
+ const claims = parseJwt(token);
2802
+ return this.remember({
2803
+ userId: String(claims.sub ?? claims.user_id ?? claims.uid),
2804
+ email: claims.email ?? emailOverride,
2805
+ provider: claims.firebase?.sign_in_provider ?? claims.provider ?? provider
2806
+ });
2807
+ }
2808
+ /** Generate (and remember) the nonce the Cavos backend expects on requests. */
2809
+ freshNonce() {
2810
+ const bytes = crypto.getRandomValues(new Uint8Array(31));
2811
+ const h = hash.computePoseidonHashOnElements([bytesToChunks(bytes)]);
2812
+ this.pendingNonce = num.toHex(h);
2813
+ return this.pendingNonce;
2814
+ }
2815
+ /** Return the pending nonce (for the verify step), clearing it. */
2816
+ consumeNonce() {
2817
+ if (!this.pendingNonce) return this.freshNonce();
2818
+ const n = this.pendingNonce;
2819
+ this.pendingNonce = null;
2820
+ return n;
2821
+ }
2822
+ remember(id) {
2823
+ this.last = id;
2824
+ return id;
2825
+ }
2826
+ async get(path) {
2827
+ const r = await fetch(`${this.backendUrl}${path}`);
2828
+ if (!r.ok) throw new Error(`kit/auth: ${path} -> ${r.status} ${await r.text()}`);
2829
+ return r.json();
2830
+ }
2831
+ async post(path, body) {
2832
+ const r = await fetch(`${this.backendUrl}${path}`, {
2833
+ method: "POST",
2834
+ headers: { "Content-Type": "application/json" },
2835
+ body: JSON.stringify(body)
2836
+ });
2837
+ if (!r.ok) throw new Error(`kit/auth: ${path} -> ${r.status} ${await r.text()}`);
2838
+ return r.json();
2839
+ }
2840
+ };
2841
+ function extractAuthData(input) {
2842
+ if (input.includes("auth_data=") || input.includes("zk_auth_data=")) {
2843
+ const params = new URLSearchParams(input.startsWith("?") ? input : `?${input}`);
2844
+ return params.get("auth_data") ?? params.get("zk_auth_data") ?? input;
2845
+ }
2846
+ return input;
2847
+ }
2848
+ function parseJwt(jwt) {
2849
+ const part = jwt.split(".")[1];
2850
+ if (!part) throw new Error("kit/auth: malformed JWT");
2851
+ const json = atob(part.replace(/-/g, "+").replace(/_/g, "/"));
2852
+ return JSON.parse(json);
2853
+ }
2854
+ function bytesToChunks(bytes) {
2855
+ let w = 0n;
2856
+ for (const b of bytes.subarray(0, 31)) w = w << 8n | BigInt(b);
2857
+ return w;
2858
+ }
2859
+ var PasskeySigner = class {
2860
+ constructor(opts = {}) {
2861
+ if (typeof window === "undefined" || !navigator.credentials) {
2862
+ throw new Error("kit/passkey: WebAuthn is only available in a browser");
2863
+ }
2864
+ this.rpId = opts.rpId ?? window.location.hostname;
2865
+ this.rpName = opts.rpName ?? this.rpId;
2866
+ if (isIpAddress(this.rpId)) {
2867
+ throw new Error(
2868
+ `kit/passkey: passkeys can't use an IP address as the domain ("${this.rpId}"). Use http://localhost, a real HTTPS domain, or a tunnel (cloudflared/ngrok) \u2014 or pass an explicit \`rpId\`. (The silent device key works over an IP; passkeys don't.)`
2869
+ );
2870
+ }
2871
+ }
2872
+ /** True if this platform advertises a usable passkey (platform authenticator). */
2873
+ static async isSupported() {
2874
+ if (typeof window === "undefined" || !window.PublicKeyCredential) return false;
2875
+ try {
2876
+ return await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
2877
+ } catch {
2878
+ return false;
2879
+ }
2880
+ }
2881
+ /** Create a new synced passkey and return its P-256 public key. */
2882
+ async enroll(params) {
2883
+ const challenge = crypto.getRandomValues(new Uint8Array(32));
2884
+ const cred = await navigator.credentials.create({
2885
+ publicKey: {
2886
+ challenge: buf(challenge),
2887
+ rp: { id: this.rpId, name: this.rpName },
2888
+ user: {
2889
+ id: buf(userHandle(params.userId)),
2890
+ name: params.userName,
2891
+ displayName: params.displayName ?? params.userName
2892
+ },
2893
+ pubKeyCredParams: [{ type: "public-key", alg: -7 }],
2894
+ // ES256 (P-256)
2895
+ authenticatorSelection: {
2896
+ residentKey: "required",
2897
+ requireResidentKey: true,
2898
+ userVerification: "preferred"
2899
+ },
2900
+ attestation: "none"
2901
+ }
2902
+ });
2903
+ if (!cred) throw new Error("kit/passkey: enrollment cancelled");
2904
+ const response = cred.response;
2905
+ const spki = new Uint8Array(response.getPublicKey());
2906
+ return { publicKey: spkiToPublicKey(spki), credentialId: new Uint8Array(cred.rawId) };
2907
+ }
2908
+ /**
2909
+ * Produce a WebAuthn assertion over `challenge` (a 32-byte value the caller
2910
+ * derives from the signer being added + the on-chain nonce). Uses discoverable
2911
+ * credentials — no `allowCredentials` — so it works on a brand-new browser.
2912
+ */
2913
+ async assert(challenge) {
2914
+ const cred = await navigator.credentials.get({
2915
+ publicKey: {
2916
+ challenge: buf(challenge),
2917
+ rpId: this.rpId,
2918
+ allowCredentials: [],
2919
+ userVerification: "preferred"
2920
+ }
2921
+ });
2922
+ if (!cred) throw new Error("kit/passkey: assertion cancelled");
2923
+ const response = cred.response;
2924
+ const authenticatorData = new Uint8Array(response.authenticatorData);
2925
+ const clientDataJSON = new Uint8Array(response.clientDataJSON);
2926
+ const { r, s } = derToRs(new Uint8Array(response.signature));
2927
+ const challengeOffset = challengeOffsetOf(clientDataJSON, base64urlEncode(challenge));
2928
+ return { authenticatorData, clientDataJSON, r, s, challengeOffset };
2929
+ }
2930
+ };
2931
+ function isIpAddress(host) {
2932
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true;
2933
+ if (host.includes(":")) return true;
2934
+ return false;
2935
+ }
2936
+ function userHandle(userId) {
2937
+ const bytes = new TextEncoder().encode(userId);
2938
+ return bytes.length <= 64 ? bytes : sha256(bytes);
2939
+ }
2940
+ function buf(bytes) {
2941
+ return bytes.slice();
2942
+ }
2943
+
2944
+ export { BackupSigner, Cavos, CavosAuth, CavosSolana, CavosStellar, DEVICE_ACCOUNT_CLASS_HASH, DEVICE_ACCOUNT_PROGRAM_ID, DEVICE_ACCOUNT_WASM_HASH, FACTORY_CONTRACT_ID, HttpRecoveryClient, HttpWalletRegistry, InMemoryWalletRegistry, NATIVE_SAC_ID, PasskeySigner, SECP256R1_PROGRAM_ID, SOLANA_NETWORKS, STARKNET_NETWORKS, STELLAR_NETWORKS, SolanaAdapter, SolanaRelayer, StarknetAdapter, StarknetDeviceSigner, StellarAdapter, StellarRelayer, UDC_ADDRESS, WebCryptoSigner, anchorDiscriminator, approveDeviceEverywhere, base64urlEncode, batchChallenge, bigIntTo32Bytes, buildSecp256r1Instruction, bytesToBigInt, bytesToHex, compressedPubkey, deriveAddressSeed, deriveAddressSeedSolana, deriveAddressSeedStellar, deriveBackupKey, deviceSignatureScVal, encodeLowSSignature, encodeLowSSignature2, generateRecoveryCode, hexToBytes, lowS, recoverCandidatePublicKeys, recoverYParity, sec1Pubkey, serializeInstructions, signatureToFelts, u256ToFelts, webauthnDigest };
2945
+ //# sourceMappingURL=chunk-F2J25XSL.mjs.map
2946
+ //# sourceMappingURL=chunk-F2J25XSL.mjs.map