@cavos/kit 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Cavos-C2KHZxxu.d.mts → Cavos-BH2_tOQ2.d.mts} +409 -5
- package/dist/{Cavos-C2KHZxxu.d.ts → Cavos-BH2_tOQ2.d.ts} +409 -5
- package/dist/{chunk-PEUQQZB5.mjs → chunk-BNGLH3Q3.mjs} +1122 -17
- package/dist/chunk-BNGLH3Q3.mjs.map +1 -0
- package/dist/index.d.mts +125 -5
- package/dist/index.d.ts +125 -5
- package/dist/index.js +1223 -12
- 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 +25 -1
- package/dist/react/index.d.ts +25 -1
- package/dist/react/index.js +1072 -8
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +21 -1
- package/dist/react/index.mjs.map +1 -1
- package/package.json +3 -1
- package/dist/chunk-PEUQQZB5.mjs.map +0 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { p256 } from '@noble/curves/p256';
|
|
2
2
|
import { sha256 } from '@noble/hashes/sha256';
|
|
3
|
-
import { hash, num, Signer, RpcProvider, PaymasterRpc, Account } from 'starknet';
|
|
3
|
+
import { hash, num, Signer, RpcProvider, PaymasterRpc, Account as Account$1 } from 'starknet';
|
|
4
4
|
import { PublicKey, TransactionInstruction, SystemProgram, SYSVAR_INSTRUCTIONS_PUBKEY, Transaction, Connection, sendAndConfirmTransaction } from '@solana/web3.js';
|
|
5
5
|
import { hkdf } from '@noble/hashes/hkdf';
|
|
6
6
|
import { pbkdf2 } from '@noble/hashes/pbkdf2';
|
|
7
7
|
import { randomBytes } from '@noble/hashes/utils';
|
|
8
|
+
import { rpc, hash as hash$1, xdr, Address, StrKey, nativeToScVal, Operation, scValToNative, Account, TransactionBuilder, BASE_FEE } from '@stellar/stellar-sdk';
|
|
8
9
|
|
|
9
10
|
// src/crypto/encoding.ts
|
|
10
11
|
var U128_MASK = (1n << 128n) - 1n;
|
|
@@ -28,6 +29,18 @@ function hexToBytes(hex) {
|
|
|
28
29
|
for (let i = 0; i < out.length; i++) out[i] = parseInt(padded.slice(i * 2, i * 2 + 2), 16);
|
|
29
30
|
return out;
|
|
30
31
|
}
|
|
32
|
+
function bytesToByteArrayCalldata(bytes) {
|
|
33
|
+
const CHUNK = 31;
|
|
34
|
+
const fullCount = Math.floor(bytes.length / CHUNK);
|
|
35
|
+
const out = [String(fullCount)];
|
|
36
|
+
for (let i = 0; i < fullCount; i++) {
|
|
37
|
+
out.push("0x" + bytesToBigInt(bytes.subarray(i * CHUNK, i * CHUNK + CHUNK)).toString(16));
|
|
38
|
+
}
|
|
39
|
+
const rem = bytes.subarray(fullCount * CHUNK);
|
|
40
|
+
out.push("0x" + (rem.length ? bytesToBigInt(rem).toString(16) : "0"));
|
|
41
|
+
out.push(String(rem.length));
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
31
44
|
function bigIntTo32Bytes(value) {
|
|
32
45
|
const out = new Uint8Array(32);
|
|
33
46
|
let v = value;
|
|
@@ -163,7 +176,7 @@ var CAVOS_PAYMASTER_URL = {
|
|
|
163
176
|
mainnet: "https://paymaster.cavos.xyz"
|
|
164
177
|
};
|
|
165
178
|
var DEVICE_ACCOUNT_CLASS_HASH = {
|
|
166
|
-
sepolia: "
|
|
179
|
+
sepolia: "0x25cbc5423e8ee895febb0ef2c3945b408da44d0039d915fbdd681fe6b6ba66b",
|
|
167
180
|
mainnet: "0x1840aded59e8a0d2b440a134cb9079a7fc11b06c77f58ed189ab436a034ca6a"
|
|
168
181
|
};
|
|
169
182
|
var StarknetAdapter = class {
|
|
@@ -225,6 +238,73 @@ var StarknetAdapter = class {
|
|
|
225
238
|
const sig = await this.opts.signer.sign(bigIntTo32Bytes(txHash));
|
|
226
239
|
return signatureToFelts(sig).map((f) => num.toHex(f));
|
|
227
240
|
}
|
|
241
|
+
// --- passkey approvers ---
|
|
242
|
+
buildAddApprover(accountAddress, passkey) {
|
|
243
|
+
return { contractAddress: accountAddress, entrypoint: "add_approver", calldata: pubkeyCalldata(passkey) };
|
|
244
|
+
}
|
|
245
|
+
buildRemoveApprover(accountAddress, passkey) {
|
|
246
|
+
return { contractAddress: accountAddress, entrypoint: "remove_approver", calldata: pubkeyCalldata(passkey) };
|
|
247
|
+
}
|
|
248
|
+
async isApprover(accountAddress, passkey) {
|
|
249
|
+
if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
|
|
250
|
+
const res = await this.opts.provider.callContract({
|
|
251
|
+
contractAddress: accountAddress,
|
|
252
|
+
entrypoint: "is_approver",
|
|
253
|
+
calldata: pubkeyCalldata(passkey)
|
|
254
|
+
});
|
|
255
|
+
return BigInt(res[0] ?? 0) !== 0n;
|
|
256
|
+
}
|
|
257
|
+
async getPasskeyNonce(accountAddress) {
|
|
258
|
+
if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
|
|
259
|
+
const res = await this.opts.provider.callContract({
|
|
260
|
+
contractAddress: accountAddress,
|
|
261
|
+
entrypoint: "get_passkey_nonce",
|
|
262
|
+
calldata: []
|
|
263
|
+
});
|
|
264
|
+
return BigInt(res[0] ?? 0);
|
|
265
|
+
}
|
|
266
|
+
/** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
|
|
267
|
+
* `sha256(new_x || new_y || nonce)` (coords 32B BE, nonce 16B BE). The batch
|
|
268
|
+
* challenge the passkey signs is `sha256(concat(leaves))` across chains. */
|
|
269
|
+
passkeyLeaf(newSigner, nonce) {
|
|
270
|
+
const msg = new Uint8Array(32 + 32 + 16);
|
|
271
|
+
msg.set(bigIntTo32Bytes(newSigner.x), 0);
|
|
272
|
+
msg.set(bigIntTo32Bytes(newSigner.y), 32);
|
|
273
|
+
msg.set(bigIntTo32Bytes(nonce).subarray(16), 64);
|
|
274
|
+
return sha256(msg);
|
|
275
|
+
}
|
|
276
|
+
/** Passkey-authorized `add_signer` call. `leaves`/`leafIndex` place this chain's
|
|
277
|
+
* leaf in the multi-chain batch (single chain → `[leaf]`, index 0). `yParity`
|
|
278
|
+
* matches the raw `(r, s)` — the contract normalizes high-S internally. */
|
|
279
|
+
buildAddSignerViaPasskey(accountAddress, newSigner, nonce, leaves, leafIndex, assertion, yParity) {
|
|
280
|
+
const [rl, rh] = u256ToFelts(assertion.r);
|
|
281
|
+
const [sl, sh] = u256ToFelts(assertion.s);
|
|
282
|
+
const leavesCalldata = [String(leaves.length)];
|
|
283
|
+
for (const leaf of leaves) {
|
|
284
|
+
const [lo, hi] = u256ToFelts(bytesToBigInt(leaf));
|
|
285
|
+
leavesCalldata.push(num.toHex(lo), num.toHex(hi));
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
contractAddress: accountAddress,
|
|
289
|
+
entrypoint: "add_signer_via_passkey",
|
|
290
|
+
calldata: [
|
|
291
|
+
...pubkeyCalldata(newSigner),
|
|
292
|
+
// new_x, new_y (u256 pairs)
|
|
293
|
+
num.toHex(nonce),
|
|
294
|
+
...leavesCalldata,
|
|
295
|
+
// Array<u256> leaves
|
|
296
|
+
String(leafIndex),
|
|
297
|
+
...bytesToByteArrayCalldata(assertion.authenticatorData),
|
|
298
|
+
...bytesToByteArrayCalldata(assertion.clientDataJSON),
|
|
299
|
+
String(assertion.challengeOffset),
|
|
300
|
+
num.toHex(rl),
|
|
301
|
+
num.toHex(rh),
|
|
302
|
+
num.toHex(sl),
|
|
303
|
+
num.toHex(sh),
|
|
304
|
+
yParity ? "0x1" : "0x0"
|
|
305
|
+
]
|
|
306
|
+
};
|
|
307
|
+
}
|
|
228
308
|
};
|
|
229
309
|
function pubkeyCalldata(pk) {
|
|
230
310
|
const [xl, xh] = u256ToFelts(pk.x);
|
|
@@ -318,6 +398,9 @@ function deriveAddressSeed({ userId, appSalt }) {
|
|
|
318
398
|
function deriveAddressSeedSolana({ userId, appSalt }) {
|
|
319
399
|
return sha256(new TextEncoder().encode(`cavos:solana:v1:${userId}:${appSalt}`));
|
|
320
400
|
}
|
|
401
|
+
function deriveAddressSeedStellar({ userId, appSalt }) {
|
|
402
|
+
return sha256(new TextEncoder().encode(`cavos:stellar:v1:${userId}:${appSalt}`));
|
|
403
|
+
}
|
|
321
404
|
function feltFromString(s) {
|
|
322
405
|
const bytes = new TextEncoder().encode(s);
|
|
323
406
|
const chunks = [];
|
|
@@ -339,6 +422,8 @@ var DOMAIN_ADD = "cavos:add_signer:v1";
|
|
|
339
422
|
var DOMAIN_REMOVE = "cavos:remove_signer:v1";
|
|
340
423
|
var DOMAIN_TRANSFER = "cavos:transfer:v1";
|
|
341
424
|
var DOMAIN_EXECUTE = "cavos:execute:v1";
|
|
425
|
+
var DOMAIN_ADD_APPROVER = "cavos:add_approver:v1";
|
|
426
|
+
var DOMAIN_REMOVE_APPROVER = "cavos:remove_approver:v1";
|
|
342
427
|
var SECP256R1_N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
|
|
343
428
|
var SOLANA_NETWORKS = {
|
|
344
429
|
"solana-devnet": "https://api.devnet.solana.com",
|
|
@@ -429,6 +514,99 @@ var SolanaAdapter = class {
|
|
|
429
514
|
});
|
|
430
515
|
return [precompileIx, ix];
|
|
431
516
|
}
|
|
517
|
+
/** `[precompile, add_approver]` bundle enrolling a passkey approver (device-signed). */
|
|
518
|
+
async buildAddApprover(account, passkey) {
|
|
519
|
+
const accountPk = new 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 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 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 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(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 PublicKey(account);
|
|
569
|
+
const newCompressed = compressedPubkey(newSigner);
|
|
570
|
+
const passkeyCompressed = compressedPubkey(passkey);
|
|
571
|
+
const clientHash = 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 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 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 PublicKey(account));
|
|
601
|
+
if (!info) return 0n;
|
|
602
|
+
const d = info.data;
|
|
603
|
+
const signersLenOff = 8 + 32 + 1 + 8 + COMPRESSED_PUBKEY_SIZE;
|
|
604
|
+
const signerCount = d.readUInt32LE(signersLenOff);
|
|
605
|
+
const approversLenOff = signersLenOff + 4 + signerCount * COMPRESSED_PUBKEY_SIZE;
|
|
606
|
+
const approverCount = d.readUInt32LE(approversLenOff);
|
|
607
|
+
const passkeyNonceOff = approversLenOff + 4 + approverCount * COMPRESSED_PUBKEY_SIZE;
|
|
608
|
+
return readU64le(d, passkeyNonceOff);
|
|
609
|
+
}
|
|
432
610
|
/** `[precompile, execute_transfer]` bundle moving lamports out of the account. */
|
|
433
611
|
async buildExecuteTransfer(account, destination, amount) {
|
|
434
612
|
const accountPk = new PublicKey(account);
|
|
@@ -550,6 +728,22 @@ var SolanaAdapter = class {
|
|
|
550
728
|
}
|
|
551
729
|
return out;
|
|
552
730
|
}
|
|
731
|
+
async fetchApprovers(account) {
|
|
732
|
+
const info = await this.requireConnection().getAccountInfo(account);
|
|
733
|
+
if (!info) return [];
|
|
734
|
+
const d = info.data;
|
|
735
|
+
const signersLenOff = 8 + 32 + 1 + 8 + COMPRESSED_PUBKEY_SIZE;
|
|
736
|
+
const signerCount = d.readUInt32LE(signersLenOff);
|
|
737
|
+
const approversLenOff = signersLenOff + 4 + signerCount * COMPRESSED_PUBKEY_SIZE;
|
|
738
|
+
const count = d.readUInt32LE(approversLenOff);
|
|
739
|
+
const out = [];
|
|
740
|
+
let off = approversLenOff + 4;
|
|
741
|
+
for (let i = 0; i < count; i++) {
|
|
742
|
+
out.push(Uint8Array.from(d.subarray(off, off + COMPRESSED_PUBKEY_SIZE)));
|
|
743
|
+
off += COMPRESSED_PUBKEY_SIZE;
|
|
744
|
+
}
|
|
745
|
+
return out;
|
|
746
|
+
}
|
|
553
747
|
requireConnection() {
|
|
554
748
|
if (!this.opts.connection) throw new Error("kit/solana: connection required for reads");
|
|
555
749
|
return this.opts.connection;
|
|
@@ -562,10 +756,10 @@ function compressedPubkey(pk) {
|
|
|
562
756
|
return out;
|
|
563
757
|
}
|
|
564
758
|
function encodeLowSSignature(r, s) {
|
|
565
|
-
const
|
|
759
|
+
const lowS2 = s > SECP256R1_N / 2n ? SECP256R1_N - s : s;
|
|
566
760
|
const out = new Uint8Array(SIGNATURE_SIZE);
|
|
567
761
|
out.set(bigIntTo32Bytes(r), 0);
|
|
568
|
-
out.set(bigIntTo32Bytes(
|
|
762
|
+
out.set(bigIntTo32Bytes(lowS2), 32);
|
|
569
763
|
return out;
|
|
570
764
|
}
|
|
571
765
|
function buildSecp256r1Instruction(compressed, signature, message) {
|
|
@@ -604,6 +798,11 @@ function buildSecp256r1Instruction(compressed, signature, message) {
|
|
|
604
798
|
function anchorDiscriminator(name) {
|
|
605
799
|
return Buffer.from(sha256(`global:${name}`).slice(0, 8));
|
|
606
800
|
}
|
|
801
|
+
function u32le(n) {
|
|
802
|
+
const b = Buffer.alloc(4);
|
|
803
|
+
b.writeUInt32LE(n);
|
|
804
|
+
return b;
|
|
805
|
+
}
|
|
607
806
|
function u64le(n) {
|
|
608
807
|
const b = Buffer.alloc(8);
|
|
609
808
|
new DataView(b.buffer, b.byteOffset, 8).setBigUint64(0, BigInt(n), true);
|
|
@@ -998,6 +1197,77 @@ var WORDLIST = [
|
|
|
998
1197
|
"beach",
|
|
999
1198
|
"dusk"
|
|
1000
1199
|
];
|
|
1200
|
+
function base64urlEncode(bytes) {
|
|
1201
|
+
let bin = "";
|
|
1202
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
1203
|
+
const b64 = typeof btoa !== "undefined" ? btoa(bin) : Buffer.from(bytes).toString("base64");
|
|
1204
|
+
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1205
|
+
}
|
|
1206
|
+
function derToRs(der) {
|
|
1207
|
+
let i = 0;
|
|
1208
|
+
if (der[i++] !== 48) throw new Error("kit/webauthn: bad DER (no SEQUENCE)");
|
|
1209
|
+
if (der[i] & 128) i += 1 + (der[i] & 127);
|
|
1210
|
+
else i += 1;
|
|
1211
|
+
if (der[i++] !== 2) throw new Error("kit/webauthn: bad DER (no r INTEGER)");
|
|
1212
|
+
const rlen = der[i++];
|
|
1213
|
+
const r = bytesToBigInt(der.subarray(i, i + rlen));
|
|
1214
|
+
i += rlen;
|
|
1215
|
+
if (der[i++] !== 2) throw new Error("kit/webauthn: bad DER (no s INTEGER)");
|
|
1216
|
+
const slen = der[i++];
|
|
1217
|
+
const s = bytesToBigInt(der.subarray(i, i + slen));
|
|
1218
|
+
return { r, s };
|
|
1219
|
+
}
|
|
1220
|
+
function spkiToPublicKey(spki) {
|
|
1221
|
+
const idx = spki.lastIndexOf(4, spki.length - 65);
|
|
1222
|
+
const start = spki.length - 65;
|
|
1223
|
+
const prefix = spki[start];
|
|
1224
|
+
if (prefix !== 4) {
|
|
1225
|
+
if (idx < 0) throw new Error("kit/webauthn: no uncompressed EC point in SPKI");
|
|
1226
|
+
return { x: bytesToBigInt(spki.subarray(idx + 1, idx + 33)), y: bytesToBigInt(spki.subarray(idx + 33, idx + 65)) };
|
|
1227
|
+
}
|
|
1228
|
+
return {
|
|
1229
|
+
x: bytesToBigInt(spki.subarray(start + 1, start + 33)),
|
|
1230
|
+
y: bytesToBigInt(spki.subarray(start + 33, start + 65))
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
function batchChallenge(leaves) {
|
|
1234
|
+
const total = leaves.reduce((n, l) => n + l.length, 0);
|
|
1235
|
+
const cat = new Uint8Array(total);
|
|
1236
|
+
let o = 0;
|
|
1237
|
+
for (const l of leaves) {
|
|
1238
|
+
cat.set(l, o);
|
|
1239
|
+
o += l.length;
|
|
1240
|
+
}
|
|
1241
|
+
return sha256(cat);
|
|
1242
|
+
}
|
|
1243
|
+
function webauthnDigest(authenticatorData, clientDataJSON) {
|
|
1244
|
+
const clientHash = sha256(clientDataJSON);
|
|
1245
|
+
const msg = new Uint8Array(authenticatorData.length + clientHash.length);
|
|
1246
|
+
msg.set(authenticatorData, 0);
|
|
1247
|
+
msg.set(clientHash, authenticatorData.length);
|
|
1248
|
+
return sha256(msg);
|
|
1249
|
+
}
|
|
1250
|
+
function recoverCandidatePublicKeys(r, s, digest) {
|
|
1251
|
+
const out = [];
|
|
1252
|
+
for (const bit of [0, 1]) {
|
|
1253
|
+
try {
|
|
1254
|
+
const point = new p256.Signature(r, s).addRecoveryBit(bit).recoverPublicKey(digest).toAffine();
|
|
1255
|
+
out.push({ publicKey: { x: point.x, y: point.y }, yParity: bit === 1 });
|
|
1256
|
+
} catch {
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return out;
|
|
1260
|
+
}
|
|
1261
|
+
function lowS(s) {
|
|
1262
|
+
const n = p256.CURVE.n;
|
|
1263
|
+
return s > n / 2n ? n - s : s;
|
|
1264
|
+
}
|
|
1265
|
+
function challengeOffsetOf(clientDataJSON, challengeB64) {
|
|
1266
|
+
const text = new TextDecoder().decode(clientDataJSON);
|
|
1267
|
+
const idx = text.indexOf(challengeB64);
|
|
1268
|
+
if (idx < 0) throw new Error("kit/webauthn: challenge not found in clientDataJSON");
|
|
1269
|
+
return idx;
|
|
1270
|
+
}
|
|
1001
1271
|
var CavosSolana = class _CavosSolana {
|
|
1002
1272
|
constructor(identity, address, status, connection, adapter, devicePubkey, relayer, feePayer) {
|
|
1003
1273
|
this.identity = identity;
|
|
@@ -1076,6 +1346,69 @@ var CavosSolana = class _CavosSolana {
|
|
|
1076
1346
|
const ixs = await this.adapter.buildAddSigner(this.address, pubkey);
|
|
1077
1347
|
return this.send(ixs);
|
|
1078
1348
|
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Enroll a passkey as an approver (2FA-style step-up). Device-signed + gasless;
|
|
1351
|
+
* requires a ready device. Idempotent. Returns the passkey pubkey + tx hash.
|
|
1352
|
+
*/
|
|
1353
|
+
async enrollPasskey(passkey, params) {
|
|
1354
|
+
const enrolled = await passkey.enroll(params);
|
|
1355
|
+
const { transactionHash } = await this.addApprover(enrolled.publicKey);
|
|
1356
|
+
return { publicKey: enrolled.publicKey, transactionHash };
|
|
1357
|
+
}
|
|
1358
|
+
/** Register an already-enrolled passkey pubkey as an approver (gasless).
|
|
1359
|
+
* Idempotent. Lets one passkey be registered across chains without re-prompting. */
|
|
1360
|
+
async addApprover(pubkey) {
|
|
1361
|
+
if (this.status !== "ready") {
|
|
1362
|
+
throw new Error("kit/solana: addApprover requires a ready, authorized device");
|
|
1363
|
+
}
|
|
1364
|
+
if (await this.adapter.isApprover(this.address, pubkey)) return {};
|
|
1365
|
+
const ixs = await this.adapter.buildAddApprover(this.address, pubkey);
|
|
1366
|
+
const transactionHash = await this.send(ixs);
|
|
1367
|
+
return { transactionHash };
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* From a fresh browser (status `needs-device-approval`), approve adding THIS
|
|
1371
|
+
* device with the user's synced passkey. Gasless via the relayer — the bundle
|
|
1372
|
+
* carries the passkey's WebAuthn assertion, so no device signature is needed.
|
|
1373
|
+
*/
|
|
1374
|
+
async approveThisDeviceWithPasskey(passkey) {
|
|
1375
|
+
if (this.status === "ready") {
|
|
1376
|
+
throw new Error("kit/solana: this device is already an authorized signer");
|
|
1377
|
+
}
|
|
1378
|
+
const { leaf, nonce } = await this.passkeyLeafForThisDevice();
|
|
1379
|
+
const leaves = [leaf];
|
|
1380
|
+
const assertion = await passkey.assert(batchChallenge(leaves));
|
|
1381
|
+
const { transactionHash } = await this.submitPasskeyApproval(assertion, leaves, 0, nonce);
|
|
1382
|
+
return transactionHash;
|
|
1383
|
+
}
|
|
1384
|
+
/** This device's leaf + passkey nonce for a (possibly multi-chain) batch. */
|
|
1385
|
+
async passkeyLeafForThisDevice() {
|
|
1386
|
+
const nonce = await this.adapter.passkeyNonce(this.address);
|
|
1387
|
+
return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
|
|
1388
|
+
}
|
|
1389
|
+
/** Submit `add_signer_via_passkey` given a shared assertion + batch position.
|
|
1390
|
+
* Used by `approveThisDeviceWithPasskey` and `approveDeviceEverywhere`. */
|
|
1391
|
+
async submitPasskeyApproval(assertion, leaves, leafIndex, _nonce) {
|
|
1392
|
+
const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
|
|
1393
|
+
const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
|
|
1394
|
+
let approver = null;
|
|
1395
|
+
for (const cand of candidates) {
|
|
1396
|
+
if (await this.adapter.isApprover(this.address, cand.publicKey)) {
|
|
1397
|
+
approver = cand.publicKey;
|
|
1398
|
+
break;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
if (!approver) throw new Error("kit/solana: this passkey is not a registered approver");
|
|
1402
|
+
const ixs = this.adapter.buildAddSignerViaPasskey(
|
|
1403
|
+
this.address,
|
|
1404
|
+
this.devicePubkey,
|
|
1405
|
+
approver,
|
|
1406
|
+
leaves,
|
|
1407
|
+
leafIndex,
|
|
1408
|
+
assertion
|
|
1409
|
+
);
|
|
1410
|
+
return { transactionHash: await this.send(ixs) };
|
|
1411
|
+
}
|
|
1079
1412
|
/** Move `amount` lamports out of the account to `destination` (device-signed). */
|
|
1080
1413
|
async execute(amount, destination) {
|
|
1081
1414
|
if (this.status !== "ready") {
|
|
@@ -1188,6 +1521,615 @@ var CavosSolana = class _CavosSolana {
|
|
|
1188
1521
|
};
|
|
1189
1522
|
var defaultRegistry = new InMemoryWalletRegistry();
|
|
1190
1523
|
|
|
1524
|
+
// src/chains/stellar/constants.ts
|
|
1525
|
+
var FACTORY_CONTRACT_ID = {
|
|
1526
|
+
// Re-deployed 2026-07-01 with the passkey-approval device-account wasm (batched
|
|
1527
|
+
// multi-chain challenge). The factory pins the wasm hash immutably, so a new
|
|
1528
|
+
// wasm needs a new factory → new account addresses; testnet has no prod wallets.
|
|
1529
|
+
"stellar-testnet": "CBCJIODXIEBOXXD66KCUCF7ZDYJARKI4ZIVQOVWPULOBH5XGNCDP6W3I",
|
|
1530
|
+
// Set once the factory is deployed to mainnet (its address differs — network id
|
|
1531
|
+
// is part of contract-address derivation).
|
|
1532
|
+
"stellar-mainnet": ""
|
|
1533
|
+
};
|
|
1534
|
+
var DEVICE_ACCOUNT_WASM_HASH = {
|
|
1535
|
+
"stellar-testnet": "2671b085578e59a385ef5a5664e42f0450322fe3249539f588e1263ed5a31dce",
|
|
1536
|
+
"stellar-mainnet": ""
|
|
1537
|
+
};
|
|
1538
|
+
var STELLAR_NETWORKS = {
|
|
1539
|
+
"stellar-testnet": {
|
|
1540
|
+
rpcUrl: "https://soroban-testnet.stellar.org",
|
|
1541
|
+
passphrase: "Test SDF Network ; September 2015"
|
|
1542
|
+
},
|
|
1543
|
+
"stellar-mainnet": {
|
|
1544
|
+
rpcUrl: "https://soroban-rpc.mainnet.stellar.gateway.fm",
|
|
1545
|
+
passphrase: "Public Global Stellar Network ; September 2015"
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
var NATIVE_SAC_ID = {
|
|
1549
|
+
"stellar-testnet": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
|
|
1550
|
+
"stellar-mainnet": "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA"
|
|
1551
|
+
};
|
|
1552
|
+
var SECP256R1_N2 = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
|
|
1553
|
+
var StellarAdapter = class {
|
|
1554
|
+
constructor(opts) {
|
|
1555
|
+
this.chain = "stellar";
|
|
1556
|
+
this.network = opts.network;
|
|
1557
|
+
this.passphrase = STELLAR_NETWORKS[opts.network].passphrase;
|
|
1558
|
+
this.rpcUrl = opts.rpcUrl ?? STELLAR_NETWORKS[opts.network].rpcUrl;
|
|
1559
|
+
this.factoryId = opts.factoryId ?? FACTORY_CONTRACT_ID[opts.network];
|
|
1560
|
+
if (!this.factoryId) {
|
|
1561
|
+
throw new Error(`kit/stellar: no factory contract id configured for ${opts.network}`);
|
|
1562
|
+
}
|
|
1563
|
+
this.signer = opts.signer;
|
|
1564
|
+
}
|
|
1565
|
+
server() {
|
|
1566
|
+
if (!this._server) {
|
|
1567
|
+
this._server = new rpc.Server(this.rpcUrl, {
|
|
1568
|
+
allowHttp: this.rpcUrl.startsWith("http://")
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
return this._server;
|
|
1572
|
+
}
|
|
1573
|
+
networkId() {
|
|
1574
|
+
return hash$1(Buffer.from(this.passphrase));
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Deterministic account address for `(addressSeed, initialSigner)` — computed
|
|
1578
|
+
* off-chain, byte-identical to the factory's on-chain `account_address`.
|
|
1579
|
+
* `contractId = sha256(HashIdPreimage(networkId, factory, salt))` with
|
|
1580
|
+
* `salt = sha256(addressSeed || sec1(initialSigner))`.
|
|
1581
|
+
*/
|
|
1582
|
+
computeAddress(addressSeed, initialSigner) {
|
|
1583
|
+
const salt = this.accountSalt(addressSeed, initialSigner);
|
|
1584
|
+
const preimage = xdr.HashIdPreimage.envelopeTypeContractId(
|
|
1585
|
+
new xdr.HashIdPreimageContractId({
|
|
1586
|
+
networkId: this.networkId(),
|
|
1587
|
+
contractIdPreimage: xdr.ContractIdPreimage.contractIdPreimageFromAddress(
|
|
1588
|
+
new xdr.ContractIdPreimageFromAddress({
|
|
1589
|
+
address: new Address(this.factoryId).toScAddress(),
|
|
1590
|
+
salt
|
|
1591
|
+
})
|
|
1592
|
+
)
|
|
1593
|
+
})
|
|
1594
|
+
);
|
|
1595
|
+
return StrKey.encodeContract(hash$1(preimage.toXDR()));
|
|
1596
|
+
}
|
|
1597
|
+
/** `salt = sha256(addressSeed(32) || sec1(initialSigner)(65))` — matches the factory. */
|
|
1598
|
+
accountSalt(addressSeed, initialSigner) {
|
|
1599
|
+
return hash$1(Buffer.concat([Buffer.from(addressSeed), Buffer.from(sec1Pubkey(initialSigner))]));
|
|
1600
|
+
}
|
|
1601
|
+
/** Host function: `factory.deploy(address_seed, initial_signer)`. */
|
|
1602
|
+
buildDeploy(addressSeed, initialSigner) {
|
|
1603
|
+
return invokeFunc(this.factoryId, "deploy", [
|
|
1604
|
+
bytesScVal(addressSeed),
|
|
1605
|
+
bytesScVal(sec1Pubkey(initialSigner))
|
|
1606
|
+
]);
|
|
1607
|
+
}
|
|
1608
|
+
/** Host function: `account.add_signer(new_signer)` (requires device auth). */
|
|
1609
|
+
buildAddSigner(accountAddress, signer) {
|
|
1610
|
+
return invokeFunc(accountAddress, "add_signer", [bytesScVal(sec1Pubkey(signer))]);
|
|
1611
|
+
}
|
|
1612
|
+
/** Host function: `account.remove_signer(signer)` (requires device auth). */
|
|
1613
|
+
buildRemoveSigner(accountAddress, signer) {
|
|
1614
|
+
return invokeFunc(accountAddress, "remove_signer", [bytesScVal(sec1Pubkey(signer))]);
|
|
1615
|
+
}
|
|
1616
|
+
/** Host function: `account.add_approver(passkey)` (requires device auth). */
|
|
1617
|
+
buildAddApprover(accountAddress, passkey) {
|
|
1618
|
+
return invokeFunc(accountAddress, "add_approver", [bytesScVal(sec1Pubkey(passkey))]);
|
|
1619
|
+
}
|
|
1620
|
+
/** Host function: `account.remove_approver(passkey)` (requires device auth). */
|
|
1621
|
+
buildRemoveApprover(accountAddress, passkey) {
|
|
1622
|
+
return invokeFunc(accountAddress, "remove_approver", [bytesScVal(sec1Pubkey(passkey))]);
|
|
1623
|
+
}
|
|
1624
|
+
/** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
|
|
1625
|
+
* `sha256(sec1(new_signer) || nonce_be8)`. The batch challenge the passkey signs
|
|
1626
|
+
* is `sha256(concat(leaves))` across chains. */
|
|
1627
|
+
passkeyLeaf(newSigner, nonce) {
|
|
1628
|
+
const msg = new Uint8Array(65 + 8);
|
|
1629
|
+
msg.set(sec1Pubkey(newSigner), 0);
|
|
1630
|
+
const n = new Uint8Array(8);
|
|
1631
|
+
let v = nonce;
|
|
1632
|
+
for (let i = 7; i >= 0; i--) {
|
|
1633
|
+
n[i] = Number(v & 0xffn);
|
|
1634
|
+
v >>= 8n;
|
|
1635
|
+
}
|
|
1636
|
+
msg.set(n, 65);
|
|
1637
|
+
return sha256(msg);
|
|
1638
|
+
}
|
|
1639
|
+
/** Host function: passkey-authorized `add_signer_via_passkey` (no device auth —
|
|
1640
|
+
* authorized by the embedded WebAuthn assertion, so any relayer can submit).
|
|
1641
|
+
* `leaves`/`leafIndex` place this chain's leaf in the multi-chain batch. */
|
|
1642
|
+
buildAddSignerViaPasskey(accountAddress, newSigner, passkey, nonce, leaves, leafIndex, assertion) {
|
|
1643
|
+
const sig = encodeLowSSignature2({ r: assertion.r, s: assertion.s});
|
|
1644
|
+
const leavesScVal = xdr.ScVal.scvVec(leaves.map((l) => bytesScVal(l)));
|
|
1645
|
+
return invokeFunc(accountAddress, "add_signer_via_passkey", [
|
|
1646
|
+
bytesScVal(sec1Pubkey(newSigner)),
|
|
1647
|
+
bytesScVal(sec1Pubkey(passkey)),
|
|
1648
|
+
nativeToScVal(nonce, { type: "u64" }),
|
|
1649
|
+
leavesScVal,
|
|
1650
|
+
nativeToScVal(leafIndex, { type: "u32" }),
|
|
1651
|
+
bytesScVal(assertion.authenticatorData),
|
|
1652
|
+
bytesScVal(assertion.clientDataJSON),
|
|
1653
|
+
nativeToScVal(assertion.challengeOffset, { type: "u32" }),
|
|
1654
|
+
bytesScVal(sig)
|
|
1655
|
+
]);
|
|
1656
|
+
}
|
|
1657
|
+
/** Read whether `passkey` is a registered approver (read-only simulation). */
|
|
1658
|
+
async isApprover(accountAddress, passkey, readSource) {
|
|
1659
|
+
if (!await this.isDeployed(accountAddress)) return false;
|
|
1660
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1661
|
+
const src = new Account3(readSource, "0");
|
|
1662
|
+
const op = Operation.invokeHostFunction({
|
|
1663
|
+
func: invokeFunc(accountAddress, "is_approver", [bytesScVal(sec1Pubkey(passkey))]),
|
|
1664
|
+
auth: []
|
|
1665
|
+
});
|
|
1666
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1667
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1668
|
+
if (rpc.Api.isSimulationError(sim)) {
|
|
1669
|
+
throw new Error(`kit/stellar: is_approver simulation failed: ${sim.error}`);
|
|
1670
|
+
}
|
|
1671
|
+
if (!sim.result?.retval) return false;
|
|
1672
|
+
return scValToNative(sim.result.retval) === true;
|
|
1673
|
+
}
|
|
1674
|
+
/** Read the current passkey-approval nonce (read-only simulation). */
|
|
1675
|
+
async passkeyNonce(accountAddress, readSource) {
|
|
1676
|
+
if (!await this.isDeployed(accountAddress)) return 0n;
|
|
1677
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1678
|
+
const src = new Account3(readSource, "0");
|
|
1679
|
+
const op = Operation.invokeHostFunction({
|
|
1680
|
+
func: invokeFunc(accountAddress, "passkey_nonce", []),
|
|
1681
|
+
auth: []
|
|
1682
|
+
});
|
|
1683
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1684
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1685
|
+
if (rpc.Api.isSimulationError(sim)) {
|
|
1686
|
+
throw new Error(`kit/stellar: passkey_nonce simulation failed: ${sim.error}`);
|
|
1687
|
+
}
|
|
1688
|
+
if (!sim.result?.retval) return 0n;
|
|
1689
|
+
return BigInt(scValToNative(sim.result.retval));
|
|
1690
|
+
}
|
|
1691
|
+
/** Host function: SEP-41 `token.transfer(from=account, to, amount)` (device auth). */
|
|
1692
|
+
buildTransfer(tokenId, accountAddress, destination, amount) {
|
|
1693
|
+
return invokeFunc(tokenId, "transfer", [
|
|
1694
|
+
new Address(accountAddress).toScVal(),
|
|
1695
|
+
new Address(destination).toScVal(),
|
|
1696
|
+
nativeToScVal(amount, { type: "i128" })
|
|
1697
|
+
]);
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Sign a Soroban authorization entry with the silent device key, producing the
|
|
1701
|
+
* `Vec<DeviceSignature>` the account's `__check_auth` verifies. The device
|
|
1702
|
+
* signs `sha256(preimage)` (WebCrypto hashes once more internally), which is
|
|
1703
|
+
* exactly what the contract recomputes. Mutates + returns the entry.
|
|
1704
|
+
*/
|
|
1705
|
+
async signAuthEntry(entry, validUntilLedger) {
|
|
1706
|
+
const addrCreds = entry.credentials().address();
|
|
1707
|
+
addrCreds.signatureExpirationLedger(validUntilLedger);
|
|
1708
|
+
const preimage = xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
|
|
1709
|
+
new xdr.HashIdPreimageSorobanAuthorization({
|
|
1710
|
+
networkId: this.networkId(),
|
|
1711
|
+
nonce: addrCreds.nonce(),
|
|
1712
|
+
signatureExpirationLedger: validUntilLedger,
|
|
1713
|
+
invocation: entry.rootInvocation()
|
|
1714
|
+
})
|
|
1715
|
+
);
|
|
1716
|
+
const payload = hash$1(preimage.toXDR());
|
|
1717
|
+
const sig = await this.signer.sign(new Uint8Array(payload));
|
|
1718
|
+
const pubkey = await this.signer.getPublicKey();
|
|
1719
|
+
addrCreds.signature(deviceSignatureScVal(pubkey, sig));
|
|
1720
|
+
return entry;
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Read a SEP-41 token balance of `account` via a read-only simulation of
|
|
1724
|
+
* `token.balance(account)`. Returns 0 when the account isn't deployed or holds
|
|
1725
|
+
* none. `readSource` is any funded G-account (used only for the simulation).
|
|
1726
|
+
*/
|
|
1727
|
+
async readBalance(tokenId, account, readSource) {
|
|
1728
|
+
if (!await this.isDeployed(account)) return 0n;
|
|
1729
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1730
|
+
const src = new Account3(readSource, "0");
|
|
1731
|
+
const op = Operation.invokeHostFunction({
|
|
1732
|
+
func: invokeFunc(tokenId, "balance", [new Address(account).toScVal()]),
|
|
1733
|
+
auth: []
|
|
1734
|
+
});
|
|
1735
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1736
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1737
|
+
if (rpc.Api.isSimulationError(sim) || !sim.result?.retval) return 0n;
|
|
1738
|
+
return BigInt(scValToNative(sim.result.retval));
|
|
1739
|
+
}
|
|
1740
|
+
/** Whether the account contract instance exists on-chain (is deployed). */
|
|
1741
|
+
async isDeployed(accountAddress) {
|
|
1742
|
+
try {
|
|
1743
|
+
const res = await this.server().getContractData(
|
|
1744
|
+
accountAddress,
|
|
1745
|
+
xdr.ScVal.scvLedgerKeyContractInstance(),
|
|
1746
|
+
rpc.Durability.Persistent
|
|
1747
|
+
);
|
|
1748
|
+
return !!res;
|
|
1749
|
+
} catch {
|
|
1750
|
+
return false;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Read whether `signer` is a currently-authorized signer of the account, via a
|
|
1755
|
+
* read-only simulation of `account.is_authorized(signer)`. `readSource` is any
|
|
1756
|
+
* funded G-account (used only for the simulation's source/sequence).
|
|
1757
|
+
*/
|
|
1758
|
+
async isAuthorizedSigner(accountAddress, signer, readSource) {
|
|
1759
|
+
if (!await this.isDeployed(accountAddress)) return false;
|
|
1760
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1761
|
+
const src = new Account3(readSource, "0");
|
|
1762
|
+
const op = Operation.invokeHostFunction({
|
|
1763
|
+
func: invokeFunc(accountAddress, "is_authorized", [bytesScVal(sec1Pubkey(signer))]),
|
|
1764
|
+
auth: []
|
|
1765
|
+
});
|
|
1766
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1767
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1768
|
+
if (rpc.Api.isSimulationError(sim)) {
|
|
1769
|
+
throw new Error(`kit/stellar: is_authorized simulation failed: ${sim.error}`);
|
|
1770
|
+
}
|
|
1771
|
+
if (!sim.result?.retval) return false;
|
|
1772
|
+
return scValToNative(sim.result.retval) === true;
|
|
1773
|
+
}
|
|
1774
|
+
};
|
|
1775
|
+
function sec1Pubkey(pk) {
|
|
1776
|
+
const out = new Uint8Array(65);
|
|
1777
|
+
out[0] = 4;
|
|
1778
|
+
out.set(bigIntTo32Bytes(pk.x), 1);
|
|
1779
|
+
out.set(bigIntTo32Bytes(pk.y), 33);
|
|
1780
|
+
return out;
|
|
1781
|
+
}
|
|
1782
|
+
function encodeLowSSignature2(sig) {
|
|
1783
|
+
const lowS2 = sig.s > SECP256R1_N2 / 2n ? SECP256R1_N2 - sig.s : sig.s;
|
|
1784
|
+
const out = new Uint8Array(64);
|
|
1785
|
+
out.set(bigIntTo32Bytes(sig.r), 0);
|
|
1786
|
+
out.set(bigIntTo32Bytes(lowS2), 32);
|
|
1787
|
+
return out;
|
|
1788
|
+
}
|
|
1789
|
+
function deviceSignatureScVal(pubkey, sig) {
|
|
1790
|
+
const element = nativeToScVal(
|
|
1791
|
+
{
|
|
1792
|
+
public_key: Buffer.from(sec1Pubkey(pubkey)),
|
|
1793
|
+
signature: Buffer.from(encodeLowSSignature2(sig))
|
|
1794
|
+
},
|
|
1795
|
+
{ type: { public_key: ["symbol", "bytes"], signature: ["symbol", "bytes"] } }
|
|
1796
|
+
);
|
|
1797
|
+
return xdr.ScVal.scvVec([element]);
|
|
1798
|
+
}
|
|
1799
|
+
function invokeFunc(contractId, method, args) {
|
|
1800
|
+
return xdr.HostFunction.hostFunctionTypeInvokeContract(
|
|
1801
|
+
new xdr.InvokeContractArgs({
|
|
1802
|
+
contractAddress: new Address(contractId).toScAddress(),
|
|
1803
|
+
functionName: method,
|
|
1804
|
+
args
|
|
1805
|
+
})
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
function bytesScVal(bytes) {
|
|
1809
|
+
return xdr.ScVal.scvBytes(Buffer.from(bytes));
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// src/chains/stellar/StellarRelayer.ts
|
|
1813
|
+
var StellarRelayer = class {
|
|
1814
|
+
constructor(opts) {
|
|
1815
|
+
this.opts = opts;
|
|
1816
|
+
}
|
|
1817
|
+
/** The relayer's source/fee-payer G-account (fetched + cached from the backend). */
|
|
1818
|
+
async getSource() {
|
|
1819
|
+
if (this.source) return this.source;
|
|
1820
|
+
const res = await fetch(`${this.opts.baseUrl}/api/stellar/relay?network=${this.opts.network}`);
|
|
1821
|
+
if (!res.ok) throw new Error(`kit/stellar: relayer source lookup failed (${res.status})`);
|
|
1822
|
+
const { fee_payer } = await res.json();
|
|
1823
|
+
this.source = fee_payer;
|
|
1824
|
+
return this.source;
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* POST the assembled, device-authorized transaction XDR to the relayer to sign
|
|
1828
|
+
* the envelope + submit. Returns the confirmed transaction hash.
|
|
1829
|
+
*/
|
|
1830
|
+
async submit(transactionXdr) {
|
|
1831
|
+
const res = await fetch(`${this.opts.baseUrl}/api/stellar/relay`, {
|
|
1832
|
+
method: "POST",
|
|
1833
|
+
headers: { "Content-Type": "application/json" },
|
|
1834
|
+
body: JSON.stringify({
|
|
1835
|
+
app_id: this.opts.appId,
|
|
1836
|
+
network: this.opts.network,
|
|
1837
|
+
transaction: transactionXdr
|
|
1838
|
+
})
|
|
1839
|
+
});
|
|
1840
|
+
if (!res.ok) {
|
|
1841
|
+
const detail = await res.text().catch(() => "");
|
|
1842
|
+
throw new Error(`kit/stellar: relay failed (${res.status}) ${detail}`);
|
|
1843
|
+
}
|
|
1844
|
+
const { hash: hash6 } = await res.json();
|
|
1845
|
+
return hash6;
|
|
1846
|
+
}
|
|
1847
|
+
};
|
|
1848
|
+
var CavosStellar = class _CavosStellar {
|
|
1849
|
+
constructor(identity, address, status, network, adapter, devicePubkey, relayer, sourceKeypair) {
|
|
1850
|
+
this.identity = identity;
|
|
1851
|
+
this.address = address;
|
|
1852
|
+
this.status = status;
|
|
1853
|
+
this.network = network;
|
|
1854
|
+
this.adapter = adapter;
|
|
1855
|
+
this.devicePubkey = devicePubkey;
|
|
1856
|
+
this.relayer = relayer;
|
|
1857
|
+
this.sourceKeypair = sourceKeypair;
|
|
1858
|
+
/** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
|
|
1859
|
+
this.chain = "stellar";
|
|
1860
|
+
}
|
|
1861
|
+
get publicKey() {
|
|
1862
|
+
return this.devicePubkey;
|
|
1863
|
+
}
|
|
1864
|
+
static async connect(opts) {
|
|
1865
|
+
const identity = opts.identity ?? await opts.auth?.authenticate();
|
|
1866
|
+
if (!identity) throw new Error("kit/stellar: connect requires `identity` or `auth`");
|
|
1867
|
+
const signer = opts.createSigner ? await opts.createSigner(`${identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${identity.userId}:${opts.appSalt}` });
|
|
1868
|
+
const devicePubkey = await signer.getPublicKey();
|
|
1869
|
+
const adapter = new StellarAdapter({
|
|
1870
|
+
network: opts.network,
|
|
1871
|
+
rpcUrl: opts.rpcUrl,
|
|
1872
|
+
factoryId: opts.factoryId,
|
|
1873
|
+
signer
|
|
1874
|
+
});
|
|
1875
|
+
const addressSeed = deriveAddressSeedStellar({ userId: identity.userId, appSalt: opts.appSalt });
|
|
1876
|
+
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
1877
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
|
|
1878
|
+
const relayer = opts.relayer ?? (opts.appId ? new StellarRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : void 0);
|
|
1879
|
+
const build = (address2, status) => new _CavosStellar(identity, address2, status, opts.network, adapter, devicePubkey, relayer, opts.sourceKeypair);
|
|
1880
|
+
const self = build("", "needs-device-approval");
|
|
1881
|
+
const readSource = await self.resolveSource();
|
|
1882
|
+
const existing = await registry.lookup(identity.userId);
|
|
1883
|
+
if (existing) {
|
|
1884
|
+
const isSigner2 = await adapter.isAuthorizedSigner(existing.address, devicePubkey, readSource);
|
|
1885
|
+
return build(existing.address, isSigner2 ? "ready" : "needs-device-approval");
|
|
1886
|
+
}
|
|
1887
|
+
const address = adapter.computeAddress(addressSeed, devicePubkey);
|
|
1888
|
+
if (!await adapter.isDeployed(address)) {
|
|
1889
|
+
const func = adapter.buildDeploy(addressSeed, devicePubkey);
|
|
1890
|
+
await self.submitHostFunction(func, void 0);
|
|
1891
|
+
}
|
|
1892
|
+
await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
|
|
1893
|
+
const isSigner = await adapter.isAuthorizedSigner(address, devicePubkey, readSource);
|
|
1894
|
+
return build(address, isSigner ? "ready" : "needs-device-approval");
|
|
1895
|
+
}
|
|
1896
|
+
/** Authorize an additional device signer (device-signed via `__check_auth`). */
|
|
1897
|
+
async addSigner(pubkey) {
|
|
1898
|
+
const func = this.adapter.buildAddSigner(this.address, pubkey);
|
|
1899
|
+
return this.submitHostFunction(func, this.address);
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Enroll a passkey as an approver (2FA-style step-up). Device-signed + gasless;
|
|
1903
|
+
* requires a ready device. Idempotent. Returns the passkey pubkey + tx hash.
|
|
1904
|
+
*/
|
|
1905
|
+
async enrollPasskey(passkey, params) {
|
|
1906
|
+
const enrolled = await passkey.enroll(params);
|
|
1907
|
+
const { transactionHash } = await this.addApprover(enrolled.publicKey);
|
|
1908
|
+
return { publicKey: enrolled.publicKey, transactionHash };
|
|
1909
|
+
}
|
|
1910
|
+
/** Register an already-enrolled passkey pubkey as an approver (gasless).
|
|
1911
|
+
* Idempotent. Lets one passkey be registered across chains without re-prompting. */
|
|
1912
|
+
async addApprover(pubkey) {
|
|
1913
|
+
if (this.status !== "ready") {
|
|
1914
|
+
throw new Error("kit/stellar: addApprover requires a ready, authorized device");
|
|
1915
|
+
}
|
|
1916
|
+
const readSource = await this.resolveSource();
|
|
1917
|
+
if (await this.adapter.isApprover(this.address, pubkey, readSource)) return {};
|
|
1918
|
+
const func = this.adapter.buildAddApprover(this.address, pubkey);
|
|
1919
|
+
const transactionHash = await this.submitHostFunction(func, this.address);
|
|
1920
|
+
return { transactionHash };
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* From a fresh browser (status `needs-device-approval`), approve adding THIS
|
|
1924
|
+
* device using the user's synced passkey. Gasless via the relayer — the call
|
|
1925
|
+
* carries the WebAuthn assertion, so no device signature is needed. Returns the
|
|
1926
|
+
* tx hash. No trip back to an already-authorized device.
|
|
1927
|
+
*/
|
|
1928
|
+
async approveThisDeviceWithPasskey(passkey) {
|
|
1929
|
+
if (this.status === "ready") {
|
|
1930
|
+
throw new Error("kit/stellar: this device is already an authorized signer");
|
|
1931
|
+
}
|
|
1932
|
+
const { leaf, nonce } = await this.passkeyLeafForThisDevice();
|
|
1933
|
+
const leaves = [leaf];
|
|
1934
|
+
const assertion = await passkey.assert(batchChallenge(leaves));
|
|
1935
|
+
const { transactionHash } = await this.submitPasskeyApproval(assertion, leaves, 0, nonce);
|
|
1936
|
+
return transactionHash;
|
|
1937
|
+
}
|
|
1938
|
+
/** This device's leaf + passkey nonce for a (possibly multi-chain) batch. */
|
|
1939
|
+
async passkeyLeafForThisDevice() {
|
|
1940
|
+
const readSource = await this.resolveSource();
|
|
1941
|
+
const nonce = await this.adapter.passkeyNonce(this.address, readSource);
|
|
1942
|
+
return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
|
|
1943
|
+
}
|
|
1944
|
+
/** Submit `add_signer_via_passkey` given a shared assertion + batch position.
|
|
1945
|
+
* No device auth entry — authorized purely by the passkey assertion. */
|
|
1946
|
+
async submitPasskeyApproval(assertion, leaves, leafIndex, nonce) {
|
|
1947
|
+
const readSource = await this.resolveSource();
|
|
1948
|
+
const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
|
|
1949
|
+
const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
|
|
1950
|
+
let approver = null;
|
|
1951
|
+
for (const cand of candidates) {
|
|
1952
|
+
if (await this.adapter.isApprover(this.address, cand.publicKey, readSource)) {
|
|
1953
|
+
approver = cand.publicKey;
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
if (!approver) throw new Error("kit/stellar: this passkey is not a registered approver");
|
|
1958
|
+
const func = this.adapter.buildAddSignerViaPasskey(
|
|
1959
|
+
this.address,
|
|
1960
|
+
this.devicePubkey,
|
|
1961
|
+
approver,
|
|
1962
|
+
nonce,
|
|
1963
|
+
leaves,
|
|
1964
|
+
leafIndex,
|
|
1965
|
+
assertion
|
|
1966
|
+
);
|
|
1967
|
+
return { transactionHash: await this.submitHostFunction(func, void 0) };
|
|
1968
|
+
}
|
|
1969
|
+
/** Move `amount` stroops of native XLM to `destination` (device-signed). */
|
|
1970
|
+
async execute(amount, destination) {
|
|
1971
|
+
return this.executeTransfer(NATIVE_SAC_ID[this.network], amount, destination);
|
|
1972
|
+
}
|
|
1973
|
+
/** Read this account's balance of `tokenId` (defaults to native XLM), in stroops. */
|
|
1974
|
+
async balance(tokenId = NATIVE_SAC_ID[this.network]) {
|
|
1975
|
+
const readSource = await this.resolveSource();
|
|
1976
|
+
return this.adapter.readBalance(tokenId, this.address, readSource);
|
|
1977
|
+
}
|
|
1978
|
+
/** Transfer `amount` of any SEP-41 token out of the account (device-signed). */
|
|
1979
|
+
async executeTransfer(tokenId, amount, destination) {
|
|
1980
|
+
if (this.status !== "ready") {
|
|
1981
|
+
throw new Error("kit/stellar: this device is not yet an authorized signer of the wallet");
|
|
1982
|
+
}
|
|
1983
|
+
const func = this.adapter.buildTransfer(tokenId, this.address, destination, amount);
|
|
1984
|
+
return this.submitHostFunction(func, this.address);
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Register the backup signer derived from `code` as an authorized signer of
|
|
1988
|
+
* this account (device-signed). Idempotent. The code never leaves the device —
|
|
1989
|
+
* only the derived public key travels on-chain. Mirrors the other chains.
|
|
1990
|
+
*/
|
|
1991
|
+
async setupRecovery(code) {
|
|
1992
|
+
if (this.status !== "ready") {
|
|
1993
|
+
throw new Error("kit/stellar: setupRecovery requires a ready, registered device");
|
|
1994
|
+
}
|
|
1995
|
+
const { publicKey: backupPubkey } = deriveBackupKey(code);
|
|
1996
|
+
const readSource = await this.resolveSource();
|
|
1997
|
+
if (await this.adapter.isAuthorizedSigner(this.address, backupPubkey, readSource)) return void 0;
|
|
1998
|
+
return this.addSigner(backupPubkey);
|
|
1999
|
+
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Recover an account after losing every device signer: derive the backup key
|
|
2002
|
+
* from `code`, use it (not the new device) to authorize `add_signer(newDevice)`,
|
|
2003
|
+
* and return a ready handle bound to the new device. The address is unchanged.
|
|
2004
|
+
*/
|
|
2005
|
+
static async recover(opts) {
|
|
2006
|
+
const signer = opts.createSigner ? await opts.createSigner(`${opts.identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${opts.identity.userId}:${opts.appSalt}` });
|
|
2007
|
+
const devicePubkey = await signer.getPublicKey();
|
|
2008
|
+
const backup = BackupSigner.fromCode(opts.code);
|
|
2009
|
+
const backupAdapter = new StellarAdapter({
|
|
2010
|
+
network: opts.network,
|
|
2011
|
+
rpcUrl: opts.rpcUrl,
|
|
2012
|
+
factoryId: opts.factoryId,
|
|
2013
|
+
signer: backup
|
|
2014
|
+
});
|
|
2015
|
+
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
2016
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
|
|
2017
|
+
const existing = await registry.lookup(opts.identity.userId);
|
|
2018
|
+
if (!existing) {
|
|
2019
|
+
throw new Error("kit/stellar: no account found for this identity \u2014 nothing to recover");
|
|
2020
|
+
}
|
|
2021
|
+
const relayer = opts.relayer ?? (opts.appId ? new StellarRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : void 0);
|
|
2022
|
+
const backupHandle = new _CavosStellar(
|
|
2023
|
+
opts.identity,
|
|
2024
|
+
existing.address,
|
|
2025
|
+
"ready",
|
|
2026
|
+
opts.network,
|
|
2027
|
+
backupAdapter,
|
|
2028
|
+
devicePubkey,
|
|
2029
|
+
relayer,
|
|
2030
|
+
opts.sourceKeypair
|
|
2031
|
+
);
|
|
2032
|
+
const readSource = await backupHandle.resolveSource();
|
|
2033
|
+
if (!await backupAdapter.isAuthorizedSigner(existing.address, devicePubkey, readSource)) {
|
|
2034
|
+
await backupHandle.addSigner(devicePubkey);
|
|
2035
|
+
}
|
|
2036
|
+
const adapter = new StellarAdapter({
|
|
2037
|
+
network: opts.network,
|
|
2038
|
+
rpcUrl: opts.rpcUrl,
|
|
2039
|
+
factoryId: opts.factoryId,
|
|
2040
|
+
signer
|
|
2041
|
+
});
|
|
2042
|
+
return new _CavosStellar(
|
|
2043
|
+
opts.identity,
|
|
2044
|
+
existing.address,
|
|
2045
|
+
"ready",
|
|
2046
|
+
opts.network,
|
|
2047
|
+
adapter,
|
|
2048
|
+
devicePubkey,
|
|
2049
|
+
relayer,
|
|
2050
|
+
opts.sourceKeypair
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
/** The transaction source/fee-payer G-address (relayer or self-funded). */
|
|
2054
|
+
async resolveSource() {
|
|
2055
|
+
if (this.relayer) return this.relayer.getSource();
|
|
2056
|
+
if (this.sourceKeypair) return this.sourceKeypair.publicKey();
|
|
2057
|
+
throw new Error("kit/stellar: a relayer (appId) or sourceKeypair is required");
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* Build → simulate → device-sign auth → assemble → submit an invoke-contract
|
|
2061
|
+
* host function. `authAccount` is the account whose `__check_auth` must sign the
|
|
2062
|
+
* operation's Soroban auth entry (undefined for a plain factory deploy).
|
|
2063
|
+
*/
|
|
2064
|
+
async submitHostFunction(func, authAccount) {
|
|
2065
|
+
const server = this.adapter.server();
|
|
2066
|
+
const sourceAddr = await this.resolveSource();
|
|
2067
|
+
const simSource = new Account(sourceAddr, "0");
|
|
2068
|
+
const unsignedOp = Operation.invokeHostFunction({ func, auth: [] });
|
|
2069
|
+
const simTx = new TransactionBuilder(simSource, {
|
|
2070
|
+
fee: BASE_FEE,
|
|
2071
|
+
networkPassphrase: this.adapter.passphrase
|
|
2072
|
+
}).addOperation(unsignedOp).setTimeout(180).build();
|
|
2073
|
+
const sim = await server.simulateTransaction(simTx);
|
|
2074
|
+
if (rpc.Api.isSimulationError(sim)) {
|
|
2075
|
+
throw new Error(`kit/stellar: simulation failed: ${sim.error}`);
|
|
2076
|
+
}
|
|
2077
|
+
const validUntil = (await server.getLatestLedger()).sequence + 100;
|
|
2078
|
+
const entries = sim.result?.auth ?? [];
|
|
2079
|
+
const signedAuth = [];
|
|
2080
|
+
for (const entry of entries) {
|
|
2081
|
+
if (authAccount && isAddressCredentialFor(entry, authAccount)) {
|
|
2082
|
+
signedAuth.push(await this.adapter.signAuthEntry(entry, validUntil));
|
|
2083
|
+
} else {
|
|
2084
|
+
signedAuth.push(entry);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
const account = await server.getAccount(sourceAddr);
|
|
2088
|
+
const finalOp = Operation.invokeHostFunction({ func, auth: signedAuth });
|
|
2089
|
+
const built = new TransactionBuilder(account, {
|
|
2090
|
+
fee: BASE_FEE,
|
|
2091
|
+
networkPassphrase: this.adapter.passphrase
|
|
2092
|
+
}).addOperation(finalOp).setTimeout(180).build();
|
|
2093
|
+
const authSim = await server.simulateTransaction(built);
|
|
2094
|
+
if (rpc.Api.isSimulationError(authSim)) {
|
|
2095
|
+
throw new Error(`kit/stellar: auth simulation failed: ${authSim.error}`);
|
|
2096
|
+
}
|
|
2097
|
+
const assembled = rpc.assembleTransaction(built, authSim).build();
|
|
2098
|
+
if (this.relayer) {
|
|
2099
|
+
return this.relayer.submit(assembled.toXDR());
|
|
2100
|
+
}
|
|
2101
|
+
if (this.sourceKeypair) {
|
|
2102
|
+
assembled.sign(this.sourceKeypair);
|
|
2103
|
+
return this.sendAndConfirm(assembled);
|
|
2104
|
+
}
|
|
2105
|
+
throw new Error("kit/stellar: no relayer or sourceKeypair configured to submit");
|
|
2106
|
+
}
|
|
2107
|
+
/** Submit a signed tx via RPC and poll to confirmation. Returns the hash. */
|
|
2108
|
+
async sendAndConfirm(tx2) {
|
|
2109
|
+
const server = this.adapter.server();
|
|
2110
|
+
const sent = await server.sendTransaction(tx2);
|
|
2111
|
+
if (sent.status === "ERROR") {
|
|
2112
|
+
throw new Error(`kit/stellar: submit rejected: ${JSON.stringify(sent.errorResult)}`);
|
|
2113
|
+
}
|
|
2114
|
+
const hash6 = sent.hash;
|
|
2115
|
+
for (let i = 0; i < 30; i++) {
|
|
2116
|
+
const got = await server.getTransaction(hash6);
|
|
2117
|
+
if (got.status === rpc.Api.GetTransactionStatus.SUCCESS) return hash6;
|
|
2118
|
+
if (got.status === rpc.Api.GetTransactionStatus.FAILED) {
|
|
2119
|
+
throw new Error(`kit/stellar: tx ${hash6} failed`);
|
|
2120
|
+
}
|
|
2121
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
2122
|
+
}
|
|
2123
|
+
throw new Error(`kit/stellar: tx ${hash6} not confirmed in time`);
|
|
2124
|
+
}
|
|
2125
|
+
};
|
|
2126
|
+
function isAddressCredentialFor(entry, accountAddress) {
|
|
2127
|
+
const creds = entry.credentials();
|
|
2128
|
+
if (creds.switch() !== xdr.SorobanCredentialsType.sorobanCredentialsAddress()) return false;
|
|
2129
|
+
return Address.fromScAddress(creds.address().address()).toString() === accountAddress;
|
|
2130
|
+
}
|
|
2131
|
+
var defaultRegistry2 = new InMemoryWalletRegistry();
|
|
2132
|
+
|
|
1191
2133
|
// src/recovery/HttpRecoveryClient.ts
|
|
1192
2134
|
function toHex2(n) {
|
|
1193
2135
|
return "0x" + n.toString(16);
|
|
@@ -1267,14 +2209,19 @@ var SOLANA_ENV = {
|
|
|
1267
2209
|
mainnet: "solana-mainnet",
|
|
1268
2210
|
testnet: "solana-devnet"
|
|
1269
2211
|
};
|
|
2212
|
+
var STELLAR_ENV = {
|
|
2213
|
+
mainnet: "stellar-mainnet",
|
|
2214
|
+
testnet: "stellar-testnet"
|
|
2215
|
+
};
|
|
1270
2216
|
var Cavos = class _Cavos {
|
|
1271
|
-
constructor(identity, address, status, account, adapter, devicePubkey) {
|
|
2217
|
+
constructor(identity, address, status, account, adapter, devicePubkey, paymaster) {
|
|
1272
2218
|
this.identity = identity;
|
|
1273
2219
|
this.address = address;
|
|
1274
2220
|
this.status = status;
|
|
1275
2221
|
this.account = account;
|
|
1276
2222
|
this.adapter = adapter;
|
|
1277
2223
|
this.devicePubkey = devicePubkey;
|
|
2224
|
+
this.paymaster = paymaster;
|
|
1278
2225
|
/** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
|
|
1279
2226
|
this.chain = "starknet";
|
|
1280
2227
|
/** Request id of the pending device-addition, when status is needs-device-approval. */
|
|
@@ -1307,6 +2254,22 @@ var Cavos = class _Cavos {
|
|
|
1307
2254
|
...opts.feePayer ? { feePayer: opts.feePayer } : {}
|
|
1308
2255
|
});
|
|
1309
2256
|
}
|
|
2257
|
+
if (opts.chain === "stellar") {
|
|
2258
|
+
return CavosStellar.connect({
|
|
2259
|
+
network: STELLAR_ENV[opts.network],
|
|
2260
|
+
...opts.auth ? { auth: opts.auth } : {},
|
|
2261
|
+
...opts.identity ? { identity: opts.identity } : {},
|
|
2262
|
+
appSalt: opts.appSalt,
|
|
2263
|
+
...opts.appId ? { appId: opts.appId } : {},
|
|
2264
|
+
...opts.backendUrl ? { backendUrl: opts.backendUrl } : {},
|
|
2265
|
+
...opts.registry ? { registry: opts.registry } : {},
|
|
2266
|
+
...opts.rpcUrl ? { rpcUrl: opts.rpcUrl } : {},
|
|
2267
|
+
...opts.factoryId ? { factoryId: opts.factoryId } : {},
|
|
2268
|
+
...opts.createSigner ? { createSigner: opts.createSigner } : {},
|
|
2269
|
+
...opts.stellarRelayer ? { relayer: opts.stellarRelayer } : {},
|
|
2270
|
+
...opts.stellarSourceKeypair ? { sourceKeypair: opts.stellarSourceKeypair } : {}
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
1310
2273
|
if (!opts.paymasterApiKey) {
|
|
1311
2274
|
throw new Error("kit: `paymasterApiKey` is required for Starknet connections");
|
|
1312
2275
|
}
|
|
@@ -1334,15 +2297,17 @@ var Cavos = class _Cavos {
|
|
|
1334
2297
|
const provider = new RpcProvider({
|
|
1335
2298
|
nodeUrl: opts.rpcUrl ?? STARKNET_NETWORKS[opts.network].rpcUrl
|
|
1336
2299
|
});
|
|
2300
|
+
const paymasterUrl = opts.paymasterUrl ?? CAVOS_PAYMASTER_URL[opts.network];
|
|
2301
|
+
const paymasterConfig = { url: paymasterUrl, apiKey: opts.paymasterApiKey };
|
|
1337
2302
|
const paymaster = new PaymasterRpc({
|
|
1338
|
-
nodeUrl:
|
|
2303
|
+
nodeUrl: paymasterUrl,
|
|
1339
2304
|
headers: { "x-paymaster-api-key": opts.paymasterApiKey }
|
|
1340
2305
|
});
|
|
1341
2306
|
const addressSeed = deriveAddressSeed({ userId: identity.userId, appSalt: opts.appSalt });
|
|
1342
2307
|
const signer = opts.createSigner ? await opts.createSigner(`${identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${identity.userId}:${opts.appSalt}` });
|
|
1343
2308
|
const devicePubkey = await signer.getPublicKey();
|
|
1344
2309
|
const adapter = new StarknetAdapter({ classHash, signer, provider });
|
|
1345
|
-
const makeAccount = (address2) => new Account({
|
|
2310
|
+
const makeAccount = (address2) => new Account$1({
|
|
1346
2311
|
provider,
|
|
1347
2312
|
address: address2,
|
|
1348
2313
|
signer: new StarknetDeviceSigner(signer),
|
|
@@ -1350,7 +2315,7 @@ var Cavos = class _Cavos {
|
|
|
1350
2315
|
cairoVersion: "1"
|
|
1351
2316
|
});
|
|
1352
2317
|
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
1353
|
-
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) :
|
|
2318
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry3);
|
|
1354
2319
|
const recovery = opts.recovery ?? (opts.appId ? new HttpRecoveryClient({ baseUrl: backendUrl, appId: opts.appId }) : null);
|
|
1355
2320
|
const existing = await registry.lookup(identity.userId);
|
|
1356
2321
|
if (existing) {
|
|
@@ -1362,7 +2327,8 @@ var Cavos = class _Cavos {
|
|
|
1362
2327
|
isSigner2 ? "ready" : "needs-device-approval",
|
|
1363
2328
|
account2,
|
|
1364
2329
|
adapter,
|
|
1365
|
-
devicePubkey
|
|
2330
|
+
devicePubkey,
|
|
2331
|
+
paymasterConfig
|
|
1366
2332
|
);
|
|
1367
2333
|
if (!isSigner2 && recovery) {
|
|
1368
2334
|
const dedup = lastDeviceRequest.get(identity.userId);
|
|
@@ -1421,7 +2387,8 @@ var Cavos = class _Cavos {
|
|
|
1421
2387
|
isSigner ? "ready" : "needs-device-approval",
|
|
1422
2388
|
account,
|
|
1423
2389
|
adapter,
|
|
1424
|
-
devicePubkey
|
|
2390
|
+
devicePubkey,
|
|
2391
|
+
paymasterConfig
|
|
1425
2392
|
);
|
|
1426
2393
|
}
|
|
1427
2394
|
/** This device's public key (e.g. to request addition to an existing wallet). */
|
|
@@ -1442,6 +2409,92 @@ var Cavos = class _Cavos {
|
|
|
1442
2409
|
async addSigner(pubkey) {
|
|
1443
2410
|
return this.execute([this.adapter.buildAddSigner(this.address, pubkey)]);
|
|
1444
2411
|
}
|
|
2412
|
+
/**
|
|
2413
|
+
* Enroll a passkey as an APPROVER so the user can later add devices from any
|
|
2414
|
+
* browser (2FA-style step-up). Requires a ready device (the enrollment call is
|
|
2415
|
+
* device-signed and gasless). Idempotent: a no-op if the passkey is already an
|
|
2416
|
+
* approver. Call this whenever the app decides to prompt "turn on device
|
|
2417
|
+
* approvals". Returns the passkey's public key + the enrollment tx hash.
|
|
2418
|
+
*/
|
|
2419
|
+
async enrollPasskey(passkey, params) {
|
|
2420
|
+
const enrolled = await passkey.enroll(params);
|
|
2421
|
+
const { transactionHash } = await this.addApprover(enrolled.publicKey);
|
|
2422
|
+
return { publicKey: enrolled.publicKey, transactionHash };
|
|
2423
|
+
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Register an ALREADY-enrolled passkey public key as an approver (gasless,
|
|
2426
|
+
* device-signed). Idempotent. Use this to register ONE passkey across multiple
|
|
2427
|
+
* chains without re-prompting `passkey.enroll()` on each: enroll once, then
|
|
2428
|
+
* call `addApprover(pubkey)` on each chain's wallet.
|
|
2429
|
+
*/
|
|
2430
|
+
async addApprover(pubkey) {
|
|
2431
|
+
if (this.status !== "ready") {
|
|
2432
|
+
throw new Error("kit: addApprover requires a ready, authorized device");
|
|
2433
|
+
}
|
|
2434
|
+
if (await this.adapter.isApprover(this.address, pubkey)) return {};
|
|
2435
|
+
const { transactionHash } = await this.execute([
|
|
2436
|
+
this.adapter.buildAddApprover(this.address, pubkey)
|
|
2437
|
+
]);
|
|
2438
|
+
return { transactionHash };
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* From a brand-new browser (status `needs-device-approval`), use the user's
|
|
2442
|
+
* synced passkey to authorize adding THIS device — no trip back to an already-
|
|
2443
|
+
* authorized device.
|
|
2444
|
+
*
|
|
2445
|
+
* `add_signer_via_passkey` is a public external authorized by the embedded
|
|
2446
|
+
* WebAuthn assertion (no device signature), so by default we sponsor it through
|
|
2447
|
+
* the Cavos paymaster's `paymaster_executeDirectTransaction` (the forwarder's
|
|
2448
|
+
* `execute_sponsored` runs a generic call — it does NOT require SNIP-9). Pass a
|
|
2449
|
+
* custom `submit` to route it through your own relayer instead. Returns the tx.
|
|
2450
|
+
*/
|
|
2451
|
+
async approveThisDeviceWithPasskey(opts) {
|
|
2452
|
+
if (this.status === "ready") {
|
|
2453
|
+
throw new Error("kit: this device is already an authorized signer");
|
|
2454
|
+
}
|
|
2455
|
+
const { leaf, nonce } = await this.passkeyLeafForThisDevice();
|
|
2456
|
+
const leaves = [leaf];
|
|
2457
|
+
const assertion = await opts.passkey.assert(batchChallenge(leaves));
|
|
2458
|
+
return this.submitPasskeyApproval(assertion, leaves, 0, nonce, opts.submit);
|
|
2459
|
+
}
|
|
2460
|
+
/** This device's leaf + the current passkey nonce, for a (possibly multi-chain)
|
|
2461
|
+
* passkey approval batch. See `approveDeviceEverywhere`. */
|
|
2462
|
+
async passkeyLeafForThisDevice() {
|
|
2463
|
+
const nonce = await this.adapter.getPasskeyNonce(this.address);
|
|
2464
|
+
return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
|
|
2465
|
+
}
|
|
2466
|
+
/** Submit `add_signer_via_passkey` given a (shared) assertion + this chain's
|
|
2467
|
+
* position in the batch. The assertion doesn't carry the passkey pubkey, so we
|
|
2468
|
+
* recover both candidates and pick the enrolled approver via the on-chain view
|
|
2469
|
+
* (no backend). Defaults to sponsoring through the paymaster. */
|
|
2470
|
+
async submitPasskeyApproval(assertion, leaves, leafIndex, nonce, submit) {
|
|
2471
|
+
const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
|
|
2472
|
+
const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
|
|
2473
|
+
let yParity = null;
|
|
2474
|
+
for (const cand of candidates) {
|
|
2475
|
+
if (await this.adapter.isApprover(this.address, cand.publicKey)) {
|
|
2476
|
+
yParity = cand.yParity;
|
|
2477
|
+
break;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
if (yParity === null) {
|
|
2481
|
+
throw new Error("kit: this passkey is not a registered approver of the wallet");
|
|
2482
|
+
}
|
|
2483
|
+
const call = this.adapter.buildAddSignerViaPasskey(
|
|
2484
|
+
this.address,
|
|
2485
|
+
this.devicePubkey,
|
|
2486
|
+
nonce,
|
|
2487
|
+
leaves,
|
|
2488
|
+
leafIndex,
|
|
2489
|
+
assertion,
|
|
2490
|
+
yParity
|
|
2491
|
+
);
|
|
2492
|
+
if (submit) return submit(call);
|
|
2493
|
+
if (!this.paymaster) {
|
|
2494
|
+
throw new Error("kit: no paymaster configured \u2014 pass a `submit` relayer to approveThisDeviceWithPasskey");
|
|
2495
|
+
}
|
|
2496
|
+
return paymasterExecuteDirect(this.paymaster, this.address, call);
|
|
2497
|
+
}
|
|
1445
2498
|
/**
|
|
1446
2499
|
* Register a self-custodial backup signer derived from `code`, so the account
|
|
1447
2500
|
* can be recovered after the user loses every device. Idempotent: if the
|
|
@@ -1483,12 +2536,12 @@ var Cavos = class _Cavos {
|
|
|
1483
2536
|
const backup = BackupSigner.fromCode(opts.code);
|
|
1484
2537
|
const backupAdapter = new StarknetAdapter({ classHash, signer: backup, provider });
|
|
1485
2538
|
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
1486
|
-
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network }) :
|
|
2539
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network }) : defaultRegistry3);
|
|
1487
2540
|
const existing = await registry.lookup(opts.identity.userId);
|
|
1488
2541
|
if (!existing) {
|
|
1489
2542
|
throw new Error("kit: no account found for this identity \u2014 nothing to recover");
|
|
1490
2543
|
}
|
|
1491
|
-
const backupAccount = new Account({
|
|
2544
|
+
const backupAccount = new Account$1({
|
|
1492
2545
|
provider,
|
|
1493
2546
|
address: existing.address,
|
|
1494
2547
|
signer: new StarknetDeviceSigner(backup),
|
|
@@ -1508,7 +2561,7 @@ var Cavos = class _Cavos {
|
|
|
1508
2561
|
}
|
|
1509
2562
|
}
|
|
1510
2563
|
const adapter = new StarknetAdapter({ classHash, signer, provider });
|
|
1511
|
-
const account = new Account({
|
|
2564
|
+
const account = new Account$1({
|
|
1512
2565
|
provider,
|
|
1513
2566
|
address: existing.address,
|
|
1514
2567
|
signer: new StarknetDeviceSigner(signer),
|
|
@@ -1518,7 +2571,7 @@ var Cavos = class _Cavos {
|
|
|
1518
2571
|
return new _Cavos(opts.identity, existing.address, "ready", account, adapter, devicePubkey);
|
|
1519
2572
|
}
|
|
1520
2573
|
};
|
|
1521
|
-
var
|
|
2574
|
+
var defaultRegistry3 = new InMemoryWalletRegistry();
|
|
1522
2575
|
var DEVICE_REQUEST_DEDUP_MS = 5 * 60 * 1e3;
|
|
1523
2576
|
var lastDeviceRequest = /* @__PURE__ */ new Map();
|
|
1524
2577
|
async function isDeployed(provider, address) {
|
|
@@ -1529,6 +2582,58 @@ async function isDeployed(provider, address) {
|
|
|
1529
2582
|
return false;
|
|
1530
2583
|
}
|
|
1531
2584
|
}
|
|
2585
|
+
async function approveDeviceEverywhere(wallets, passkey) {
|
|
2586
|
+
const targets = wallets.filter((w) => w.status === "needs-device-approval");
|
|
2587
|
+
if (targets.length === 0) return [];
|
|
2588
|
+
const infos = await Promise.all(targets.map((w) => w.passkeyLeafForThisDevice()));
|
|
2589
|
+
const leaves = infos.map((i) => i.leaf);
|
|
2590
|
+
const assertion = await passkey.assert(batchChallenge(leaves));
|
|
2591
|
+
const out = [];
|
|
2592
|
+
for (let i = 0; i < targets.length; i++) {
|
|
2593
|
+
const { transactionHash } = await targets[i].submitPasskeyApproval(
|
|
2594
|
+
assertion,
|
|
2595
|
+
leaves,
|
|
2596
|
+
i,
|
|
2597
|
+
infos[i].nonce
|
|
2598
|
+
);
|
|
2599
|
+
out.push({ chain: targets[i].chain, transactionHash });
|
|
2600
|
+
}
|
|
2601
|
+
return out;
|
|
2602
|
+
}
|
|
2603
|
+
async function paymasterExecuteDirect(paymaster, userAddress, call) {
|
|
2604
|
+
const body = {
|
|
2605
|
+
jsonrpc: "2.0",
|
|
2606
|
+
id: 1,
|
|
2607
|
+
method: "paymaster_executeDirectTransaction",
|
|
2608
|
+
params: {
|
|
2609
|
+
transaction: {
|
|
2610
|
+
type: "invoke",
|
|
2611
|
+
invoke: {
|
|
2612
|
+
user_address: userAddress,
|
|
2613
|
+
execute_from_outside_call: {
|
|
2614
|
+
to: call.contractAddress,
|
|
2615
|
+
selector: hash.getSelectorFromName(call.entrypoint),
|
|
2616
|
+
calldata: call.calldata.map((c) => num.toHex(c))
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
},
|
|
2620
|
+
parameters: { version: "0x1", fee_mode: { mode: "sponsored" } }
|
|
2621
|
+
}
|
|
2622
|
+
};
|
|
2623
|
+
const res = await fetch(paymaster.url, {
|
|
2624
|
+
method: "POST",
|
|
2625
|
+
headers: {
|
|
2626
|
+
"Content-Type": "application/json",
|
|
2627
|
+
...paymaster.apiKey ? { "x-paymaster-api-key": paymaster.apiKey } : {}
|
|
2628
|
+
},
|
|
2629
|
+
body: JSON.stringify(body)
|
|
2630
|
+
});
|
|
2631
|
+
const json = await res.json();
|
|
2632
|
+
if (json.error) {
|
|
2633
|
+
throw new Error(`kit: paymaster passkey approval failed: ${JSON.stringify(json.error)}`);
|
|
2634
|
+
}
|
|
2635
|
+
return { transactionHash: json.result?.transaction_hash ?? json.result?.tracking_id };
|
|
2636
|
+
}
|
|
1532
2637
|
var CavosAuth = class {
|
|
1533
2638
|
constructor(opts = {}) {
|
|
1534
2639
|
this.opts = opts;
|
|
@@ -1667,6 +2772,6 @@ function bytesToChunks(bytes) {
|
|
|
1667
2772
|
return w;
|
|
1668
2773
|
}
|
|
1669
2774
|
|
|
1670
|
-
export { BackupSigner, Cavos, CavosAuth, CavosSolana, DEVICE_ACCOUNT_CLASS_HASH, DEVICE_ACCOUNT_PROGRAM_ID, HttpRecoveryClient, HttpWalletRegistry, InMemoryWalletRegistry, SECP256R1_PROGRAM_ID, SOLANA_NETWORKS, STARKNET_NETWORKS, SolanaAdapter, SolanaRelayer, StarknetAdapter, StarknetDeviceSigner, UDC_ADDRESS, WebCryptoSigner, anchorDiscriminator, bigIntTo32Bytes, buildSecp256r1Instruction, bytesToBigInt, bytesToHex, compressedPubkey, deriveAddressSeed, deriveAddressSeedSolana, deriveBackupKey, encodeLowSSignature, generateRecoveryCode, hexToBytes, recoverYParity, serializeInstructions, signatureToFelts, u256ToFelts };
|
|
1671
|
-
//# sourceMappingURL=chunk-
|
|
1672
|
-
//# sourceMappingURL=chunk-
|
|
2775
|
+
export { BackupSigner, Cavos, CavosAuth, CavosSolana, CavosStellar, DEVICE_ACCOUNT_CLASS_HASH, DEVICE_ACCOUNT_PROGRAM_ID, DEVICE_ACCOUNT_WASM_HASH, FACTORY_CONTRACT_ID, HttpRecoveryClient, HttpWalletRegistry, InMemoryWalletRegistry, NATIVE_SAC_ID, SECP256R1_PROGRAM_ID, SOLANA_NETWORKS, STARKNET_NETWORKS, STELLAR_NETWORKS, SolanaAdapter, SolanaRelayer, StarknetAdapter, StarknetDeviceSigner, StellarAdapter, StellarRelayer, UDC_ADDRESS, WebCryptoSigner, anchorDiscriminator, approveDeviceEverywhere, base64urlEncode, batchChallenge, bigIntTo32Bytes, buildSecp256r1Instruction, bytesToBigInt, bytesToHex, challengeOffsetOf, compressedPubkey, derToRs, deriveAddressSeed, deriveAddressSeedSolana, deriveAddressSeedStellar, deriveBackupKey, deviceSignatureScVal, encodeLowSSignature, encodeLowSSignature2, generateRecoveryCode, hexToBytes, lowS, recoverCandidatePublicKeys, recoverYParity, sec1Pubkey, serializeInstructions, signatureToFelts, spkiToPublicKey, u256ToFelts, webauthnDigest };
|
|
2776
|
+
//# sourceMappingURL=chunk-BNGLH3Q3.mjs.map
|
|
2777
|
+
//# sourceMappingURL=chunk-BNGLH3Q3.mjs.map
|