@cavos/kit 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Cavos-C2KHZxxu.d.mts → Cavos-x9qFpOvJ.d.mts} +438 -5
- package/dist/{Cavos-C2KHZxxu.d.ts → Cavos-x9qFpOvJ.d.ts} +438 -5
- package/dist/chunk-F2J25XSL.mjs +2946 -0
- package/dist/chunk-F2J25XSL.mjs.map +1 -0
- package/dist/index.d.mts +131 -5
- package/dist/index.d.ts +131 -5
- package/dist/index.js +1314 -18
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/react/index.d.mts +48 -16
- package/dist/react/index.d.ts +48 -16
- package/dist/react/index.js +1605 -92
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +336 -77
- package/dist/react/index.mjs.map +1 -1
- package/package.json +3 -1
- package/dist/chunk-PEUQQZB5.mjs +0 -1672
- package/dist/chunk-PEUQQZB5.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ var web3_js = require('@solana/web3.js');
|
|
|
7
7
|
var hkdf = require('@noble/hashes/hkdf');
|
|
8
8
|
var pbkdf2 = require('@noble/hashes/pbkdf2');
|
|
9
9
|
var utils = require('@noble/hashes/utils');
|
|
10
|
+
var stellarSdk = require('@stellar/stellar-sdk');
|
|
10
11
|
|
|
11
12
|
// src/Cavos.ts
|
|
12
13
|
|
|
@@ -32,6 +33,18 @@ function hexToBytes(hex) {
|
|
|
32
33
|
for (let i = 0; i < out.length; i++) out[i] = parseInt(padded.slice(i * 2, i * 2 + 2), 16);
|
|
33
34
|
return out;
|
|
34
35
|
}
|
|
36
|
+
function bytesToByteArrayCalldata(bytes) {
|
|
37
|
+
const CHUNK = 31;
|
|
38
|
+
const fullCount = Math.floor(bytes.length / CHUNK);
|
|
39
|
+
const out = [String(fullCount)];
|
|
40
|
+
for (let i = 0; i < fullCount; i++) {
|
|
41
|
+
out.push("0x" + bytesToBigInt(bytes.subarray(i * CHUNK, i * CHUNK + CHUNK)).toString(16));
|
|
42
|
+
}
|
|
43
|
+
const rem = bytes.subarray(fullCount * CHUNK);
|
|
44
|
+
out.push("0x" + (rem.length ? bytesToBigInt(rem).toString(16) : "0"));
|
|
45
|
+
out.push(String(rem.length));
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
35
48
|
function bigIntTo32Bytes(value) {
|
|
36
49
|
const out = new Uint8Array(32);
|
|
37
50
|
let v = value;
|
|
@@ -169,7 +182,7 @@ var CAVOS_PAYMASTER_URL = {
|
|
|
169
182
|
mainnet: "https://paymaster.cavos.xyz"
|
|
170
183
|
};
|
|
171
184
|
var DEVICE_ACCOUNT_CLASS_HASH = {
|
|
172
|
-
sepolia: "
|
|
185
|
+
sepolia: "0x25cbc5423e8ee895febb0ef2c3945b408da44d0039d915fbdd681fe6b6ba66b",
|
|
173
186
|
mainnet: "0x1840aded59e8a0d2b440a134cb9079a7fc11b06c77f58ed189ab436a034ca6a"
|
|
174
187
|
};
|
|
175
188
|
|
|
@@ -233,6 +246,84 @@ var StarknetAdapter = class {
|
|
|
233
246
|
const sig = await this.opts.signer.sign(bigIntTo32Bytes(txHash));
|
|
234
247
|
return signatureToFelts(sig).map((f) => starknet.num.toHex(f));
|
|
235
248
|
}
|
|
249
|
+
// --- passkey approvers ---
|
|
250
|
+
buildAddApprover(accountAddress, passkey) {
|
|
251
|
+
return { contractAddress: accountAddress, entrypoint: "add_approver", calldata: pubkeyCalldata(passkey) };
|
|
252
|
+
}
|
|
253
|
+
buildRemoveApprover(accountAddress, passkey) {
|
|
254
|
+
return { contractAddress: accountAddress, entrypoint: "remove_approver", calldata: pubkeyCalldata(passkey) };
|
|
255
|
+
}
|
|
256
|
+
async isApprover(accountAddress, passkey) {
|
|
257
|
+
if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
|
|
258
|
+
const res = await this.opts.provider.callContract({
|
|
259
|
+
contractAddress: accountAddress,
|
|
260
|
+
entrypoint: "is_approver",
|
|
261
|
+
calldata: pubkeyCalldata(passkey)
|
|
262
|
+
});
|
|
263
|
+
return BigInt(res[0] ?? 0) !== 0n;
|
|
264
|
+
}
|
|
265
|
+
/** True if the account has at least one passkey registered as an approver.
|
|
266
|
+
* Lets the UI decide whether to offer passkey approval before prompting. */
|
|
267
|
+
async hasPasskeyApprover(accountAddress) {
|
|
268
|
+
if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
|
|
269
|
+
const res = await this.opts.provider.callContract({
|
|
270
|
+
contractAddress: accountAddress,
|
|
271
|
+
entrypoint: "get_approver_count",
|
|
272
|
+
calldata: []
|
|
273
|
+
});
|
|
274
|
+
return BigInt(res[0] ?? 0) > 0n;
|
|
275
|
+
}
|
|
276
|
+
async getPasskeyNonce(accountAddress) {
|
|
277
|
+
if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
|
|
278
|
+
const res = await this.opts.provider.callContract({
|
|
279
|
+
contractAddress: accountAddress,
|
|
280
|
+
entrypoint: "get_passkey_nonce",
|
|
281
|
+
calldata: []
|
|
282
|
+
});
|
|
283
|
+
return BigInt(res[0] ?? 0);
|
|
284
|
+
}
|
|
285
|
+
/** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
|
|
286
|
+
* `sha256(new_x || new_y || nonce)` (coords 32B BE, nonce 16B BE). The batch
|
|
287
|
+
* challenge the passkey signs is `sha256(concat(leaves))` across chains. */
|
|
288
|
+
passkeyLeaf(newSigner, nonce) {
|
|
289
|
+
const msg = new Uint8Array(32 + 32 + 16);
|
|
290
|
+
msg.set(bigIntTo32Bytes(newSigner.x), 0);
|
|
291
|
+
msg.set(bigIntTo32Bytes(newSigner.y), 32);
|
|
292
|
+
msg.set(bigIntTo32Bytes(nonce).subarray(16), 64);
|
|
293
|
+
return sha256.sha256(msg);
|
|
294
|
+
}
|
|
295
|
+
/** Passkey-authorized `add_signer` call. `leaves`/`leafIndex` place this chain's
|
|
296
|
+
* leaf in the multi-chain batch (single chain → `[leaf]`, index 0). `yParity`
|
|
297
|
+
* matches the raw `(r, s)` — the contract normalizes high-S internally. */
|
|
298
|
+
buildAddSignerViaPasskey(accountAddress, newSigner, nonce, leaves, leafIndex, assertion, yParity) {
|
|
299
|
+
const [rl, rh] = u256ToFelts(assertion.r);
|
|
300
|
+
const [sl, sh] = u256ToFelts(assertion.s);
|
|
301
|
+
const leavesCalldata = [String(leaves.length)];
|
|
302
|
+
for (const leaf of leaves) {
|
|
303
|
+
const [lo, hi] = u256ToFelts(bytesToBigInt(leaf));
|
|
304
|
+
leavesCalldata.push(starknet.num.toHex(lo), starknet.num.toHex(hi));
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
contractAddress: accountAddress,
|
|
308
|
+
entrypoint: "add_signer_via_passkey",
|
|
309
|
+
calldata: [
|
|
310
|
+
...pubkeyCalldata(newSigner),
|
|
311
|
+
// new_x, new_y (u256 pairs)
|
|
312
|
+
starknet.num.toHex(nonce),
|
|
313
|
+
...leavesCalldata,
|
|
314
|
+
// Array<u256> leaves
|
|
315
|
+
String(leafIndex),
|
|
316
|
+
...bytesToByteArrayCalldata(assertion.authenticatorData),
|
|
317
|
+
...bytesToByteArrayCalldata(assertion.clientDataJSON),
|
|
318
|
+
String(assertion.challengeOffset),
|
|
319
|
+
starknet.num.toHex(rl),
|
|
320
|
+
starknet.num.toHex(rh),
|
|
321
|
+
starknet.num.toHex(sl),
|
|
322
|
+
starknet.num.toHex(sh),
|
|
323
|
+
yParity ? "0x1" : "0x0"
|
|
324
|
+
]
|
|
325
|
+
};
|
|
326
|
+
}
|
|
236
327
|
};
|
|
237
328
|
function pubkeyCalldata(pk) {
|
|
238
329
|
const [xl, xh] = u256ToFelts(pk.x);
|
|
@@ -326,6 +417,9 @@ function deriveAddressSeed({ userId, appSalt }) {
|
|
|
326
417
|
function deriveAddressSeedSolana({ userId, appSalt }) {
|
|
327
418
|
return sha256.sha256(new TextEncoder().encode(`cavos:solana:v1:${userId}:${appSalt}`));
|
|
328
419
|
}
|
|
420
|
+
function deriveAddressSeedStellar({ userId, appSalt }) {
|
|
421
|
+
return sha256.sha256(new TextEncoder().encode(`cavos:stellar:v1:${userId}:${appSalt}`));
|
|
422
|
+
}
|
|
329
423
|
function feltFromString(s) {
|
|
330
424
|
const bytes = new TextEncoder().encode(s);
|
|
331
425
|
const chunks = [];
|
|
@@ -347,6 +441,8 @@ var DOMAIN_ADD = "cavos:add_signer:v1";
|
|
|
347
441
|
var DOMAIN_REMOVE = "cavos:remove_signer:v1";
|
|
348
442
|
var DOMAIN_TRANSFER = "cavos:transfer:v1";
|
|
349
443
|
var DOMAIN_EXECUTE = "cavos:execute:v1";
|
|
444
|
+
var DOMAIN_ADD_APPROVER = "cavos:add_approver:v1";
|
|
445
|
+
var DOMAIN_REMOVE_APPROVER = "cavos:remove_approver:v1";
|
|
350
446
|
var SECP256R1_N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
|
|
351
447
|
var SOLANA_NETWORKS = {
|
|
352
448
|
"solana-devnet": "https://api.devnet.solana.com",
|
|
@@ -439,6 +535,104 @@ var SolanaAdapter = class {
|
|
|
439
535
|
});
|
|
440
536
|
return [precompileIx, ix];
|
|
441
537
|
}
|
|
538
|
+
/** `[precompile, add_approver]` bundle enrolling a passkey approver (device-signed). */
|
|
539
|
+
async buildAddApprover(account, passkey) {
|
|
540
|
+
const accountPk = new web3_js.PublicKey(account);
|
|
541
|
+
const compressed = compressedPubkey(passkey);
|
|
542
|
+
const nonce = await this.fetchNonce(accountPk);
|
|
543
|
+
const message = concatBytes(
|
|
544
|
+
Buffer.from(DOMAIN_ADD_APPROVER),
|
|
545
|
+
accountPk.toBuffer(),
|
|
546
|
+
compressed,
|
|
547
|
+
u64le(nonce)
|
|
548
|
+
);
|
|
549
|
+
const { precompileIx } = await this.signToPrecompile(message);
|
|
550
|
+
const ix = new web3_js.TransactionInstruction({
|
|
551
|
+
programId: this.programId,
|
|
552
|
+
keys: this.guardedKeys(accountPk),
|
|
553
|
+
data: Buffer.concat([anchorDiscriminator("add_approver"), Buffer.from(compressed)])
|
|
554
|
+
});
|
|
555
|
+
return [precompileIx, ix];
|
|
556
|
+
}
|
|
557
|
+
/** `[precompile, remove_approver]` bundle (device-signed). */
|
|
558
|
+
async buildRemoveApprover(account, passkey) {
|
|
559
|
+
const accountPk = new web3_js.PublicKey(account);
|
|
560
|
+
const compressed = compressedPubkey(passkey);
|
|
561
|
+
const nonce = await this.fetchNonce(accountPk);
|
|
562
|
+
const message = concatBytes(
|
|
563
|
+
Buffer.from(DOMAIN_REMOVE_APPROVER),
|
|
564
|
+
accountPk.toBuffer(),
|
|
565
|
+
compressed,
|
|
566
|
+
u64le(nonce)
|
|
567
|
+
);
|
|
568
|
+
const { precompileIx } = await this.signToPrecompile(message);
|
|
569
|
+
const ix = new web3_js.TransactionInstruction({
|
|
570
|
+
programId: this.programId,
|
|
571
|
+
keys: this.guardedKeys(accountPk),
|
|
572
|
+
data: Buffer.concat([anchorDiscriminator("remove_approver"), Buffer.from(compressed)])
|
|
573
|
+
});
|
|
574
|
+
return [precompileIx, ix];
|
|
575
|
+
}
|
|
576
|
+
/** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
|
|
577
|
+
* `sha256(compressed(new_signer) || passkey_nonce_le8)`. The batch challenge the
|
|
578
|
+
* passkey signs is `sha256(concat(leaves))` across chains. */
|
|
579
|
+
passkeyLeaf(newSigner, nonce) {
|
|
580
|
+
return sha256.sha256(concatBytes(compressedPubkey(newSigner), u64le(nonce)));
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* `[precompile(passkey), add_signer_via_passkey]` bundle. The precompile ix
|
|
584
|
+
* verifies the PASSKEY's WebAuthn assertion over `authData || sha256(clientDataJSON)`;
|
|
585
|
+
* the program ix binds the challenge to `newSigner` + the passkey nonce and adds
|
|
586
|
+
* the signer. No device signature — a gasless relayer can submit it.
|
|
587
|
+
*/
|
|
588
|
+
buildAddSignerViaPasskey(account, newSigner, passkey, leaves, leafIndex, assertion) {
|
|
589
|
+
const accountPk = new web3_js.PublicKey(account);
|
|
590
|
+
const newCompressed = compressedPubkey(newSigner);
|
|
591
|
+
const passkeyCompressed = compressedPubkey(passkey);
|
|
592
|
+
const clientHash = sha256.sha256(assertion.clientDataJSON);
|
|
593
|
+
const message = concatBytes(assertion.authenticatorData, clientHash);
|
|
594
|
+
const signature = encodeLowSSignature(assertion.r, assertion.s);
|
|
595
|
+
const precompileIx = buildSecp256r1Instruction(passkeyCompressed, signature, message);
|
|
596
|
+
const leavesBlob = Buffer.concat([u32le(leaves.length), ...leaves.map((l) => Buffer.from(l))]);
|
|
597
|
+
const data = Buffer.concat([
|
|
598
|
+
anchorDiscriminator("add_signer_via_passkey"),
|
|
599
|
+
Buffer.from(newCompressed),
|
|
600
|
+
leavesBlob,
|
|
601
|
+
u32le(leafIndex),
|
|
602
|
+
serializeVecU8(assertion.authenticatorData),
|
|
603
|
+
serializeVecU8(assertion.clientDataJSON),
|
|
604
|
+
u32le(assertion.challengeOffset)
|
|
605
|
+
]);
|
|
606
|
+
const ix = new web3_js.TransactionInstruction({
|
|
607
|
+
programId: this.programId,
|
|
608
|
+
keys: this.guardedKeys(accountPk),
|
|
609
|
+
data
|
|
610
|
+
});
|
|
611
|
+
return [precompileIx, ix];
|
|
612
|
+
}
|
|
613
|
+
/** Read whether `passkey` is a registered approver. */
|
|
614
|
+
/** True if the account has at least one passkey registered as an approver. */
|
|
615
|
+
async hasPasskeyApprover(account) {
|
|
616
|
+
const approvers = await this.fetchApprovers(new web3_js.PublicKey(account));
|
|
617
|
+
return approvers.length > 0;
|
|
618
|
+
}
|
|
619
|
+
async isApprover(account, passkey) {
|
|
620
|
+
const approvers = await this.fetchApprovers(new web3_js.PublicKey(account));
|
|
621
|
+
const target = Buffer.from(compressedPubkey(passkey)).toString("hex");
|
|
622
|
+
return approvers.some((a) => Buffer.from(a).toString("hex") === target);
|
|
623
|
+
}
|
|
624
|
+
/** Read the current passkey-approval nonce. */
|
|
625
|
+
async passkeyNonce(account) {
|
|
626
|
+
const info = await this.requireConnection().getAccountInfo(new web3_js.PublicKey(account));
|
|
627
|
+
if (!info) return 0n;
|
|
628
|
+
const d = info.data;
|
|
629
|
+
const signersLenOff = 8 + 32 + 1 + 8 + COMPRESSED_PUBKEY_SIZE;
|
|
630
|
+
const signerCount = d.readUInt32LE(signersLenOff);
|
|
631
|
+
const approversLenOff = signersLenOff + 4 + signerCount * COMPRESSED_PUBKEY_SIZE;
|
|
632
|
+
const approverCount = d.readUInt32LE(approversLenOff);
|
|
633
|
+
const passkeyNonceOff = approversLenOff + 4 + approverCount * COMPRESSED_PUBKEY_SIZE;
|
|
634
|
+
return readU64le(d, passkeyNonceOff);
|
|
635
|
+
}
|
|
442
636
|
/** `[precompile, execute_transfer]` bundle moving lamports out of the account. */
|
|
443
637
|
async buildExecuteTransfer(account, destination, amount) {
|
|
444
638
|
const accountPk = new web3_js.PublicKey(account);
|
|
@@ -560,6 +754,22 @@ var SolanaAdapter = class {
|
|
|
560
754
|
}
|
|
561
755
|
return out;
|
|
562
756
|
}
|
|
757
|
+
async fetchApprovers(account) {
|
|
758
|
+
const info = await this.requireConnection().getAccountInfo(account);
|
|
759
|
+
if (!info) return [];
|
|
760
|
+
const d = info.data;
|
|
761
|
+
const signersLenOff = 8 + 32 + 1 + 8 + COMPRESSED_PUBKEY_SIZE;
|
|
762
|
+
const signerCount = d.readUInt32LE(signersLenOff);
|
|
763
|
+
const approversLenOff = signersLenOff + 4 + signerCount * COMPRESSED_PUBKEY_SIZE;
|
|
764
|
+
const count = d.readUInt32LE(approversLenOff);
|
|
765
|
+
const out = [];
|
|
766
|
+
let off = approversLenOff + 4;
|
|
767
|
+
for (let i = 0; i < count; i++) {
|
|
768
|
+
out.push(Uint8Array.from(d.subarray(off, off + COMPRESSED_PUBKEY_SIZE)));
|
|
769
|
+
off += COMPRESSED_PUBKEY_SIZE;
|
|
770
|
+
}
|
|
771
|
+
return out;
|
|
772
|
+
}
|
|
563
773
|
requireConnection() {
|
|
564
774
|
if (!this.opts.connection) throw new Error("kit/solana: connection required for reads");
|
|
565
775
|
return this.opts.connection;
|
|
@@ -572,10 +782,10 @@ function compressedPubkey(pk) {
|
|
|
572
782
|
return out;
|
|
573
783
|
}
|
|
574
784
|
function encodeLowSSignature(r, s) {
|
|
575
|
-
const
|
|
785
|
+
const lowS2 = s > SECP256R1_N / 2n ? SECP256R1_N - s : s;
|
|
576
786
|
const out = new Uint8Array(SIGNATURE_SIZE);
|
|
577
787
|
out.set(bigIntTo32Bytes(r), 0);
|
|
578
|
-
out.set(bigIntTo32Bytes(
|
|
788
|
+
out.set(bigIntTo32Bytes(lowS2), 32);
|
|
579
789
|
return out;
|
|
580
790
|
}
|
|
581
791
|
function buildSecp256r1Instruction(compressed, signature, message) {
|
|
@@ -614,13 +824,18 @@ function buildSecp256r1Instruction(compressed, signature, message) {
|
|
|
614
824
|
function anchorDiscriminator(name) {
|
|
615
825
|
return Buffer.from(sha256.sha256(`global:${name}`).slice(0, 8));
|
|
616
826
|
}
|
|
827
|
+
function u32le(n) {
|
|
828
|
+
const b = Buffer.alloc(4);
|
|
829
|
+
b.writeUInt32LE(n);
|
|
830
|
+
return b;
|
|
831
|
+
}
|
|
617
832
|
function u64le(n) {
|
|
618
833
|
const b = Buffer.alloc(8);
|
|
619
834
|
new DataView(b.buffer, b.byteOffset, 8).setBigUint64(0, BigInt(n), true);
|
|
620
835
|
return b;
|
|
621
836
|
}
|
|
622
|
-
function readU64le(
|
|
623
|
-
return new DataView(
|
|
837
|
+
function readU64le(buf2, offset) {
|
|
838
|
+
return new DataView(buf2.buffer, buf2.byteOffset, buf2.length).getBigUint64(
|
|
624
839
|
offset,
|
|
625
840
|
true
|
|
626
841
|
);
|
|
@@ -1008,6 +1223,77 @@ var WORDLIST = [
|
|
|
1008
1223
|
"beach",
|
|
1009
1224
|
"dusk"
|
|
1010
1225
|
];
|
|
1226
|
+
function base64urlEncode(bytes) {
|
|
1227
|
+
let bin = "";
|
|
1228
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
1229
|
+
const b64 = typeof btoa !== "undefined" ? btoa(bin) : Buffer.from(bytes).toString("base64");
|
|
1230
|
+
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1231
|
+
}
|
|
1232
|
+
function derToRs(der) {
|
|
1233
|
+
let i = 0;
|
|
1234
|
+
if (der[i++] !== 48) throw new Error("kit/webauthn: bad DER (no SEQUENCE)");
|
|
1235
|
+
if (der[i] & 128) i += 1 + (der[i] & 127);
|
|
1236
|
+
else i += 1;
|
|
1237
|
+
if (der[i++] !== 2) throw new Error("kit/webauthn: bad DER (no r INTEGER)");
|
|
1238
|
+
const rlen = der[i++];
|
|
1239
|
+
const r = bytesToBigInt(der.subarray(i, i + rlen));
|
|
1240
|
+
i += rlen;
|
|
1241
|
+
if (der[i++] !== 2) throw new Error("kit/webauthn: bad DER (no s INTEGER)");
|
|
1242
|
+
const slen = der[i++];
|
|
1243
|
+
const s = bytesToBigInt(der.subarray(i, i + slen));
|
|
1244
|
+
return { r, s };
|
|
1245
|
+
}
|
|
1246
|
+
function spkiToPublicKey(spki) {
|
|
1247
|
+
const idx = spki.lastIndexOf(4, spki.length - 65);
|
|
1248
|
+
const start = spki.length - 65;
|
|
1249
|
+
const prefix = spki[start];
|
|
1250
|
+
if (prefix !== 4) {
|
|
1251
|
+
if (idx < 0) throw new Error("kit/webauthn: no uncompressed EC point in SPKI");
|
|
1252
|
+
return { x: bytesToBigInt(spki.subarray(idx + 1, idx + 33)), y: bytesToBigInt(spki.subarray(idx + 33, idx + 65)) };
|
|
1253
|
+
}
|
|
1254
|
+
return {
|
|
1255
|
+
x: bytesToBigInt(spki.subarray(start + 1, start + 33)),
|
|
1256
|
+
y: bytesToBigInt(spki.subarray(start + 33, start + 65))
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
function batchChallenge(leaves) {
|
|
1260
|
+
const total = leaves.reduce((n, l) => n + l.length, 0);
|
|
1261
|
+
const cat = new Uint8Array(total);
|
|
1262
|
+
let o = 0;
|
|
1263
|
+
for (const l of leaves) {
|
|
1264
|
+
cat.set(l, o);
|
|
1265
|
+
o += l.length;
|
|
1266
|
+
}
|
|
1267
|
+
return sha256.sha256(cat);
|
|
1268
|
+
}
|
|
1269
|
+
function webauthnDigest(authenticatorData, clientDataJSON) {
|
|
1270
|
+
const clientHash = sha256.sha256(clientDataJSON);
|
|
1271
|
+
const msg = new Uint8Array(authenticatorData.length + clientHash.length);
|
|
1272
|
+
msg.set(authenticatorData, 0);
|
|
1273
|
+
msg.set(clientHash, authenticatorData.length);
|
|
1274
|
+
return sha256.sha256(msg);
|
|
1275
|
+
}
|
|
1276
|
+
function recoverCandidatePublicKeys(r, s, digest) {
|
|
1277
|
+
const out = [];
|
|
1278
|
+
for (const bit of [0, 1]) {
|
|
1279
|
+
try {
|
|
1280
|
+
const point = new p256.p256.Signature(r, s).addRecoveryBit(bit).recoverPublicKey(digest).toAffine();
|
|
1281
|
+
out.push({ publicKey: { x: point.x, y: point.y }, yParity: bit === 1 });
|
|
1282
|
+
} catch {
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return out;
|
|
1286
|
+
}
|
|
1287
|
+
function lowS(s) {
|
|
1288
|
+
const n = p256.p256.CURVE.n;
|
|
1289
|
+
return s > n / 2n ? n - s : s;
|
|
1290
|
+
}
|
|
1291
|
+
function challengeOffsetOf(clientDataJSON, challengeB64) {
|
|
1292
|
+
const text = new TextDecoder().decode(clientDataJSON);
|
|
1293
|
+
const idx = text.indexOf(challengeB64);
|
|
1294
|
+
if (idx < 0) throw new Error("kit/webauthn: challenge not found in clientDataJSON");
|
|
1295
|
+
return idx;
|
|
1296
|
+
}
|
|
1011
1297
|
|
|
1012
1298
|
// src/chains/solana/CavosSolana.ts
|
|
1013
1299
|
var CavosSolana = class _CavosSolana {
|
|
@@ -1022,6 +1308,8 @@ var CavosSolana = class _CavosSolana {
|
|
|
1022
1308
|
this.feePayer = feePayer;
|
|
1023
1309
|
/** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
|
|
1024
1310
|
this.chain = "solana";
|
|
1311
|
+
/** True when this connect just created a brand-new account (first sign-up). */
|
|
1312
|
+
this.isNewAccount = false;
|
|
1025
1313
|
}
|
|
1026
1314
|
get publicKey() {
|
|
1027
1315
|
return this.devicePubkey;
|
|
@@ -1072,7 +1360,7 @@ var CavosSolana = class _CavosSolana {
|
|
|
1072
1360
|
}
|
|
1073
1361
|
await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
|
|
1074
1362
|
const isSigner = await adapter.isAuthorizedSigner(address, devicePubkey);
|
|
1075
|
-
|
|
1363
|
+
const wallet = new _CavosSolana(
|
|
1076
1364
|
identity,
|
|
1077
1365
|
address,
|
|
1078
1366
|
isSigner ? "ready" : "needs-device-approval",
|
|
@@ -1082,12 +1370,87 @@ var CavosSolana = class _CavosSolana {
|
|
|
1082
1370
|
relayer,
|
|
1083
1371
|
opts.feePayer
|
|
1084
1372
|
);
|
|
1373
|
+
wallet.isNewAccount = !deployed && isSigner;
|
|
1374
|
+
return wallet;
|
|
1085
1375
|
}
|
|
1086
1376
|
/** Authorize an additional device signer (device-signed via precompile). */
|
|
1087
1377
|
async addSigner(pubkey) {
|
|
1088
1378
|
const ixs = await this.adapter.buildAddSigner(this.address, pubkey);
|
|
1089
1379
|
return this.send(ixs);
|
|
1090
1380
|
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Enroll a passkey as an approver (2FA-style step-up). Device-signed + gasless;
|
|
1383
|
+
* requires a ready device. Idempotent. Returns the passkey pubkey + tx hash.
|
|
1384
|
+
*/
|
|
1385
|
+
async enrollPasskey(passkey, params) {
|
|
1386
|
+
const enrolled = await passkey.enroll(params);
|
|
1387
|
+
const { transactionHash } = await this.addApprover(enrolled.publicKey);
|
|
1388
|
+
return { publicKey: enrolled.publicKey, transactionHash };
|
|
1389
|
+
}
|
|
1390
|
+
/** Register an already-enrolled passkey pubkey as an approver (gasless).
|
|
1391
|
+
* Idempotent. Lets one passkey be registered across chains without re-prompting. */
|
|
1392
|
+
async addApprover(pubkey) {
|
|
1393
|
+
if (this.status !== "ready") {
|
|
1394
|
+
throw new Error("kit/solana: addApprover requires a ready, authorized device");
|
|
1395
|
+
}
|
|
1396
|
+
if (await this.adapter.isApprover(this.address, pubkey)) return {};
|
|
1397
|
+
const ixs = await this.adapter.buildAddApprover(this.address, pubkey);
|
|
1398
|
+
const transactionHash = await this.send(ixs);
|
|
1399
|
+
return { transactionHash };
|
|
1400
|
+
}
|
|
1401
|
+
/** True if this account already has a passkey enrolled as an approver, so a
|
|
1402
|
+
* new device can be approved with the passkey instead of the email flow. */
|
|
1403
|
+
async hasPasskey() {
|
|
1404
|
+
return this.adapter.hasPasskeyApprover(this.address);
|
|
1405
|
+
}
|
|
1406
|
+
/** Re-read (from chain) whether THIS device is now an authorized signer.
|
|
1407
|
+
* Used to poll for readiness after a passkey approval before it's indexed. */
|
|
1408
|
+
async isReady() {
|
|
1409
|
+
return this.adapter.isAuthorizedSigner(this.address, this.devicePubkey);
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* From a fresh browser (status `needs-device-approval`), approve adding THIS
|
|
1413
|
+
* device with the user's synced passkey. Gasless via the relayer — the bundle
|
|
1414
|
+
* carries the passkey's WebAuthn assertion, so no device signature is needed.
|
|
1415
|
+
*/
|
|
1416
|
+
async approveThisDeviceWithPasskey(passkey) {
|
|
1417
|
+
if (this.status === "ready") {
|
|
1418
|
+
throw new Error("kit/solana: this device is already an authorized signer");
|
|
1419
|
+
}
|
|
1420
|
+
const { leaf, nonce } = await this.passkeyLeafForThisDevice();
|
|
1421
|
+
const leaves = [leaf];
|
|
1422
|
+
const assertion = await passkey.assert(batchChallenge(leaves));
|
|
1423
|
+
const { transactionHash } = await this.submitPasskeyApproval(assertion, leaves, 0, nonce);
|
|
1424
|
+
return transactionHash;
|
|
1425
|
+
}
|
|
1426
|
+
/** This device's leaf + passkey nonce for a (possibly multi-chain) batch. */
|
|
1427
|
+
async passkeyLeafForThisDevice() {
|
|
1428
|
+
const nonce = await this.adapter.passkeyNonce(this.address);
|
|
1429
|
+
return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
|
|
1430
|
+
}
|
|
1431
|
+
/** Submit `add_signer_via_passkey` given a shared assertion + batch position.
|
|
1432
|
+
* Used by `approveThisDeviceWithPasskey` and `approveDeviceEverywhere`. */
|
|
1433
|
+
async submitPasskeyApproval(assertion, leaves, leafIndex, _nonce) {
|
|
1434
|
+
const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
|
|
1435
|
+
const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
|
|
1436
|
+
let approver = null;
|
|
1437
|
+
for (const cand of candidates) {
|
|
1438
|
+
if (await this.adapter.isApprover(this.address, cand.publicKey)) {
|
|
1439
|
+
approver = cand.publicKey;
|
|
1440
|
+
break;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
if (!approver) throw new Error("kit/solana: this passkey is not a registered approver");
|
|
1444
|
+
const ixs = this.adapter.buildAddSignerViaPasskey(
|
|
1445
|
+
this.address,
|
|
1446
|
+
this.devicePubkey,
|
|
1447
|
+
approver,
|
|
1448
|
+
leaves,
|
|
1449
|
+
leafIndex,
|
|
1450
|
+
assertion
|
|
1451
|
+
);
|
|
1452
|
+
return { transactionHash: await this.send(ixs) };
|
|
1453
|
+
}
|
|
1091
1454
|
/** Move `amount` lamports out of the account to `destination` (device-signed). */
|
|
1092
1455
|
async execute(amount, destination) {
|
|
1093
1456
|
if (this.status !== "ready") {
|
|
@@ -1200,6 +1563,655 @@ var CavosSolana = class _CavosSolana {
|
|
|
1200
1563
|
};
|
|
1201
1564
|
var defaultRegistry = new InMemoryWalletRegistry();
|
|
1202
1565
|
|
|
1566
|
+
// src/chains/stellar/constants.ts
|
|
1567
|
+
var FACTORY_CONTRACT_ID = {
|
|
1568
|
+
// Re-deployed 2026-07-01 with the passkey-approval device-account wasm (batched
|
|
1569
|
+
// multi-chain challenge). The factory pins the wasm hash immutably, so a new
|
|
1570
|
+
// wasm needs a new factory → new account addresses; testnet has no prod wallets.
|
|
1571
|
+
"stellar-testnet": "CBCJIODXIEBOXXD66KCUCF7ZDYJARKI4ZIVQOVWPULOBH5XGNCDP6W3I",
|
|
1572
|
+
// Set once the factory is deployed to mainnet (its address differs — network id
|
|
1573
|
+
// is part of contract-address derivation).
|
|
1574
|
+
"stellar-mainnet": ""
|
|
1575
|
+
};
|
|
1576
|
+
var DEVICE_ACCOUNT_WASM_HASH = {
|
|
1577
|
+
"stellar-testnet": "2671b085578e59a385ef5a5664e42f0450322fe3249539f588e1263ed5a31dce",
|
|
1578
|
+
"stellar-mainnet": ""
|
|
1579
|
+
};
|
|
1580
|
+
var STELLAR_NETWORKS = {
|
|
1581
|
+
"stellar-testnet": {
|
|
1582
|
+
rpcUrl: "https://soroban-testnet.stellar.org",
|
|
1583
|
+
passphrase: "Test SDF Network ; September 2015"
|
|
1584
|
+
},
|
|
1585
|
+
"stellar-mainnet": {
|
|
1586
|
+
rpcUrl: "https://soroban-rpc.mainnet.stellar.gateway.fm",
|
|
1587
|
+
passphrase: "Public Global Stellar Network ; September 2015"
|
|
1588
|
+
}
|
|
1589
|
+
};
|
|
1590
|
+
var NATIVE_SAC_ID = {
|
|
1591
|
+
"stellar-testnet": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
|
|
1592
|
+
"stellar-mainnet": "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA"
|
|
1593
|
+
};
|
|
1594
|
+
|
|
1595
|
+
// src/chains/stellar/StellarAdapter.ts
|
|
1596
|
+
var SECP256R1_N2 = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
|
|
1597
|
+
var StellarAdapter = class {
|
|
1598
|
+
constructor(opts) {
|
|
1599
|
+
this.chain = "stellar";
|
|
1600
|
+
this.network = opts.network;
|
|
1601
|
+
this.passphrase = STELLAR_NETWORKS[opts.network].passphrase;
|
|
1602
|
+
this.rpcUrl = opts.rpcUrl ?? STELLAR_NETWORKS[opts.network].rpcUrl;
|
|
1603
|
+
this.factoryId = opts.factoryId ?? FACTORY_CONTRACT_ID[opts.network];
|
|
1604
|
+
if (!this.factoryId) {
|
|
1605
|
+
throw new Error(`kit/stellar: no factory contract id configured for ${opts.network}`);
|
|
1606
|
+
}
|
|
1607
|
+
this.signer = opts.signer;
|
|
1608
|
+
}
|
|
1609
|
+
server() {
|
|
1610
|
+
if (!this._server) {
|
|
1611
|
+
this._server = new stellarSdk.rpc.Server(this.rpcUrl, {
|
|
1612
|
+
allowHttp: this.rpcUrl.startsWith("http://")
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
return this._server;
|
|
1616
|
+
}
|
|
1617
|
+
networkId() {
|
|
1618
|
+
return stellarSdk.hash(Buffer.from(this.passphrase));
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Deterministic account address for `(addressSeed, initialSigner)` — computed
|
|
1622
|
+
* off-chain, byte-identical to the factory's on-chain `account_address`.
|
|
1623
|
+
* `contractId = sha256(HashIdPreimage(networkId, factory, salt))` with
|
|
1624
|
+
* `salt = sha256(addressSeed || sec1(initialSigner))`.
|
|
1625
|
+
*/
|
|
1626
|
+
computeAddress(addressSeed, initialSigner) {
|
|
1627
|
+
const salt = this.accountSalt(addressSeed, initialSigner);
|
|
1628
|
+
const preimage = stellarSdk.xdr.HashIdPreimage.envelopeTypeContractId(
|
|
1629
|
+
new stellarSdk.xdr.HashIdPreimageContractId({
|
|
1630
|
+
networkId: this.networkId(),
|
|
1631
|
+
contractIdPreimage: stellarSdk.xdr.ContractIdPreimage.contractIdPreimageFromAddress(
|
|
1632
|
+
new stellarSdk.xdr.ContractIdPreimageFromAddress({
|
|
1633
|
+
address: new stellarSdk.Address(this.factoryId).toScAddress(),
|
|
1634
|
+
salt
|
|
1635
|
+
})
|
|
1636
|
+
)
|
|
1637
|
+
})
|
|
1638
|
+
);
|
|
1639
|
+
return stellarSdk.StrKey.encodeContract(stellarSdk.hash(preimage.toXDR()));
|
|
1640
|
+
}
|
|
1641
|
+
/** `salt = sha256(addressSeed(32) || sec1(initialSigner)(65))` — matches the factory. */
|
|
1642
|
+
accountSalt(addressSeed, initialSigner) {
|
|
1643
|
+
return stellarSdk.hash(Buffer.concat([Buffer.from(addressSeed), Buffer.from(sec1Pubkey(initialSigner))]));
|
|
1644
|
+
}
|
|
1645
|
+
/** Host function: `factory.deploy(address_seed, initial_signer)`. */
|
|
1646
|
+
buildDeploy(addressSeed, initialSigner) {
|
|
1647
|
+
return invokeFunc(this.factoryId, "deploy", [
|
|
1648
|
+
bytesScVal(addressSeed),
|
|
1649
|
+
bytesScVal(sec1Pubkey(initialSigner))
|
|
1650
|
+
]);
|
|
1651
|
+
}
|
|
1652
|
+
/** Host function: `account.add_signer(new_signer)` (requires device auth). */
|
|
1653
|
+
buildAddSigner(accountAddress, signer) {
|
|
1654
|
+
return invokeFunc(accountAddress, "add_signer", [bytesScVal(sec1Pubkey(signer))]);
|
|
1655
|
+
}
|
|
1656
|
+
/** Host function: `account.remove_signer(signer)` (requires device auth). */
|
|
1657
|
+
buildRemoveSigner(accountAddress, signer) {
|
|
1658
|
+
return invokeFunc(accountAddress, "remove_signer", [bytesScVal(sec1Pubkey(signer))]);
|
|
1659
|
+
}
|
|
1660
|
+
/** Host function: `account.add_approver(passkey)` (requires device auth). */
|
|
1661
|
+
buildAddApprover(accountAddress, passkey) {
|
|
1662
|
+
return invokeFunc(accountAddress, "add_approver", [bytesScVal(sec1Pubkey(passkey))]);
|
|
1663
|
+
}
|
|
1664
|
+
/** Host function: `account.remove_approver(passkey)` (requires device auth). */
|
|
1665
|
+
buildRemoveApprover(accountAddress, passkey) {
|
|
1666
|
+
return invokeFunc(accountAddress, "remove_approver", [bytesScVal(sec1Pubkey(passkey))]);
|
|
1667
|
+
}
|
|
1668
|
+
/** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
|
|
1669
|
+
* `sha256(sec1(new_signer) || nonce_be8)`. The batch challenge the passkey signs
|
|
1670
|
+
* is `sha256(concat(leaves))` across chains. */
|
|
1671
|
+
passkeyLeaf(newSigner, nonce) {
|
|
1672
|
+
const msg = new Uint8Array(65 + 8);
|
|
1673
|
+
msg.set(sec1Pubkey(newSigner), 0);
|
|
1674
|
+
const n = new Uint8Array(8);
|
|
1675
|
+
let v = nonce;
|
|
1676
|
+
for (let i = 7; i >= 0; i--) {
|
|
1677
|
+
n[i] = Number(v & 0xffn);
|
|
1678
|
+
v >>= 8n;
|
|
1679
|
+
}
|
|
1680
|
+
msg.set(n, 65);
|
|
1681
|
+
return sha256.sha256(msg);
|
|
1682
|
+
}
|
|
1683
|
+
/** Host function: passkey-authorized `add_signer_via_passkey` (no device auth —
|
|
1684
|
+
* authorized by the embedded WebAuthn assertion, so any relayer can submit).
|
|
1685
|
+
* `leaves`/`leafIndex` place this chain's leaf in the multi-chain batch. */
|
|
1686
|
+
buildAddSignerViaPasskey(accountAddress, newSigner, passkey, nonce, leaves, leafIndex, assertion) {
|
|
1687
|
+
const sig = encodeLowSSignature2({ r: assertion.r, s: assertion.s});
|
|
1688
|
+
const leavesScVal = stellarSdk.xdr.ScVal.scvVec(leaves.map((l) => bytesScVal(l)));
|
|
1689
|
+
return invokeFunc(accountAddress, "add_signer_via_passkey", [
|
|
1690
|
+
bytesScVal(sec1Pubkey(newSigner)),
|
|
1691
|
+
bytesScVal(sec1Pubkey(passkey)),
|
|
1692
|
+
stellarSdk.nativeToScVal(nonce, { type: "u64" }),
|
|
1693
|
+
leavesScVal,
|
|
1694
|
+
stellarSdk.nativeToScVal(leafIndex, { type: "u32" }),
|
|
1695
|
+
bytesScVal(assertion.authenticatorData),
|
|
1696
|
+
bytesScVal(assertion.clientDataJSON),
|
|
1697
|
+
stellarSdk.nativeToScVal(assertion.challengeOffset, { type: "u32" }),
|
|
1698
|
+
bytesScVal(sig)
|
|
1699
|
+
]);
|
|
1700
|
+
}
|
|
1701
|
+
/** Read whether `passkey` is a registered approver (read-only simulation). */
|
|
1702
|
+
/** True if the account has at least one passkey registered as an approver.
|
|
1703
|
+
* Reads the contract's `approvers` view and checks the list is non-empty. */
|
|
1704
|
+
async hasPasskeyApprover(accountAddress, readSource) {
|
|
1705
|
+
if (!await this.isDeployed(accountAddress)) return false;
|
|
1706
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1707
|
+
const src = new Account3(readSource, "0");
|
|
1708
|
+
const op = stellarSdk.Operation.invokeHostFunction({
|
|
1709
|
+
func: invokeFunc(accountAddress, "approvers", []),
|
|
1710
|
+
auth: []
|
|
1711
|
+
});
|
|
1712
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1713
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1714
|
+
if (stellarSdk.rpc.Api.isSimulationError(sim)) {
|
|
1715
|
+
throw new Error(`kit/stellar: approvers simulation failed: ${sim.error}`);
|
|
1716
|
+
}
|
|
1717
|
+
if (!sim.result?.retval) return false;
|
|
1718
|
+
const list = stellarSdk.scValToNative(sim.result.retval);
|
|
1719
|
+
return Array.isArray(list) && list.length > 0;
|
|
1720
|
+
}
|
|
1721
|
+
async isApprover(accountAddress, passkey, readSource) {
|
|
1722
|
+
if (!await this.isDeployed(accountAddress)) return false;
|
|
1723
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1724
|
+
const src = new Account3(readSource, "0");
|
|
1725
|
+
const op = stellarSdk.Operation.invokeHostFunction({
|
|
1726
|
+
func: invokeFunc(accountAddress, "is_approver", [bytesScVal(sec1Pubkey(passkey))]),
|
|
1727
|
+
auth: []
|
|
1728
|
+
});
|
|
1729
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1730
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1731
|
+
if (stellarSdk.rpc.Api.isSimulationError(sim)) {
|
|
1732
|
+
throw new Error(`kit/stellar: is_approver simulation failed: ${sim.error}`);
|
|
1733
|
+
}
|
|
1734
|
+
if (!sim.result?.retval) return false;
|
|
1735
|
+
return stellarSdk.scValToNative(sim.result.retval) === true;
|
|
1736
|
+
}
|
|
1737
|
+
/** Read the current passkey-approval nonce (read-only simulation). */
|
|
1738
|
+
async passkeyNonce(accountAddress, readSource) {
|
|
1739
|
+
if (!await this.isDeployed(accountAddress)) return 0n;
|
|
1740
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1741
|
+
const src = new Account3(readSource, "0");
|
|
1742
|
+
const op = stellarSdk.Operation.invokeHostFunction({
|
|
1743
|
+
func: invokeFunc(accountAddress, "passkey_nonce", []),
|
|
1744
|
+
auth: []
|
|
1745
|
+
});
|
|
1746
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1747
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1748
|
+
if (stellarSdk.rpc.Api.isSimulationError(sim)) {
|
|
1749
|
+
throw new Error(`kit/stellar: passkey_nonce simulation failed: ${sim.error}`);
|
|
1750
|
+
}
|
|
1751
|
+
if (!sim.result?.retval) return 0n;
|
|
1752
|
+
return BigInt(stellarSdk.scValToNative(sim.result.retval));
|
|
1753
|
+
}
|
|
1754
|
+
/** Host function: SEP-41 `token.transfer(from=account, to, amount)` (device auth). */
|
|
1755
|
+
buildTransfer(tokenId, accountAddress, destination, amount) {
|
|
1756
|
+
return invokeFunc(tokenId, "transfer", [
|
|
1757
|
+
new stellarSdk.Address(accountAddress).toScVal(),
|
|
1758
|
+
new stellarSdk.Address(destination).toScVal(),
|
|
1759
|
+
stellarSdk.nativeToScVal(amount, { type: "i128" })
|
|
1760
|
+
]);
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Sign a Soroban authorization entry with the silent device key, producing the
|
|
1764
|
+
* `Vec<DeviceSignature>` the account's `__check_auth` verifies. The device
|
|
1765
|
+
* signs `sha256(preimage)` (WebCrypto hashes once more internally), which is
|
|
1766
|
+
* exactly what the contract recomputes. Mutates + returns the entry.
|
|
1767
|
+
*/
|
|
1768
|
+
async signAuthEntry(entry, validUntilLedger) {
|
|
1769
|
+
const addrCreds = entry.credentials().address();
|
|
1770
|
+
addrCreds.signatureExpirationLedger(validUntilLedger);
|
|
1771
|
+
const preimage = stellarSdk.xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
|
|
1772
|
+
new stellarSdk.xdr.HashIdPreimageSorobanAuthorization({
|
|
1773
|
+
networkId: this.networkId(),
|
|
1774
|
+
nonce: addrCreds.nonce(),
|
|
1775
|
+
signatureExpirationLedger: validUntilLedger,
|
|
1776
|
+
invocation: entry.rootInvocation()
|
|
1777
|
+
})
|
|
1778
|
+
);
|
|
1779
|
+
const payload = stellarSdk.hash(preimage.toXDR());
|
|
1780
|
+
const sig = await this.signer.sign(new Uint8Array(payload));
|
|
1781
|
+
const pubkey = await this.signer.getPublicKey();
|
|
1782
|
+
addrCreds.signature(deviceSignatureScVal(pubkey, sig));
|
|
1783
|
+
return entry;
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Read a SEP-41 token balance of `account` via a read-only simulation of
|
|
1787
|
+
* `token.balance(account)`. Returns 0 when the account isn't deployed or holds
|
|
1788
|
+
* none. `readSource` is any funded G-account (used only for the simulation).
|
|
1789
|
+
*/
|
|
1790
|
+
async readBalance(tokenId, account, readSource) {
|
|
1791
|
+
if (!await this.isDeployed(account)) return 0n;
|
|
1792
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1793
|
+
const src = new Account3(readSource, "0");
|
|
1794
|
+
const op = stellarSdk.Operation.invokeHostFunction({
|
|
1795
|
+
func: invokeFunc(tokenId, "balance", [new stellarSdk.Address(account).toScVal()]),
|
|
1796
|
+
auth: []
|
|
1797
|
+
});
|
|
1798
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1799
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1800
|
+
if (stellarSdk.rpc.Api.isSimulationError(sim) || !sim.result?.retval) return 0n;
|
|
1801
|
+
return BigInt(stellarSdk.scValToNative(sim.result.retval));
|
|
1802
|
+
}
|
|
1803
|
+
/** Whether the account contract instance exists on-chain (is deployed). */
|
|
1804
|
+
async isDeployed(accountAddress) {
|
|
1805
|
+
try {
|
|
1806
|
+
const res = await this.server().getContractData(
|
|
1807
|
+
accountAddress,
|
|
1808
|
+
stellarSdk.xdr.ScVal.scvLedgerKeyContractInstance(),
|
|
1809
|
+
stellarSdk.rpc.Durability.Persistent
|
|
1810
|
+
);
|
|
1811
|
+
return !!res;
|
|
1812
|
+
} catch {
|
|
1813
|
+
return false;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Read whether `signer` is a currently-authorized signer of the account, via a
|
|
1818
|
+
* read-only simulation of `account.is_authorized(signer)`. `readSource` is any
|
|
1819
|
+
* funded G-account (used only for the simulation's source/sequence).
|
|
1820
|
+
*/
|
|
1821
|
+
async isAuthorizedSigner(accountAddress, signer, readSource) {
|
|
1822
|
+
if (!await this.isDeployed(accountAddress)) return false;
|
|
1823
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1824
|
+
const src = new Account3(readSource, "0");
|
|
1825
|
+
const op = stellarSdk.Operation.invokeHostFunction({
|
|
1826
|
+
func: invokeFunc(accountAddress, "is_authorized", [bytesScVal(sec1Pubkey(signer))]),
|
|
1827
|
+
auth: []
|
|
1828
|
+
});
|
|
1829
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1830
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1831
|
+
if (stellarSdk.rpc.Api.isSimulationError(sim)) {
|
|
1832
|
+
throw new Error(`kit/stellar: is_authorized simulation failed: ${sim.error}`);
|
|
1833
|
+
}
|
|
1834
|
+
if (!sim.result?.retval) return false;
|
|
1835
|
+
return stellarSdk.scValToNative(sim.result.retval) === true;
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
function sec1Pubkey(pk) {
|
|
1839
|
+
const out = new Uint8Array(65);
|
|
1840
|
+
out[0] = 4;
|
|
1841
|
+
out.set(bigIntTo32Bytes(pk.x), 1);
|
|
1842
|
+
out.set(bigIntTo32Bytes(pk.y), 33);
|
|
1843
|
+
return out;
|
|
1844
|
+
}
|
|
1845
|
+
function encodeLowSSignature2(sig) {
|
|
1846
|
+
const lowS2 = sig.s > SECP256R1_N2 / 2n ? SECP256R1_N2 - sig.s : sig.s;
|
|
1847
|
+
const out = new Uint8Array(64);
|
|
1848
|
+
out.set(bigIntTo32Bytes(sig.r), 0);
|
|
1849
|
+
out.set(bigIntTo32Bytes(lowS2), 32);
|
|
1850
|
+
return out;
|
|
1851
|
+
}
|
|
1852
|
+
function deviceSignatureScVal(pubkey, sig) {
|
|
1853
|
+
const element = stellarSdk.nativeToScVal(
|
|
1854
|
+
{
|
|
1855
|
+
public_key: Buffer.from(sec1Pubkey(pubkey)),
|
|
1856
|
+
signature: Buffer.from(encodeLowSSignature2(sig))
|
|
1857
|
+
},
|
|
1858
|
+
{ type: { public_key: ["symbol", "bytes"], signature: ["symbol", "bytes"] } }
|
|
1859
|
+
);
|
|
1860
|
+
return stellarSdk.xdr.ScVal.scvVec([element]);
|
|
1861
|
+
}
|
|
1862
|
+
function invokeFunc(contractId, method, args) {
|
|
1863
|
+
return stellarSdk.xdr.HostFunction.hostFunctionTypeInvokeContract(
|
|
1864
|
+
new stellarSdk.xdr.InvokeContractArgs({
|
|
1865
|
+
contractAddress: new stellarSdk.Address(contractId).toScAddress(),
|
|
1866
|
+
functionName: method,
|
|
1867
|
+
args
|
|
1868
|
+
})
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
function bytesScVal(bytes) {
|
|
1872
|
+
return stellarSdk.xdr.ScVal.scvBytes(Buffer.from(bytes));
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// src/chains/stellar/StellarRelayer.ts
|
|
1876
|
+
var StellarRelayer = class {
|
|
1877
|
+
constructor(opts) {
|
|
1878
|
+
this.opts = opts;
|
|
1879
|
+
}
|
|
1880
|
+
/** The relayer's source/fee-payer G-account (fetched + cached from the backend). */
|
|
1881
|
+
async getSource() {
|
|
1882
|
+
if (this.source) return this.source;
|
|
1883
|
+
const res = await fetch(`${this.opts.baseUrl}/api/stellar/relay?network=${this.opts.network}`);
|
|
1884
|
+
if (!res.ok) throw new Error(`kit/stellar: relayer source lookup failed (${res.status})`);
|
|
1885
|
+
const { fee_payer } = await res.json();
|
|
1886
|
+
this.source = fee_payer;
|
|
1887
|
+
return this.source;
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* POST the assembled, device-authorized transaction XDR to the relayer to sign
|
|
1891
|
+
* the envelope + submit. Returns the confirmed transaction hash.
|
|
1892
|
+
*/
|
|
1893
|
+
async submit(transactionXdr) {
|
|
1894
|
+
const res = await fetch(`${this.opts.baseUrl}/api/stellar/relay`, {
|
|
1895
|
+
method: "POST",
|
|
1896
|
+
headers: { "Content-Type": "application/json" },
|
|
1897
|
+
body: JSON.stringify({
|
|
1898
|
+
app_id: this.opts.appId,
|
|
1899
|
+
network: this.opts.network,
|
|
1900
|
+
transaction: transactionXdr
|
|
1901
|
+
})
|
|
1902
|
+
});
|
|
1903
|
+
if (!res.ok) {
|
|
1904
|
+
const detail = await res.text().catch(() => "");
|
|
1905
|
+
throw new Error(`kit/stellar: relay failed (${res.status}) ${detail}`);
|
|
1906
|
+
}
|
|
1907
|
+
const { hash: hash6 } = await res.json();
|
|
1908
|
+
return hash6;
|
|
1909
|
+
}
|
|
1910
|
+
};
|
|
1911
|
+
|
|
1912
|
+
// src/chains/stellar/CavosStellar.ts
|
|
1913
|
+
var CavosStellar = class _CavosStellar {
|
|
1914
|
+
constructor(identity, address, status, network, adapter, devicePubkey, relayer, sourceKeypair) {
|
|
1915
|
+
this.identity = identity;
|
|
1916
|
+
this.address = address;
|
|
1917
|
+
this.status = status;
|
|
1918
|
+
this.network = network;
|
|
1919
|
+
this.adapter = adapter;
|
|
1920
|
+
this.devicePubkey = devicePubkey;
|
|
1921
|
+
this.relayer = relayer;
|
|
1922
|
+
this.sourceKeypair = sourceKeypair;
|
|
1923
|
+
/** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
|
|
1924
|
+
this.chain = "stellar";
|
|
1925
|
+
/** True when this connect just created a brand-new account (first sign-up). */
|
|
1926
|
+
this.isNewAccount = false;
|
|
1927
|
+
}
|
|
1928
|
+
get publicKey() {
|
|
1929
|
+
return this.devicePubkey;
|
|
1930
|
+
}
|
|
1931
|
+
static async connect(opts) {
|
|
1932
|
+
const identity = opts.identity ?? await opts.auth?.authenticate();
|
|
1933
|
+
if (!identity) throw new Error("kit/stellar: connect requires `identity` or `auth`");
|
|
1934
|
+
const signer = opts.createSigner ? await opts.createSigner(`${identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${identity.userId}:${opts.appSalt}` });
|
|
1935
|
+
const devicePubkey = await signer.getPublicKey();
|
|
1936
|
+
const adapter = new StellarAdapter({
|
|
1937
|
+
network: opts.network,
|
|
1938
|
+
rpcUrl: opts.rpcUrl,
|
|
1939
|
+
factoryId: opts.factoryId,
|
|
1940
|
+
signer
|
|
1941
|
+
});
|
|
1942
|
+
const addressSeed = deriveAddressSeedStellar({ userId: identity.userId, appSalt: opts.appSalt });
|
|
1943
|
+
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
1944
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
|
|
1945
|
+
const relayer = opts.relayer ?? (opts.appId ? new StellarRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : void 0);
|
|
1946
|
+
const build = (address2, status) => new _CavosStellar(identity, address2, status, opts.network, adapter, devicePubkey, relayer, opts.sourceKeypair);
|
|
1947
|
+
const self = build("", "needs-device-approval");
|
|
1948
|
+
const readSource = await self.resolveSource();
|
|
1949
|
+
const existing = await registry.lookup(identity.userId);
|
|
1950
|
+
if (existing) {
|
|
1951
|
+
const isSigner2 = await adapter.isAuthorizedSigner(existing.address, devicePubkey, readSource);
|
|
1952
|
+
return build(existing.address, isSigner2 ? "ready" : "needs-device-approval");
|
|
1953
|
+
}
|
|
1954
|
+
const address = adapter.computeAddress(addressSeed, devicePubkey);
|
|
1955
|
+
const wasDeployed = await adapter.isDeployed(address);
|
|
1956
|
+
if (!wasDeployed) {
|
|
1957
|
+
const func = adapter.buildDeploy(addressSeed, devicePubkey);
|
|
1958
|
+
await self.submitHostFunction(func, void 0);
|
|
1959
|
+
}
|
|
1960
|
+
await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
|
|
1961
|
+
const isSigner = await adapter.isAuthorizedSigner(address, devicePubkey, readSource);
|
|
1962
|
+
const wallet = build(address, isSigner ? "ready" : "needs-device-approval");
|
|
1963
|
+
wallet.isNewAccount = !wasDeployed && isSigner;
|
|
1964
|
+
return wallet;
|
|
1965
|
+
}
|
|
1966
|
+
/** Authorize an additional device signer (device-signed via `__check_auth`). */
|
|
1967
|
+
async addSigner(pubkey) {
|
|
1968
|
+
const func = this.adapter.buildAddSigner(this.address, pubkey);
|
|
1969
|
+
return this.submitHostFunction(func, this.address);
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Enroll a passkey as an approver (2FA-style step-up). Device-signed + gasless;
|
|
1973
|
+
* requires a ready device. Idempotent. Returns the passkey pubkey + tx hash.
|
|
1974
|
+
*/
|
|
1975
|
+
async enrollPasskey(passkey, params) {
|
|
1976
|
+
const enrolled = await passkey.enroll(params);
|
|
1977
|
+
const { transactionHash } = await this.addApprover(enrolled.publicKey);
|
|
1978
|
+
return { publicKey: enrolled.publicKey, transactionHash };
|
|
1979
|
+
}
|
|
1980
|
+
/** Register an already-enrolled passkey pubkey as an approver (gasless).
|
|
1981
|
+
* Idempotent. Lets one passkey be registered across chains without re-prompting. */
|
|
1982
|
+
async addApprover(pubkey) {
|
|
1983
|
+
if (this.status !== "ready") {
|
|
1984
|
+
throw new Error("kit/stellar: addApprover requires a ready, authorized device");
|
|
1985
|
+
}
|
|
1986
|
+
const readSource = await this.resolveSource();
|
|
1987
|
+
if (await this.adapter.isApprover(this.address, pubkey, readSource)) return {};
|
|
1988
|
+
const func = this.adapter.buildAddApprover(this.address, pubkey);
|
|
1989
|
+
const transactionHash = await this.submitHostFunction(func, this.address);
|
|
1990
|
+
return { transactionHash };
|
|
1991
|
+
}
|
|
1992
|
+
/** True if this account already has a passkey enrolled as an approver, so a
|
|
1993
|
+
* new device can be approved with the passkey instead of the email flow. */
|
|
1994
|
+
async hasPasskey() {
|
|
1995
|
+
const readSource = await this.resolveSource();
|
|
1996
|
+
return this.adapter.hasPasskeyApprover(this.address, readSource);
|
|
1997
|
+
}
|
|
1998
|
+
/** Re-read (from chain) whether THIS device is now an authorized signer.
|
|
1999
|
+
* Used to poll for readiness after a passkey approval before it's indexed. */
|
|
2000
|
+
async isReady() {
|
|
2001
|
+
const readSource = await this.resolveSource();
|
|
2002
|
+
return this.adapter.isAuthorizedSigner(this.address, this.devicePubkey, readSource);
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* From a fresh browser (status `needs-device-approval`), approve adding THIS
|
|
2006
|
+
* device using the user's synced passkey. Gasless via the relayer — the call
|
|
2007
|
+
* carries the WebAuthn assertion, so no device signature is needed. Returns the
|
|
2008
|
+
* tx hash. No trip back to an already-authorized device.
|
|
2009
|
+
*/
|
|
2010
|
+
async approveThisDeviceWithPasskey(passkey) {
|
|
2011
|
+
if (this.status === "ready") {
|
|
2012
|
+
throw new Error("kit/stellar: this device is already an authorized signer");
|
|
2013
|
+
}
|
|
2014
|
+
const { leaf, nonce } = await this.passkeyLeafForThisDevice();
|
|
2015
|
+
const leaves = [leaf];
|
|
2016
|
+
const assertion = await passkey.assert(batchChallenge(leaves));
|
|
2017
|
+
const { transactionHash } = await this.submitPasskeyApproval(assertion, leaves, 0, nonce);
|
|
2018
|
+
return transactionHash;
|
|
2019
|
+
}
|
|
2020
|
+
/** This device's leaf + passkey nonce for a (possibly multi-chain) batch. */
|
|
2021
|
+
async passkeyLeafForThisDevice() {
|
|
2022
|
+
const readSource = await this.resolveSource();
|
|
2023
|
+
const nonce = await this.adapter.passkeyNonce(this.address, readSource);
|
|
2024
|
+
return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
|
|
2025
|
+
}
|
|
2026
|
+
/** Submit `add_signer_via_passkey` given a shared assertion + batch position.
|
|
2027
|
+
* No device auth entry — authorized purely by the passkey assertion. */
|
|
2028
|
+
async submitPasskeyApproval(assertion, leaves, leafIndex, nonce) {
|
|
2029
|
+
const readSource = await this.resolveSource();
|
|
2030
|
+
const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
|
|
2031
|
+
const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
|
|
2032
|
+
let approver = null;
|
|
2033
|
+
for (const cand of candidates) {
|
|
2034
|
+
if (await this.adapter.isApprover(this.address, cand.publicKey, readSource)) {
|
|
2035
|
+
approver = cand.publicKey;
|
|
2036
|
+
break;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
if (!approver) throw new Error("kit/stellar: this passkey is not a registered approver");
|
|
2040
|
+
const func = this.adapter.buildAddSignerViaPasskey(
|
|
2041
|
+
this.address,
|
|
2042
|
+
this.devicePubkey,
|
|
2043
|
+
approver,
|
|
2044
|
+
nonce,
|
|
2045
|
+
leaves,
|
|
2046
|
+
leafIndex,
|
|
2047
|
+
assertion
|
|
2048
|
+
);
|
|
2049
|
+
return { transactionHash: await this.submitHostFunction(func, void 0) };
|
|
2050
|
+
}
|
|
2051
|
+
/** Move `amount` stroops of native XLM to `destination` (device-signed). */
|
|
2052
|
+
async execute(amount, destination) {
|
|
2053
|
+
return this.executeTransfer(NATIVE_SAC_ID[this.network], amount, destination);
|
|
2054
|
+
}
|
|
2055
|
+
/** Read this account's balance of `tokenId` (defaults to native XLM), in stroops. */
|
|
2056
|
+
async balance(tokenId = NATIVE_SAC_ID[this.network]) {
|
|
2057
|
+
const readSource = await this.resolveSource();
|
|
2058
|
+
return this.adapter.readBalance(tokenId, this.address, readSource);
|
|
2059
|
+
}
|
|
2060
|
+
/** Transfer `amount` of any SEP-41 token out of the account (device-signed). */
|
|
2061
|
+
async executeTransfer(tokenId, amount, destination) {
|
|
2062
|
+
if (this.status !== "ready") {
|
|
2063
|
+
throw new Error("kit/stellar: this device is not yet an authorized signer of the wallet");
|
|
2064
|
+
}
|
|
2065
|
+
const func = this.adapter.buildTransfer(tokenId, this.address, destination, amount);
|
|
2066
|
+
return this.submitHostFunction(func, this.address);
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Register the backup signer derived from `code` as an authorized signer of
|
|
2070
|
+
* this account (device-signed). Idempotent. The code never leaves the device —
|
|
2071
|
+
* only the derived public key travels on-chain. Mirrors the other chains.
|
|
2072
|
+
*/
|
|
2073
|
+
async setupRecovery(code) {
|
|
2074
|
+
if (this.status !== "ready") {
|
|
2075
|
+
throw new Error("kit/stellar: setupRecovery requires a ready, registered device");
|
|
2076
|
+
}
|
|
2077
|
+
const { publicKey: backupPubkey } = deriveBackupKey(code);
|
|
2078
|
+
const readSource = await this.resolveSource();
|
|
2079
|
+
if (await this.adapter.isAuthorizedSigner(this.address, backupPubkey, readSource)) return void 0;
|
|
2080
|
+
return this.addSigner(backupPubkey);
|
|
2081
|
+
}
|
|
2082
|
+
/**
|
|
2083
|
+
* Recover an account after losing every device signer: derive the backup key
|
|
2084
|
+
* from `code`, use it (not the new device) to authorize `add_signer(newDevice)`,
|
|
2085
|
+
* and return a ready handle bound to the new device. The address is unchanged.
|
|
2086
|
+
*/
|
|
2087
|
+
static async recover(opts) {
|
|
2088
|
+
const signer = opts.createSigner ? await opts.createSigner(`${opts.identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${opts.identity.userId}:${opts.appSalt}` });
|
|
2089
|
+
const devicePubkey = await signer.getPublicKey();
|
|
2090
|
+
const backup = BackupSigner.fromCode(opts.code);
|
|
2091
|
+
const backupAdapter = new StellarAdapter({
|
|
2092
|
+
network: opts.network,
|
|
2093
|
+
rpcUrl: opts.rpcUrl,
|
|
2094
|
+
factoryId: opts.factoryId,
|
|
2095
|
+
signer: backup
|
|
2096
|
+
});
|
|
2097
|
+
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
2098
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
|
|
2099
|
+
const existing = await registry.lookup(opts.identity.userId);
|
|
2100
|
+
if (!existing) {
|
|
2101
|
+
throw new Error("kit/stellar: no account found for this identity \u2014 nothing to recover");
|
|
2102
|
+
}
|
|
2103
|
+
const relayer = opts.relayer ?? (opts.appId ? new StellarRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : void 0);
|
|
2104
|
+
const backupHandle = new _CavosStellar(
|
|
2105
|
+
opts.identity,
|
|
2106
|
+
existing.address,
|
|
2107
|
+
"ready",
|
|
2108
|
+
opts.network,
|
|
2109
|
+
backupAdapter,
|
|
2110
|
+
devicePubkey,
|
|
2111
|
+
relayer,
|
|
2112
|
+
opts.sourceKeypair
|
|
2113
|
+
);
|
|
2114
|
+
const readSource = await backupHandle.resolveSource();
|
|
2115
|
+
if (!await backupAdapter.isAuthorizedSigner(existing.address, devicePubkey, readSource)) {
|
|
2116
|
+
await backupHandle.addSigner(devicePubkey);
|
|
2117
|
+
}
|
|
2118
|
+
const adapter = new StellarAdapter({
|
|
2119
|
+
network: opts.network,
|
|
2120
|
+
rpcUrl: opts.rpcUrl,
|
|
2121
|
+
factoryId: opts.factoryId,
|
|
2122
|
+
signer
|
|
2123
|
+
});
|
|
2124
|
+
return new _CavosStellar(
|
|
2125
|
+
opts.identity,
|
|
2126
|
+
existing.address,
|
|
2127
|
+
"ready",
|
|
2128
|
+
opts.network,
|
|
2129
|
+
adapter,
|
|
2130
|
+
devicePubkey,
|
|
2131
|
+
relayer,
|
|
2132
|
+
opts.sourceKeypair
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
/** The transaction source/fee-payer G-address (relayer or self-funded). */
|
|
2136
|
+
async resolveSource() {
|
|
2137
|
+
if (this.relayer) return this.relayer.getSource();
|
|
2138
|
+
if (this.sourceKeypair) return this.sourceKeypair.publicKey();
|
|
2139
|
+
throw new Error("kit/stellar: a relayer (appId) or sourceKeypair is required");
|
|
2140
|
+
}
|
|
2141
|
+
/**
|
|
2142
|
+
* Build → simulate → device-sign auth → assemble → submit an invoke-contract
|
|
2143
|
+
* host function. `authAccount` is the account whose `__check_auth` must sign the
|
|
2144
|
+
* operation's Soroban auth entry (undefined for a plain factory deploy).
|
|
2145
|
+
*/
|
|
2146
|
+
async submitHostFunction(func, authAccount) {
|
|
2147
|
+
const server = this.adapter.server();
|
|
2148
|
+
const sourceAddr = await this.resolveSource();
|
|
2149
|
+
const simSource = new stellarSdk.Account(sourceAddr, "0");
|
|
2150
|
+
const unsignedOp = stellarSdk.Operation.invokeHostFunction({ func, auth: [] });
|
|
2151
|
+
const simTx = new stellarSdk.TransactionBuilder(simSource, {
|
|
2152
|
+
fee: stellarSdk.BASE_FEE,
|
|
2153
|
+
networkPassphrase: this.adapter.passphrase
|
|
2154
|
+
}).addOperation(unsignedOp).setTimeout(180).build();
|
|
2155
|
+
const sim = await server.simulateTransaction(simTx);
|
|
2156
|
+
if (stellarSdk.rpc.Api.isSimulationError(sim)) {
|
|
2157
|
+
throw new Error(`kit/stellar: simulation failed: ${sim.error}`);
|
|
2158
|
+
}
|
|
2159
|
+
const validUntil = (await server.getLatestLedger()).sequence + 100;
|
|
2160
|
+
const entries = sim.result?.auth ?? [];
|
|
2161
|
+
const signedAuth = [];
|
|
2162
|
+
for (const entry of entries) {
|
|
2163
|
+
if (authAccount && isAddressCredentialFor(entry, authAccount)) {
|
|
2164
|
+
signedAuth.push(await this.adapter.signAuthEntry(entry, validUntil));
|
|
2165
|
+
} else {
|
|
2166
|
+
signedAuth.push(entry);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
const account = await server.getAccount(sourceAddr);
|
|
2170
|
+
const finalOp = stellarSdk.Operation.invokeHostFunction({ func, auth: signedAuth });
|
|
2171
|
+
const built = new stellarSdk.TransactionBuilder(account, {
|
|
2172
|
+
fee: stellarSdk.BASE_FEE,
|
|
2173
|
+
networkPassphrase: this.adapter.passphrase
|
|
2174
|
+
}).addOperation(finalOp).setTimeout(180).build();
|
|
2175
|
+
const authSim = await server.simulateTransaction(built);
|
|
2176
|
+
if (stellarSdk.rpc.Api.isSimulationError(authSim)) {
|
|
2177
|
+
throw new Error(`kit/stellar: auth simulation failed: ${authSim.error}`);
|
|
2178
|
+
}
|
|
2179
|
+
const assembled = stellarSdk.rpc.assembleTransaction(built, authSim).build();
|
|
2180
|
+
if (this.relayer) {
|
|
2181
|
+
return this.relayer.submit(assembled.toXDR());
|
|
2182
|
+
}
|
|
2183
|
+
if (this.sourceKeypair) {
|
|
2184
|
+
assembled.sign(this.sourceKeypair);
|
|
2185
|
+
return this.sendAndConfirm(assembled);
|
|
2186
|
+
}
|
|
2187
|
+
throw new Error("kit/stellar: no relayer or sourceKeypair configured to submit");
|
|
2188
|
+
}
|
|
2189
|
+
/** Submit a signed tx via RPC and poll to confirmation. Returns the hash. */
|
|
2190
|
+
async sendAndConfirm(tx2) {
|
|
2191
|
+
const server = this.adapter.server();
|
|
2192
|
+
const sent = await server.sendTransaction(tx2);
|
|
2193
|
+
if (sent.status === "ERROR") {
|
|
2194
|
+
throw new Error(`kit/stellar: submit rejected: ${JSON.stringify(sent.errorResult)}`);
|
|
2195
|
+
}
|
|
2196
|
+
const hash6 = sent.hash;
|
|
2197
|
+
for (let i = 0; i < 30; i++) {
|
|
2198
|
+
const got = await server.getTransaction(hash6);
|
|
2199
|
+
if (got.status === stellarSdk.rpc.Api.GetTransactionStatus.SUCCESS) return hash6;
|
|
2200
|
+
if (got.status === stellarSdk.rpc.Api.GetTransactionStatus.FAILED) {
|
|
2201
|
+
throw new Error(`kit/stellar: tx ${hash6} failed`);
|
|
2202
|
+
}
|
|
2203
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
2204
|
+
}
|
|
2205
|
+
throw new Error(`kit/stellar: tx ${hash6} not confirmed in time`);
|
|
2206
|
+
}
|
|
2207
|
+
};
|
|
2208
|
+
function isAddressCredentialFor(entry, accountAddress) {
|
|
2209
|
+
const creds = entry.credentials();
|
|
2210
|
+
if (creds.switch() !== stellarSdk.xdr.SorobanCredentialsType.sorobanCredentialsAddress()) return false;
|
|
2211
|
+
return stellarSdk.Address.fromScAddress(creds.address().address()).toString() === accountAddress;
|
|
2212
|
+
}
|
|
2213
|
+
var defaultRegistry2 = new InMemoryWalletRegistry();
|
|
2214
|
+
|
|
1203
2215
|
// src/recovery/HttpRecoveryClient.ts
|
|
1204
2216
|
function toHex2(n) {
|
|
1205
2217
|
return "0x" + n.toString(16);
|
|
@@ -1281,18 +2293,26 @@ var SOLANA_ENV = {
|
|
|
1281
2293
|
mainnet: "solana-mainnet",
|
|
1282
2294
|
testnet: "solana-devnet"
|
|
1283
2295
|
};
|
|
2296
|
+
var STELLAR_ENV = {
|
|
2297
|
+
mainnet: "stellar-mainnet",
|
|
2298
|
+
testnet: "stellar-testnet"
|
|
2299
|
+
};
|
|
1284
2300
|
var Cavos = class _Cavos {
|
|
1285
|
-
constructor(identity, address, status, account, adapter, devicePubkey) {
|
|
2301
|
+
constructor(identity, address, status, account, adapter, devicePubkey, paymaster) {
|
|
1286
2302
|
this.identity = identity;
|
|
1287
2303
|
this.address = address;
|
|
1288
2304
|
this.status = status;
|
|
1289
2305
|
this.account = account;
|
|
1290
2306
|
this.adapter = adapter;
|
|
1291
2307
|
this.devicePubkey = devicePubkey;
|
|
2308
|
+
this.paymaster = paymaster;
|
|
1292
2309
|
/** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
|
|
1293
2310
|
this.chain = "starknet";
|
|
1294
2311
|
/** Request id of the pending device-addition, when status is needs-device-approval. */
|
|
1295
2312
|
this.pendingRequestId = null;
|
|
2313
|
+
/** True when this connect just created & deployed a brand-new account (first
|
|
2314
|
+
* sign-up), so the UI can offer a one-time "secure your account" step. */
|
|
2315
|
+
this.isNewAccount = false;
|
|
1296
2316
|
}
|
|
1297
2317
|
/**
|
|
1298
2318
|
* Unified entry point. Pick a `chain` and an `network` environment; the kit
|
|
@@ -1321,6 +2341,22 @@ var Cavos = class _Cavos {
|
|
|
1321
2341
|
...opts.feePayer ? { feePayer: opts.feePayer } : {}
|
|
1322
2342
|
});
|
|
1323
2343
|
}
|
|
2344
|
+
if (opts.chain === "stellar") {
|
|
2345
|
+
return CavosStellar.connect({
|
|
2346
|
+
network: STELLAR_ENV[opts.network],
|
|
2347
|
+
...opts.auth ? { auth: opts.auth } : {},
|
|
2348
|
+
...opts.identity ? { identity: opts.identity } : {},
|
|
2349
|
+
appSalt: opts.appSalt,
|
|
2350
|
+
...opts.appId ? { appId: opts.appId } : {},
|
|
2351
|
+
...opts.backendUrl ? { backendUrl: opts.backendUrl } : {},
|
|
2352
|
+
...opts.registry ? { registry: opts.registry } : {},
|
|
2353
|
+
...opts.rpcUrl ? { rpcUrl: opts.rpcUrl } : {},
|
|
2354
|
+
...opts.factoryId ? { factoryId: opts.factoryId } : {},
|
|
2355
|
+
...opts.createSigner ? { createSigner: opts.createSigner } : {},
|
|
2356
|
+
...opts.stellarRelayer ? { relayer: opts.stellarRelayer } : {},
|
|
2357
|
+
...opts.stellarSourceKeypair ? { sourceKeypair: opts.stellarSourceKeypair } : {}
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
1324
2360
|
if (!opts.paymasterApiKey) {
|
|
1325
2361
|
throw new Error("kit: `paymasterApiKey` is required for Starknet connections");
|
|
1326
2362
|
}
|
|
@@ -1348,8 +2384,10 @@ var Cavos = class _Cavos {
|
|
|
1348
2384
|
const provider = new starknet.RpcProvider({
|
|
1349
2385
|
nodeUrl: opts.rpcUrl ?? STARKNET_NETWORKS[opts.network].rpcUrl
|
|
1350
2386
|
});
|
|
2387
|
+
const paymasterUrl = opts.paymasterUrl ?? CAVOS_PAYMASTER_URL[opts.network];
|
|
2388
|
+
const paymasterConfig = { url: paymasterUrl, apiKey: opts.paymasterApiKey };
|
|
1351
2389
|
const paymaster = new starknet.PaymasterRpc({
|
|
1352
|
-
nodeUrl:
|
|
2390
|
+
nodeUrl: paymasterUrl,
|
|
1353
2391
|
headers: { "x-paymaster-api-key": opts.paymasterApiKey }
|
|
1354
2392
|
});
|
|
1355
2393
|
const addressSeed = deriveAddressSeed({ userId: identity.userId, appSalt: opts.appSalt });
|
|
@@ -1364,26 +2402,27 @@ var Cavos = class _Cavos {
|
|
|
1364
2402
|
cairoVersion: "1"
|
|
1365
2403
|
});
|
|
1366
2404
|
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
1367
|
-
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) :
|
|
2405
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry3);
|
|
1368
2406
|
const recovery = opts.recovery ?? (opts.appId ? new HttpRecoveryClient({ baseUrl: backendUrl, appId: opts.appId }) : null);
|
|
1369
2407
|
const existing = await registry.lookup(identity.userId);
|
|
1370
2408
|
if (existing) {
|
|
1371
2409
|
const account2 = makeAccount(existing.address);
|
|
1372
2410
|
const isSigner2 = await adapter.isAuthorizedSigner(existing.address, devicePubkey);
|
|
1373
|
-
const
|
|
2411
|
+
const cavos2 = new _Cavos(
|
|
1374
2412
|
identity,
|
|
1375
2413
|
existing.address,
|
|
1376
2414
|
isSigner2 ? "ready" : "needs-device-approval",
|
|
1377
2415
|
account2,
|
|
1378
2416
|
adapter,
|
|
1379
|
-
devicePubkey
|
|
2417
|
+
devicePubkey,
|
|
2418
|
+
paymasterConfig
|
|
1380
2419
|
);
|
|
1381
2420
|
if (!isSigner2 && recovery) {
|
|
1382
2421
|
const dedup = lastDeviceRequest.get(identity.userId);
|
|
1383
2422
|
const fresh = dedup && Date.now() - dedup.requestedAt < DEVICE_REQUEST_DEDUP_MS;
|
|
1384
2423
|
try {
|
|
1385
2424
|
if (fresh) {
|
|
1386
|
-
|
|
2425
|
+
cavos2.pendingRequestId = dedup.requestId;
|
|
1387
2426
|
} else {
|
|
1388
2427
|
const { requestId } = await recovery.requestDeviceAddition({
|
|
1389
2428
|
userId: identity.userId,
|
|
@@ -1391,14 +2430,14 @@ var Cavos = class _Cavos {
|
|
|
1391
2430
|
newSigner: devicePubkey,
|
|
1392
2431
|
...identity.email ? { email: identity.email } : {}
|
|
1393
2432
|
});
|
|
1394
|
-
|
|
2433
|
+
cavos2.pendingRequestId = requestId;
|
|
1395
2434
|
lastDeviceRequest.set(identity.userId, { requestId, requestedAt: Date.now() });
|
|
1396
2435
|
}
|
|
1397
2436
|
} catch (e) {
|
|
1398
2437
|
console.warn("[Cavos] requestDeviceAddition failed:", e);
|
|
1399
2438
|
}
|
|
1400
2439
|
}
|
|
1401
|
-
return
|
|
2440
|
+
return cavos2;
|
|
1402
2441
|
}
|
|
1403
2442
|
const address = adapter.computeAddress({ addressSeed, initialSigner: devicePubkey });
|
|
1404
2443
|
const account = makeAccount(address);
|
|
@@ -1429,14 +2468,17 @@ var Cavos = class _Cavos {
|
|
|
1429
2468
|
console.warn("[Cavos] isAuthorizedSigner read failed:", e);
|
|
1430
2469
|
isSigner = !alreadyDeployed;
|
|
1431
2470
|
}
|
|
1432
|
-
|
|
2471
|
+
const cavos = new _Cavos(
|
|
1433
2472
|
identity,
|
|
1434
2473
|
address,
|
|
1435
2474
|
isSigner ? "ready" : "needs-device-approval",
|
|
1436
2475
|
account,
|
|
1437
2476
|
adapter,
|
|
1438
|
-
devicePubkey
|
|
2477
|
+
devicePubkey,
|
|
2478
|
+
paymasterConfig
|
|
1439
2479
|
);
|
|
2480
|
+
cavos.isNewAccount = !alreadyDeployed && isSigner;
|
|
2481
|
+
return cavos;
|
|
1440
2482
|
}
|
|
1441
2483
|
/** This device's public key (e.g. to request addition to an existing wallet). */
|
|
1442
2484
|
get publicKey() {
|
|
@@ -1456,6 +2498,108 @@ var Cavos = class _Cavos {
|
|
|
1456
2498
|
async addSigner(pubkey) {
|
|
1457
2499
|
return this.execute([this.adapter.buildAddSigner(this.address, pubkey)]);
|
|
1458
2500
|
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Enroll a passkey as an APPROVER so the user can later add devices from any
|
|
2503
|
+
* browser (2FA-style step-up). Requires a ready device (the enrollment call is
|
|
2504
|
+
* device-signed and gasless). Idempotent: a no-op if the passkey is already an
|
|
2505
|
+
* approver. Call this whenever the app decides to prompt "turn on device
|
|
2506
|
+
* approvals". Returns the passkey's public key + the enrollment tx hash.
|
|
2507
|
+
*/
|
|
2508
|
+
async enrollPasskey(passkey, params) {
|
|
2509
|
+
const enrolled = await passkey.enroll(params);
|
|
2510
|
+
const { transactionHash } = await this.addApprover(enrolled.publicKey);
|
|
2511
|
+
return { publicKey: enrolled.publicKey, transactionHash };
|
|
2512
|
+
}
|
|
2513
|
+
/**
|
|
2514
|
+
* Register an ALREADY-enrolled passkey public key as an approver (gasless,
|
|
2515
|
+
* device-signed). Idempotent. Use this to register ONE passkey across multiple
|
|
2516
|
+
* chains without re-prompting `passkey.enroll()` on each: enroll once, then
|
|
2517
|
+
* call `addApprover(pubkey)` on each chain's wallet.
|
|
2518
|
+
*/
|
|
2519
|
+
async addApprover(pubkey) {
|
|
2520
|
+
if (this.status !== "ready") {
|
|
2521
|
+
throw new Error("kit: addApprover requires a ready, authorized device");
|
|
2522
|
+
}
|
|
2523
|
+
if (await this.adapter.isApprover(this.address, pubkey)) return {};
|
|
2524
|
+
const { transactionHash } = await this.execute([
|
|
2525
|
+
this.adapter.buildAddApprover(this.address, pubkey)
|
|
2526
|
+
]);
|
|
2527
|
+
try {
|
|
2528
|
+
await this.account.waitForTransaction(transactionHash);
|
|
2529
|
+
} catch (e) {
|
|
2530
|
+
console.warn("[Cavos] add_approver receipt wait failed:", e);
|
|
2531
|
+
}
|
|
2532
|
+
return { transactionHash };
|
|
2533
|
+
}
|
|
2534
|
+
/** True if this account already has a passkey enrolled as an approver, so a
|
|
2535
|
+
* new device can be approved with the passkey instead of the email flow. */
|
|
2536
|
+
async hasPasskey() {
|
|
2537
|
+
return this.adapter.hasPasskeyApprover(this.address);
|
|
2538
|
+
}
|
|
2539
|
+
/** Re-read (from chain) whether THIS device is now an authorized signer.
|
|
2540
|
+
* Cheap and side-effect free — used to poll for readiness after a passkey /
|
|
2541
|
+
* device approval submits, before the new signer is indexed. */
|
|
2542
|
+
async isReady() {
|
|
2543
|
+
return this.adapter.isAuthorizedSigner(this.address, this.devicePubkey);
|
|
2544
|
+
}
|
|
2545
|
+
/**
|
|
2546
|
+
* From a brand-new browser (status `needs-device-approval`), use the user's
|
|
2547
|
+
* synced passkey to authorize adding THIS device — no trip back to an already-
|
|
2548
|
+
* authorized device.
|
|
2549
|
+
*
|
|
2550
|
+
* `add_signer_via_passkey` is a public external authorized by the embedded
|
|
2551
|
+
* WebAuthn assertion (no device signature), so by default we sponsor it through
|
|
2552
|
+
* the Cavos paymaster's `paymaster_executeDirectTransaction` (the forwarder's
|
|
2553
|
+
* `execute_sponsored` runs a generic call — it does NOT require SNIP-9). Pass a
|
|
2554
|
+
* custom `submit` to route it through your own relayer instead. Returns the tx.
|
|
2555
|
+
*/
|
|
2556
|
+
async approveThisDeviceWithPasskey(opts) {
|
|
2557
|
+
if (this.status === "ready") {
|
|
2558
|
+
throw new Error("kit: this device is already an authorized signer");
|
|
2559
|
+
}
|
|
2560
|
+
const { leaf, nonce } = await this.passkeyLeafForThisDevice();
|
|
2561
|
+
const leaves = [leaf];
|
|
2562
|
+
const assertion = await opts.passkey.assert(batchChallenge(leaves));
|
|
2563
|
+
return this.submitPasskeyApproval(assertion, leaves, 0, nonce, opts.submit);
|
|
2564
|
+
}
|
|
2565
|
+
/** This device's leaf + the current passkey nonce, for a (possibly multi-chain)
|
|
2566
|
+
* passkey approval batch. See `approveDeviceEverywhere`. */
|
|
2567
|
+
async passkeyLeafForThisDevice() {
|
|
2568
|
+
const nonce = await this.adapter.getPasskeyNonce(this.address);
|
|
2569
|
+
return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
|
|
2570
|
+
}
|
|
2571
|
+
/** Submit `add_signer_via_passkey` given a (shared) assertion + this chain's
|
|
2572
|
+
* position in the batch. The assertion doesn't carry the passkey pubkey, so we
|
|
2573
|
+
* recover both candidates and pick the enrolled approver via the on-chain view
|
|
2574
|
+
* (no backend). Defaults to sponsoring through the paymaster. */
|
|
2575
|
+
async submitPasskeyApproval(assertion, leaves, leafIndex, nonce, submit) {
|
|
2576
|
+
const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
|
|
2577
|
+
const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
|
|
2578
|
+
let yParity = null;
|
|
2579
|
+
for (const cand of candidates) {
|
|
2580
|
+
if (await this.adapter.isApprover(this.address, cand.publicKey)) {
|
|
2581
|
+
yParity = cand.yParity;
|
|
2582
|
+
break;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
if (yParity === null) {
|
|
2586
|
+
throw new Error("kit: this passkey is not a registered approver of the wallet");
|
|
2587
|
+
}
|
|
2588
|
+
const call = this.adapter.buildAddSignerViaPasskey(
|
|
2589
|
+
this.address,
|
|
2590
|
+
this.devicePubkey,
|
|
2591
|
+
nonce,
|
|
2592
|
+
leaves,
|
|
2593
|
+
leafIndex,
|
|
2594
|
+
assertion,
|
|
2595
|
+
yParity
|
|
2596
|
+
);
|
|
2597
|
+
if (submit) return submit(call);
|
|
2598
|
+
if (!this.paymaster) {
|
|
2599
|
+
throw new Error("kit: no paymaster configured \u2014 pass a `submit` relayer to approveThisDeviceWithPasskey");
|
|
2600
|
+
}
|
|
2601
|
+
return paymasterExecuteDirect(this.paymaster, this.address, call);
|
|
2602
|
+
}
|
|
1459
2603
|
/**
|
|
1460
2604
|
* Register a self-custodial backup signer derived from `code`, so the account
|
|
1461
2605
|
* can be recovered after the user loses every device. Idempotent: if the
|
|
@@ -1497,7 +2641,7 @@ var Cavos = class _Cavos {
|
|
|
1497
2641
|
const backup = BackupSigner.fromCode(opts.code);
|
|
1498
2642
|
const backupAdapter = new StarknetAdapter({ classHash, signer: backup, provider });
|
|
1499
2643
|
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
1500
|
-
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network }) :
|
|
2644
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network }) : defaultRegistry3);
|
|
1501
2645
|
const existing = await registry.lookup(opts.identity.userId);
|
|
1502
2646
|
if (!existing) {
|
|
1503
2647
|
throw new Error("kit: no account found for this identity \u2014 nothing to recover");
|
|
@@ -1532,7 +2676,7 @@ var Cavos = class _Cavos {
|
|
|
1532
2676
|
return new _Cavos(opts.identity, existing.address, "ready", account, adapter, devicePubkey);
|
|
1533
2677
|
}
|
|
1534
2678
|
};
|
|
1535
|
-
var
|
|
2679
|
+
var defaultRegistry3 = new InMemoryWalletRegistry();
|
|
1536
2680
|
var DEVICE_REQUEST_DEDUP_MS = 5 * 60 * 1e3;
|
|
1537
2681
|
var lastDeviceRequest = /* @__PURE__ */ new Map();
|
|
1538
2682
|
async function isDeployed(provider, address) {
|
|
@@ -1543,6 +2687,56 @@ async function isDeployed(provider, address) {
|
|
|
1543
2687
|
return false;
|
|
1544
2688
|
}
|
|
1545
2689
|
}
|
|
2690
|
+
async function approveDeviceEverywhere(wallets, passkey) {
|
|
2691
|
+
const targets = wallets.filter((w) => w.status === "needs-device-approval");
|
|
2692
|
+
if (targets.length === 0) return [];
|
|
2693
|
+
const infos = await Promise.all(targets.map((w) => w.passkeyLeafForThisDevice()));
|
|
2694
|
+
const leaves = infos.map((i) => i.leaf);
|
|
2695
|
+
const assertion = await passkey.assert(batchChallenge(leaves));
|
|
2696
|
+
const settled = await Promise.allSettled(
|
|
2697
|
+
targets.map((w, i) => w.submitPasskeyApproval(assertion, leaves, i, infos[i].nonce))
|
|
2698
|
+
);
|
|
2699
|
+
return settled.map(
|
|
2700
|
+
(r, i) => r.status === "fulfilled" ? { chain: targets[i].chain, transactionHash: r.value.transactionHash } : {
|
|
2701
|
+
chain: targets[i].chain,
|
|
2702
|
+
error: r.reason instanceof Error ? r.reason.message : String(r.reason)
|
|
2703
|
+
}
|
|
2704
|
+
);
|
|
2705
|
+
}
|
|
2706
|
+
async function paymasterExecuteDirect(paymaster, userAddress, call) {
|
|
2707
|
+
const body = {
|
|
2708
|
+
jsonrpc: "2.0",
|
|
2709
|
+
id: 1,
|
|
2710
|
+
method: "paymaster_executeDirectTransaction",
|
|
2711
|
+
params: {
|
|
2712
|
+
transaction: {
|
|
2713
|
+
type: "invoke",
|
|
2714
|
+
invoke: {
|
|
2715
|
+
user_address: userAddress,
|
|
2716
|
+
execute_from_outside_call: {
|
|
2717
|
+
to: call.contractAddress,
|
|
2718
|
+
selector: starknet.hash.getSelectorFromName(call.entrypoint),
|
|
2719
|
+
calldata: call.calldata.map((c) => starknet.num.toHex(c))
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
},
|
|
2723
|
+
parameters: { version: "0x1", fee_mode: { mode: "sponsored" } }
|
|
2724
|
+
}
|
|
2725
|
+
};
|
|
2726
|
+
const res = await fetch(paymaster.url, {
|
|
2727
|
+
method: "POST",
|
|
2728
|
+
headers: {
|
|
2729
|
+
"Content-Type": "application/json",
|
|
2730
|
+
...paymaster.apiKey ? { "x-paymaster-api-key": paymaster.apiKey } : {}
|
|
2731
|
+
},
|
|
2732
|
+
body: JSON.stringify(body)
|
|
2733
|
+
});
|
|
2734
|
+
const json = await res.json();
|
|
2735
|
+
if (json.error) {
|
|
2736
|
+
throw new Error(`kit: paymaster passkey approval failed: ${JSON.stringify(json.error)}`);
|
|
2737
|
+
}
|
|
2738
|
+
return { transactionHash: json.result?.transaction_hash ?? json.result?.tracking_id };
|
|
2739
|
+
}
|
|
1546
2740
|
|
|
1547
2741
|
// src/auth/AuthProvider.ts
|
|
1548
2742
|
var StaticIdentity = class {
|
|
@@ -1690,27 +2884,122 @@ function bytesToChunks(bytes) {
|
|
|
1690
2884
|
for (const b of bytes.subarray(0, 31)) w = w << 8n | BigInt(b);
|
|
1691
2885
|
return w;
|
|
1692
2886
|
}
|
|
2887
|
+
var PasskeySigner = class {
|
|
2888
|
+
constructor(opts = {}) {
|
|
2889
|
+
if (typeof window === "undefined" || !navigator.credentials) {
|
|
2890
|
+
throw new Error("kit/passkey: WebAuthn is only available in a browser");
|
|
2891
|
+
}
|
|
2892
|
+
this.rpId = opts.rpId ?? window.location.hostname;
|
|
2893
|
+
this.rpName = opts.rpName ?? this.rpId;
|
|
2894
|
+
if (isIpAddress(this.rpId)) {
|
|
2895
|
+
throw new Error(
|
|
2896
|
+
`kit/passkey: passkeys can't use an IP address as the domain ("${this.rpId}"). Use http://localhost, a real HTTPS domain, or a tunnel (cloudflared/ngrok) \u2014 or pass an explicit \`rpId\`. (The silent device key works over an IP; passkeys don't.)`
|
|
2897
|
+
);
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
/** True if this platform advertises a usable passkey (platform authenticator). */
|
|
2901
|
+
static async isSupported() {
|
|
2902
|
+
if (typeof window === "undefined" || !window.PublicKeyCredential) return false;
|
|
2903
|
+
try {
|
|
2904
|
+
return await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
|
2905
|
+
} catch {
|
|
2906
|
+
return false;
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
/** Create a new synced passkey and return its P-256 public key. */
|
|
2910
|
+
async enroll(params) {
|
|
2911
|
+
const challenge = crypto.getRandomValues(new Uint8Array(32));
|
|
2912
|
+
const cred = await navigator.credentials.create({
|
|
2913
|
+
publicKey: {
|
|
2914
|
+
challenge: buf(challenge),
|
|
2915
|
+
rp: { id: this.rpId, name: this.rpName },
|
|
2916
|
+
user: {
|
|
2917
|
+
id: buf(userHandle(params.userId)),
|
|
2918
|
+
name: params.userName,
|
|
2919
|
+
displayName: params.displayName ?? params.userName
|
|
2920
|
+
},
|
|
2921
|
+
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
|
|
2922
|
+
// ES256 (P-256)
|
|
2923
|
+
authenticatorSelection: {
|
|
2924
|
+
residentKey: "required",
|
|
2925
|
+
requireResidentKey: true,
|
|
2926
|
+
userVerification: "preferred"
|
|
2927
|
+
},
|
|
2928
|
+
attestation: "none"
|
|
2929
|
+
}
|
|
2930
|
+
});
|
|
2931
|
+
if (!cred) throw new Error("kit/passkey: enrollment cancelled");
|
|
2932
|
+
const response = cred.response;
|
|
2933
|
+
const spki = new Uint8Array(response.getPublicKey());
|
|
2934
|
+
return { publicKey: spkiToPublicKey(spki), credentialId: new Uint8Array(cred.rawId) };
|
|
2935
|
+
}
|
|
2936
|
+
/**
|
|
2937
|
+
* Produce a WebAuthn assertion over `challenge` (a 32-byte value the caller
|
|
2938
|
+
* derives from the signer being added + the on-chain nonce). Uses discoverable
|
|
2939
|
+
* credentials — no `allowCredentials` — so it works on a brand-new browser.
|
|
2940
|
+
*/
|
|
2941
|
+
async assert(challenge) {
|
|
2942
|
+
const cred = await navigator.credentials.get({
|
|
2943
|
+
publicKey: {
|
|
2944
|
+
challenge: buf(challenge),
|
|
2945
|
+
rpId: this.rpId,
|
|
2946
|
+
allowCredentials: [],
|
|
2947
|
+
userVerification: "preferred"
|
|
2948
|
+
}
|
|
2949
|
+
});
|
|
2950
|
+
if (!cred) throw new Error("kit/passkey: assertion cancelled");
|
|
2951
|
+
const response = cred.response;
|
|
2952
|
+
const authenticatorData = new Uint8Array(response.authenticatorData);
|
|
2953
|
+
const clientDataJSON = new Uint8Array(response.clientDataJSON);
|
|
2954
|
+
const { r, s } = derToRs(new Uint8Array(response.signature));
|
|
2955
|
+
const challengeOffset = challengeOffsetOf(clientDataJSON, base64urlEncode(challenge));
|
|
2956
|
+
return { authenticatorData, clientDataJSON, r, s, challengeOffset };
|
|
2957
|
+
}
|
|
2958
|
+
};
|
|
2959
|
+
function isIpAddress(host) {
|
|
2960
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true;
|
|
2961
|
+
if (host.includes(":")) return true;
|
|
2962
|
+
return false;
|
|
2963
|
+
}
|
|
2964
|
+
function userHandle(userId) {
|
|
2965
|
+
const bytes = new TextEncoder().encode(userId);
|
|
2966
|
+
return bytes.length <= 64 ? bytes : sha256.sha256(bytes);
|
|
2967
|
+
}
|
|
2968
|
+
function buf(bytes) {
|
|
2969
|
+
return bytes.slice();
|
|
2970
|
+
}
|
|
1693
2971
|
|
|
1694
2972
|
exports.BackupSigner = BackupSigner;
|
|
1695
2973
|
exports.Cavos = Cavos;
|
|
1696
2974
|
exports.CavosAuth = CavosAuth;
|
|
1697
2975
|
exports.CavosSolana = CavosSolana;
|
|
2976
|
+
exports.CavosStellar = CavosStellar;
|
|
1698
2977
|
exports.DEVICE_ACCOUNT_CLASS_HASH = DEVICE_ACCOUNT_CLASS_HASH;
|
|
1699
2978
|
exports.DEVICE_ACCOUNT_PROGRAM_ID = DEVICE_ACCOUNT_PROGRAM_ID;
|
|
2979
|
+
exports.DEVICE_ACCOUNT_WASM_HASH = DEVICE_ACCOUNT_WASM_HASH;
|
|
2980
|
+
exports.FACTORY_CONTRACT_ID = FACTORY_CONTRACT_ID;
|
|
1700
2981
|
exports.HttpRecoveryClient = HttpRecoveryClient;
|
|
1701
2982
|
exports.HttpWalletRegistry = HttpWalletRegistry;
|
|
1702
2983
|
exports.InMemoryWalletRegistry = InMemoryWalletRegistry;
|
|
2984
|
+
exports.NATIVE_SAC_ID = NATIVE_SAC_ID;
|
|
2985
|
+
exports.PasskeySigner = PasskeySigner;
|
|
1703
2986
|
exports.SECP256R1_PROGRAM_ID = SECP256R1_PROGRAM_ID;
|
|
1704
2987
|
exports.SOLANA_NETWORKS = SOLANA_NETWORKS;
|
|
1705
2988
|
exports.STARKNET_NETWORKS = STARKNET_NETWORKS;
|
|
2989
|
+
exports.STELLAR_NETWORKS = STELLAR_NETWORKS;
|
|
1706
2990
|
exports.SolanaAdapter = SolanaAdapter;
|
|
1707
2991
|
exports.SolanaRelayer = SolanaRelayer;
|
|
1708
2992
|
exports.StarknetAdapter = StarknetAdapter;
|
|
1709
2993
|
exports.StarknetDeviceSigner = StarknetDeviceSigner;
|
|
1710
2994
|
exports.StaticIdentity = StaticIdentity;
|
|
2995
|
+
exports.StellarAdapter = StellarAdapter;
|
|
2996
|
+
exports.StellarRelayer = StellarRelayer;
|
|
1711
2997
|
exports.UDC_ADDRESS = UDC_ADDRESS;
|
|
1712
2998
|
exports.WebCryptoSigner = WebCryptoSigner;
|
|
1713
2999
|
exports.anchorDiscriminator = anchorDiscriminator;
|
|
3000
|
+
exports.approveDeviceEverywhere = approveDeviceEverywhere;
|
|
3001
|
+
exports.base64urlEncode = base64urlEncode;
|
|
3002
|
+
exports.batchChallenge = batchChallenge;
|
|
1714
3003
|
exports.bigIntTo32Bytes = bigIntTo32Bytes;
|
|
1715
3004
|
exports.buildSecp256r1Instruction = buildSecp256r1Instruction;
|
|
1716
3005
|
exports.bytesToBigInt = bytesToBigInt;
|
|
@@ -1718,13 +3007,20 @@ exports.bytesToHex = bytesToHex;
|
|
|
1718
3007
|
exports.compressedPubkey = compressedPubkey;
|
|
1719
3008
|
exports.deriveAddressSeed = deriveAddressSeed;
|
|
1720
3009
|
exports.deriveAddressSeedSolana = deriveAddressSeedSolana;
|
|
3010
|
+
exports.deriveAddressSeedStellar = deriveAddressSeedStellar;
|
|
1721
3011
|
exports.deriveBackupKey = deriveBackupKey;
|
|
3012
|
+
exports.deviceSignatureScVal = deviceSignatureScVal;
|
|
1722
3013
|
exports.encodeLowSSignature = encodeLowSSignature;
|
|
3014
|
+
exports.encodeStellarLowSSignature = encodeLowSSignature2;
|
|
1723
3015
|
exports.generateRecoveryCode = generateRecoveryCode;
|
|
1724
3016
|
exports.hexToBytes = hexToBytes;
|
|
3017
|
+
exports.lowS = lowS;
|
|
3018
|
+
exports.recoverCandidatePublicKeys = recoverCandidatePublicKeys;
|
|
1725
3019
|
exports.recoverYParity = recoverYParity;
|
|
3020
|
+
exports.sec1Pubkey = sec1Pubkey;
|
|
1726
3021
|
exports.serializeInstructions = serializeInstructions;
|
|
1727
3022
|
exports.signatureToFelts = signatureToFelts;
|
|
1728
3023
|
exports.u256ToFelts = u256ToFelts;
|
|
3024
|
+
exports.webauthnDigest = webauthnDigest;
|
|
1729
3025
|
//# sourceMappingURL=index.js.map
|
|
1730
3026
|
//# sourceMappingURL=index.js.map
|