@cavos/kit 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -42
- package/dist/Cavos-BH2_tOQ2.d.mts +994 -0
- package/dist/Cavos-BH2_tOQ2.d.ts +994 -0
- package/dist/chunk-BNGLH3Q3.mjs +2777 -0
- package/dist/chunk-BNGLH3Q3.mjs.map +1 -0
- package/dist/index.d.mts +156 -242
- package/dist/index.d.ts +156 -242
- package/dist/index.js +1989 -151
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +88 -2
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.d.mts +42 -5
- package/dist/react/index.d.ts +42 -5
- package/dist/react/index.js +1786 -86
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +48 -7
- package/dist/react/index.mjs.map +1 -1
- package/package.json +4 -1
- package/dist/chunk-XWBX2ZIO.mjs +0 -1061
- package/dist/chunk-XWBX2ZIO.mjs.map +0 -1
- package/dist/constants-C530TZFF.d.mts +0 -89
- package/dist/constants-C530TZFF.d.ts +0 -89
package/dist/react/index.js
CHANGED
|
@@ -4,9 +4,11 @@ var react = require('react');
|
|
|
4
4
|
var starknet = require('starknet');
|
|
5
5
|
var sha256 = require('@noble/hashes/sha256');
|
|
6
6
|
var p256 = require('@noble/curves/p256');
|
|
7
|
+
var web3_js = require('@solana/web3.js');
|
|
7
8
|
var hkdf = require('@noble/hashes/hkdf');
|
|
8
9
|
var pbkdf2 = require('@noble/hashes/pbkdf2');
|
|
9
10
|
var utils = require('@noble/hashes/utils');
|
|
11
|
+
var stellarSdk = require('@stellar/stellar-sdk');
|
|
10
12
|
var jsxRuntime = require('react/jsx-runtime');
|
|
11
13
|
|
|
12
14
|
// src/react/CavosProvider.tsx
|
|
@@ -21,6 +23,18 @@ function bytesToBigInt(bytes) {
|
|
|
21
23
|
for (const b of bytes) out = out << 8n | BigInt(b);
|
|
22
24
|
return out;
|
|
23
25
|
}
|
|
26
|
+
function bytesToByteArrayCalldata(bytes) {
|
|
27
|
+
const CHUNK = 31;
|
|
28
|
+
const fullCount = Math.floor(bytes.length / CHUNK);
|
|
29
|
+
const out = [String(fullCount)];
|
|
30
|
+
for (let i = 0; i < fullCount; i++) {
|
|
31
|
+
out.push("0x" + bytesToBigInt(bytes.subarray(i * CHUNK, i * CHUNK + CHUNK)).toString(16));
|
|
32
|
+
}
|
|
33
|
+
const rem = bytes.subarray(fullCount * CHUNK);
|
|
34
|
+
out.push("0x" + (rem.length ? bytesToBigInt(rem).toString(16) : "0"));
|
|
35
|
+
out.push(String(rem.length));
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
24
38
|
function bigIntTo32Bytes(value) {
|
|
25
39
|
const out = new Uint8Array(32);
|
|
26
40
|
let v = value;
|
|
@@ -158,8 +172,8 @@ var CAVOS_PAYMASTER_URL = {
|
|
|
158
172
|
mainnet: "https://paymaster.cavos.xyz"
|
|
159
173
|
};
|
|
160
174
|
var DEVICE_ACCOUNT_CLASS_HASH = {
|
|
161
|
-
sepolia: "
|
|
162
|
-
mainnet: ""
|
|
175
|
+
sepolia: "0x25cbc5423e8ee895febb0ef2c3945b408da44d0039d915fbdd681fe6b6ba66b",
|
|
176
|
+
mainnet: "0x1840aded59e8a0d2b440a134cb9079a7fc11b06c77f58ed189ab436a034ca6a"
|
|
163
177
|
};
|
|
164
178
|
|
|
165
179
|
// src/chains/starknet/StarknetAdapter.ts
|
|
@@ -222,6 +236,73 @@ var StarknetAdapter = class {
|
|
|
222
236
|
const sig = await this.opts.signer.sign(bigIntTo32Bytes(txHash));
|
|
223
237
|
return signatureToFelts(sig).map((f) => starknet.num.toHex(f));
|
|
224
238
|
}
|
|
239
|
+
// --- passkey approvers ---
|
|
240
|
+
buildAddApprover(accountAddress, passkey) {
|
|
241
|
+
return { contractAddress: accountAddress, entrypoint: "add_approver", calldata: pubkeyCalldata(passkey) };
|
|
242
|
+
}
|
|
243
|
+
buildRemoveApprover(accountAddress, passkey) {
|
|
244
|
+
return { contractAddress: accountAddress, entrypoint: "remove_approver", calldata: pubkeyCalldata(passkey) };
|
|
245
|
+
}
|
|
246
|
+
async isApprover(accountAddress, passkey) {
|
|
247
|
+
if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
|
|
248
|
+
const res = await this.opts.provider.callContract({
|
|
249
|
+
contractAddress: accountAddress,
|
|
250
|
+
entrypoint: "is_approver",
|
|
251
|
+
calldata: pubkeyCalldata(passkey)
|
|
252
|
+
});
|
|
253
|
+
return BigInt(res[0] ?? 0) !== 0n;
|
|
254
|
+
}
|
|
255
|
+
async getPasskeyNonce(accountAddress) {
|
|
256
|
+
if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
|
|
257
|
+
const res = await this.opts.provider.callContract({
|
|
258
|
+
contractAddress: accountAddress,
|
|
259
|
+
entrypoint: "get_passkey_nonce",
|
|
260
|
+
calldata: []
|
|
261
|
+
});
|
|
262
|
+
return BigInt(res[0] ?? 0);
|
|
263
|
+
}
|
|
264
|
+
/** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
|
|
265
|
+
* `sha256(new_x || new_y || nonce)` (coords 32B BE, nonce 16B BE). The batch
|
|
266
|
+
* challenge the passkey signs is `sha256(concat(leaves))` across chains. */
|
|
267
|
+
passkeyLeaf(newSigner, nonce) {
|
|
268
|
+
const msg = new Uint8Array(32 + 32 + 16);
|
|
269
|
+
msg.set(bigIntTo32Bytes(newSigner.x), 0);
|
|
270
|
+
msg.set(bigIntTo32Bytes(newSigner.y), 32);
|
|
271
|
+
msg.set(bigIntTo32Bytes(nonce).subarray(16), 64);
|
|
272
|
+
return sha256.sha256(msg);
|
|
273
|
+
}
|
|
274
|
+
/** Passkey-authorized `add_signer` call. `leaves`/`leafIndex` place this chain's
|
|
275
|
+
* leaf in the multi-chain batch (single chain → `[leaf]`, index 0). `yParity`
|
|
276
|
+
* matches the raw `(r, s)` — the contract normalizes high-S internally. */
|
|
277
|
+
buildAddSignerViaPasskey(accountAddress, newSigner, nonce, leaves, leafIndex, assertion, yParity) {
|
|
278
|
+
const [rl, rh] = u256ToFelts(assertion.r);
|
|
279
|
+
const [sl, sh] = u256ToFelts(assertion.s);
|
|
280
|
+
const leavesCalldata = [String(leaves.length)];
|
|
281
|
+
for (const leaf of leaves) {
|
|
282
|
+
const [lo, hi] = u256ToFelts(bytesToBigInt(leaf));
|
|
283
|
+
leavesCalldata.push(starknet.num.toHex(lo), starknet.num.toHex(hi));
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
contractAddress: accountAddress,
|
|
287
|
+
entrypoint: "add_signer_via_passkey",
|
|
288
|
+
calldata: [
|
|
289
|
+
...pubkeyCalldata(newSigner),
|
|
290
|
+
// new_x, new_y (u256 pairs)
|
|
291
|
+
starknet.num.toHex(nonce),
|
|
292
|
+
...leavesCalldata,
|
|
293
|
+
// Array<u256> leaves
|
|
294
|
+
String(leafIndex),
|
|
295
|
+
...bytesToByteArrayCalldata(assertion.authenticatorData),
|
|
296
|
+
...bytesToByteArrayCalldata(assertion.clientDataJSON),
|
|
297
|
+
String(assertion.challengeOffset),
|
|
298
|
+
starknet.num.toHex(rl),
|
|
299
|
+
starknet.num.toHex(rh),
|
|
300
|
+
starknet.num.toHex(sl),
|
|
301
|
+
starknet.num.toHex(sh),
|
|
302
|
+
yParity ? "0x1" : "0x0"
|
|
303
|
+
]
|
|
304
|
+
};
|
|
305
|
+
}
|
|
225
306
|
};
|
|
226
307
|
function pubkeyCalldata(pk) {
|
|
227
308
|
const [xl, xh] = u256ToFelts(pk.x);
|
|
@@ -308,76 +389,505 @@ var HttpWalletRegistry = class {
|
|
|
308
389
|
async addDevice(params) {
|
|
309
390
|
}
|
|
310
391
|
};
|
|
392
|
+
function deriveAddressSeed({ userId, appSalt }) {
|
|
393
|
+
const h = starknet.hash.computePoseidonHashOnElements([feltFromString(userId), feltFromString(appSalt)]);
|
|
394
|
+
return BigInt(h);
|
|
395
|
+
}
|
|
396
|
+
function deriveAddressSeedSolana({ userId, appSalt }) {
|
|
397
|
+
return sha256.sha256(new TextEncoder().encode(`cavos:solana:v1:${userId}:${appSalt}`));
|
|
398
|
+
}
|
|
399
|
+
function deriveAddressSeedStellar({ userId, appSalt }) {
|
|
400
|
+
return sha256.sha256(new TextEncoder().encode(`cavos:stellar:v1:${userId}:${appSalt}`));
|
|
401
|
+
}
|
|
402
|
+
function feltFromString(s) {
|
|
403
|
+
const bytes = new TextEncoder().encode(s);
|
|
404
|
+
const chunks = [];
|
|
405
|
+
for (let i = 0; i < bytes.length; i += 31) {
|
|
406
|
+
let w = 0n;
|
|
407
|
+
for (const b of bytes.subarray(i, i + 31)) w = w << 8n | BigInt(b);
|
|
408
|
+
chunks.push(w);
|
|
409
|
+
}
|
|
410
|
+
if (chunks.length === 0) return 0n;
|
|
411
|
+
if (chunks.length === 1) return chunks[0];
|
|
412
|
+
return BigInt(starknet.hash.computePoseidonHashOnElements(chunks));
|
|
413
|
+
}
|
|
311
414
|
|
|
312
|
-
// src/
|
|
313
|
-
|
|
314
|
-
|
|
415
|
+
// src/chains/solana/constants.ts
|
|
416
|
+
var DEVICE_ACCOUNT_PROGRAM_ID = "FHnoYNfYAmFrwt18gcBGG7G1S5q3RAbCBvrV2D29izNJ";
|
|
417
|
+
var SECP256R1_PROGRAM_ID = "Secp256r1SigVerify1111111111111111111111111";
|
|
418
|
+
var ACCOUNT_SEED = "cavos-account";
|
|
419
|
+
var DOMAIN_ADD = "cavos:add_signer:v1";
|
|
420
|
+
var DOMAIN_REMOVE = "cavos:remove_signer:v1";
|
|
421
|
+
var DOMAIN_TRANSFER = "cavos:transfer:v1";
|
|
422
|
+
var DOMAIN_EXECUTE = "cavos:execute:v1";
|
|
423
|
+
var DOMAIN_ADD_APPROVER = "cavos:add_approver:v1";
|
|
424
|
+
var DOMAIN_REMOVE_APPROVER = "cavos:remove_approver:v1";
|
|
425
|
+
var SECP256R1_N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
|
|
426
|
+
var SOLANA_NETWORKS = {
|
|
427
|
+
"solana-devnet": "https://api.devnet.solana.com",
|
|
428
|
+
"solana-mainnet": "https://api.mainnet-beta.solana.com",
|
|
429
|
+
"solana-localnet": "http://127.0.0.1:8899"
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// src/chains/solana/SolanaAdapter.ts
|
|
433
|
+
var COMPRESSED_PUBKEY_SIZE = 33;
|
|
434
|
+
var SIGNATURE_SIZE = 64;
|
|
435
|
+
var CURRENT_IX = 65535;
|
|
436
|
+
var SolanaAdapter = class {
|
|
437
|
+
constructor(opts = {}) {
|
|
438
|
+
this.opts = opts;
|
|
439
|
+
this.chain = "solana";
|
|
440
|
+
this.programId = new web3_js.PublicKey(opts.programId ?? DEVICE_ACCOUNT_PROGRAM_ID);
|
|
441
|
+
}
|
|
442
|
+
/** Deterministic account address: PDA of [seed, address_seed, initial_signer_x]. */
|
|
443
|
+
computeAddress(addressSeed, initialSigner) {
|
|
444
|
+
return this.pda(addressSeed, compressedPubkey(initialSigner)).toBase58();
|
|
445
|
+
}
|
|
446
|
+
pda(addressSeed, initialCompressed) {
|
|
447
|
+
const [pda] = web3_js.PublicKey.findProgramAddressSync(
|
|
448
|
+
[
|
|
449
|
+
Buffer.from(ACCOUNT_SEED),
|
|
450
|
+
Buffer.from(addressSeed),
|
|
451
|
+
Buffer.from(initialCompressed.slice(1, 33))
|
|
452
|
+
// x-coordinate
|
|
453
|
+
],
|
|
454
|
+
this.programId
|
|
455
|
+
);
|
|
456
|
+
return pda;
|
|
457
|
+
}
|
|
458
|
+
/** `initialize` instruction creating the account with its first device signer. */
|
|
459
|
+
buildInitialize(addressSeed, payer, initialSigner) {
|
|
460
|
+
const initialCompressed = compressedPubkey(initialSigner);
|
|
461
|
+
const account = this.pda(addressSeed, initialCompressed);
|
|
462
|
+
const data = Buffer.concat([
|
|
463
|
+
anchorDiscriminator("initialize"),
|
|
464
|
+
Buffer.from(addressSeed),
|
|
465
|
+
// [u8;32]
|
|
466
|
+
Buffer.from(initialCompressed)
|
|
467
|
+
// [u8;33]
|
|
468
|
+
]);
|
|
469
|
+
return new web3_js.TransactionInstruction({
|
|
470
|
+
programId: this.programId,
|
|
471
|
+
keys: [
|
|
472
|
+
{ pubkey: account, isSigner: false, isWritable: true },
|
|
473
|
+
{ pubkey: new web3_js.PublicKey(payer), isSigner: true, isWritable: true },
|
|
474
|
+
{ pubkey: web3_js.SystemProgram.programId, isSigner: false, isWritable: false }
|
|
475
|
+
],
|
|
476
|
+
data
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
/** `[precompile, add_signer]` bundle, authorized by an existing device signer. */
|
|
480
|
+
async buildAddSigner(account, newSigner) {
|
|
481
|
+
const accountPk = new web3_js.PublicKey(account);
|
|
482
|
+
const newCompressed = compressedPubkey(newSigner);
|
|
483
|
+
const nonce = await this.fetchNonce(accountPk);
|
|
484
|
+
const message = concatBytes(
|
|
485
|
+
Buffer.from(DOMAIN_ADD),
|
|
486
|
+
accountPk.toBuffer(),
|
|
487
|
+
newCompressed,
|
|
488
|
+
u64le(nonce)
|
|
489
|
+
);
|
|
490
|
+
const { precompileIx } = await this.signToPrecompile(message);
|
|
491
|
+
const ix = new web3_js.TransactionInstruction({
|
|
492
|
+
programId: this.programId,
|
|
493
|
+
keys: this.guardedKeys(accountPk),
|
|
494
|
+
data: Buffer.concat([anchorDiscriminator("add_signer"), Buffer.from(newCompressed)])
|
|
495
|
+
});
|
|
496
|
+
return [precompileIx, ix];
|
|
497
|
+
}
|
|
498
|
+
/** `[precompile, remove_signer]` bundle, authorized by an existing device signer. */
|
|
499
|
+
async buildRemoveSigner(account, signer) {
|
|
500
|
+
const accountPk = new web3_js.PublicKey(account);
|
|
501
|
+
const compressed = compressedPubkey(signer);
|
|
502
|
+
const nonce = await this.fetchNonce(accountPk);
|
|
503
|
+
const message = concatBytes(
|
|
504
|
+
Buffer.from(DOMAIN_REMOVE),
|
|
505
|
+
accountPk.toBuffer(),
|
|
506
|
+
compressed,
|
|
507
|
+
u64le(nonce)
|
|
508
|
+
);
|
|
509
|
+
const { precompileIx } = await this.signToPrecompile(message);
|
|
510
|
+
const ix = new web3_js.TransactionInstruction({
|
|
511
|
+
programId: this.programId,
|
|
512
|
+
keys: this.guardedKeys(accountPk),
|
|
513
|
+
data: Buffer.concat([anchorDiscriminator("remove_signer"), Buffer.from(compressed)])
|
|
514
|
+
});
|
|
515
|
+
return [precompileIx, ix];
|
|
516
|
+
}
|
|
517
|
+
/** `[precompile, add_approver]` bundle enrolling a passkey approver (device-signed). */
|
|
518
|
+
async buildAddApprover(account, passkey) {
|
|
519
|
+
const accountPk = new web3_js.PublicKey(account);
|
|
520
|
+
const compressed = compressedPubkey(passkey);
|
|
521
|
+
const nonce = await this.fetchNonce(accountPk);
|
|
522
|
+
const message = concatBytes(
|
|
523
|
+
Buffer.from(DOMAIN_ADD_APPROVER),
|
|
524
|
+
accountPk.toBuffer(),
|
|
525
|
+
compressed,
|
|
526
|
+
u64le(nonce)
|
|
527
|
+
);
|
|
528
|
+
const { precompileIx } = await this.signToPrecompile(message);
|
|
529
|
+
const ix = new web3_js.TransactionInstruction({
|
|
530
|
+
programId: this.programId,
|
|
531
|
+
keys: this.guardedKeys(accountPk),
|
|
532
|
+
data: Buffer.concat([anchorDiscriminator("add_approver"), Buffer.from(compressed)])
|
|
533
|
+
});
|
|
534
|
+
return [precompileIx, ix];
|
|
535
|
+
}
|
|
536
|
+
/** `[precompile, remove_approver]` bundle (device-signed). */
|
|
537
|
+
async buildRemoveApprover(account, passkey) {
|
|
538
|
+
const accountPk = new web3_js.PublicKey(account);
|
|
539
|
+
const compressed = compressedPubkey(passkey);
|
|
540
|
+
const nonce = await this.fetchNonce(accountPk);
|
|
541
|
+
const message = concatBytes(
|
|
542
|
+
Buffer.from(DOMAIN_REMOVE_APPROVER),
|
|
543
|
+
accountPk.toBuffer(),
|
|
544
|
+
compressed,
|
|
545
|
+
u64le(nonce)
|
|
546
|
+
);
|
|
547
|
+
const { precompileIx } = await this.signToPrecompile(message);
|
|
548
|
+
const ix = new web3_js.TransactionInstruction({
|
|
549
|
+
programId: this.programId,
|
|
550
|
+
keys: this.guardedKeys(accountPk),
|
|
551
|
+
data: Buffer.concat([anchorDiscriminator("remove_approver"), Buffer.from(compressed)])
|
|
552
|
+
});
|
|
553
|
+
return [precompileIx, ix];
|
|
554
|
+
}
|
|
555
|
+
/** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
|
|
556
|
+
* `sha256(compressed(new_signer) || passkey_nonce_le8)`. The batch challenge the
|
|
557
|
+
* passkey signs is `sha256(concat(leaves))` across chains. */
|
|
558
|
+
passkeyLeaf(newSigner, nonce) {
|
|
559
|
+
return sha256.sha256(concatBytes(compressedPubkey(newSigner), u64le(nonce)));
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* `[precompile(passkey), add_signer_via_passkey]` bundle. The precompile ix
|
|
563
|
+
* verifies the PASSKEY's WebAuthn assertion over `authData || sha256(clientDataJSON)`;
|
|
564
|
+
* the program ix binds the challenge to `newSigner` + the passkey nonce and adds
|
|
565
|
+
* the signer. No device signature — a gasless relayer can submit it.
|
|
566
|
+
*/
|
|
567
|
+
buildAddSignerViaPasskey(account, newSigner, passkey, leaves, leafIndex, assertion) {
|
|
568
|
+
const accountPk = new web3_js.PublicKey(account);
|
|
569
|
+
const newCompressed = compressedPubkey(newSigner);
|
|
570
|
+
const passkeyCompressed = compressedPubkey(passkey);
|
|
571
|
+
const clientHash = sha256.sha256(assertion.clientDataJSON);
|
|
572
|
+
const message = concatBytes(assertion.authenticatorData, clientHash);
|
|
573
|
+
const signature = encodeLowSSignature(assertion.r, assertion.s);
|
|
574
|
+
const precompileIx = buildSecp256r1Instruction(passkeyCompressed, signature, message);
|
|
575
|
+
const leavesBlob = Buffer.concat([u32le(leaves.length), ...leaves.map((l) => Buffer.from(l))]);
|
|
576
|
+
const data = Buffer.concat([
|
|
577
|
+
anchorDiscriminator("add_signer_via_passkey"),
|
|
578
|
+
Buffer.from(newCompressed),
|
|
579
|
+
leavesBlob,
|
|
580
|
+
u32le(leafIndex),
|
|
581
|
+
serializeVecU8(assertion.authenticatorData),
|
|
582
|
+
serializeVecU8(assertion.clientDataJSON),
|
|
583
|
+
u32le(assertion.challengeOffset)
|
|
584
|
+
]);
|
|
585
|
+
const ix = new web3_js.TransactionInstruction({
|
|
586
|
+
programId: this.programId,
|
|
587
|
+
keys: this.guardedKeys(accountPk),
|
|
588
|
+
data
|
|
589
|
+
});
|
|
590
|
+
return [precompileIx, ix];
|
|
591
|
+
}
|
|
592
|
+
/** Read whether `passkey` is a registered approver. */
|
|
593
|
+
async isApprover(account, passkey) {
|
|
594
|
+
const approvers = await this.fetchApprovers(new web3_js.PublicKey(account));
|
|
595
|
+
const target = Buffer.from(compressedPubkey(passkey)).toString("hex");
|
|
596
|
+
return approvers.some((a) => Buffer.from(a).toString("hex") === target);
|
|
597
|
+
}
|
|
598
|
+
/** Read the current passkey-approval nonce. */
|
|
599
|
+
async passkeyNonce(account) {
|
|
600
|
+
const info = await this.requireConnection().getAccountInfo(new web3_js.PublicKey(account));
|
|
601
|
+
if (!info) return 0n;
|
|
602
|
+
const d = info.data;
|
|
603
|
+
const signersLenOff = 8 + 32 + 1 + 8 + COMPRESSED_PUBKEY_SIZE;
|
|
604
|
+
const signerCount = d.readUInt32LE(signersLenOff);
|
|
605
|
+
const approversLenOff = signersLenOff + 4 + signerCount * COMPRESSED_PUBKEY_SIZE;
|
|
606
|
+
const approverCount = d.readUInt32LE(approversLenOff);
|
|
607
|
+
const passkeyNonceOff = approversLenOff + 4 + approverCount * COMPRESSED_PUBKEY_SIZE;
|
|
608
|
+
return readU64le(d, passkeyNonceOff);
|
|
609
|
+
}
|
|
610
|
+
/** `[precompile, execute_transfer]` bundle moving lamports out of the account. */
|
|
611
|
+
async buildExecuteTransfer(account, destination, amount) {
|
|
612
|
+
const accountPk = new web3_js.PublicKey(account);
|
|
613
|
+
const destPk = new web3_js.PublicKey(destination);
|
|
614
|
+
const nonce = await this.fetchNonce(accountPk);
|
|
615
|
+
const message = concatBytes(
|
|
616
|
+
Buffer.from(DOMAIN_TRANSFER),
|
|
617
|
+
accountPk.toBuffer(),
|
|
618
|
+
destPk.toBuffer(),
|
|
619
|
+
u64le(amount),
|
|
620
|
+
u64le(nonce)
|
|
621
|
+
);
|
|
622
|
+
const { precompileIx } = await this.signToPrecompile(message);
|
|
623
|
+
const ix = new web3_js.TransactionInstruction({
|
|
624
|
+
programId: this.programId,
|
|
625
|
+
keys: [
|
|
626
|
+
{ pubkey: accountPk, isSigner: false, isWritable: true },
|
|
627
|
+
{ pubkey: destPk, isSigner: false, isWritable: true },
|
|
628
|
+
{ pubkey: web3_js.SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false }
|
|
629
|
+
],
|
|
630
|
+
data: Buffer.concat([anchorDiscriminator("execute_transfer"), u64le(amount)])
|
|
631
|
+
});
|
|
632
|
+
return [precompileIx, ix];
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* `[precompile, execute]` bundle running arbitrary CPI instructions with the
|
|
636
|
+
* account PDA as signer. The device key signs over
|
|
637
|
+
* `DOMAIN_EXECUTE || account || sha256(canonical(instructions)) || nonce`, so
|
|
638
|
+
* the signature commits to the EXACT instruction set the program will invoke —
|
|
639
|
+
* no account/data substitution is possible after signing.
|
|
640
|
+
*
|
|
641
|
+
* The instructions' accounts are passed to the program via `remaining_accounts`
|
|
642
|
+
* (flattened, in order); the program enforces an exact, ordered mapping.
|
|
643
|
+
*/
|
|
644
|
+
async buildExecute(account, instructions) {
|
|
645
|
+
if (instructions.length === 0) throw new Error("kit/solana: execute requires at least one instruction");
|
|
646
|
+
const accountPk = new web3_js.PublicKey(account);
|
|
647
|
+
const nonce = await this.fetchNonce(accountPk);
|
|
648
|
+
const blob = serializeInstructions(instructions);
|
|
649
|
+
const ixsHash = sha256.sha256(blob);
|
|
650
|
+
const message = concatBytes(
|
|
651
|
+
Buffer.from(DOMAIN_EXECUTE),
|
|
652
|
+
accountPk.toBuffer(),
|
|
653
|
+
Buffer.from(ixsHash),
|
|
654
|
+
u64le(nonce)
|
|
655
|
+
);
|
|
656
|
+
const { precompileIx } = await this.signToPrecompile(message);
|
|
657
|
+
const blobLen = Buffer.alloc(4);
|
|
658
|
+
new DataView(blobLen.buffer).setUint32(0, blob.length, true);
|
|
659
|
+
const data = Buffer.concat([anchorDiscriminator("execute"), blobLen, blob]);
|
|
660
|
+
const remainingAccounts = [];
|
|
661
|
+
for (const ix2 of instructions) {
|
|
662
|
+
for (const acc of ix2.accounts) {
|
|
663
|
+
remainingAccounts.push({
|
|
664
|
+
pubkey: new web3_js.PublicKey(acc.pubkey),
|
|
665
|
+
isSigner: false,
|
|
666
|
+
// signer flags are part of the signed InstructionData
|
|
667
|
+
isWritable: acc.isWritable
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
remainingAccounts.push({
|
|
671
|
+
pubkey: new web3_js.PublicKey(ix2.programId),
|
|
672
|
+
isSigner: false,
|
|
673
|
+
isWritable: false
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
const ix = new web3_js.TransactionInstruction({
|
|
677
|
+
programId: this.programId,
|
|
678
|
+
keys: [
|
|
679
|
+
{ pubkey: accountPk, isSigner: false, isWritable: true },
|
|
680
|
+
{ pubkey: web3_js.SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false },
|
|
681
|
+
...remainingAccounts
|
|
682
|
+
],
|
|
683
|
+
data
|
|
684
|
+
});
|
|
685
|
+
return [precompileIx, ix];
|
|
686
|
+
}
|
|
687
|
+
/** Read whether `signer` is currently an authorized signer of `account`. */
|
|
688
|
+
async isAuthorizedSigner(account, signer) {
|
|
689
|
+
const signers = await this.fetchSigners(new web3_js.PublicKey(account));
|
|
690
|
+
const target = Buffer.from(compressedPubkey(signer)).toString("hex");
|
|
691
|
+
return signers.some((s) => Buffer.from(s).toString("hex") === target);
|
|
692
|
+
}
|
|
693
|
+
guardedKeys(account) {
|
|
694
|
+
return [
|
|
695
|
+
{ pubkey: account, isSigner: false, isWritable: true },
|
|
696
|
+
{ pubkey: web3_js.SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false }
|
|
697
|
+
];
|
|
698
|
+
}
|
|
699
|
+
/** Sign `message` with the device key and build the matching precompile ix. */
|
|
700
|
+
async signToPrecompile(message) {
|
|
701
|
+
if (!this.opts.signer) throw new Error("kit/solana: signer required to authorize");
|
|
702
|
+
const pubkey = await this.opts.signer.getPublicKey();
|
|
703
|
+
const sig = await this.opts.signer.sign(message);
|
|
704
|
+
const signature = encodeLowSSignature(sig.r, sig.s);
|
|
705
|
+
const precompileIx = buildSecp256r1Instruction(
|
|
706
|
+
compressedPubkey(pubkey),
|
|
707
|
+
signature,
|
|
708
|
+
message
|
|
709
|
+
);
|
|
710
|
+
return { precompileIx };
|
|
711
|
+
}
|
|
712
|
+
async fetchNonce(account) {
|
|
713
|
+
const info = await this.requireConnection().getAccountInfo(account);
|
|
714
|
+
if (!info) return 0n;
|
|
715
|
+
return readU64le(info.data, 41);
|
|
716
|
+
}
|
|
717
|
+
async fetchSigners(account) {
|
|
718
|
+
const info = await this.requireConnection().getAccountInfo(account);
|
|
719
|
+
if (!info) return [];
|
|
720
|
+
const d = info.data;
|
|
721
|
+
const lenOffset = 8 + 32 + 1 + 8 + COMPRESSED_PUBKEY_SIZE;
|
|
722
|
+
const count = d.readUInt32LE(lenOffset);
|
|
723
|
+
const out = [];
|
|
724
|
+
let off = lenOffset + 4;
|
|
725
|
+
for (let i = 0; i < count; i++) {
|
|
726
|
+
out.push(Uint8Array.from(d.subarray(off, off + COMPRESSED_PUBKEY_SIZE)));
|
|
727
|
+
off += COMPRESSED_PUBKEY_SIZE;
|
|
728
|
+
}
|
|
729
|
+
return out;
|
|
730
|
+
}
|
|
731
|
+
async fetchApprovers(account) {
|
|
732
|
+
const info = await this.requireConnection().getAccountInfo(account);
|
|
733
|
+
if (!info) return [];
|
|
734
|
+
const d = info.data;
|
|
735
|
+
const signersLenOff = 8 + 32 + 1 + 8 + COMPRESSED_PUBKEY_SIZE;
|
|
736
|
+
const signerCount = d.readUInt32LE(signersLenOff);
|
|
737
|
+
const approversLenOff = signersLenOff + 4 + signerCount * COMPRESSED_PUBKEY_SIZE;
|
|
738
|
+
const count = d.readUInt32LE(approversLenOff);
|
|
739
|
+
const out = [];
|
|
740
|
+
let off = approversLenOff + 4;
|
|
741
|
+
for (let i = 0; i < count; i++) {
|
|
742
|
+
out.push(Uint8Array.from(d.subarray(off, off + COMPRESSED_PUBKEY_SIZE)));
|
|
743
|
+
off += COMPRESSED_PUBKEY_SIZE;
|
|
744
|
+
}
|
|
745
|
+
return out;
|
|
746
|
+
}
|
|
747
|
+
requireConnection() {
|
|
748
|
+
if (!this.opts.connection) throw new Error("kit/solana: connection required for reads");
|
|
749
|
+
return this.opts.connection;
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
function compressedPubkey(pk) {
|
|
753
|
+
const out = new Uint8Array(COMPRESSED_PUBKEY_SIZE);
|
|
754
|
+
out[0] = pk.y % 2n === 0n ? 2 : 3;
|
|
755
|
+
out.set(bigIntTo32Bytes(pk.x), 1);
|
|
756
|
+
return out;
|
|
315
757
|
}
|
|
316
|
-
function
|
|
317
|
-
|
|
758
|
+
function encodeLowSSignature(r, s) {
|
|
759
|
+
const lowS = s > SECP256R1_N / 2n ? SECP256R1_N - s : s;
|
|
760
|
+
const out = new Uint8Array(SIGNATURE_SIZE);
|
|
761
|
+
out.set(bigIntTo32Bytes(r), 0);
|
|
762
|
+
out.set(bigIntTo32Bytes(lowS), 32);
|
|
763
|
+
return out;
|
|
318
764
|
}
|
|
319
|
-
function
|
|
320
|
-
|
|
321
|
-
|
|
765
|
+
function buildSecp256r1Instruction(compressed, signature, message) {
|
|
766
|
+
const headerLen = 2;
|
|
767
|
+
const offsetsLen = 14;
|
|
768
|
+
const pubkeyOffset = headerLen + offsetsLen;
|
|
769
|
+
const sigOffset = pubkeyOffset + COMPRESSED_PUBKEY_SIZE;
|
|
770
|
+
const msgOffset = sigOffset + SIGNATURE_SIZE;
|
|
771
|
+
const data = Buffer.alloc(msgOffset + message.length);
|
|
772
|
+
data.writeUInt8(1, 0);
|
|
773
|
+
data.writeUInt8(0, 1);
|
|
774
|
+
let o = headerLen;
|
|
775
|
+
data.writeUInt16LE(sigOffset, o);
|
|
776
|
+
o += 2;
|
|
777
|
+
data.writeUInt16LE(CURRENT_IX, o);
|
|
778
|
+
o += 2;
|
|
779
|
+
data.writeUInt16LE(pubkeyOffset, o);
|
|
780
|
+
o += 2;
|
|
781
|
+
data.writeUInt16LE(CURRENT_IX, o);
|
|
782
|
+
o += 2;
|
|
783
|
+
data.writeUInt16LE(msgOffset, o);
|
|
784
|
+
o += 2;
|
|
785
|
+
data.writeUInt16LE(message.length, o);
|
|
786
|
+
o += 2;
|
|
787
|
+
data.writeUInt16LE(CURRENT_IX, o);
|
|
788
|
+
o += 2;
|
|
789
|
+
Buffer.from(compressed).copy(data, pubkeyOffset);
|
|
790
|
+
Buffer.from(signature).copy(data, sigOffset);
|
|
791
|
+
Buffer.from(message).copy(data, msgOffset);
|
|
792
|
+
return new web3_js.TransactionInstruction({
|
|
793
|
+
keys: [],
|
|
794
|
+
programId: new web3_js.PublicKey(SECP256R1_PROGRAM_ID),
|
|
795
|
+
data
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
function anchorDiscriminator(name) {
|
|
799
|
+
return Buffer.from(sha256.sha256(`global:${name}`).slice(0, 8));
|
|
800
|
+
}
|
|
801
|
+
function u32le(n) {
|
|
802
|
+
const b = Buffer.alloc(4);
|
|
803
|
+
b.writeUInt32LE(n);
|
|
804
|
+
return b;
|
|
805
|
+
}
|
|
806
|
+
function u64le(n) {
|
|
807
|
+
const b = Buffer.alloc(8);
|
|
808
|
+
new DataView(b.buffer, b.byteOffset, 8).setBigUint64(0, BigInt(n), true);
|
|
809
|
+
return b;
|
|
810
|
+
}
|
|
811
|
+
function readU64le(buf, offset) {
|
|
812
|
+
return new DataView(buf.buffer, buf.byteOffset, buf.length).getBigUint64(
|
|
813
|
+
offset,
|
|
814
|
+
true
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
function concatBytes(...parts) {
|
|
818
|
+
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
819
|
+
const out = new Uint8Array(total);
|
|
820
|
+
let off = 0;
|
|
821
|
+
for (const p of parts) {
|
|
822
|
+
out.set(p, off);
|
|
823
|
+
off += p.length;
|
|
322
824
|
}
|
|
323
|
-
return
|
|
825
|
+
return out;
|
|
324
826
|
}
|
|
325
|
-
|
|
827
|
+
function serializeInstruction(ix) {
|
|
828
|
+
const programId = new web3_js.PublicKey(ix.programId).toBuffer();
|
|
829
|
+
const accounts = serializeAccounts(ix.accounts);
|
|
830
|
+
const data = serializeVecU8(ix.data);
|
|
831
|
+
return Buffer.concat([programId, accounts, data]);
|
|
832
|
+
}
|
|
833
|
+
function serializeAccounts(metas) {
|
|
834
|
+
const len = Buffer.alloc(4);
|
|
835
|
+
new DataView(len.buffer).setUint32(0, metas.length, true);
|
|
836
|
+
const parts = metas.map(serializeAccountMeta);
|
|
837
|
+
return Buffer.concat([len, ...parts]);
|
|
838
|
+
}
|
|
839
|
+
function serializeAccountMeta(meta) {
|
|
840
|
+
const pubkey = new web3_js.PublicKey(meta.pubkey).toBuffer();
|
|
841
|
+
return Buffer.concat([pubkey, Buffer.from([meta.isSigner ? 1 : 0, meta.isWritable ? 1 : 0])]);
|
|
842
|
+
}
|
|
843
|
+
function serializeVecU8(data) {
|
|
844
|
+
const len = Buffer.alloc(4);
|
|
845
|
+
new DataView(len.buffer).setUint32(0, data.length, true);
|
|
846
|
+
return Buffer.concat([len, Buffer.from(data)]);
|
|
847
|
+
}
|
|
848
|
+
function serializeInstructions(instructions) {
|
|
849
|
+
return Buffer.concat(instructions.map(serializeInstruction));
|
|
850
|
+
}
|
|
851
|
+
var SolanaRelayer = class {
|
|
326
852
|
constructor(opts) {
|
|
327
853
|
this.opts = opts;
|
|
328
854
|
}
|
|
329
|
-
|
|
330
|
-
|
|
855
|
+
/** The relayer's fee-payer pubkey (fetched + cached from the backend). */
|
|
856
|
+
async getFeePayer() {
|
|
857
|
+
if (this.feePayer) return this.feePayer;
|
|
858
|
+
const res = await fetch(`${this.opts.baseUrl}/api/solana/relay?network=${this.opts.network}`);
|
|
859
|
+
if (!res.ok) throw new Error(`kit/solana: relayer fee-payer lookup failed (${res.status})`);
|
|
860
|
+
const { fee_payer } = await res.json();
|
|
861
|
+
this.feePayer = new web3_js.PublicKey(fee_payer);
|
|
862
|
+
return this.feePayer;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Build a tx with the relayer as fee payer, serialize it unsigned, and POST it
|
|
866
|
+
* to the relayer to co-sign + submit. Returns the confirmed signature.
|
|
867
|
+
*/
|
|
868
|
+
async send(instructions) {
|
|
869
|
+
const feePayer = await this.getFeePayer();
|
|
870
|
+
const { blockhash } = await this.opts.connection.getLatestBlockhash("confirmed");
|
|
871
|
+
const tx2 = new web3_js.Transaction();
|
|
872
|
+
tx2.feePayer = feePayer;
|
|
873
|
+
tx2.recentBlockhash = blockhash;
|
|
874
|
+
tx2.add(...instructions);
|
|
875
|
+
const serialized = tx2.serialize({ requireAllSignatures: false, verifySignatures: false }).toString("base64");
|
|
876
|
+
const res = await fetch(`${this.opts.baseUrl}/api/solana/relay`, {
|
|
331
877
|
method: "POST",
|
|
332
878
|
headers: { "Content-Type": "application/json" },
|
|
333
879
|
body: JSON.stringify({
|
|
334
880
|
app_id: this.opts.appId,
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
new_pub_y: toHex2(params.newSigner.y),
|
|
338
|
-
device_label: params.deviceLabel ?? deviceLabel(),
|
|
339
|
-
...params.email ? { email: params.email } : {}
|
|
881
|
+
network: this.opts.network,
|
|
882
|
+
transaction: serialized
|
|
340
883
|
})
|
|
341
884
|
});
|
|
342
885
|
if (!res.ok) {
|
|
343
|
-
const
|
|
344
|
-
throw new Error(`
|
|
345
|
-
}
|
|
346
|
-
const data = await res.json();
|
|
347
|
-
return { requestId: data.request_id };
|
|
348
|
-
}
|
|
349
|
-
async getPendingRequest(requestId) {
|
|
350
|
-
const url = new URL("/api/devices/request", this.opts.baseUrl);
|
|
351
|
-
url.searchParams.set("id", requestId);
|
|
352
|
-
const res = await fetch(url, { headers: { "Content-Type": "application/json" } });
|
|
353
|
-
if (!res.ok) throw new Error(`getPendingRequest failed: ${res.status}`);
|
|
354
|
-
const data = await res.json();
|
|
355
|
-
if (!data.found) return null;
|
|
356
|
-
const status = data.status;
|
|
357
|
-
return {
|
|
358
|
-
requestId: data.request_id,
|
|
359
|
-
appId: data.app_id,
|
|
360
|
-
userId: "",
|
|
361
|
-
// the approving device already knows its own identity
|
|
362
|
-
accountAddress: data.wallet_address,
|
|
363
|
-
newSigner: { x: fromHex2(data.new_pub_x), y: fromHex2(data.new_pub_y) },
|
|
364
|
-
createdAt: data.created_at,
|
|
365
|
-
status
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
async confirmDeviceAddition(params) {
|
|
369
|
-
const res = await fetch(
|
|
370
|
-
new URL(`/api/devices/request/${params.requestId}/confirm`, this.opts.baseUrl),
|
|
371
|
-
{
|
|
372
|
-
method: "POST",
|
|
373
|
-
headers: { "Content-Type": "application/json" },
|
|
374
|
-
body: JSON.stringify({ tx_hash: params.txHash })
|
|
375
|
-
}
|
|
376
|
-
);
|
|
377
|
-
if (!res.ok) {
|
|
378
|
-
const t = await res.text().catch(() => "");
|
|
379
|
-
throw new Error(`confirmDeviceAddition failed: ${res.status} ${t}`);
|
|
886
|
+
const detail = await res.text().catch(() => "");
|
|
887
|
+
throw new Error(`kit/solana: relay failed (${res.status}) ${detail}`);
|
|
380
888
|
}
|
|
889
|
+
const { signature } = await res.json();
|
|
890
|
+
return signature;
|
|
381
891
|
}
|
|
382
892
|
};
|
|
383
893
|
var BACKUP_KDF_SALT = "cavos-recovery-v1";
|
|
@@ -687,36 +1197,1060 @@ var WORDLIST = [
|
|
|
687
1197
|
"beach",
|
|
688
1198
|
"dusk"
|
|
689
1199
|
];
|
|
690
|
-
function
|
|
691
|
-
const
|
|
692
|
-
|
|
1200
|
+
function batchChallenge(leaves) {
|
|
1201
|
+
const total = leaves.reduce((n, l) => n + l.length, 0);
|
|
1202
|
+
const cat = new Uint8Array(total);
|
|
1203
|
+
let o = 0;
|
|
1204
|
+
for (const l of leaves) {
|
|
1205
|
+
cat.set(l, o);
|
|
1206
|
+
o += l.length;
|
|
1207
|
+
}
|
|
1208
|
+
return sha256.sha256(cat);
|
|
1209
|
+
}
|
|
1210
|
+
function webauthnDigest(authenticatorData, clientDataJSON) {
|
|
1211
|
+
const clientHash = sha256.sha256(clientDataJSON);
|
|
1212
|
+
const msg = new Uint8Array(authenticatorData.length + clientHash.length);
|
|
1213
|
+
msg.set(authenticatorData, 0);
|
|
1214
|
+
msg.set(clientHash, authenticatorData.length);
|
|
1215
|
+
return sha256.sha256(msg);
|
|
1216
|
+
}
|
|
1217
|
+
function recoverCandidatePublicKeys(r, s, digest) {
|
|
1218
|
+
const out = [];
|
|
1219
|
+
for (const bit of [0, 1]) {
|
|
1220
|
+
try {
|
|
1221
|
+
const point = new p256.p256.Signature(r, s).addRecoveryBit(bit).recoverPublicKey(digest).toAffine();
|
|
1222
|
+
out.push({ publicKey: { x: point.x, y: point.y }, yParity: bit === 1 });
|
|
1223
|
+
} catch {
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
return out;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// src/chains/solana/CavosSolana.ts
|
|
1230
|
+
var CavosSolana = class _CavosSolana {
|
|
1231
|
+
constructor(identity, address, status, connection, adapter, devicePubkey, relayer, feePayer) {
|
|
1232
|
+
this.identity = identity;
|
|
1233
|
+
this.address = address;
|
|
1234
|
+
this.status = status;
|
|
1235
|
+
this.connection = connection;
|
|
1236
|
+
this.adapter = adapter;
|
|
1237
|
+
this.devicePubkey = devicePubkey;
|
|
1238
|
+
this.relayer = relayer;
|
|
1239
|
+
this.feePayer = feePayer;
|
|
1240
|
+
/** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
|
|
1241
|
+
this.chain = "solana";
|
|
1242
|
+
}
|
|
1243
|
+
get publicKey() {
|
|
1244
|
+
return this.devicePubkey;
|
|
1245
|
+
}
|
|
1246
|
+
static async connect(opts) {
|
|
1247
|
+
const identity = opts.identity ?? await opts.auth?.authenticate();
|
|
1248
|
+
if (!identity) throw new Error("kit/solana: connect requires `identity` or `auth`");
|
|
1249
|
+
if (opts.network === "solana-mainnet" && !opts.rpcUrl) {
|
|
1250
|
+
console.warn(
|
|
1251
|
+
"[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."
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
const connection = new web3_js.Connection(opts.rpcUrl ?? SOLANA_NETWORKS[opts.network], "confirmed");
|
|
1255
|
+
const signer = opts.createSigner ? await opts.createSigner(`${identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${identity.userId}:${opts.appSalt}` });
|
|
1256
|
+
const devicePubkey = await signer.getPublicKey();
|
|
1257
|
+
const adapter = new SolanaAdapter({ programId: opts.programId, connection, signer });
|
|
1258
|
+
const addressSeed = deriveAddressSeedSolana({ userId: identity.userId, appSalt: opts.appSalt });
|
|
1259
|
+
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
1260
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry);
|
|
1261
|
+
const relayer = opts.relayer ?? (opts.appId ? new SolanaRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network, connection }) : void 0);
|
|
1262
|
+
const existing = await registry.lookup(identity.userId);
|
|
1263
|
+
if (existing) {
|
|
1264
|
+
const isSigner2 = await adapter.isAuthorizedSigner(existing.address, devicePubkey);
|
|
1265
|
+
return new _CavosSolana(
|
|
1266
|
+
identity,
|
|
1267
|
+
existing.address,
|
|
1268
|
+
isSigner2 ? "ready" : "needs-device-approval",
|
|
1269
|
+
connection,
|
|
1270
|
+
adapter,
|
|
1271
|
+
devicePubkey,
|
|
1272
|
+
relayer,
|
|
1273
|
+
opts.feePayer
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
const address = adapter.computeAddress(addressSeed, devicePubkey);
|
|
1277
|
+
const deployed = await connection.getAccountInfo(new web3_js.PublicKey(address)) !== null;
|
|
1278
|
+
if (!deployed) {
|
|
1279
|
+
if (relayer) {
|
|
1280
|
+
const payer = await relayer.getFeePayer();
|
|
1281
|
+
const ix = adapter.buildInitialize(addressSeed, payer.toBase58(), devicePubkey);
|
|
1282
|
+
await relayer.send([ix]);
|
|
1283
|
+
} else if (opts.feePayer) {
|
|
1284
|
+
const ix = adapter.buildInitialize(addressSeed, opts.feePayer.publicKey.toBase58(), devicePubkey);
|
|
1285
|
+
await web3_js.sendAndConfirmTransaction(connection, new web3_js.Transaction().add(ix), [opts.feePayer]);
|
|
1286
|
+
} else {
|
|
1287
|
+
throw new Error("kit/solana: a relayer (appId) or feePayer is required to initialize a new account");
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
|
|
1291
|
+
const isSigner = await adapter.isAuthorizedSigner(address, devicePubkey);
|
|
1292
|
+
return new _CavosSolana(
|
|
1293
|
+
identity,
|
|
1294
|
+
address,
|
|
1295
|
+
isSigner ? "ready" : "needs-device-approval",
|
|
1296
|
+
connection,
|
|
1297
|
+
adapter,
|
|
1298
|
+
devicePubkey,
|
|
1299
|
+
relayer,
|
|
1300
|
+
opts.feePayer
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
/** Authorize an additional device signer (device-signed via precompile). */
|
|
1304
|
+
async addSigner(pubkey) {
|
|
1305
|
+
const ixs = await this.adapter.buildAddSigner(this.address, pubkey);
|
|
1306
|
+
return this.send(ixs);
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Enroll a passkey as an approver (2FA-style step-up). Device-signed + gasless;
|
|
1310
|
+
* requires a ready device. Idempotent. Returns the passkey pubkey + tx hash.
|
|
1311
|
+
*/
|
|
1312
|
+
async enrollPasskey(passkey, params) {
|
|
1313
|
+
const enrolled = await passkey.enroll(params);
|
|
1314
|
+
const { transactionHash } = await this.addApprover(enrolled.publicKey);
|
|
1315
|
+
return { publicKey: enrolled.publicKey, transactionHash };
|
|
1316
|
+
}
|
|
1317
|
+
/** Register an already-enrolled passkey pubkey as an approver (gasless).
|
|
1318
|
+
* Idempotent. Lets one passkey be registered across chains without re-prompting. */
|
|
1319
|
+
async addApprover(pubkey) {
|
|
1320
|
+
if (this.status !== "ready") {
|
|
1321
|
+
throw new Error("kit/solana: addApprover requires a ready, authorized device");
|
|
1322
|
+
}
|
|
1323
|
+
if (await this.adapter.isApprover(this.address, pubkey)) return {};
|
|
1324
|
+
const ixs = await this.adapter.buildAddApprover(this.address, pubkey);
|
|
1325
|
+
const transactionHash = await this.send(ixs);
|
|
1326
|
+
return { transactionHash };
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* From a fresh browser (status `needs-device-approval`), approve adding THIS
|
|
1330
|
+
* device with the user's synced passkey. Gasless via the relayer — the bundle
|
|
1331
|
+
* carries the passkey's WebAuthn assertion, so no device signature is needed.
|
|
1332
|
+
*/
|
|
1333
|
+
async approveThisDeviceWithPasskey(passkey) {
|
|
1334
|
+
if (this.status === "ready") {
|
|
1335
|
+
throw new Error("kit/solana: this device is already an authorized signer");
|
|
1336
|
+
}
|
|
1337
|
+
const { leaf, nonce } = await this.passkeyLeafForThisDevice();
|
|
1338
|
+
const leaves = [leaf];
|
|
1339
|
+
const assertion = await passkey.assert(batchChallenge(leaves));
|
|
1340
|
+
const { transactionHash } = await this.submitPasskeyApproval(assertion, leaves, 0, nonce);
|
|
1341
|
+
return transactionHash;
|
|
1342
|
+
}
|
|
1343
|
+
/** This device's leaf + passkey nonce for a (possibly multi-chain) batch. */
|
|
1344
|
+
async passkeyLeafForThisDevice() {
|
|
1345
|
+
const nonce = await this.adapter.passkeyNonce(this.address);
|
|
1346
|
+
return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
|
|
1347
|
+
}
|
|
1348
|
+
/** Submit `add_signer_via_passkey` given a shared assertion + batch position.
|
|
1349
|
+
* Used by `approveThisDeviceWithPasskey` and `approveDeviceEverywhere`. */
|
|
1350
|
+
async submitPasskeyApproval(assertion, leaves, leafIndex, _nonce) {
|
|
1351
|
+
const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
|
|
1352
|
+
const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
|
|
1353
|
+
let approver = null;
|
|
1354
|
+
for (const cand of candidates) {
|
|
1355
|
+
if (await this.adapter.isApprover(this.address, cand.publicKey)) {
|
|
1356
|
+
approver = cand.publicKey;
|
|
1357
|
+
break;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
if (!approver) throw new Error("kit/solana: this passkey is not a registered approver");
|
|
1361
|
+
const ixs = this.adapter.buildAddSignerViaPasskey(
|
|
1362
|
+
this.address,
|
|
1363
|
+
this.devicePubkey,
|
|
1364
|
+
approver,
|
|
1365
|
+
leaves,
|
|
1366
|
+
leafIndex,
|
|
1367
|
+
assertion
|
|
1368
|
+
);
|
|
1369
|
+
return { transactionHash: await this.send(ixs) };
|
|
1370
|
+
}
|
|
1371
|
+
/** Move `amount` lamports out of the account to `destination` (device-signed). */
|
|
1372
|
+
async execute(amount, destination) {
|
|
1373
|
+
if (this.status !== "ready") {
|
|
1374
|
+
throw new Error("kit/solana: this device is not yet an authorized signer of the wallet");
|
|
1375
|
+
}
|
|
1376
|
+
const ixs = await this.adapter.buildExecuteTransfer(this.address, destination, amount);
|
|
1377
|
+
return this.send(ixs);
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Run arbitrary CPI `instructions` with the account PDA as signer (device-
|
|
1381
|
+
* signed). The signature commits to sha256 of the canonical Borsh
|
|
1382
|
+
* serialization of the instructions, so it binds exactly the operations the
|
|
1383
|
+
* program will invoke. Unlocks SPL transfers, swaps, staking, etc.
|
|
1384
|
+
*
|
|
1385
|
+
* What the relayer will sponsor is constrained by the app's Solana program
|
|
1386
|
+
* allowlist (configured in the dashboard) — programs outside the allowlist are
|
|
1387
|
+
* rejected before co-signing.
|
|
1388
|
+
*/
|
|
1389
|
+
async executeInstructions(instructions) {
|
|
1390
|
+
if (this.status !== "ready") {
|
|
1391
|
+
throw new Error("kit/solana: this device is not yet an authorized signer of the wallet");
|
|
1392
|
+
}
|
|
1393
|
+
const ixs = await this.adapter.buildExecute(this.address, instructions);
|
|
1394
|
+
return this.send(ixs);
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Register the backup signer derived from `code` as an authorized signer of this
|
|
1398
|
+
* account (device-signed via precompile). Idempotent: returns without a tx if
|
|
1399
|
+
* the backup signer is already registered. The code never leaves the device —
|
|
1400
|
+
* only the derived public key travels on-chain.
|
|
1401
|
+
*
|
|
1402
|
+
* Self-custodial: anyone who can re-derive the backup key from the code (i.e.
|
|
1403
|
+
* the rightful owner) can later recover the account with `CavosSolana.recover`.
|
|
1404
|
+
* Run this once, on a registered device, and have the user store the code.
|
|
1405
|
+
*/
|
|
1406
|
+
async setupRecovery(code) {
|
|
1407
|
+
if (this.status !== "ready") {
|
|
1408
|
+
throw new Error("kit/solana: setupRecovery requires a ready, registered device");
|
|
1409
|
+
}
|
|
1410
|
+
const { publicKey: backupPubkey } = deriveBackupKey(code);
|
|
1411
|
+
const already = await this.adapter.isAuthorizedSigner(this.address, backupPubkey);
|
|
1412
|
+
if (already) return void 0;
|
|
1413
|
+
return this.addSigner(backupPubkey);
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Recover an account after losing every device signer. Derives the backup key
|
|
1417
|
+
* from `code`, uses it (not the new device key) to sign an `add_signer` for the
|
|
1418
|
+
* new device, and returns a ready CavosSolana bound to the new device. The
|
|
1419
|
+
* account address is unchanged.
|
|
1420
|
+
*
|
|
1421
|
+
* Self-custodial: only someone holding the code (i.e. the rightful owner) can
|
|
1422
|
+
* re-derive the backup key. The backend never sees the code.
|
|
1423
|
+
*
|
|
1424
|
+
* This mirrors `Cavos.recover` (Starknet): the backup key is just another
|
|
1425
|
+
* authorized signer, so recovery is an `add_signer(newDevice)` bundle signed by
|
|
1426
|
+
* the backup key. The on-chain program needs no recovery-specific entrypoint.
|
|
1427
|
+
*/
|
|
1428
|
+
static async recover(opts) {
|
|
1429
|
+
if (opts.network === "solana-mainnet" && !opts.rpcUrl) {
|
|
1430
|
+
console.warn(
|
|
1431
|
+
"[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."
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
const connection = new web3_js.Connection(opts.rpcUrl ?? SOLANA_NETWORKS[opts.network], "confirmed");
|
|
1435
|
+
const signer = opts.createSigner ? await opts.createSigner(`${opts.identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${opts.identity.userId}:${opts.appSalt}` });
|
|
1436
|
+
const devicePubkey = await signer.getPublicKey();
|
|
1437
|
+
const backup = BackupSigner.fromCode(opts.code);
|
|
1438
|
+
const backupAdapter = new SolanaAdapter({
|
|
1439
|
+
programId: opts.programId,
|
|
1440
|
+
connection,
|
|
1441
|
+
signer: backup
|
|
1442
|
+
});
|
|
1443
|
+
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
1444
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry);
|
|
1445
|
+
const existing = await registry.lookup(opts.identity.userId);
|
|
1446
|
+
if (!existing) {
|
|
1447
|
+
throw new Error("kit/solana: no account found for this identity \u2014 nothing to recover");
|
|
1448
|
+
}
|
|
1449
|
+
const relayer = opts.relayer ?? (opts.appId ? new SolanaRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network, connection }) : void 0);
|
|
1450
|
+
const alreadyAuthed = await backupAdapter.isAuthorizedSigner(existing.address, devicePubkey);
|
|
1451
|
+
if (!alreadyAuthed) {
|
|
1452
|
+
const ixs = await backupAdapter.buildAddSigner(existing.address, devicePubkey);
|
|
1453
|
+
if (relayer) {
|
|
1454
|
+
await relayer.send(ixs);
|
|
1455
|
+
} else if (opts.feePayer) {
|
|
1456
|
+
await web3_js.sendAndConfirmTransaction(connection, new web3_js.Transaction().add(...ixs), [opts.feePayer]);
|
|
1457
|
+
} else {
|
|
1458
|
+
throw new Error("kit/solana: a relayer (appId) or feePayer is required to recover");
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
const adapter = new SolanaAdapter({ programId: opts.programId, connection, signer });
|
|
1462
|
+
return new _CavosSolana(
|
|
1463
|
+
opts.identity,
|
|
1464
|
+
existing.address,
|
|
1465
|
+
"ready",
|
|
1466
|
+
connection,
|
|
1467
|
+
adapter,
|
|
1468
|
+
devicePubkey,
|
|
1469
|
+
relayer,
|
|
1470
|
+
opts.feePayer
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
async send(ixs) {
|
|
1474
|
+
if (this.relayer) return this.relayer.send(ixs);
|
|
1475
|
+
if (this.feePayer) {
|
|
1476
|
+
return web3_js.sendAndConfirmTransaction(this.connection, new web3_js.Transaction().add(...ixs), [this.feePayer]);
|
|
1477
|
+
}
|
|
1478
|
+
throw new Error("kit/solana: no relayer or feePayer configured to submit transactions");
|
|
1479
|
+
}
|
|
1480
|
+
};
|
|
1481
|
+
var defaultRegistry = new InMemoryWalletRegistry();
|
|
1482
|
+
|
|
1483
|
+
// src/chains/stellar/constants.ts
|
|
1484
|
+
var FACTORY_CONTRACT_ID = {
|
|
1485
|
+
// Re-deployed 2026-07-01 with the passkey-approval device-account wasm (batched
|
|
1486
|
+
// multi-chain challenge). The factory pins the wasm hash immutably, so a new
|
|
1487
|
+
// wasm needs a new factory → new account addresses; testnet has no prod wallets.
|
|
1488
|
+
"stellar-testnet": "CBCJIODXIEBOXXD66KCUCF7ZDYJARKI4ZIVQOVWPULOBH5XGNCDP6W3I",
|
|
1489
|
+
// Set once the factory is deployed to mainnet (its address differs — network id
|
|
1490
|
+
// is part of contract-address derivation).
|
|
1491
|
+
"stellar-mainnet": ""
|
|
1492
|
+
};
|
|
1493
|
+
var STELLAR_NETWORKS = {
|
|
1494
|
+
"stellar-testnet": {
|
|
1495
|
+
rpcUrl: "https://soroban-testnet.stellar.org",
|
|
1496
|
+
passphrase: "Test SDF Network ; September 2015"
|
|
1497
|
+
},
|
|
1498
|
+
"stellar-mainnet": {
|
|
1499
|
+
rpcUrl: "https://soroban-rpc.mainnet.stellar.gateway.fm",
|
|
1500
|
+
passphrase: "Public Global Stellar Network ; September 2015"
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
var NATIVE_SAC_ID = {
|
|
1504
|
+
"stellar-testnet": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
|
|
1505
|
+
"stellar-mainnet": "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA"
|
|
1506
|
+
};
|
|
1507
|
+
|
|
1508
|
+
// src/chains/stellar/StellarAdapter.ts
|
|
1509
|
+
var SECP256R1_N2 = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
|
|
1510
|
+
var StellarAdapter = class {
|
|
1511
|
+
constructor(opts) {
|
|
1512
|
+
this.chain = "stellar";
|
|
1513
|
+
this.network = opts.network;
|
|
1514
|
+
this.passphrase = STELLAR_NETWORKS[opts.network].passphrase;
|
|
1515
|
+
this.rpcUrl = opts.rpcUrl ?? STELLAR_NETWORKS[opts.network].rpcUrl;
|
|
1516
|
+
this.factoryId = opts.factoryId ?? FACTORY_CONTRACT_ID[opts.network];
|
|
1517
|
+
if (!this.factoryId) {
|
|
1518
|
+
throw new Error(`kit/stellar: no factory contract id configured for ${opts.network}`);
|
|
1519
|
+
}
|
|
1520
|
+
this.signer = opts.signer;
|
|
1521
|
+
}
|
|
1522
|
+
server() {
|
|
1523
|
+
if (!this._server) {
|
|
1524
|
+
this._server = new stellarSdk.rpc.Server(this.rpcUrl, {
|
|
1525
|
+
allowHttp: this.rpcUrl.startsWith("http://")
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
return this._server;
|
|
1529
|
+
}
|
|
1530
|
+
networkId() {
|
|
1531
|
+
return stellarSdk.hash(Buffer.from(this.passphrase));
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Deterministic account address for `(addressSeed, initialSigner)` — computed
|
|
1535
|
+
* off-chain, byte-identical to the factory's on-chain `account_address`.
|
|
1536
|
+
* `contractId = sha256(HashIdPreimage(networkId, factory, salt))` with
|
|
1537
|
+
* `salt = sha256(addressSeed || sec1(initialSigner))`.
|
|
1538
|
+
*/
|
|
1539
|
+
computeAddress(addressSeed, initialSigner) {
|
|
1540
|
+
const salt = this.accountSalt(addressSeed, initialSigner);
|
|
1541
|
+
const preimage = stellarSdk.xdr.HashIdPreimage.envelopeTypeContractId(
|
|
1542
|
+
new stellarSdk.xdr.HashIdPreimageContractId({
|
|
1543
|
+
networkId: this.networkId(),
|
|
1544
|
+
contractIdPreimage: stellarSdk.xdr.ContractIdPreimage.contractIdPreimageFromAddress(
|
|
1545
|
+
new stellarSdk.xdr.ContractIdPreimageFromAddress({
|
|
1546
|
+
address: new stellarSdk.Address(this.factoryId).toScAddress(),
|
|
1547
|
+
salt
|
|
1548
|
+
})
|
|
1549
|
+
)
|
|
1550
|
+
})
|
|
1551
|
+
);
|
|
1552
|
+
return stellarSdk.StrKey.encodeContract(stellarSdk.hash(preimage.toXDR()));
|
|
1553
|
+
}
|
|
1554
|
+
/** `salt = sha256(addressSeed(32) || sec1(initialSigner)(65))` — matches the factory. */
|
|
1555
|
+
accountSalt(addressSeed, initialSigner) {
|
|
1556
|
+
return stellarSdk.hash(Buffer.concat([Buffer.from(addressSeed), Buffer.from(sec1Pubkey(initialSigner))]));
|
|
1557
|
+
}
|
|
1558
|
+
/** Host function: `factory.deploy(address_seed, initial_signer)`. */
|
|
1559
|
+
buildDeploy(addressSeed, initialSigner) {
|
|
1560
|
+
return invokeFunc(this.factoryId, "deploy", [
|
|
1561
|
+
bytesScVal(addressSeed),
|
|
1562
|
+
bytesScVal(sec1Pubkey(initialSigner))
|
|
1563
|
+
]);
|
|
1564
|
+
}
|
|
1565
|
+
/** Host function: `account.add_signer(new_signer)` (requires device auth). */
|
|
1566
|
+
buildAddSigner(accountAddress, signer) {
|
|
1567
|
+
return invokeFunc(accountAddress, "add_signer", [bytesScVal(sec1Pubkey(signer))]);
|
|
1568
|
+
}
|
|
1569
|
+
/** Host function: `account.remove_signer(signer)` (requires device auth). */
|
|
1570
|
+
buildRemoveSigner(accountAddress, signer) {
|
|
1571
|
+
return invokeFunc(accountAddress, "remove_signer", [bytesScVal(sec1Pubkey(signer))]);
|
|
1572
|
+
}
|
|
1573
|
+
/** Host function: `account.add_approver(passkey)` (requires device auth). */
|
|
1574
|
+
buildAddApprover(accountAddress, passkey) {
|
|
1575
|
+
return invokeFunc(accountAddress, "add_approver", [bytesScVal(sec1Pubkey(passkey))]);
|
|
1576
|
+
}
|
|
1577
|
+
/** Host function: `account.remove_approver(passkey)` (requires device auth). */
|
|
1578
|
+
buildRemoveApprover(accountAddress, passkey) {
|
|
1579
|
+
return invokeFunc(accountAddress, "remove_approver", [bytesScVal(sec1Pubkey(passkey))]);
|
|
1580
|
+
}
|
|
1581
|
+
/** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
|
|
1582
|
+
* `sha256(sec1(new_signer) || nonce_be8)`. The batch challenge the passkey signs
|
|
1583
|
+
* is `sha256(concat(leaves))` across chains. */
|
|
1584
|
+
passkeyLeaf(newSigner, nonce) {
|
|
1585
|
+
const msg = new Uint8Array(65 + 8);
|
|
1586
|
+
msg.set(sec1Pubkey(newSigner), 0);
|
|
1587
|
+
const n = new Uint8Array(8);
|
|
1588
|
+
let v = nonce;
|
|
1589
|
+
for (let i = 7; i >= 0; i--) {
|
|
1590
|
+
n[i] = Number(v & 0xffn);
|
|
1591
|
+
v >>= 8n;
|
|
1592
|
+
}
|
|
1593
|
+
msg.set(n, 65);
|
|
1594
|
+
return sha256.sha256(msg);
|
|
1595
|
+
}
|
|
1596
|
+
/** Host function: passkey-authorized `add_signer_via_passkey` (no device auth —
|
|
1597
|
+
* authorized by the embedded WebAuthn assertion, so any relayer can submit).
|
|
1598
|
+
* `leaves`/`leafIndex` place this chain's leaf in the multi-chain batch. */
|
|
1599
|
+
buildAddSignerViaPasskey(accountAddress, newSigner, passkey, nonce, leaves, leafIndex, assertion) {
|
|
1600
|
+
const sig = encodeLowSSignature2({ r: assertion.r, s: assertion.s});
|
|
1601
|
+
const leavesScVal = stellarSdk.xdr.ScVal.scvVec(leaves.map((l) => bytesScVal(l)));
|
|
1602
|
+
return invokeFunc(accountAddress, "add_signer_via_passkey", [
|
|
1603
|
+
bytesScVal(sec1Pubkey(newSigner)),
|
|
1604
|
+
bytesScVal(sec1Pubkey(passkey)),
|
|
1605
|
+
stellarSdk.nativeToScVal(nonce, { type: "u64" }),
|
|
1606
|
+
leavesScVal,
|
|
1607
|
+
stellarSdk.nativeToScVal(leafIndex, { type: "u32" }),
|
|
1608
|
+
bytesScVal(assertion.authenticatorData),
|
|
1609
|
+
bytesScVal(assertion.clientDataJSON),
|
|
1610
|
+
stellarSdk.nativeToScVal(assertion.challengeOffset, { type: "u32" }),
|
|
1611
|
+
bytesScVal(sig)
|
|
1612
|
+
]);
|
|
1613
|
+
}
|
|
1614
|
+
/** Read whether `passkey` is a registered approver (read-only simulation). */
|
|
1615
|
+
async isApprover(accountAddress, passkey, readSource) {
|
|
1616
|
+
if (!await this.isDeployed(accountAddress)) return false;
|
|
1617
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1618
|
+
const src = new Account3(readSource, "0");
|
|
1619
|
+
const op = stellarSdk.Operation.invokeHostFunction({
|
|
1620
|
+
func: invokeFunc(accountAddress, "is_approver", [bytesScVal(sec1Pubkey(passkey))]),
|
|
1621
|
+
auth: []
|
|
1622
|
+
});
|
|
1623
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1624
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1625
|
+
if (stellarSdk.rpc.Api.isSimulationError(sim)) {
|
|
1626
|
+
throw new Error(`kit/stellar: is_approver simulation failed: ${sim.error}`);
|
|
1627
|
+
}
|
|
1628
|
+
if (!sim.result?.retval) return false;
|
|
1629
|
+
return stellarSdk.scValToNative(sim.result.retval) === true;
|
|
1630
|
+
}
|
|
1631
|
+
/** Read the current passkey-approval nonce (read-only simulation). */
|
|
1632
|
+
async passkeyNonce(accountAddress, readSource) {
|
|
1633
|
+
if (!await this.isDeployed(accountAddress)) return 0n;
|
|
1634
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1635
|
+
const src = new Account3(readSource, "0");
|
|
1636
|
+
const op = stellarSdk.Operation.invokeHostFunction({
|
|
1637
|
+
func: invokeFunc(accountAddress, "passkey_nonce", []),
|
|
1638
|
+
auth: []
|
|
1639
|
+
});
|
|
1640
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1641
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1642
|
+
if (stellarSdk.rpc.Api.isSimulationError(sim)) {
|
|
1643
|
+
throw new Error(`kit/stellar: passkey_nonce simulation failed: ${sim.error}`);
|
|
1644
|
+
}
|
|
1645
|
+
if (!sim.result?.retval) return 0n;
|
|
1646
|
+
return BigInt(stellarSdk.scValToNative(sim.result.retval));
|
|
1647
|
+
}
|
|
1648
|
+
/** Host function: SEP-41 `token.transfer(from=account, to, amount)` (device auth). */
|
|
1649
|
+
buildTransfer(tokenId, accountAddress, destination, amount) {
|
|
1650
|
+
return invokeFunc(tokenId, "transfer", [
|
|
1651
|
+
new stellarSdk.Address(accountAddress).toScVal(),
|
|
1652
|
+
new stellarSdk.Address(destination).toScVal(),
|
|
1653
|
+
stellarSdk.nativeToScVal(amount, { type: "i128" })
|
|
1654
|
+
]);
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Sign a Soroban authorization entry with the silent device key, producing the
|
|
1658
|
+
* `Vec<DeviceSignature>` the account's `__check_auth` verifies. The device
|
|
1659
|
+
* signs `sha256(preimage)` (WebCrypto hashes once more internally), which is
|
|
1660
|
+
* exactly what the contract recomputes. Mutates + returns the entry.
|
|
1661
|
+
*/
|
|
1662
|
+
async signAuthEntry(entry, validUntilLedger) {
|
|
1663
|
+
const addrCreds = entry.credentials().address();
|
|
1664
|
+
addrCreds.signatureExpirationLedger(validUntilLedger);
|
|
1665
|
+
const preimage = stellarSdk.xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
|
|
1666
|
+
new stellarSdk.xdr.HashIdPreimageSorobanAuthorization({
|
|
1667
|
+
networkId: this.networkId(),
|
|
1668
|
+
nonce: addrCreds.nonce(),
|
|
1669
|
+
signatureExpirationLedger: validUntilLedger,
|
|
1670
|
+
invocation: entry.rootInvocation()
|
|
1671
|
+
})
|
|
1672
|
+
);
|
|
1673
|
+
const payload = stellarSdk.hash(preimage.toXDR());
|
|
1674
|
+
const sig = await this.signer.sign(new Uint8Array(payload));
|
|
1675
|
+
const pubkey = await this.signer.getPublicKey();
|
|
1676
|
+
addrCreds.signature(deviceSignatureScVal(pubkey, sig));
|
|
1677
|
+
return entry;
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Read a SEP-41 token balance of `account` via a read-only simulation of
|
|
1681
|
+
* `token.balance(account)`. Returns 0 when the account isn't deployed or holds
|
|
1682
|
+
* none. `readSource` is any funded G-account (used only for the simulation).
|
|
1683
|
+
*/
|
|
1684
|
+
async readBalance(tokenId, account, readSource) {
|
|
1685
|
+
if (!await this.isDeployed(account)) return 0n;
|
|
1686
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1687
|
+
const src = new Account3(readSource, "0");
|
|
1688
|
+
const op = stellarSdk.Operation.invokeHostFunction({
|
|
1689
|
+
func: invokeFunc(tokenId, "balance", [new stellarSdk.Address(account).toScVal()]),
|
|
1690
|
+
auth: []
|
|
1691
|
+
});
|
|
1692
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1693
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1694
|
+
if (stellarSdk.rpc.Api.isSimulationError(sim) || !sim.result?.retval) return 0n;
|
|
1695
|
+
return BigInt(stellarSdk.scValToNative(sim.result.retval));
|
|
1696
|
+
}
|
|
1697
|
+
/** Whether the account contract instance exists on-chain (is deployed). */
|
|
1698
|
+
async isDeployed(accountAddress) {
|
|
1699
|
+
try {
|
|
1700
|
+
const res = await this.server().getContractData(
|
|
1701
|
+
accountAddress,
|
|
1702
|
+
stellarSdk.xdr.ScVal.scvLedgerKeyContractInstance(),
|
|
1703
|
+
stellarSdk.rpc.Durability.Persistent
|
|
1704
|
+
);
|
|
1705
|
+
return !!res;
|
|
1706
|
+
} catch {
|
|
1707
|
+
return false;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Read whether `signer` is a currently-authorized signer of the account, via a
|
|
1712
|
+
* read-only simulation of `account.is_authorized(signer)`. `readSource` is any
|
|
1713
|
+
* funded G-account (used only for the simulation's source/sequence).
|
|
1714
|
+
*/
|
|
1715
|
+
async isAuthorizedSigner(accountAddress, signer, readSource) {
|
|
1716
|
+
if (!await this.isDeployed(accountAddress)) return false;
|
|
1717
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1718
|
+
const src = new Account3(readSource, "0");
|
|
1719
|
+
const op = stellarSdk.Operation.invokeHostFunction({
|
|
1720
|
+
func: invokeFunc(accountAddress, "is_authorized", [bytesScVal(sec1Pubkey(signer))]),
|
|
1721
|
+
auth: []
|
|
1722
|
+
});
|
|
1723
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1724
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1725
|
+
if (stellarSdk.rpc.Api.isSimulationError(sim)) {
|
|
1726
|
+
throw new Error(`kit/stellar: is_authorized simulation failed: ${sim.error}`);
|
|
1727
|
+
}
|
|
1728
|
+
if (!sim.result?.retval) return false;
|
|
1729
|
+
return stellarSdk.scValToNative(sim.result.retval) === true;
|
|
1730
|
+
}
|
|
1731
|
+
};
|
|
1732
|
+
function sec1Pubkey(pk) {
|
|
1733
|
+
const out = new Uint8Array(65);
|
|
1734
|
+
out[0] = 4;
|
|
1735
|
+
out.set(bigIntTo32Bytes(pk.x), 1);
|
|
1736
|
+
out.set(bigIntTo32Bytes(pk.y), 33);
|
|
1737
|
+
return out;
|
|
693
1738
|
}
|
|
694
|
-
function
|
|
695
|
-
const
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1739
|
+
function encodeLowSSignature2(sig) {
|
|
1740
|
+
const lowS = sig.s > SECP256R1_N2 / 2n ? SECP256R1_N2 - sig.s : sig.s;
|
|
1741
|
+
const out = new Uint8Array(64);
|
|
1742
|
+
out.set(bigIntTo32Bytes(sig.r), 0);
|
|
1743
|
+
out.set(bigIntTo32Bytes(lowS), 32);
|
|
1744
|
+
return out;
|
|
1745
|
+
}
|
|
1746
|
+
function deviceSignatureScVal(pubkey, sig) {
|
|
1747
|
+
const element = stellarSdk.nativeToScVal(
|
|
1748
|
+
{
|
|
1749
|
+
public_key: Buffer.from(sec1Pubkey(pubkey)),
|
|
1750
|
+
signature: Buffer.from(encodeLowSSignature2(sig))
|
|
1751
|
+
},
|
|
1752
|
+
{ type: { public_key: ["symbol", "bytes"], signature: ["symbol", "bytes"] } }
|
|
1753
|
+
);
|
|
1754
|
+
return stellarSdk.xdr.ScVal.scvVec([element]);
|
|
1755
|
+
}
|
|
1756
|
+
function invokeFunc(contractId, method, args) {
|
|
1757
|
+
return stellarSdk.xdr.HostFunction.hostFunctionTypeInvokeContract(
|
|
1758
|
+
new stellarSdk.xdr.InvokeContractArgs({
|
|
1759
|
+
contractAddress: new stellarSdk.Address(contractId).toScAddress(),
|
|
1760
|
+
functionName: method,
|
|
1761
|
+
args
|
|
1762
|
+
})
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
function bytesScVal(bytes) {
|
|
1766
|
+
return stellarSdk.xdr.ScVal.scvBytes(Buffer.from(bytes));
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// src/chains/stellar/StellarRelayer.ts
|
|
1770
|
+
var StellarRelayer = class {
|
|
1771
|
+
constructor(opts) {
|
|
1772
|
+
this.opts = opts;
|
|
701
1773
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
1774
|
+
/** The relayer's source/fee-payer G-account (fetched + cached from the backend). */
|
|
1775
|
+
async getSource() {
|
|
1776
|
+
if (this.source) return this.source;
|
|
1777
|
+
const res = await fetch(`${this.opts.baseUrl}/api/stellar/relay?network=${this.opts.network}`);
|
|
1778
|
+
if (!res.ok) throw new Error(`kit/stellar: relayer source lookup failed (${res.status})`);
|
|
1779
|
+
const { fee_payer } = await res.json();
|
|
1780
|
+
this.source = fee_payer;
|
|
1781
|
+
return this.source;
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* POST the assembled, device-authorized transaction XDR to the relayer to sign
|
|
1785
|
+
* the envelope + submit. Returns the confirmed transaction hash.
|
|
1786
|
+
*/
|
|
1787
|
+
async submit(transactionXdr) {
|
|
1788
|
+
const res = await fetch(`${this.opts.baseUrl}/api/stellar/relay`, {
|
|
1789
|
+
method: "POST",
|
|
1790
|
+
headers: { "Content-Type": "application/json" },
|
|
1791
|
+
body: JSON.stringify({
|
|
1792
|
+
app_id: this.opts.appId,
|
|
1793
|
+
network: this.opts.network,
|
|
1794
|
+
transaction: transactionXdr
|
|
1795
|
+
})
|
|
1796
|
+
});
|
|
1797
|
+
if (!res.ok) {
|
|
1798
|
+
const detail = await res.text().catch(() => "");
|
|
1799
|
+
throw new Error(`kit/stellar: relay failed (${res.status}) ${detail}`);
|
|
1800
|
+
}
|
|
1801
|
+
const { hash: hash6 } = await res.json();
|
|
1802
|
+
return hash6;
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
|
|
1806
|
+
// src/chains/stellar/CavosStellar.ts
|
|
1807
|
+
var CavosStellar = class _CavosStellar {
|
|
1808
|
+
constructor(identity, address, status, network, adapter, devicePubkey, relayer, sourceKeypair) {
|
|
1809
|
+
this.identity = identity;
|
|
1810
|
+
this.address = address;
|
|
1811
|
+
this.status = status;
|
|
1812
|
+
this.network = network;
|
|
1813
|
+
this.adapter = adapter;
|
|
1814
|
+
this.devicePubkey = devicePubkey;
|
|
1815
|
+
this.relayer = relayer;
|
|
1816
|
+
this.sourceKeypair = sourceKeypair;
|
|
1817
|
+
/** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
|
|
1818
|
+
this.chain = "stellar";
|
|
1819
|
+
}
|
|
1820
|
+
get publicKey() {
|
|
1821
|
+
return this.devicePubkey;
|
|
1822
|
+
}
|
|
1823
|
+
static async connect(opts) {
|
|
1824
|
+
const identity = opts.identity ?? await opts.auth?.authenticate();
|
|
1825
|
+
if (!identity) throw new Error("kit/stellar: connect requires `identity` or `auth`");
|
|
1826
|
+
const signer = opts.createSigner ? await opts.createSigner(`${identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${identity.userId}:${opts.appSalt}` });
|
|
1827
|
+
const devicePubkey = await signer.getPublicKey();
|
|
1828
|
+
const adapter = new StellarAdapter({
|
|
1829
|
+
network: opts.network,
|
|
1830
|
+
rpcUrl: opts.rpcUrl,
|
|
1831
|
+
factoryId: opts.factoryId,
|
|
1832
|
+
signer
|
|
1833
|
+
});
|
|
1834
|
+
const addressSeed = deriveAddressSeedStellar({ userId: identity.userId, appSalt: opts.appSalt });
|
|
1835
|
+
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
1836
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
|
|
1837
|
+
const relayer = opts.relayer ?? (opts.appId ? new StellarRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : void 0);
|
|
1838
|
+
const build = (address2, status) => new _CavosStellar(identity, address2, status, opts.network, adapter, devicePubkey, relayer, opts.sourceKeypair);
|
|
1839
|
+
const self = build("", "needs-device-approval");
|
|
1840
|
+
const readSource = await self.resolveSource();
|
|
1841
|
+
const existing = await registry.lookup(identity.userId);
|
|
1842
|
+
if (existing) {
|
|
1843
|
+
const isSigner2 = await adapter.isAuthorizedSigner(existing.address, devicePubkey, readSource);
|
|
1844
|
+
return build(existing.address, isSigner2 ? "ready" : "needs-device-approval");
|
|
1845
|
+
}
|
|
1846
|
+
const address = adapter.computeAddress(addressSeed, devicePubkey);
|
|
1847
|
+
if (!await adapter.isDeployed(address)) {
|
|
1848
|
+
const func = adapter.buildDeploy(addressSeed, devicePubkey);
|
|
1849
|
+
await self.submitHostFunction(func, void 0);
|
|
1850
|
+
}
|
|
1851
|
+
await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
|
|
1852
|
+
const isSigner = await adapter.isAuthorizedSigner(address, devicePubkey, readSource);
|
|
1853
|
+
return build(address, isSigner ? "ready" : "needs-device-approval");
|
|
1854
|
+
}
|
|
1855
|
+
/** Authorize an additional device signer (device-signed via `__check_auth`). */
|
|
1856
|
+
async addSigner(pubkey) {
|
|
1857
|
+
const func = this.adapter.buildAddSigner(this.address, pubkey);
|
|
1858
|
+
return this.submitHostFunction(func, this.address);
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Enroll a passkey as an approver (2FA-style step-up). Device-signed + gasless;
|
|
1862
|
+
* requires a ready device. Idempotent. Returns the passkey pubkey + tx hash.
|
|
1863
|
+
*/
|
|
1864
|
+
async enrollPasskey(passkey, params) {
|
|
1865
|
+
const enrolled = await passkey.enroll(params);
|
|
1866
|
+
const { transactionHash } = await this.addApprover(enrolled.publicKey);
|
|
1867
|
+
return { publicKey: enrolled.publicKey, transactionHash };
|
|
1868
|
+
}
|
|
1869
|
+
/** Register an already-enrolled passkey pubkey as an approver (gasless).
|
|
1870
|
+
* Idempotent. Lets one passkey be registered across chains without re-prompting. */
|
|
1871
|
+
async addApprover(pubkey) {
|
|
1872
|
+
if (this.status !== "ready") {
|
|
1873
|
+
throw new Error("kit/stellar: addApprover requires a ready, authorized device");
|
|
1874
|
+
}
|
|
1875
|
+
const readSource = await this.resolveSource();
|
|
1876
|
+
if (await this.adapter.isApprover(this.address, pubkey, readSource)) return {};
|
|
1877
|
+
const func = this.adapter.buildAddApprover(this.address, pubkey);
|
|
1878
|
+
const transactionHash = await this.submitHostFunction(func, this.address);
|
|
1879
|
+
return { transactionHash };
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* From a fresh browser (status `needs-device-approval`), approve adding THIS
|
|
1883
|
+
* device using the user's synced passkey. Gasless via the relayer — the call
|
|
1884
|
+
* carries the WebAuthn assertion, so no device signature is needed. Returns the
|
|
1885
|
+
* tx hash. No trip back to an already-authorized device.
|
|
1886
|
+
*/
|
|
1887
|
+
async approveThisDeviceWithPasskey(passkey) {
|
|
1888
|
+
if (this.status === "ready") {
|
|
1889
|
+
throw new Error("kit/stellar: this device is already an authorized signer");
|
|
1890
|
+
}
|
|
1891
|
+
const { leaf, nonce } = await this.passkeyLeafForThisDevice();
|
|
1892
|
+
const leaves = [leaf];
|
|
1893
|
+
const assertion = await passkey.assert(batchChallenge(leaves));
|
|
1894
|
+
const { transactionHash } = await this.submitPasskeyApproval(assertion, leaves, 0, nonce);
|
|
1895
|
+
return transactionHash;
|
|
1896
|
+
}
|
|
1897
|
+
/** This device's leaf + passkey nonce for a (possibly multi-chain) batch. */
|
|
1898
|
+
async passkeyLeafForThisDevice() {
|
|
1899
|
+
const readSource = await this.resolveSource();
|
|
1900
|
+
const nonce = await this.adapter.passkeyNonce(this.address, readSource);
|
|
1901
|
+
return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
|
|
1902
|
+
}
|
|
1903
|
+
/** Submit `add_signer_via_passkey` given a shared assertion + batch position.
|
|
1904
|
+
* No device auth entry — authorized purely by the passkey assertion. */
|
|
1905
|
+
async submitPasskeyApproval(assertion, leaves, leafIndex, nonce) {
|
|
1906
|
+
const readSource = await this.resolveSource();
|
|
1907
|
+
const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
|
|
1908
|
+
const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
|
|
1909
|
+
let approver = null;
|
|
1910
|
+
for (const cand of candidates) {
|
|
1911
|
+
if (await this.adapter.isApprover(this.address, cand.publicKey, readSource)) {
|
|
1912
|
+
approver = cand.publicKey;
|
|
1913
|
+
break;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
if (!approver) throw new Error("kit/stellar: this passkey is not a registered approver");
|
|
1917
|
+
const func = this.adapter.buildAddSignerViaPasskey(
|
|
1918
|
+
this.address,
|
|
1919
|
+
this.devicePubkey,
|
|
1920
|
+
approver,
|
|
1921
|
+
nonce,
|
|
1922
|
+
leaves,
|
|
1923
|
+
leafIndex,
|
|
1924
|
+
assertion
|
|
1925
|
+
);
|
|
1926
|
+
return { transactionHash: await this.submitHostFunction(func, void 0) };
|
|
1927
|
+
}
|
|
1928
|
+
/** Move `amount` stroops of native XLM to `destination` (device-signed). */
|
|
1929
|
+
async execute(amount, destination) {
|
|
1930
|
+
return this.executeTransfer(NATIVE_SAC_ID[this.network], amount, destination);
|
|
1931
|
+
}
|
|
1932
|
+
/** Read this account's balance of `tokenId` (defaults to native XLM), in stroops. */
|
|
1933
|
+
async balance(tokenId = NATIVE_SAC_ID[this.network]) {
|
|
1934
|
+
const readSource = await this.resolveSource();
|
|
1935
|
+
return this.adapter.readBalance(tokenId, this.address, readSource);
|
|
1936
|
+
}
|
|
1937
|
+
/** Transfer `amount` of any SEP-41 token out of the account (device-signed). */
|
|
1938
|
+
async executeTransfer(tokenId, amount, destination) {
|
|
1939
|
+
if (this.status !== "ready") {
|
|
1940
|
+
throw new Error("kit/stellar: this device is not yet an authorized signer of the wallet");
|
|
1941
|
+
}
|
|
1942
|
+
const func = this.adapter.buildTransfer(tokenId, this.address, destination, amount);
|
|
1943
|
+
return this.submitHostFunction(func, this.address);
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Register the backup signer derived from `code` as an authorized signer of
|
|
1947
|
+
* this account (device-signed). Idempotent. The code never leaves the device —
|
|
1948
|
+
* only the derived public key travels on-chain. Mirrors the other chains.
|
|
1949
|
+
*/
|
|
1950
|
+
async setupRecovery(code) {
|
|
1951
|
+
if (this.status !== "ready") {
|
|
1952
|
+
throw new Error("kit/stellar: setupRecovery requires a ready, registered device");
|
|
1953
|
+
}
|
|
1954
|
+
const { publicKey: backupPubkey } = deriveBackupKey(code);
|
|
1955
|
+
const readSource = await this.resolveSource();
|
|
1956
|
+
if (await this.adapter.isAuthorizedSigner(this.address, backupPubkey, readSource)) return void 0;
|
|
1957
|
+
return this.addSigner(backupPubkey);
|
|
1958
|
+
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Recover an account after losing every device signer: derive the backup key
|
|
1961
|
+
* from `code`, use it (not the new device) to authorize `add_signer(newDevice)`,
|
|
1962
|
+
* and return a ready handle bound to the new device. The address is unchanged.
|
|
1963
|
+
*/
|
|
1964
|
+
static async recover(opts) {
|
|
1965
|
+
const signer = opts.createSigner ? await opts.createSigner(`${opts.identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${opts.identity.userId}:${opts.appSalt}` });
|
|
1966
|
+
const devicePubkey = await signer.getPublicKey();
|
|
1967
|
+
const backup = BackupSigner.fromCode(opts.code);
|
|
1968
|
+
const backupAdapter = new StellarAdapter({
|
|
1969
|
+
network: opts.network,
|
|
1970
|
+
rpcUrl: opts.rpcUrl,
|
|
1971
|
+
factoryId: opts.factoryId,
|
|
1972
|
+
signer: backup
|
|
1973
|
+
});
|
|
1974
|
+
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
1975
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
|
|
1976
|
+
const existing = await registry.lookup(opts.identity.userId);
|
|
1977
|
+
if (!existing) {
|
|
1978
|
+
throw new Error("kit/stellar: no account found for this identity \u2014 nothing to recover");
|
|
1979
|
+
}
|
|
1980
|
+
const relayer = opts.relayer ?? (opts.appId ? new StellarRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : void 0);
|
|
1981
|
+
const backupHandle = new _CavosStellar(
|
|
1982
|
+
opts.identity,
|
|
1983
|
+
existing.address,
|
|
1984
|
+
"ready",
|
|
1985
|
+
opts.network,
|
|
1986
|
+
backupAdapter,
|
|
1987
|
+
devicePubkey,
|
|
1988
|
+
relayer,
|
|
1989
|
+
opts.sourceKeypair
|
|
1990
|
+
);
|
|
1991
|
+
const readSource = await backupHandle.resolveSource();
|
|
1992
|
+
if (!await backupAdapter.isAuthorizedSigner(existing.address, devicePubkey, readSource)) {
|
|
1993
|
+
await backupHandle.addSigner(devicePubkey);
|
|
1994
|
+
}
|
|
1995
|
+
const adapter = new StellarAdapter({
|
|
1996
|
+
network: opts.network,
|
|
1997
|
+
rpcUrl: opts.rpcUrl,
|
|
1998
|
+
factoryId: opts.factoryId,
|
|
1999
|
+
signer
|
|
2000
|
+
});
|
|
2001
|
+
return new _CavosStellar(
|
|
2002
|
+
opts.identity,
|
|
2003
|
+
existing.address,
|
|
2004
|
+
"ready",
|
|
2005
|
+
opts.network,
|
|
2006
|
+
adapter,
|
|
2007
|
+
devicePubkey,
|
|
2008
|
+
relayer,
|
|
2009
|
+
opts.sourceKeypair
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
/** The transaction source/fee-payer G-address (relayer or self-funded). */
|
|
2013
|
+
async resolveSource() {
|
|
2014
|
+
if (this.relayer) return this.relayer.getSource();
|
|
2015
|
+
if (this.sourceKeypair) return this.sourceKeypair.publicKey();
|
|
2016
|
+
throw new Error("kit/stellar: a relayer (appId) or sourceKeypair is required");
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Build → simulate → device-sign auth → assemble → submit an invoke-contract
|
|
2020
|
+
* host function. `authAccount` is the account whose `__check_auth` must sign the
|
|
2021
|
+
* operation's Soroban auth entry (undefined for a plain factory deploy).
|
|
2022
|
+
*/
|
|
2023
|
+
async submitHostFunction(func, authAccount) {
|
|
2024
|
+
const server = this.adapter.server();
|
|
2025
|
+
const sourceAddr = await this.resolveSource();
|
|
2026
|
+
const simSource = new stellarSdk.Account(sourceAddr, "0");
|
|
2027
|
+
const unsignedOp = stellarSdk.Operation.invokeHostFunction({ func, auth: [] });
|
|
2028
|
+
const simTx = new stellarSdk.TransactionBuilder(simSource, {
|
|
2029
|
+
fee: stellarSdk.BASE_FEE,
|
|
2030
|
+
networkPassphrase: this.adapter.passphrase
|
|
2031
|
+
}).addOperation(unsignedOp).setTimeout(180).build();
|
|
2032
|
+
const sim = await server.simulateTransaction(simTx);
|
|
2033
|
+
if (stellarSdk.rpc.Api.isSimulationError(sim)) {
|
|
2034
|
+
throw new Error(`kit/stellar: simulation failed: ${sim.error}`);
|
|
2035
|
+
}
|
|
2036
|
+
const validUntil = (await server.getLatestLedger()).sequence + 100;
|
|
2037
|
+
const entries = sim.result?.auth ?? [];
|
|
2038
|
+
const signedAuth = [];
|
|
2039
|
+
for (const entry of entries) {
|
|
2040
|
+
if (authAccount && isAddressCredentialFor(entry, authAccount)) {
|
|
2041
|
+
signedAuth.push(await this.adapter.signAuthEntry(entry, validUntil));
|
|
2042
|
+
} else {
|
|
2043
|
+
signedAuth.push(entry);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
const account = await server.getAccount(sourceAddr);
|
|
2047
|
+
const finalOp = stellarSdk.Operation.invokeHostFunction({ func, auth: signedAuth });
|
|
2048
|
+
const built = new stellarSdk.TransactionBuilder(account, {
|
|
2049
|
+
fee: stellarSdk.BASE_FEE,
|
|
2050
|
+
networkPassphrase: this.adapter.passphrase
|
|
2051
|
+
}).addOperation(finalOp).setTimeout(180).build();
|
|
2052
|
+
const authSim = await server.simulateTransaction(built);
|
|
2053
|
+
if (stellarSdk.rpc.Api.isSimulationError(authSim)) {
|
|
2054
|
+
throw new Error(`kit/stellar: auth simulation failed: ${authSim.error}`);
|
|
2055
|
+
}
|
|
2056
|
+
const assembled = stellarSdk.rpc.assembleTransaction(built, authSim).build();
|
|
2057
|
+
if (this.relayer) {
|
|
2058
|
+
return this.relayer.submit(assembled.toXDR());
|
|
2059
|
+
}
|
|
2060
|
+
if (this.sourceKeypair) {
|
|
2061
|
+
assembled.sign(this.sourceKeypair);
|
|
2062
|
+
return this.sendAndConfirm(assembled);
|
|
2063
|
+
}
|
|
2064
|
+
throw new Error("kit/stellar: no relayer or sourceKeypair configured to submit");
|
|
2065
|
+
}
|
|
2066
|
+
/** Submit a signed tx via RPC and poll to confirmation. Returns the hash. */
|
|
2067
|
+
async sendAndConfirm(tx2) {
|
|
2068
|
+
const server = this.adapter.server();
|
|
2069
|
+
const sent = await server.sendTransaction(tx2);
|
|
2070
|
+
if (sent.status === "ERROR") {
|
|
2071
|
+
throw new Error(`kit/stellar: submit rejected: ${JSON.stringify(sent.errorResult)}`);
|
|
2072
|
+
}
|
|
2073
|
+
const hash6 = sent.hash;
|
|
2074
|
+
for (let i = 0; i < 30; i++) {
|
|
2075
|
+
const got = await server.getTransaction(hash6);
|
|
2076
|
+
if (got.status === stellarSdk.rpc.Api.GetTransactionStatus.SUCCESS) return hash6;
|
|
2077
|
+
if (got.status === stellarSdk.rpc.Api.GetTransactionStatus.FAILED) {
|
|
2078
|
+
throw new Error(`kit/stellar: tx ${hash6} failed`);
|
|
2079
|
+
}
|
|
2080
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
2081
|
+
}
|
|
2082
|
+
throw new Error(`kit/stellar: tx ${hash6} not confirmed in time`);
|
|
2083
|
+
}
|
|
2084
|
+
};
|
|
2085
|
+
function isAddressCredentialFor(entry, accountAddress) {
|
|
2086
|
+
const creds = entry.credentials();
|
|
2087
|
+
if (creds.switch() !== stellarSdk.xdr.SorobanCredentialsType.sorobanCredentialsAddress()) return false;
|
|
2088
|
+
return stellarSdk.Address.fromScAddress(creds.address().address()).toString() === accountAddress;
|
|
2089
|
+
}
|
|
2090
|
+
var defaultRegistry2 = new InMemoryWalletRegistry();
|
|
2091
|
+
|
|
2092
|
+
// src/recovery/HttpRecoveryClient.ts
|
|
2093
|
+
function toHex2(n) {
|
|
2094
|
+
return "0x" + n.toString(16);
|
|
2095
|
+
}
|
|
2096
|
+
function fromHex2(s) {
|
|
2097
|
+
return BigInt(s);
|
|
705
2098
|
}
|
|
2099
|
+
function deviceLabel() {
|
|
2100
|
+
if (typeof navigator !== "undefined") {
|
|
2101
|
+
return navigator.userAgent || "a new device";
|
|
2102
|
+
}
|
|
2103
|
+
return "a new device";
|
|
2104
|
+
}
|
|
2105
|
+
var HttpRecoveryClient = class {
|
|
2106
|
+
constructor(opts) {
|
|
2107
|
+
this.opts = opts;
|
|
2108
|
+
}
|
|
2109
|
+
async requestDeviceAddition(params) {
|
|
2110
|
+
const res = await fetch(new URL("/api/devices/request", this.opts.baseUrl), {
|
|
2111
|
+
method: "POST",
|
|
2112
|
+
headers: { "Content-Type": "application/json" },
|
|
2113
|
+
body: JSON.stringify({
|
|
2114
|
+
app_id: this.opts.appId,
|
|
2115
|
+
wallet_address: params.accountAddress,
|
|
2116
|
+
new_pub_x: toHex2(params.newSigner.x),
|
|
2117
|
+
new_pub_y: toHex2(params.newSigner.y),
|
|
2118
|
+
device_label: params.deviceLabel ?? deviceLabel(),
|
|
2119
|
+
...params.email ? { email: params.email } : {}
|
|
2120
|
+
})
|
|
2121
|
+
});
|
|
2122
|
+
if (!res.ok) {
|
|
2123
|
+
const t = await res.text().catch(() => "");
|
|
2124
|
+
throw new Error(`requestDeviceAddition failed: ${res.status} ${t}`);
|
|
2125
|
+
}
|
|
2126
|
+
const data = await res.json();
|
|
2127
|
+
return { requestId: data.request_id };
|
|
2128
|
+
}
|
|
2129
|
+
async getPendingRequest(requestId) {
|
|
2130
|
+
const url = new URL("/api/devices/request", this.opts.baseUrl);
|
|
2131
|
+
url.searchParams.set("id", requestId);
|
|
2132
|
+
const res = await fetch(url, { headers: { "Content-Type": "application/json" } });
|
|
2133
|
+
if (!res.ok) throw new Error(`getPendingRequest failed: ${res.status}`);
|
|
2134
|
+
const data = await res.json();
|
|
2135
|
+
if (!data.found) return null;
|
|
2136
|
+
const status = data.status;
|
|
2137
|
+
return {
|
|
2138
|
+
requestId: data.request_id,
|
|
2139
|
+
appId: data.app_id,
|
|
2140
|
+
userId: "",
|
|
2141
|
+
// the approving device already knows its own identity
|
|
2142
|
+
accountAddress: data.wallet_address,
|
|
2143
|
+
newSigner: { x: fromHex2(data.new_pub_x), y: fromHex2(data.new_pub_y) },
|
|
2144
|
+
createdAt: data.created_at,
|
|
2145
|
+
status
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
async confirmDeviceAddition(params) {
|
|
2149
|
+
const res = await fetch(
|
|
2150
|
+
new URL(`/api/devices/request/${params.requestId}/confirm`, this.opts.baseUrl),
|
|
2151
|
+
{
|
|
2152
|
+
method: "POST",
|
|
2153
|
+
headers: { "Content-Type": "application/json" },
|
|
2154
|
+
body: JSON.stringify({ tx_hash: params.txHash })
|
|
2155
|
+
}
|
|
2156
|
+
);
|
|
2157
|
+
if (!res.ok) {
|
|
2158
|
+
const t = await res.text().catch(() => "");
|
|
2159
|
+
throw new Error(`confirmDeviceAddition failed: ${res.status} ${t}`);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
};
|
|
706
2163
|
|
|
707
2164
|
// src/Cavos.ts
|
|
2165
|
+
var STARKNET_ENV = {
|
|
2166
|
+
mainnet: "mainnet",
|
|
2167
|
+
testnet: "sepolia"
|
|
2168
|
+
};
|
|
2169
|
+
var SOLANA_ENV = {
|
|
2170
|
+
mainnet: "solana-mainnet",
|
|
2171
|
+
testnet: "solana-devnet"
|
|
2172
|
+
};
|
|
2173
|
+
var STELLAR_ENV = {
|
|
2174
|
+
mainnet: "stellar-mainnet",
|
|
2175
|
+
testnet: "stellar-testnet"
|
|
2176
|
+
};
|
|
708
2177
|
var Cavos = class _Cavos {
|
|
709
|
-
constructor(identity, address, status, account, adapter, devicePubkey) {
|
|
2178
|
+
constructor(identity, address, status, account, adapter, devicePubkey, paymaster) {
|
|
710
2179
|
this.identity = identity;
|
|
711
2180
|
this.address = address;
|
|
712
2181
|
this.status = status;
|
|
713
2182
|
this.account = account;
|
|
714
2183
|
this.adapter = adapter;
|
|
715
2184
|
this.devicePubkey = devicePubkey;
|
|
2185
|
+
this.paymaster = paymaster;
|
|
2186
|
+
/** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
|
|
2187
|
+
this.chain = "starknet";
|
|
716
2188
|
/** Request id of the pending device-addition, when status is needs-device-approval. */
|
|
717
2189
|
this.pendingRequestId = null;
|
|
718
2190
|
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Unified entry point. Pick a `chain` and an `network` environment; the kit
|
|
2193
|
+
* resolves the concrete network (sepolia/devnet for testnet, mainnet for
|
|
2194
|
+
* mainnet) and returns a chain-native wallet. The result is a discriminated
|
|
2195
|
+
* union (`wallet.chain`), so `execute()` keeps each chain's native signature:
|
|
2196
|
+
*
|
|
2197
|
+
* const wallet = await Cavos.connect({ chain: "solana", network: "testnet", identity, appSalt, appId });
|
|
2198
|
+
* if (wallet.chain === "starknet") await wallet.execute(calls);
|
|
2199
|
+
* else await wallet.execute(amount, dest);
|
|
2200
|
+
*/
|
|
719
2201
|
static async connect(opts) {
|
|
2202
|
+
if (opts.chain === "solana") {
|
|
2203
|
+
return CavosSolana.connect({
|
|
2204
|
+
network: SOLANA_ENV[opts.network],
|
|
2205
|
+
...opts.auth ? { auth: opts.auth } : {},
|
|
2206
|
+
...opts.identity ? { identity: opts.identity } : {},
|
|
2207
|
+
appSalt: opts.appSalt,
|
|
2208
|
+
...opts.appId ? { appId: opts.appId } : {},
|
|
2209
|
+
...opts.backendUrl ? { backendUrl: opts.backendUrl } : {},
|
|
2210
|
+
...opts.registry ? { registry: opts.registry } : {},
|
|
2211
|
+
...opts.rpcUrl ? { rpcUrl: opts.rpcUrl } : {},
|
|
2212
|
+
...opts.programId ? { programId: opts.programId } : {},
|
|
2213
|
+
...opts.createSigner ? { createSigner: opts.createSigner } : {},
|
|
2214
|
+
...opts.relayer ? { relayer: opts.relayer } : {},
|
|
2215
|
+
...opts.feePayer ? { feePayer: opts.feePayer } : {}
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
if (opts.chain === "stellar") {
|
|
2219
|
+
return CavosStellar.connect({
|
|
2220
|
+
network: STELLAR_ENV[opts.network],
|
|
2221
|
+
...opts.auth ? { auth: opts.auth } : {},
|
|
2222
|
+
...opts.identity ? { identity: opts.identity } : {},
|
|
2223
|
+
appSalt: opts.appSalt,
|
|
2224
|
+
...opts.appId ? { appId: opts.appId } : {},
|
|
2225
|
+
...opts.backendUrl ? { backendUrl: opts.backendUrl } : {},
|
|
2226
|
+
...opts.registry ? { registry: opts.registry } : {},
|
|
2227
|
+
...opts.rpcUrl ? { rpcUrl: opts.rpcUrl } : {},
|
|
2228
|
+
...opts.factoryId ? { factoryId: opts.factoryId } : {},
|
|
2229
|
+
...opts.createSigner ? { createSigner: opts.createSigner } : {},
|
|
2230
|
+
...opts.stellarRelayer ? { relayer: opts.stellarRelayer } : {},
|
|
2231
|
+
...opts.stellarSourceKeypair ? { sourceKeypair: opts.stellarSourceKeypair } : {}
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
if (!opts.paymasterApiKey) {
|
|
2235
|
+
throw new Error("kit: `paymasterApiKey` is required for Starknet connections");
|
|
2236
|
+
}
|
|
2237
|
+
return _Cavos.connectStarknet({
|
|
2238
|
+
network: STARKNET_ENV[opts.network],
|
|
2239
|
+
auth: opts.auth,
|
|
2240
|
+
identity: opts.identity,
|
|
2241
|
+
appSalt: opts.appSalt,
|
|
2242
|
+
appId: opts.appId,
|
|
2243
|
+
backendUrl: opts.backendUrl,
|
|
2244
|
+
registry: opts.registry,
|
|
2245
|
+
recovery: opts.recovery,
|
|
2246
|
+
paymasterApiKey: opts.paymasterApiKey,
|
|
2247
|
+
paymasterUrl: opts.paymasterUrl,
|
|
2248
|
+
rpcUrl: opts.rpcUrl,
|
|
2249
|
+
classHash: opts.classHash,
|
|
2250
|
+
createSigner: opts.createSigner
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
static async connectStarknet(opts) {
|
|
720
2254
|
const identity = opts.identity ?? await opts.auth?.authenticate();
|
|
721
2255
|
if (!identity) throw new Error("kit: connect requires `identity` or `auth`");
|
|
722
2256
|
const classHash = opts.classHash ?? DEVICE_ACCOUNT_CLASS_HASH[opts.network];
|
|
@@ -724,8 +2258,10 @@ var Cavos = class _Cavos {
|
|
|
724
2258
|
const provider = new starknet.RpcProvider({
|
|
725
2259
|
nodeUrl: opts.rpcUrl ?? STARKNET_NETWORKS[opts.network].rpcUrl
|
|
726
2260
|
});
|
|
2261
|
+
const paymasterUrl = opts.paymasterUrl ?? CAVOS_PAYMASTER_URL[opts.network];
|
|
2262
|
+
const paymasterConfig = { url: paymasterUrl, apiKey: opts.paymasterApiKey };
|
|
727
2263
|
const paymaster = new starknet.PaymasterRpc({
|
|
728
|
-
nodeUrl:
|
|
2264
|
+
nodeUrl: paymasterUrl,
|
|
729
2265
|
headers: { "x-paymaster-api-key": opts.paymasterApiKey }
|
|
730
2266
|
});
|
|
731
2267
|
const addressSeed = deriveAddressSeed({ userId: identity.userId, appSalt: opts.appSalt });
|
|
@@ -740,7 +2276,7 @@ var Cavos = class _Cavos {
|
|
|
740
2276
|
cairoVersion: "1"
|
|
741
2277
|
});
|
|
742
2278
|
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
743
|
-
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) :
|
|
2279
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry3);
|
|
744
2280
|
const recovery = opts.recovery ?? (opts.appId ? new HttpRecoveryClient({ baseUrl: backendUrl, appId: opts.appId }) : null);
|
|
745
2281
|
const existing = await registry.lookup(identity.userId);
|
|
746
2282
|
if (existing) {
|
|
@@ -752,7 +2288,8 @@ var Cavos = class _Cavos {
|
|
|
752
2288
|
isSigner2 ? "ready" : "needs-device-approval",
|
|
753
2289
|
account2,
|
|
754
2290
|
adapter,
|
|
755
|
-
devicePubkey
|
|
2291
|
+
devicePubkey,
|
|
2292
|
+
paymasterConfig
|
|
756
2293
|
);
|
|
757
2294
|
if (!isSigner2 && recovery) {
|
|
758
2295
|
const dedup = lastDeviceRequest.get(identity.userId);
|
|
@@ -811,7 +2348,8 @@ var Cavos = class _Cavos {
|
|
|
811
2348
|
isSigner ? "ready" : "needs-device-approval",
|
|
812
2349
|
account,
|
|
813
2350
|
adapter,
|
|
814
|
-
devicePubkey
|
|
2351
|
+
devicePubkey,
|
|
2352
|
+
paymasterConfig
|
|
815
2353
|
);
|
|
816
2354
|
}
|
|
817
2355
|
/** This device's public key (e.g. to request addition to an existing wallet). */
|
|
@@ -832,6 +2370,92 @@ var Cavos = class _Cavos {
|
|
|
832
2370
|
async addSigner(pubkey) {
|
|
833
2371
|
return this.execute([this.adapter.buildAddSigner(this.address, pubkey)]);
|
|
834
2372
|
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Enroll a passkey as an APPROVER so the user can later add devices from any
|
|
2375
|
+
* browser (2FA-style step-up). Requires a ready device (the enrollment call is
|
|
2376
|
+
* device-signed and gasless). Idempotent: a no-op if the passkey is already an
|
|
2377
|
+
* approver. Call this whenever the app decides to prompt "turn on device
|
|
2378
|
+
* approvals". Returns the passkey's public key + the enrollment tx hash.
|
|
2379
|
+
*/
|
|
2380
|
+
async enrollPasskey(passkey, params) {
|
|
2381
|
+
const enrolled = await passkey.enroll(params);
|
|
2382
|
+
const { transactionHash } = await this.addApprover(enrolled.publicKey);
|
|
2383
|
+
return { publicKey: enrolled.publicKey, transactionHash };
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Register an ALREADY-enrolled passkey public key as an approver (gasless,
|
|
2387
|
+
* device-signed). Idempotent. Use this to register ONE passkey across multiple
|
|
2388
|
+
* chains without re-prompting `passkey.enroll()` on each: enroll once, then
|
|
2389
|
+
* call `addApprover(pubkey)` on each chain's wallet.
|
|
2390
|
+
*/
|
|
2391
|
+
async addApprover(pubkey) {
|
|
2392
|
+
if (this.status !== "ready") {
|
|
2393
|
+
throw new Error("kit: addApprover requires a ready, authorized device");
|
|
2394
|
+
}
|
|
2395
|
+
if (await this.adapter.isApprover(this.address, pubkey)) return {};
|
|
2396
|
+
const { transactionHash } = await this.execute([
|
|
2397
|
+
this.adapter.buildAddApprover(this.address, pubkey)
|
|
2398
|
+
]);
|
|
2399
|
+
return { transactionHash };
|
|
2400
|
+
}
|
|
2401
|
+
/**
|
|
2402
|
+
* From a brand-new browser (status `needs-device-approval`), use the user's
|
|
2403
|
+
* synced passkey to authorize adding THIS device — no trip back to an already-
|
|
2404
|
+
* authorized device.
|
|
2405
|
+
*
|
|
2406
|
+
* `add_signer_via_passkey` is a public external authorized by the embedded
|
|
2407
|
+
* WebAuthn assertion (no device signature), so by default we sponsor it through
|
|
2408
|
+
* the Cavos paymaster's `paymaster_executeDirectTransaction` (the forwarder's
|
|
2409
|
+
* `execute_sponsored` runs a generic call — it does NOT require SNIP-9). Pass a
|
|
2410
|
+
* custom `submit` to route it through your own relayer instead. Returns the tx.
|
|
2411
|
+
*/
|
|
2412
|
+
async approveThisDeviceWithPasskey(opts) {
|
|
2413
|
+
if (this.status === "ready") {
|
|
2414
|
+
throw new Error("kit: this device is already an authorized signer");
|
|
2415
|
+
}
|
|
2416
|
+
const { leaf, nonce } = await this.passkeyLeafForThisDevice();
|
|
2417
|
+
const leaves = [leaf];
|
|
2418
|
+
const assertion = await opts.passkey.assert(batchChallenge(leaves));
|
|
2419
|
+
return this.submitPasskeyApproval(assertion, leaves, 0, nonce, opts.submit);
|
|
2420
|
+
}
|
|
2421
|
+
/** This device's leaf + the current passkey nonce, for a (possibly multi-chain)
|
|
2422
|
+
* passkey approval batch. See `approveDeviceEverywhere`. */
|
|
2423
|
+
async passkeyLeafForThisDevice() {
|
|
2424
|
+
const nonce = await this.adapter.getPasskeyNonce(this.address);
|
|
2425
|
+
return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
|
|
2426
|
+
}
|
|
2427
|
+
/** Submit `add_signer_via_passkey` given a (shared) assertion + this chain's
|
|
2428
|
+
* position in the batch. The assertion doesn't carry the passkey pubkey, so we
|
|
2429
|
+
* recover both candidates and pick the enrolled approver via the on-chain view
|
|
2430
|
+
* (no backend). Defaults to sponsoring through the paymaster. */
|
|
2431
|
+
async submitPasskeyApproval(assertion, leaves, leafIndex, nonce, submit) {
|
|
2432
|
+
const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
|
|
2433
|
+
const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
|
|
2434
|
+
let yParity = null;
|
|
2435
|
+
for (const cand of candidates) {
|
|
2436
|
+
if (await this.adapter.isApprover(this.address, cand.publicKey)) {
|
|
2437
|
+
yParity = cand.yParity;
|
|
2438
|
+
break;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
if (yParity === null) {
|
|
2442
|
+
throw new Error("kit: this passkey is not a registered approver of the wallet");
|
|
2443
|
+
}
|
|
2444
|
+
const call = this.adapter.buildAddSignerViaPasskey(
|
|
2445
|
+
this.address,
|
|
2446
|
+
this.devicePubkey,
|
|
2447
|
+
nonce,
|
|
2448
|
+
leaves,
|
|
2449
|
+
leafIndex,
|
|
2450
|
+
assertion,
|
|
2451
|
+
yParity
|
|
2452
|
+
);
|
|
2453
|
+
if (submit) return submit(call);
|
|
2454
|
+
if (!this.paymaster) {
|
|
2455
|
+
throw new Error("kit: no paymaster configured \u2014 pass a `submit` relayer to approveThisDeviceWithPasskey");
|
|
2456
|
+
}
|
|
2457
|
+
return paymasterExecuteDirect(this.paymaster, this.address, call);
|
|
2458
|
+
}
|
|
835
2459
|
/**
|
|
836
2460
|
* Register a self-custodial backup signer derived from `code`, so the account
|
|
837
2461
|
* can be recovered after the user loses every device. Idempotent: if the
|
|
@@ -858,13 +2482,14 @@ var Cavos = class _Cavos {
|
|
|
858
2482
|
* re-derive the backup key. The backend never sees the code.
|
|
859
2483
|
*/
|
|
860
2484
|
static async recover(opts) {
|
|
861
|
-
const
|
|
862
|
-
|
|
2485
|
+
const network = STARKNET_ENV[opts.network];
|
|
2486
|
+
const classHash = opts.classHash ?? DEVICE_ACCOUNT_CLASS_HASH[network];
|
|
2487
|
+
if (!classHash) throw new Error(`kit: no DeviceAccount class hash for ${network}`);
|
|
863
2488
|
const provider = new starknet.RpcProvider({
|
|
864
|
-
nodeUrl: opts.rpcUrl ?? STARKNET_NETWORKS[
|
|
2489
|
+
nodeUrl: opts.rpcUrl ?? STARKNET_NETWORKS[network].rpcUrl
|
|
865
2490
|
});
|
|
866
2491
|
const paymaster = new starknet.PaymasterRpc({
|
|
867
|
-
nodeUrl: opts.paymasterUrl ?? CAVOS_PAYMASTER_URL[
|
|
2492
|
+
nodeUrl: opts.paymasterUrl ?? CAVOS_PAYMASTER_URL[network],
|
|
868
2493
|
headers: { "x-paymaster-api-key": opts.paymasterApiKey }
|
|
869
2494
|
});
|
|
870
2495
|
const signer = opts.createSigner ? await opts.createSigner(`${opts.identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${opts.identity.userId}:${opts.appSalt}` });
|
|
@@ -872,7 +2497,7 @@ var Cavos = class _Cavos {
|
|
|
872
2497
|
const backup = BackupSigner.fromCode(opts.code);
|
|
873
2498
|
const backupAdapter = new StarknetAdapter({ classHash, signer: backup, provider });
|
|
874
2499
|
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
875
|
-
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network
|
|
2500
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network }) : defaultRegistry3);
|
|
876
2501
|
const existing = await registry.lookup(opts.identity.userId);
|
|
877
2502
|
if (!existing) {
|
|
878
2503
|
throw new Error("kit: no account found for this identity \u2014 nothing to recover");
|
|
@@ -907,7 +2532,7 @@ var Cavos = class _Cavos {
|
|
|
907
2532
|
return new _Cavos(opts.identity, existing.address, "ready", account, adapter, devicePubkey);
|
|
908
2533
|
}
|
|
909
2534
|
};
|
|
910
|
-
var
|
|
2535
|
+
var defaultRegistry3 = new InMemoryWalletRegistry();
|
|
911
2536
|
var DEVICE_REQUEST_DEDUP_MS = 5 * 60 * 1e3;
|
|
912
2537
|
var lastDeviceRequest = /* @__PURE__ */ new Map();
|
|
913
2538
|
async function isDeployed(provider, address) {
|
|
@@ -918,6 +2543,40 @@ async function isDeployed(provider, address) {
|
|
|
918
2543
|
return false;
|
|
919
2544
|
}
|
|
920
2545
|
}
|
|
2546
|
+
async function paymasterExecuteDirect(paymaster, userAddress, call) {
|
|
2547
|
+
const body = {
|
|
2548
|
+
jsonrpc: "2.0",
|
|
2549
|
+
id: 1,
|
|
2550
|
+
method: "paymaster_executeDirectTransaction",
|
|
2551
|
+
params: {
|
|
2552
|
+
transaction: {
|
|
2553
|
+
type: "invoke",
|
|
2554
|
+
invoke: {
|
|
2555
|
+
user_address: userAddress,
|
|
2556
|
+
execute_from_outside_call: {
|
|
2557
|
+
to: call.contractAddress,
|
|
2558
|
+
selector: starknet.hash.getSelectorFromName(call.entrypoint),
|
|
2559
|
+
calldata: call.calldata.map((c) => starknet.num.toHex(c))
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
},
|
|
2563
|
+
parameters: { version: "0x1", fee_mode: { mode: "sponsored" } }
|
|
2564
|
+
}
|
|
2565
|
+
};
|
|
2566
|
+
const res = await fetch(paymaster.url, {
|
|
2567
|
+
method: "POST",
|
|
2568
|
+
headers: {
|
|
2569
|
+
"Content-Type": "application/json",
|
|
2570
|
+
...paymaster.apiKey ? { "x-paymaster-api-key": paymaster.apiKey } : {}
|
|
2571
|
+
},
|
|
2572
|
+
body: JSON.stringify(body)
|
|
2573
|
+
});
|
|
2574
|
+
const json = await res.json();
|
|
2575
|
+
if (json.error) {
|
|
2576
|
+
throw new Error(`kit: paymaster passkey approval failed: ${JSON.stringify(json.error)}`);
|
|
2577
|
+
}
|
|
2578
|
+
return { transactionHash: json.result?.transaction_hash ?? json.result?.tracking_id };
|
|
2579
|
+
}
|
|
921
2580
|
var CavosAuth = class {
|
|
922
2581
|
constructor(opts = {}) {
|
|
923
2582
|
this.opts = opts;
|
|
@@ -1844,7 +3503,7 @@ function CavosProvider({ config, modal, children }) {
|
|
|
1844
3503
|
const closeModal = react.useCallback(() => setModalOpen(false), []);
|
|
1845
3504
|
const clearAuthError = react.useCallback(() => setAuthError(null), []);
|
|
1846
3505
|
const resendDeviceApproval = react.useCallback(async () => {
|
|
1847
|
-
if (!identity || !cavos || !cavos.pendingRequestId) return;
|
|
3506
|
+
if (!identity || !cavos || cavos.chain !== "starknet" || !cavos.pendingRequestId) return;
|
|
1848
3507
|
const backendUrl = configRef.current.authBackendUrl ?? "https://cavos.xyz";
|
|
1849
3508
|
if (!configRef.current.appId) return;
|
|
1850
3509
|
const recovery = new HttpRecoveryClient({ baseUrl: backendUrl, appId: configRef.current.appId });
|
|
@@ -1858,22 +3517,24 @@ function CavosProvider({ config, modal, children }) {
|
|
|
1858
3517
|
const connect = react.useCallback(async (id) => {
|
|
1859
3518
|
setWalletStatus({ ...INITIAL_STATUS, isDeploying: true });
|
|
1860
3519
|
const c = await Cavos.connect({
|
|
3520
|
+
chain: configRef.current.chain ?? "starknet",
|
|
1861
3521
|
network: configRef.current.network,
|
|
1862
3522
|
identity: id,
|
|
1863
3523
|
appSalt: configRef.current.appSalt,
|
|
1864
|
-
paymasterApiKey: configRef.current.paymasterApiKey,
|
|
3524
|
+
...configRef.current.paymasterApiKey ? { paymasterApiKey: configRef.current.paymasterApiKey } : {},
|
|
1865
3525
|
...configRef.current.appId ? { appId: configRef.current.appId } : {},
|
|
1866
3526
|
...configRef.current.authBackendUrl ? { backendUrl: configRef.current.authBackendUrl } : {},
|
|
1867
3527
|
...configRef.current.rpcUrl ? { rpcUrl: configRef.current.rpcUrl } : {}
|
|
1868
3528
|
});
|
|
1869
3529
|
setCavos(c);
|
|
1870
3530
|
setIdentity(id);
|
|
3531
|
+
const pendingRequestId = c.chain === "starknet" ? c.pendingRequestId : null;
|
|
1871
3532
|
setWalletStatus({
|
|
1872
3533
|
isDeploying: false,
|
|
1873
3534
|
isReady: c.status === "ready",
|
|
1874
3535
|
needsDeviceApproval: c.status === "needs-device-approval",
|
|
1875
|
-
awaitingApproval: c.status === "needs-device-approval" && !!
|
|
1876
|
-
pendingRequestId
|
|
3536
|
+
awaitingApproval: c.status === "needs-device-approval" && !!pendingRequestId,
|
|
3537
|
+
pendingRequestId
|
|
1877
3538
|
});
|
|
1878
3539
|
modal?.onSuccess?.(c.address);
|
|
1879
3540
|
return c;
|
|
@@ -1929,15 +3590,41 @@ function CavosProvider({ config, modal, children }) {
|
|
|
1929
3590
|
}, [auth, connect]);
|
|
1930
3591
|
const execute = react.useCallback(async (calls) => {
|
|
1931
3592
|
if (!cavos) throw new Error("Not logged in");
|
|
3593
|
+
if (cavos.chain !== "starknet") {
|
|
3594
|
+
throw new Error(
|
|
3595
|
+
"kit: useCavos().execute(calls) is Starknet-only. On Solana use the `wallet` handle: wallet.execute(amount, dest)."
|
|
3596
|
+
);
|
|
3597
|
+
}
|
|
1932
3598
|
return cavos.execute(calls);
|
|
1933
3599
|
}, [cavos]);
|
|
1934
3600
|
const addSigner = react.useCallback(
|
|
1935
3601
|
async (pubkey) => {
|
|
1936
3602
|
if (!cavos) throw new Error("Not logged in");
|
|
3603
|
+
if (cavos.chain !== "starknet") {
|
|
3604
|
+
throw new Error("kit: addSigner via useCavos() is Starknet-only; use the `wallet` handle on Solana.");
|
|
3605
|
+
}
|
|
1937
3606
|
return cavos.addSigner(pubkey);
|
|
1938
3607
|
},
|
|
1939
3608
|
[cavos]
|
|
1940
3609
|
);
|
|
3610
|
+
const enrollPasskey = react.useCallback(
|
|
3611
|
+
async (passkey, params) => {
|
|
3612
|
+
if (!cavos) throw new Error("Not logged in");
|
|
3613
|
+
return cavos.enrollPasskey(passkey, params);
|
|
3614
|
+
},
|
|
3615
|
+
[cavos]
|
|
3616
|
+
);
|
|
3617
|
+
const approveThisDeviceWithPasskey = react.useCallback(
|
|
3618
|
+
async (passkey, submit) => {
|
|
3619
|
+
if (!cavos) throw new Error("Not logged in");
|
|
3620
|
+
if (cavos.chain === "starknet") {
|
|
3621
|
+
return cavos.approveThisDeviceWithPasskey({ passkey, ...submit ? { submit } : {} });
|
|
3622
|
+
}
|
|
3623
|
+
const transactionHash = await cavos.approveThisDeviceWithPasskey(passkey);
|
|
3624
|
+
return { transactionHash };
|
|
3625
|
+
},
|
|
3626
|
+
[cavos]
|
|
3627
|
+
);
|
|
1941
3628
|
const setupRecovery = react.useCallback(async () => {
|
|
1942
3629
|
if (!cavos) throw new Error("Not logged in");
|
|
1943
3630
|
const code = generateRecoveryCode();
|
|
@@ -1949,12 +3636,21 @@ function CavosProvider({ config, modal, children }) {
|
|
|
1949
3636
|
setAuthError(null);
|
|
1950
3637
|
setWalletStatus({ ...INITIAL_STATUS, isDeploying: true });
|
|
1951
3638
|
try {
|
|
1952
|
-
const
|
|
3639
|
+
const chain = configRef.current.chain ?? "starknet";
|
|
3640
|
+
const c = chain === "solana" ? await CavosSolana.recover({
|
|
3641
|
+
code,
|
|
3642
|
+
identity,
|
|
3643
|
+
network: configRef.current.network === "mainnet" ? "solana-mainnet" : "solana-devnet",
|
|
3644
|
+
appSalt: configRef.current.appSalt,
|
|
3645
|
+
...configRef.current.appId ? { appId: configRef.current.appId } : {},
|
|
3646
|
+
...configRef.current.authBackendUrl ? { backendUrl: configRef.current.authBackendUrl } : {},
|
|
3647
|
+
...configRef.current.rpcUrl ? { rpcUrl: configRef.current.rpcUrl } : {}
|
|
3648
|
+
}) : await Cavos.recover({
|
|
1953
3649
|
code,
|
|
1954
3650
|
identity,
|
|
1955
3651
|
network: configRef.current.network,
|
|
1956
3652
|
appSalt: configRef.current.appSalt,
|
|
1957
|
-
paymasterApiKey: configRef.current.paymasterApiKey,
|
|
3653
|
+
paymasterApiKey: configRef.current.paymasterApiKey ?? "",
|
|
1958
3654
|
...configRef.current.appId ? { appId: configRef.current.appId } : {},
|
|
1959
3655
|
...configRef.current.authBackendUrl ? { backendUrl: configRef.current.authBackendUrl } : {},
|
|
1960
3656
|
...configRef.current.rpcUrl ? { rpcUrl: configRef.current.rpcUrl } : {}
|
|
@@ -2007,6 +3703,8 @@ function CavosProvider({ config, modal, children }) {
|
|
|
2007
3703
|
closeModal,
|
|
2008
3704
|
isAuthenticated: !!cavos,
|
|
2009
3705
|
user: identity ? { userId: identity.userId, email: identity.email, provider: identity.provider } : null,
|
|
3706
|
+
chain: config.chain ?? "starknet",
|
|
3707
|
+
wallet: cavos,
|
|
2010
3708
|
address: cavos?.address ?? null,
|
|
2011
3709
|
walletStatus,
|
|
2012
3710
|
isLoading,
|
|
@@ -2019,6 +3717,8 @@ function CavosProvider({ config, modal, children }) {
|
|
|
2019
3717
|
handleCallback,
|
|
2020
3718
|
execute,
|
|
2021
3719
|
addSigner,
|
|
3720
|
+
enrollPasskey,
|
|
3721
|
+
approveThisDeviceWithPasskey,
|
|
2022
3722
|
resendDeviceApproval,
|
|
2023
3723
|
setupRecovery,
|
|
2024
3724
|
recover,
|