@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/react/index.js
CHANGED
|
@@ -8,6 +8,7 @@ var web3_js = require('@solana/web3.js');
|
|
|
8
8
|
var hkdf = require('@noble/hashes/hkdf');
|
|
9
9
|
var pbkdf2 = require('@noble/hashes/pbkdf2');
|
|
10
10
|
var utils = require('@noble/hashes/utils');
|
|
11
|
+
var stellarSdk = require('@stellar/stellar-sdk');
|
|
11
12
|
var jsxRuntime = require('react/jsx-runtime');
|
|
12
13
|
|
|
13
14
|
// src/react/CavosProvider.tsx
|
|
@@ -22,6 +23,18 @@ function bytesToBigInt(bytes) {
|
|
|
22
23
|
for (const b of bytes) out = out << 8n | BigInt(b);
|
|
23
24
|
return out;
|
|
24
25
|
}
|
|
26
|
+
function bytesToByteArrayCalldata(bytes) {
|
|
27
|
+
const CHUNK = 31;
|
|
28
|
+
const fullCount = Math.floor(bytes.length / CHUNK);
|
|
29
|
+
const out = [String(fullCount)];
|
|
30
|
+
for (let i = 0; i < fullCount; i++) {
|
|
31
|
+
out.push("0x" + bytesToBigInt(bytes.subarray(i * CHUNK, i * CHUNK + CHUNK)).toString(16));
|
|
32
|
+
}
|
|
33
|
+
const rem = bytes.subarray(fullCount * CHUNK);
|
|
34
|
+
out.push("0x" + (rem.length ? bytesToBigInt(rem).toString(16) : "0"));
|
|
35
|
+
out.push(String(rem.length));
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
25
38
|
function bigIntTo32Bytes(value) {
|
|
26
39
|
const out = new Uint8Array(32);
|
|
27
40
|
let v = value;
|
|
@@ -159,7 +172,7 @@ var CAVOS_PAYMASTER_URL = {
|
|
|
159
172
|
mainnet: "https://paymaster.cavos.xyz"
|
|
160
173
|
};
|
|
161
174
|
var DEVICE_ACCOUNT_CLASS_HASH = {
|
|
162
|
-
sepolia: "
|
|
175
|
+
sepolia: "0x25cbc5423e8ee895febb0ef2c3945b408da44d0039d915fbdd681fe6b6ba66b",
|
|
163
176
|
mainnet: "0x1840aded59e8a0d2b440a134cb9079a7fc11b06c77f58ed189ab436a034ca6a"
|
|
164
177
|
};
|
|
165
178
|
|
|
@@ -223,6 +236,84 @@ var StarknetAdapter = class {
|
|
|
223
236
|
const sig = await this.opts.signer.sign(bigIntTo32Bytes(txHash));
|
|
224
237
|
return signatureToFelts(sig).map((f) => starknet.num.toHex(f));
|
|
225
238
|
}
|
|
239
|
+
// --- passkey approvers ---
|
|
240
|
+
buildAddApprover(accountAddress, passkey) {
|
|
241
|
+
return { contractAddress: accountAddress, entrypoint: "add_approver", calldata: pubkeyCalldata(passkey) };
|
|
242
|
+
}
|
|
243
|
+
buildRemoveApprover(accountAddress, passkey) {
|
|
244
|
+
return { contractAddress: accountAddress, entrypoint: "remove_approver", calldata: pubkeyCalldata(passkey) };
|
|
245
|
+
}
|
|
246
|
+
async isApprover(accountAddress, passkey) {
|
|
247
|
+
if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
|
|
248
|
+
const res = await this.opts.provider.callContract({
|
|
249
|
+
contractAddress: accountAddress,
|
|
250
|
+
entrypoint: "is_approver",
|
|
251
|
+
calldata: pubkeyCalldata(passkey)
|
|
252
|
+
});
|
|
253
|
+
return BigInt(res[0] ?? 0) !== 0n;
|
|
254
|
+
}
|
|
255
|
+
/** True if the account has at least one passkey registered as an approver.
|
|
256
|
+
* Lets the UI decide whether to offer passkey approval before prompting. */
|
|
257
|
+
async hasPasskeyApprover(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_approver_count",
|
|
262
|
+
calldata: []
|
|
263
|
+
});
|
|
264
|
+
return BigInt(res[0] ?? 0) > 0n;
|
|
265
|
+
}
|
|
266
|
+
async getPasskeyNonce(accountAddress) {
|
|
267
|
+
if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
|
|
268
|
+
const res = await this.opts.provider.callContract({
|
|
269
|
+
contractAddress: accountAddress,
|
|
270
|
+
entrypoint: "get_passkey_nonce",
|
|
271
|
+
calldata: []
|
|
272
|
+
});
|
|
273
|
+
return BigInt(res[0] ?? 0);
|
|
274
|
+
}
|
|
275
|
+
/** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
|
|
276
|
+
* `sha256(new_x || new_y || nonce)` (coords 32B BE, nonce 16B BE). The batch
|
|
277
|
+
* challenge the passkey signs is `sha256(concat(leaves))` across chains. */
|
|
278
|
+
passkeyLeaf(newSigner, nonce) {
|
|
279
|
+
const msg = new Uint8Array(32 + 32 + 16);
|
|
280
|
+
msg.set(bigIntTo32Bytes(newSigner.x), 0);
|
|
281
|
+
msg.set(bigIntTo32Bytes(newSigner.y), 32);
|
|
282
|
+
msg.set(bigIntTo32Bytes(nonce).subarray(16), 64);
|
|
283
|
+
return sha256.sha256(msg);
|
|
284
|
+
}
|
|
285
|
+
/** Passkey-authorized `add_signer` call. `leaves`/`leafIndex` place this chain's
|
|
286
|
+
* leaf in the multi-chain batch (single chain → `[leaf]`, index 0). `yParity`
|
|
287
|
+
* matches the raw `(r, s)` — the contract normalizes high-S internally. */
|
|
288
|
+
buildAddSignerViaPasskey(accountAddress, newSigner, nonce, leaves, leafIndex, assertion, yParity) {
|
|
289
|
+
const [rl, rh] = u256ToFelts(assertion.r);
|
|
290
|
+
const [sl, sh] = u256ToFelts(assertion.s);
|
|
291
|
+
const leavesCalldata = [String(leaves.length)];
|
|
292
|
+
for (const leaf of leaves) {
|
|
293
|
+
const [lo, hi] = u256ToFelts(bytesToBigInt(leaf));
|
|
294
|
+
leavesCalldata.push(starknet.num.toHex(lo), starknet.num.toHex(hi));
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
contractAddress: accountAddress,
|
|
298
|
+
entrypoint: "add_signer_via_passkey",
|
|
299
|
+
calldata: [
|
|
300
|
+
...pubkeyCalldata(newSigner),
|
|
301
|
+
// new_x, new_y (u256 pairs)
|
|
302
|
+
starknet.num.toHex(nonce),
|
|
303
|
+
...leavesCalldata,
|
|
304
|
+
// Array<u256> leaves
|
|
305
|
+
String(leafIndex),
|
|
306
|
+
...bytesToByteArrayCalldata(assertion.authenticatorData),
|
|
307
|
+
...bytesToByteArrayCalldata(assertion.clientDataJSON),
|
|
308
|
+
String(assertion.challengeOffset),
|
|
309
|
+
starknet.num.toHex(rl),
|
|
310
|
+
starknet.num.toHex(rh),
|
|
311
|
+
starknet.num.toHex(sl),
|
|
312
|
+
starknet.num.toHex(sh),
|
|
313
|
+
yParity ? "0x1" : "0x0"
|
|
314
|
+
]
|
|
315
|
+
};
|
|
316
|
+
}
|
|
226
317
|
};
|
|
227
318
|
function pubkeyCalldata(pk) {
|
|
228
319
|
const [xl, xh] = u256ToFelts(pk.x);
|
|
@@ -316,6 +407,9 @@ function deriveAddressSeed({ userId, appSalt }) {
|
|
|
316
407
|
function deriveAddressSeedSolana({ userId, appSalt }) {
|
|
317
408
|
return sha256.sha256(new TextEncoder().encode(`cavos:solana:v1:${userId}:${appSalt}`));
|
|
318
409
|
}
|
|
410
|
+
function deriveAddressSeedStellar({ userId, appSalt }) {
|
|
411
|
+
return sha256.sha256(new TextEncoder().encode(`cavos:stellar:v1:${userId}:${appSalt}`));
|
|
412
|
+
}
|
|
319
413
|
function feltFromString(s) {
|
|
320
414
|
const bytes = new TextEncoder().encode(s);
|
|
321
415
|
const chunks = [];
|
|
@@ -337,6 +431,8 @@ var DOMAIN_ADD = "cavos:add_signer:v1";
|
|
|
337
431
|
var DOMAIN_REMOVE = "cavos:remove_signer:v1";
|
|
338
432
|
var DOMAIN_TRANSFER = "cavos:transfer:v1";
|
|
339
433
|
var DOMAIN_EXECUTE = "cavos:execute:v1";
|
|
434
|
+
var DOMAIN_ADD_APPROVER = "cavos:add_approver:v1";
|
|
435
|
+
var DOMAIN_REMOVE_APPROVER = "cavos:remove_approver:v1";
|
|
340
436
|
var SECP256R1_N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
|
|
341
437
|
var SOLANA_NETWORKS = {
|
|
342
438
|
"solana-devnet": "https://api.devnet.solana.com",
|
|
@@ -429,6 +525,104 @@ var SolanaAdapter = class {
|
|
|
429
525
|
});
|
|
430
526
|
return [precompileIx, ix];
|
|
431
527
|
}
|
|
528
|
+
/** `[precompile, add_approver]` bundle enrolling a passkey approver (device-signed). */
|
|
529
|
+
async buildAddApprover(account, passkey) {
|
|
530
|
+
const accountPk = new web3_js.PublicKey(account);
|
|
531
|
+
const compressed = compressedPubkey(passkey);
|
|
532
|
+
const nonce = await this.fetchNonce(accountPk);
|
|
533
|
+
const message = concatBytes(
|
|
534
|
+
Buffer.from(DOMAIN_ADD_APPROVER),
|
|
535
|
+
accountPk.toBuffer(),
|
|
536
|
+
compressed,
|
|
537
|
+
u64le(nonce)
|
|
538
|
+
);
|
|
539
|
+
const { precompileIx } = await this.signToPrecompile(message);
|
|
540
|
+
const ix = new web3_js.TransactionInstruction({
|
|
541
|
+
programId: this.programId,
|
|
542
|
+
keys: this.guardedKeys(accountPk),
|
|
543
|
+
data: Buffer.concat([anchorDiscriminator("add_approver"), Buffer.from(compressed)])
|
|
544
|
+
});
|
|
545
|
+
return [precompileIx, ix];
|
|
546
|
+
}
|
|
547
|
+
/** `[precompile, remove_approver]` bundle (device-signed). */
|
|
548
|
+
async buildRemoveApprover(account, passkey) {
|
|
549
|
+
const accountPk = new web3_js.PublicKey(account);
|
|
550
|
+
const compressed = compressedPubkey(passkey);
|
|
551
|
+
const nonce = await this.fetchNonce(accountPk);
|
|
552
|
+
const message = concatBytes(
|
|
553
|
+
Buffer.from(DOMAIN_REMOVE_APPROVER),
|
|
554
|
+
accountPk.toBuffer(),
|
|
555
|
+
compressed,
|
|
556
|
+
u64le(nonce)
|
|
557
|
+
);
|
|
558
|
+
const { precompileIx } = await this.signToPrecompile(message);
|
|
559
|
+
const ix = new web3_js.TransactionInstruction({
|
|
560
|
+
programId: this.programId,
|
|
561
|
+
keys: this.guardedKeys(accountPk),
|
|
562
|
+
data: Buffer.concat([anchorDiscriminator("remove_approver"), Buffer.from(compressed)])
|
|
563
|
+
});
|
|
564
|
+
return [precompileIx, ix];
|
|
565
|
+
}
|
|
566
|
+
/** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
|
|
567
|
+
* `sha256(compressed(new_signer) || passkey_nonce_le8)`. The batch challenge the
|
|
568
|
+
* passkey signs is `sha256(concat(leaves))` across chains. */
|
|
569
|
+
passkeyLeaf(newSigner, nonce) {
|
|
570
|
+
return sha256.sha256(concatBytes(compressedPubkey(newSigner), u64le(nonce)));
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* `[precompile(passkey), add_signer_via_passkey]` bundle. The precompile ix
|
|
574
|
+
* verifies the PASSKEY's WebAuthn assertion over `authData || sha256(clientDataJSON)`;
|
|
575
|
+
* the program ix binds the challenge to `newSigner` + the passkey nonce and adds
|
|
576
|
+
* the signer. No device signature — a gasless relayer can submit it.
|
|
577
|
+
*/
|
|
578
|
+
buildAddSignerViaPasskey(account, newSigner, passkey, leaves, leafIndex, assertion) {
|
|
579
|
+
const accountPk = new web3_js.PublicKey(account);
|
|
580
|
+
const newCompressed = compressedPubkey(newSigner);
|
|
581
|
+
const passkeyCompressed = compressedPubkey(passkey);
|
|
582
|
+
const clientHash = sha256.sha256(assertion.clientDataJSON);
|
|
583
|
+
const message = concatBytes(assertion.authenticatorData, clientHash);
|
|
584
|
+
const signature = encodeLowSSignature(assertion.r, assertion.s);
|
|
585
|
+
const precompileIx = buildSecp256r1Instruction(passkeyCompressed, signature, message);
|
|
586
|
+
const leavesBlob = Buffer.concat([u32le(leaves.length), ...leaves.map((l) => Buffer.from(l))]);
|
|
587
|
+
const data = Buffer.concat([
|
|
588
|
+
anchorDiscriminator("add_signer_via_passkey"),
|
|
589
|
+
Buffer.from(newCompressed),
|
|
590
|
+
leavesBlob,
|
|
591
|
+
u32le(leafIndex),
|
|
592
|
+
serializeVecU8(assertion.authenticatorData),
|
|
593
|
+
serializeVecU8(assertion.clientDataJSON),
|
|
594
|
+
u32le(assertion.challengeOffset)
|
|
595
|
+
]);
|
|
596
|
+
const ix = new web3_js.TransactionInstruction({
|
|
597
|
+
programId: this.programId,
|
|
598
|
+
keys: this.guardedKeys(accountPk),
|
|
599
|
+
data
|
|
600
|
+
});
|
|
601
|
+
return [precompileIx, ix];
|
|
602
|
+
}
|
|
603
|
+
/** Read whether `passkey` is a registered approver. */
|
|
604
|
+
/** True if the account has at least one passkey registered as an approver. */
|
|
605
|
+
async hasPasskeyApprover(account) {
|
|
606
|
+
const approvers = await this.fetchApprovers(new web3_js.PublicKey(account));
|
|
607
|
+
return approvers.length > 0;
|
|
608
|
+
}
|
|
609
|
+
async isApprover(account, passkey) {
|
|
610
|
+
const approvers = await this.fetchApprovers(new web3_js.PublicKey(account));
|
|
611
|
+
const target = Buffer.from(compressedPubkey(passkey)).toString("hex");
|
|
612
|
+
return approvers.some((a) => Buffer.from(a).toString("hex") === target);
|
|
613
|
+
}
|
|
614
|
+
/** Read the current passkey-approval nonce. */
|
|
615
|
+
async passkeyNonce(account) {
|
|
616
|
+
const info = await this.requireConnection().getAccountInfo(new web3_js.PublicKey(account));
|
|
617
|
+
if (!info) return 0n;
|
|
618
|
+
const d = info.data;
|
|
619
|
+
const signersLenOff = 8 + 32 + 1 + 8 + COMPRESSED_PUBKEY_SIZE;
|
|
620
|
+
const signerCount = d.readUInt32LE(signersLenOff);
|
|
621
|
+
const approversLenOff = signersLenOff + 4 + signerCount * COMPRESSED_PUBKEY_SIZE;
|
|
622
|
+
const approverCount = d.readUInt32LE(approversLenOff);
|
|
623
|
+
const passkeyNonceOff = approversLenOff + 4 + approverCount * COMPRESSED_PUBKEY_SIZE;
|
|
624
|
+
return readU64le(d, passkeyNonceOff);
|
|
625
|
+
}
|
|
432
626
|
/** `[precompile, execute_transfer]` bundle moving lamports out of the account. */
|
|
433
627
|
async buildExecuteTransfer(account, destination, amount) {
|
|
434
628
|
const accountPk = new web3_js.PublicKey(account);
|
|
@@ -550,6 +744,22 @@ var SolanaAdapter = class {
|
|
|
550
744
|
}
|
|
551
745
|
return out;
|
|
552
746
|
}
|
|
747
|
+
async fetchApprovers(account) {
|
|
748
|
+
const info = await this.requireConnection().getAccountInfo(account);
|
|
749
|
+
if (!info) return [];
|
|
750
|
+
const d = info.data;
|
|
751
|
+
const signersLenOff = 8 + 32 + 1 + 8 + COMPRESSED_PUBKEY_SIZE;
|
|
752
|
+
const signerCount = d.readUInt32LE(signersLenOff);
|
|
753
|
+
const approversLenOff = signersLenOff + 4 + signerCount * COMPRESSED_PUBKEY_SIZE;
|
|
754
|
+
const count = d.readUInt32LE(approversLenOff);
|
|
755
|
+
const out = [];
|
|
756
|
+
let off = approversLenOff + 4;
|
|
757
|
+
for (let i = 0; i < count; i++) {
|
|
758
|
+
out.push(Uint8Array.from(d.subarray(off, off + COMPRESSED_PUBKEY_SIZE)));
|
|
759
|
+
off += COMPRESSED_PUBKEY_SIZE;
|
|
760
|
+
}
|
|
761
|
+
return out;
|
|
762
|
+
}
|
|
553
763
|
requireConnection() {
|
|
554
764
|
if (!this.opts.connection) throw new Error("kit/solana: connection required for reads");
|
|
555
765
|
return this.opts.connection;
|
|
@@ -604,13 +814,18 @@ function buildSecp256r1Instruction(compressed, signature, message) {
|
|
|
604
814
|
function anchorDiscriminator(name) {
|
|
605
815
|
return Buffer.from(sha256.sha256(`global:${name}`).slice(0, 8));
|
|
606
816
|
}
|
|
817
|
+
function u32le(n) {
|
|
818
|
+
const b = Buffer.alloc(4);
|
|
819
|
+
b.writeUInt32LE(n);
|
|
820
|
+
return b;
|
|
821
|
+
}
|
|
607
822
|
function u64le(n) {
|
|
608
823
|
const b = Buffer.alloc(8);
|
|
609
824
|
new DataView(b.buffer, b.byteOffset, 8).setBigUint64(0, BigInt(n), true);
|
|
610
825
|
return b;
|
|
611
826
|
}
|
|
612
|
-
function readU64le(
|
|
613
|
-
return new DataView(
|
|
827
|
+
function readU64le(buf2, offset) {
|
|
828
|
+
return new DataView(buf2.buffer, buf2.byteOffset, buf2.length).getBigUint64(
|
|
614
829
|
offset,
|
|
615
830
|
true
|
|
616
831
|
);
|
|
@@ -998,6 +1213,73 @@ var WORDLIST = [
|
|
|
998
1213
|
"beach",
|
|
999
1214
|
"dusk"
|
|
1000
1215
|
];
|
|
1216
|
+
function base64urlEncode(bytes) {
|
|
1217
|
+
let bin = "";
|
|
1218
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
1219
|
+
const b64 = typeof btoa !== "undefined" ? btoa(bin) : Buffer.from(bytes).toString("base64");
|
|
1220
|
+
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1221
|
+
}
|
|
1222
|
+
function derToRs(der) {
|
|
1223
|
+
let i = 0;
|
|
1224
|
+
if (der[i++] !== 48) throw new Error("kit/webauthn: bad DER (no SEQUENCE)");
|
|
1225
|
+
if (der[i] & 128) i += 1 + (der[i] & 127);
|
|
1226
|
+
else i += 1;
|
|
1227
|
+
if (der[i++] !== 2) throw new Error("kit/webauthn: bad DER (no r INTEGER)");
|
|
1228
|
+
const rlen = der[i++];
|
|
1229
|
+
const r = bytesToBigInt(der.subarray(i, i + rlen));
|
|
1230
|
+
i += rlen;
|
|
1231
|
+
if (der[i++] !== 2) throw new Error("kit/webauthn: bad DER (no s INTEGER)");
|
|
1232
|
+
const slen = der[i++];
|
|
1233
|
+
const s = bytesToBigInt(der.subarray(i, i + slen));
|
|
1234
|
+
return { r, s };
|
|
1235
|
+
}
|
|
1236
|
+
function spkiToPublicKey(spki) {
|
|
1237
|
+
const idx = spki.lastIndexOf(4, spki.length - 65);
|
|
1238
|
+
const start = spki.length - 65;
|
|
1239
|
+
const prefix = spki[start];
|
|
1240
|
+
if (prefix !== 4) {
|
|
1241
|
+
if (idx < 0) throw new Error("kit/webauthn: no uncompressed EC point in SPKI");
|
|
1242
|
+
return { x: bytesToBigInt(spki.subarray(idx + 1, idx + 33)), y: bytesToBigInt(spki.subarray(idx + 33, idx + 65)) };
|
|
1243
|
+
}
|
|
1244
|
+
return {
|
|
1245
|
+
x: bytesToBigInt(spki.subarray(start + 1, start + 33)),
|
|
1246
|
+
y: bytesToBigInt(spki.subarray(start + 33, start + 65))
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
function batchChallenge(leaves) {
|
|
1250
|
+
const total = leaves.reduce((n, l) => n + l.length, 0);
|
|
1251
|
+
const cat = new Uint8Array(total);
|
|
1252
|
+
let o = 0;
|
|
1253
|
+
for (const l of leaves) {
|
|
1254
|
+
cat.set(l, o);
|
|
1255
|
+
o += l.length;
|
|
1256
|
+
}
|
|
1257
|
+
return sha256.sha256(cat);
|
|
1258
|
+
}
|
|
1259
|
+
function webauthnDigest(authenticatorData, clientDataJSON) {
|
|
1260
|
+
const clientHash = sha256.sha256(clientDataJSON);
|
|
1261
|
+
const msg = new Uint8Array(authenticatorData.length + clientHash.length);
|
|
1262
|
+
msg.set(authenticatorData, 0);
|
|
1263
|
+
msg.set(clientHash, authenticatorData.length);
|
|
1264
|
+
return sha256.sha256(msg);
|
|
1265
|
+
}
|
|
1266
|
+
function recoverCandidatePublicKeys(r, s, digest) {
|
|
1267
|
+
const out = [];
|
|
1268
|
+
for (const bit of [0, 1]) {
|
|
1269
|
+
try {
|
|
1270
|
+
const point = new p256.p256.Signature(r, s).addRecoveryBit(bit).recoverPublicKey(digest).toAffine();
|
|
1271
|
+
out.push({ publicKey: { x: point.x, y: point.y }, yParity: bit === 1 });
|
|
1272
|
+
} catch {
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
return out;
|
|
1276
|
+
}
|
|
1277
|
+
function challengeOffsetOf(clientDataJSON, challengeB64) {
|
|
1278
|
+
const text = new TextDecoder().decode(clientDataJSON);
|
|
1279
|
+
const idx = text.indexOf(challengeB64);
|
|
1280
|
+
if (idx < 0) throw new Error("kit/webauthn: challenge not found in clientDataJSON");
|
|
1281
|
+
return idx;
|
|
1282
|
+
}
|
|
1001
1283
|
|
|
1002
1284
|
// src/chains/solana/CavosSolana.ts
|
|
1003
1285
|
var CavosSolana = class _CavosSolana {
|
|
@@ -1012,6 +1294,8 @@ var CavosSolana = class _CavosSolana {
|
|
|
1012
1294
|
this.feePayer = feePayer;
|
|
1013
1295
|
/** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
|
|
1014
1296
|
this.chain = "solana";
|
|
1297
|
+
/** True when this connect just created a brand-new account (first sign-up). */
|
|
1298
|
+
this.isNewAccount = false;
|
|
1015
1299
|
}
|
|
1016
1300
|
get publicKey() {
|
|
1017
1301
|
return this.devicePubkey;
|
|
@@ -1062,7 +1346,7 @@ var CavosSolana = class _CavosSolana {
|
|
|
1062
1346
|
}
|
|
1063
1347
|
await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
|
|
1064
1348
|
const isSigner = await adapter.isAuthorizedSigner(address, devicePubkey);
|
|
1065
|
-
|
|
1349
|
+
const wallet = new _CavosSolana(
|
|
1066
1350
|
identity,
|
|
1067
1351
|
address,
|
|
1068
1352
|
isSigner ? "ready" : "needs-device-approval",
|
|
@@ -1072,12 +1356,87 @@ var CavosSolana = class _CavosSolana {
|
|
|
1072
1356
|
relayer,
|
|
1073
1357
|
opts.feePayer
|
|
1074
1358
|
);
|
|
1359
|
+
wallet.isNewAccount = !deployed && isSigner;
|
|
1360
|
+
return wallet;
|
|
1075
1361
|
}
|
|
1076
1362
|
/** Authorize an additional device signer (device-signed via precompile). */
|
|
1077
1363
|
async addSigner(pubkey) {
|
|
1078
1364
|
const ixs = await this.adapter.buildAddSigner(this.address, pubkey);
|
|
1079
1365
|
return this.send(ixs);
|
|
1080
1366
|
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Enroll a passkey as an approver (2FA-style step-up). Device-signed + gasless;
|
|
1369
|
+
* requires a ready device. Idempotent. Returns the passkey pubkey + tx hash.
|
|
1370
|
+
*/
|
|
1371
|
+
async enrollPasskey(passkey, params) {
|
|
1372
|
+
const enrolled = await passkey.enroll(params);
|
|
1373
|
+
const { transactionHash } = await this.addApprover(enrolled.publicKey);
|
|
1374
|
+
return { publicKey: enrolled.publicKey, transactionHash };
|
|
1375
|
+
}
|
|
1376
|
+
/** Register an already-enrolled passkey pubkey as an approver (gasless).
|
|
1377
|
+
* Idempotent. Lets one passkey be registered across chains without re-prompting. */
|
|
1378
|
+
async addApprover(pubkey) {
|
|
1379
|
+
if (this.status !== "ready") {
|
|
1380
|
+
throw new Error("kit/solana: addApprover requires a ready, authorized device");
|
|
1381
|
+
}
|
|
1382
|
+
if (await this.adapter.isApprover(this.address, pubkey)) return {};
|
|
1383
|
+
const ixs = await this.adapter.buildAddApprover(this.address, pubkey);
|
|
1384
|
+
const transactionHash = await this.send(ixs);
|
|
1385
|
+
return { transactionHash };
|
|
1386
|
+
}
|
|
1387
|
+
/** True if this account already has a passkey enrolled as an approver, so a
|
|
1388
|
+
* new device can be approved with the passkey instead of the email flow. */
|
|
1389
|
+
async hasPasskey() {
|
|
1390
|
+
return this.adapter.hasPasskeyApprover(this.address);
|
|
1391
|
+
}
|
|
1392
|
+
/** Re-read (from chain) whether THIS device is now an authorized signer.
|
|
1393
|
+
* Used to poll for readiness after a passkey approval before it's indexed. */
|
|
1394
|
+
async isReady() {
|
|
1395
|
+
return this.adapter.isAuthorizedSigner(this.address, this.devicePubkey);
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* From a fresh browser (status `needs-device-approval`), approve adding THIS
|
|
1399
|
+
* device with the user's synced passkey. Gasless via the relayer — the bundle
|
|
1400
|
+
* carries the passkey's WebAuthn assertion, so no device signature is needed.
|
|
1401
|
+
*/
|
|
1402
|
+
async approveThisDeviceWithPasskey(passkey) {
|
|
1403
|
+
if (this.status === "ready") {
|
|
1404
|
+
throw new Error("kit/solana: this device is already an authorized signer");
|
|
1405
|
+
}
|
|
1406
|
+
const { leaf, nonce } = await this.passkeyLeafForThisDevice();
|
|
1407
|
+
const leaves = [leaf];
|
|
1408
|
+
const assertion = await passkey.assert(batchChallenge(leaves));
|
|
1409
|
+
const { transactionHash } = await this.submitPasskeyApproval(assertion, leaves, 0, nonce);
|
|
1410
|
+
return transactionHash;
|
|
1411
|
+
}
|
|
1412
|
+
/** This device's leaf + passkey nonce for a (possibly multi-chain) batch. */
|
|
1413
|
+
async passkeyLeafForThisDevice() {
|
|
1414
|
+
const nonce = await this.adapter.passkeyNonce(this.address);
|
|
1415
|
+
return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
|
|
1416
|
+
}
|
|
1417
|
+
/** Submit `add_signer_via_passkey` given a shared assertion + batch position.
|
|
1418
|
+
* Used by `approveThisDeviceWithPasskey` and `approveDeviceEverywhere`. */
|
|
1419
|
+
async submitPasskeyApproval(assertion, leaves, leafIndex, _nonce) {
|
|
1420
|
+
const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
|
|
1421
|
+
const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
|
|
1422
|
+
let approver = null;
|
|
1423
|
+
for (const cand of candidates) {
|
|
1424
|
+
if (await this.adapter.isApprover(this.address, cand.publicKey)) {
|
|
1425
|
+
approver = cand.publicKey;
|
|
1426
|
+
break;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
if (!approver) throw new Error("kit/solana: this passkey is not a registered approver");
|
|
1430
|
+
const ixs = this.adapter.buildAddSignerViaPasskey(
|
|
1431
|
+
this.address,
|
|
1432
|
+
this.devicePubkey,
|
|
1433
|
+
approver,
|
|
1434
|
+
leaves,
|
|
1435
|
+
leafIndex,
|
|
1436
|
+
assertion
|
|
1437
|
+
);
|
|
1438
|
+
return { transactionHash: await this.send(ixs) };
|
|
1439
|
+
}
|
|
1081
1440
|
/** Move `amount` lamports out of the account to `destination` (device-signed). */
|
|
1082
1441
|
async execute(amount, destination) {
|
|
1083
1442
|
if (this.status !== "ready") {
|
|
@@ -1190,6 +1549,651 @@ var CavosSolana = class _CavosSolana {
|
|
|
1190
1549
|
};
|
|
1191
1550
|
var defaultRegistry = new InMemoryWalletRegistry();
|
|
1192
1551
|
|
|
1552
|
+
// src/chains/stellar/constants.ts
|
|
1553
|
+
var FACTORY_CONTRACT_ID = {
|
|
1554
|
+
// Re-deployed 2026-07-01 with the passkey-approval device-account wasm (batched
|
|
1555
|
+
// multi-chain challenge). The factory pins the wasm hash immutably, so a new
|
|
1556
|
+
// wasm needs a new factory → new account addresses; testnet has no prod wallets.
|
|
1557
|
+
"stellar-testnet": "CBCJIODXIEBOXXD66KCUCF7ZDYJARKI4ZIVQOVWPULOBH5XGNCDP6W3I",
|
|
1558
|
+
// Set once the factory is deployed to mainnet (its address differs — network id
|
|
1559
|
+
// is part of contract-address derivation).
|
|
1560
|
+
"stellar-mainnet": ""
|
|
1561
|
+
};
|
|
1562
|
+
var STELLAR_NETWORKS = {
|
|
1563
|
+
"stellar-testnet": {
|
|
1564
|
+
rpcUrl: "https://soroban-testnet.stellar.org",
|
|
1565
|
+
passphrase: "Test SDF Network ; September 2015"
|
|
1566
|
+
},
|
|
1567
|
+
"stellar-mainnet": {
|
|
1568
|
+
rpcUrl: "https://soroban-rpc.mainnet.stellar.gateway.fm",
|
|
1569
|
+
passphrase: "Public Global Stellar Network ; September 2015"
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
var NATIVE_SAC_ID = {
|
|
1573
|
+
"stellar-testnet": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
|
|
1574
|
+
"stellar-mainnet": "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA"
|
|
1575
|
+
};
|
|
1576
|
+
|
|
1577
|
+
// src/chains/stellar/StellarAdapter.ts
|
|
1578
|
+
var SECP256R1_N2 = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
|
|
1579
|
+
var StellarAdapter = class {
|
|
1580
|
+
constructor(opts) {
|
|
1581
|
+
this.chain = "stellar";
|
|
1582
|
+
this.network = opts.network;
|
|
1583
|
+
this.passphrase = STELLAR_NETWORKS[opts.network].passphrase;
|
|
1584
|
+
this.rpcUrl = opts.rpcUrl ?? STELLAR_NETWORKS[opts.network].rpcUrl;
|
|
1585
|
+
this.factoryId = opts.factoryId ?? FACTORY_CONTRACT_ID[opts.network];
|
|
1586
|
+
if (!this.factoryId) {
|
|
1587
|
+
throw new Error(`kit/stellar: no factory contract id configured for ${opts.network}`);
|
|
1588
|
+
}
|
|
1589
|
+
this.signer = opts.signer;
|
|
1590
|
+
}
|
|
1591
|
+
server() {
|
|
1592
|
+
if (!this._server) {
|
|
1593
|
+
this._server = new stellarSdk.rpc.Server(this.rpcUrl, {
|
|
1594
|
+
allowHttp: this.rpcUrl.startsWith("http://")
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
return this._server;
|
|
1598
|
+
}
|
|
1599
|
+
networkId() {
|
|
1600
|
+
return stellarSdk.hash(Buffer.from(this.passphrase));
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Deterministic account address for `(addressSeed, initialSigner)` — computed
|
|
1604
|
+
* off-chain, byte-identical to the factory's on-chain `account_address`.
|
|
1605
|
+
* `contractId = sha256(HashIdPreimage(networkId, factory, salt))` with
|
|
1606
|
+
* `salt = sha256(addressSeed || sec1(initialSigner))`.
|
|
1607
|
+
*/
|
|
1608
|
+
computeAddress(addressSeed, initialSigner) {
|
|
1609
|
+
const salt = this.accountSalt(addressSeed, initialSigner);
|
|
1610
|
+
const preimage = stellarSdk.xdr.HashIdPreimage.envelopeTypeContractId(
|
|
1611
|
+
new stellarSdk.xdr.HashIdPreimageContractId({
|
|
1612
|
+
networkId: this.networkId(),
|
|
1613
|
+
contractIdPreimage: stellarSdk.xdr.ContractIdPreimage.contractIdPreimageFromAddress(
|
|
1614
|
+
new stellarSdk.xdr.ContractIdPreimageFromAddress({
|
|
1615
|
+
address: new stellarSdk.Address(this.factoryId).toScAddress(),
|
|
1616
|
+
salt
|
|
1617
|
+
})
|
|
1618
|
+
)
|
|
1619
|
+
})
|
|
1620
|
+
);
|
|
1621
|
+
return stellarSdk.StrKey.encodeContract(stellarSdk.hash(preimage.toXDR()));
|
|
1622
|
+
}
|
|
1623
|
+
/** `salt = sha256(addressSeed(32) || sec1(initialSigner)(65))` — matches the factory. */
|
|
1624
|
+
accountSalt(addressSeed, initialSigner) {
|
|
1625
|
+
return stellarSdk.hash(Buffer.concat([Buffer.from(addressSeed), Buffer.from(sec1Pubkey(initialSigner))]));
|
|
1626
|
+
}
|
|
1627
|
+
/** Host function: `factory.deploy(address_seed, initial_signer)`. */
|
|
1628
|
+
buildDeploy(addressSeed, initialSigner) {
|
|
1629
|
+
return invokeFunc(this.factoryId, "deploy", [
|
|
1630
|
+
bytesScVal(addressSeed),
|
|
1631
|
+
bytesScVal(sec1Pubkey(initialSigner))
|
|
1632
|
+
]);
|
|
1633
|
+
}
|
|
1634
|
+
/** Host function: `account.add_signer(new_signer)` (requires device auth). */
|
|
1635
|
+
buildAddSigner(accountAddress, signer) {
|
|
1636
|
+
return invokeFunc(accountAddress, "add_signer", [bytesScVal(sec1Pubkey(signer))]);
|
|
1637
|
+
}
|
|
1638
|
+
/** Host function: `account.remove_signer(signer)` (requires device auth). */
|
|
1639
|
+
buildRemoveSigner(accountAddress, signer) {
|
|
1640
|
+
return invokeFunc(accountAddress, "remove_signer", [bytesScVal(sec1Pubkey(signer))]);
|
|
1641
|
+
}
|
|
1642
|
+
/** Host function: `account.add_approver(passkey)` (requires device auth). */
|
|
1643
|
+
buildAddApprover(accountAddress, passkey) {
|
|
1644
|
+
return invokeFunc(accountAddress, "add_approver", [bytesScVal(sec1Pubkey(passkey))]);
|
|
1645
|
+
}
|
|
1646
|
+
/** Host function: `account.remove_approver(passkey)` (requires device auth). */
|
|
1647
|
+
buildRemoveApprover(accountAddress, passkey) {
|
|
1648
|
+
return invokeFunc(accountAddress, "remove_approver", [bytesScVal(sec1Pubkey(passkey))]);
|
|
1649
|
+
}
|
|
1650
|
+
/** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
|
|
1651
|
+
* `sha256(sec1(new_signer) || nonce_be8)`. The batch challenge the passkey signs
|
|
1652
|
+
* is `sha256(concat(leaves))` across chains. */
|
|
1653
|
+
passkeyLeaf(newSigner, nonce) {
|
|
1654
|
+
const msg = new Uint8Array(65 + 8);
|
|
1655
|
+
msg.set(sec1Pubkey(newSigner), 0);
|
|
1656
|
+
const n = new Uint8Array(8);
|
|
1657
|
+
let v = nonce;
|
|
1658
|
+
for (let i = 7; i >= 0; i--) {
|
|
1659
|
+
n[i] = Number(v & 0xffn);
|
|
1660
|
+
v >>= 8n;
|
|
1661
|
+
}
|
|
1662
|
+
msg.set(n, 65);
|
|
1663
|
+
return sha256.sha256(msg);
|
|
1664
|
+
}
|
|
1665
|
+
/** Host function: passkey-authorized `add_signer_via_passkey` (no device auth —
|
|
1666
|
+
* authorized by the embedded WebAuthn assertion, so any relayer can submit).
|
|
1667
|
+
* `leaves`/`leafIndex` place this chain's leaf in the multi-chain batch. */
|
|
1668
|
+
buildAddSignerViaPasskey(accountAddress, newSigner, passkey, nonce, leaves, leafIndex, assertion) {
|
|
1669
|
+
const sig = encodeLowSSignature2({ r: assertion.r, s: assertion.s});
|
|
1670
|
+
const leavesScVal = stellarSdk.xdr.ScVal.scvVec(leaves.map((l) => bytesScVal(l)));
|
|
1671
|
+
return invokeFunc(accountAddress, "add_signer_via_passkey", [
|
|
1672
|
+
bytesScVal(sec1Pubkey(newSigner)),
|
|
1673
|
+
bytesScVal(sec1Pubkey(passkey)),
|
|
1674
|
+
stellarSdk.nativeToScVal(nonce, { type: "u64" }),
|
|
1675
|
+
leavesScVal,
|
|
1676
|
+
stellarSdk.nativeToScVal(leafIndex, { type: "u32" }),
|
|
1677
|
+
bytesScVal(assertion.authenticatorData),
|
|
1678
|
+
bytesScVal(assertion.clientDataJSON),
|
|
1679
|
+
stellarSdk.nativeToScVal(assertion.challengeOffset, { type: "u32" }),
|
|
1680
|
+
bytesScVal(sig)
|
|
1681
|
+
]);
|
|
1682
|
+
}
|
|
1683
|
+
/** Read whether `passkey` is a registered approver (read-only simulation). */
|
|
1684
|
+
/** True if the account has at least one passkey registered as an approver.
|
|
1685
|
+
* Reads the contract's `approvers` view and checks the list is non-empty. */
|
|
1686
|
+
async hasPasskeyApprover(accountAddress, readSource) {
|
|
1687
|
+
if (!await this.isDeployed(accountAddress)) return false;
|
|
1688
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1689
|
+
const src = new Account3(readSource, "0");
|
|
1690
|
+
const op = stellarSdk.Operation.invokeHostFunction({
|
|
1691
|
+
func: invokeFunc(accountAddress, "approvers", []),
|
|
1692
|
+
auth: []
|
|
1693
|
+
});
|
|
1694
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1695
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1696
|
+
if (stellarSdk.rpc.Api.isSimulationError(sim)) {
|
|
1697
|
+
throw new Error(`kit/stellar: approvers simulation failed: ${sim.error}`);
|
|
1698
|
+
}
|
|
1699
|
+
if (!sim.result?.retval) return false;
|
|
1700
|
+
const list = stellarSdk.scValToNative(sim.result.retval);
|
|
1701
|
+
return Array.isArray(list) && list.length > 0;
|
|
1702
|
+
}
|
|
1703
|
+
async isApprover(accountAddress, passkey, readSource) {
|
|
1704
|
+
if (!await this.isDeployed(accountAddress)) return false;
|
|
1705
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1706
|
+
const src = new Account3(readSource, "0");
|
|
1707
|
+
const op = stellarSdk.Operation.invokeHostFunction({
|
|
1708
|
+
func: invokeFunc(accountAddress, "is_approver", [bytesScVal(sec1Pubkey(passkey))]),
|
|
1709
|
+
auth: []
|
|
1710
|
+
});
|
|
1711
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1712
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1713
|
+
if (stellarSdk.rpc.Api.isSimulationError(sim)) {
|
|
1714
|
+
throw new Error(`kit/stellar: is_approver simulation failed: ${sim.error}`);
|
|
1715
|
+
}
|
|
1716
|
+
if (!sim.result?.retval) return false;
|
|
1717
|
+
return stellarSdk.scValToNative(sim.result.retval) === true;
|
|
1718
|
+
}
|
|
1719
|
+
/** Read the current passkey-approval nonce (read-only simulation). */
|
|
1720
|
+
async passkeyNonce(accountAddress, readSource) {
|
|
1721
|
+
if (!await this.isDeployed(accountAddress)) return 0n;
|
|
1722
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1723
|
+
const src = new Account3(readSource, "0");
|
|
1724
|
+
const op = stellarSdk.Operation.invokeHostFunction({
|
|
1725
|
+
func: invokeFunc(accountAddress, "passkey_nonce", []),
|
|
1726
|
+
auth: []
|
|
1727
|
+
});
|
|
1728
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1729
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1730
|
+
if (stellarSdk.rpc.Api.isSimulationError(sim)) {
|
|
1731
|
+
throw new Error(`kit/stellar: passkey_nonce simulation failed: ${sim.error}`);
|
|
1732
|
+
}
|
|
1733
|
+
if (!sim.result?.retval) return 0n;
|
|
1734
|
+
return BigInt(stellarSdk.scValToNative(sim.result.retval));
|
|
1735
|
+
}
|
|
1736
|
+
/** Host function: SEP-41 `token.transfer(from=account, to, amount)` (device auth). */
|
|
1737
|
+
buildTransfer(tokenId, accountAddress, destination, amount) {
|
|
1738
|
+
return invokeFunc(tokenId, "transfer", [
|
|
1739
|
+
new stellarSdk.Address(accountAddress).toScVal(),
|
|
1740
|
+
new stellarSdk.Address(destination).toScVal(),
|
|
1741
|
+
stellarSdk.nativeToScVal(amount, { type: "i128" })
|
|
1742
|
+
]);
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Sign a Soroban authorization entry with the silent device key, producing the
|
|
1746
|
+
* `Vec<DeviceSignature>` the account's `__check_auth` verifies. The device
|
|
1747
|
+
* signs `sha256(preimage)` (WebCrypto hashes once more internally), which is
|
|
1748
|
+
* exactly what the contract recomputes. Mutates + returns the entry.
|
|
1749
|
+
*/
|
|
1750
|
+
async signAuthEntry(entry, validUntilLedger) {
|
|
1751
|
+
const addrCreds = entry.credentials().address();
|
|
1752
|
+
addrCreds.signatureExpirationLedger(validUntilLedger);
|
|
1753
|
+
const preimage = stellarSdk.xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
|
|
1754
|
+
new stellarSdk.xdr.HashIdPreimageSorobanAuthorization({
|
|
1755
|
+
networkId: this.networkId(),
|
|
1756
|
+
nonce: addrCreds.nonce(),
|
|
1757
|
+
signatureExpirationLedger: validUntilLedger,
|
|
1758
|
+
invocation: entry.rootInvocation()
|
|
1759
|
+
})
|
|
1760
|
+
);
|
|
1761
|
+
const payload = stellarSdk.hash(preimage.toXDR());
|
|
1762
|
+
const sig = await this.signer.sign(new Uint8Array(payload));
|
|
1763
|
+
const pubkey = await this.signer.getPublicKey();
|
|
1764
|
+
addrCreds.signature(deviceSignatureScVal(pubkey, sig));
|
|
1765
|
+
return entry;
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Read a SEP-41 token balance of `account` via a read-only simulation of
|
|
1769
|
+
* `token.balance(account)`. Returns 0 when the account isn't deployed or holds
|
|
1770
|
+
* none. `readSource` is any funded G-account (used only for the simulation).
|
|
1771
|
+
*/
|
|
1772
|
+
async readBalance(tokenId, account, readSource) {
|
|
1773
|
+
if (!await this.isDeployed(account)) return 0n;
|
|
1774
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1775
|
+
const src = new Account3(readSource, "0");
|
|
1776
|
+
const op = stellarSdk.Operation.invokeHostFunction({
|
|
1777
|
+
func: invokeFunc(tokenId, "balance", [new stellarSdk.Address(account).toScVal()]),
|
|
1778
|
+
auth: []
|
|
1779
|
+
});
|
|
1780
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1781
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1782
|
+
if (stellarSdk.rpc.Api.isSimulationError(sim) || !sim.result?.retval) return 0n;
|
|
1783
|
+
return BigInt(stellarSdk.scValToNative(sim.result.retval));
|
|
1784
|
+
}
|
|
1785
|
+
/** Whether the account contract instance exists on-chain (is deployed). */
|
|
1786
|
+
async isDeployed(accountAddress) {
|
|
1787
|
+
try {
|
|
1788
|
+
const res = await this.server().getContractData(
|
|
1789
|
+
accountAddress,
|
|
1790
|
+
stellarSdk.xdr.ScVal.scvLedgerKeyContractInstance(),
|
|
1791
|
+
stellarSdk.rpc.Durability.Persistent
|
|
1792
|
+
);
|
|
1793
|
+
return !!res;
|
|
1794
|
+
} catch {
|
|
1795
|
+
return false;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Read whether `signer` is a currently-authorized signer of the account, via a
|
|
1800
|
+
* read-only simulation of `account.is_authorized(signer)`. `readSource` is any
|
|
1801
|
+
* funded G-account (used only for the simulation's source/sequence).
|
|
1802
|
+
*/
|
|
1803
|
+
async isAuthorizedSigner(accountAddress, signer, readSource) {
|
|
1804
|
+
if (!await this.isDeployed(accountAddress)) return false;
|
|
1805
|
+
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1806
|
+
const src = new Account3(readSource, "0");
|
|
1807
|
+
const op = stellarSdk.Operation.invokeHostFunction({
|
|
1808
|
+
func: invokeFunc(accountAddress, "is_authorized", [bytesScVal(sec1Pubkey(signer))]),
|
|
1809
|
+
auth: []
|
|
1810
|
+
});
|
|
1811
|
+
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1812
|
+
const sim = await this.server().simulateTransaction(tx2);
|
|
1813
|
+
if (stellarSdk.rpc.Api.isSimulationError(sim)) {
|
|
1814
|
+
throw new Error(`kit/stellar: is_authorized simulation failed: ${sim.error}`);
|
|
1815
|
+
}
|
|
1816
|
+
if (!sim.result?.retval) return false;
|
|
1817
|
+
return stellarSdk.scValToNative(sim.result.retval) === true;
|
|
1818
|
+
}
|
|
1819
|
+
};
|
|
1820
|
+
function sec1Pubkey(pk) {
|
|
1821
|
+
const out = new Uint8Array(65);
|
|
1822
|
+
out[0] = 4;
|
|
1823
|
+
out.set(bigIntTo32Bytes(pk.x), 1);
|
|
1824
|
+
out.set(bigIntTo32Bytes(pk.y), 33);
|
|
1825
|
+
return out;
|
|
1826
|
+
}
|
|
1827
|
+
function encodeLowSSignature2(sig) {
|
|
1828
|
+
const lowS = sig.s > SECP256R1_N2 / 2n ? SECP256R1_N2 - sig.s : sig.s;
|
|
1829
|
+
const out = new Uint8Array(64);
|
|
1830
|
+
out.set(bigIntTo32Bytes(sig.r), 0);
|
|
1831
|
+
out.set(bigIntTo32Bytes(lowS), 32);
|
|
1832
|
+
return out;
|
|
1833
|
+
}
|
|
1834
|
+
function deviceSignatureScVal(pubkey, sig) {
|
|
1835
|
+
const element = stellarSdk.nativeToScVal(
|
|
1836
|
+
{
|
|
1837
|
+
public_key: Buffer.from(sec1Pubkey(pubkey)),
|
|
1838
|
+
signature: Buffer.from(encodeLowSSignature2(sig))
|
|
1839
|
+
},
|
|
1840
|
+
{ type: { public_key: ["symbol", "bytes"], signature: ["symbol", "bytes"] } }
|
|
1841
|
+
);
|
|
1842
|
+
return stellarSdk.xdr.ScVal.scvVec([element]);
|
|
1843
|
+
}
|
|
1844
|
+
function invokeFunc(contractId, method, args) {
|
|
1845
|
+
return stellarSdk.xdr.HostFunction.hostFunctionTypeInvokeContract(
|
|
1846
|
+
new stellarSdk.xdr.InvokeContractArgs({
|
|
1847
|
+
contractAddress: new stellarSdk.Address(contractId).toScAddress(),
|
|
1848
|
+
functionName: method,
|
|
1849
|
+
args
|
|
1850
|
+
})
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
function bytesScVal(bytes) {
|
|
1854
|
+
return stellarSdk.xdr.ScVal.scvBytes(Buffer.from(bytes));
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// src/chains/stellar/StellarRelayer.ts
|
|
1858
|
+
var StellarRelayer = class {
|
|
1859
|
+
constructor(opts) {
|
|
1860
|
+
this.opts = opts;
|
|
1861
|
+
}
|
|
1862
|
+
/** The relayer's source/fee-payer G-account (fetched + cached from the backend). */
|
|
1863
|
+
async getSource() {
|
|
1864
|
+
if (this.source) return this.source;
|
|
1865
|
+
const res = await fetch(`${this.opts.baseUrl}/api/stellar/relay?network=${this.opts.network}`);
|
|
1866
|
+
if (!res.ok) throw new Error(`kit/stellar: relayer source lookup failed (${res.status})`);
|
|
1867
|
+
const { fee_payer } = await res.json();
|
|
1868
|
+
this.source = fee_payer;
|
|
1869
|
+
return this.source;
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* POST the assembled, device-authorized transaction XDR to the relayer to sign
|
|
1873
|
+
* the envelope + submit. Returns the confirmed transaction hash.
|
|
1874
|
+
*/
|
|
1875
|
+
async submit(transactionXdr) {
|
|
1876
|
+
const res = await fetch(`${this.opts.baseUrl}/api/stellar/relay`, {
|
|
1877
|
+
method: "POST",
|
|
1878
|
+
headers: { "Content-Type": "application/json" },
|
|
1879
|
+
body: JSON.stringify({
|
|
1880
|
+
app_id: this.opts.appId,
|
|
1881
|
+
network: this.opts.network,
|
|
1882
|
+
transaction: transactionXdr
|
|
1883
|
+
})
|
|
1884
|
+
});
|
|
1885
|
+
if (!res.ok) {
|
|
1886
|
+
const detail = await res.text().catch(() => "");
|
|
1887
|
+
throw new Error(`kit/stellar: relay failed (${res.status}) ${detail}`);
|
|
1888
|
+
}
|
|
1889
|
+
const { hash: hash6 } = await res.json();
|
|
1890
|
+
return hash6;
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
|
|
1894
|
+
// src/chains/stellar/CavosStellar.ts
|
|
1895
|
+
var CavosStellar = class _CavosStellar {
|
|
1896
|
+
constructor(identity, address, status, network, adapter, devicePubkey, relayer, sourceKeypair) {
|
|
1897
|
+
this.identity = identity;
|
|
1898
|
+
this.address = address;
|
|
1899
|
+
this.status = status;
|
|
1900
|
+
this.network = network;
|
|
1901
|
+
this.adapter = adapter;
|
|
1902
|
+
this.devicePubkey = devicePubkey;
|
|
1903
|
+
this.relayer = relayer;
|
|
1904
|
+
this.sourceKeypair = sourceKeypair;
|
|
1905
|
+
/** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
|
|
1906
|
+
this.chain = "stellar";
|
|
1907
|
+
/** True when this connect just created a brand-new account (first sign-up). */
|
|
1908
|
+
this.isNewAccount = false;
|
|
1909
|
+
}
|
|
1910
|
+
get publicKey() {
|
|
1911
|
+
return this.devicePubkey;
|
|
1912
|
+
}
|
|
1913
|
+
static async connect(opts) {
|
|
1914
|
+
const identity = opts.identity ?? await opts.auth?.authenticate();
|
|
1915
|
+
if (!identity) throw new Error("kit/stellar: connect requires `identity` or `auth`");
|
|
1916
|
+
const signer = opts.createSigner ? await opts.createSigner(`${identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${identity.userId}:${opts.appSalt}` });
|
|
1917
|
+
const devicePubkey = await signer.getPublicKey();
|
|
1918
|
+
const adapter = new StellarAdapter({
|
|
1919
|
+
network: opts.network,
|
|
1920
|
+
rpcUrl: opts.rpcUrl,
|
|
1921
|
+
factoryId: opts.factoryId,
|
|
1922
|
+
signer
|
|
1923
|
+
});
|
|
1924
|
+
const addressSeed = deriveAddressSeedStellar({ userId: identity.userId, appSalt: opts.appSalt });
|
|
1925
|
+
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
1926
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
|
|
1927
|
+
const relayer = opts.relayer ?? (opts.appId ? new StellarRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : void 0);
|
|
1928
|
+
const build = (address2, status) => new _CavosStellar(identity, address2, status, opts.network, adapter, devicePubkey, relayer, opts.sourceKeypair);
|
|
1929
|
+
const self = build("", "needs-device-approval");
|
|
1930
|
+
const readSource = await self.resolveSource();
|
|
1931
|
+
const existing = await registry.lookup(identity.userId);
|
|
1932
|
+
if (existing) {
|
|
1933
|
+
const isSigner2 = await adapter.isAuthorizedSigner(existing.address, devicePubkey, readSource);
|
|
1934
|
+
return build(existing.address, isSigner2 ? "ready" : "needs-device-approval");
|
|
1935
|
+
}
|
|
1936
|
+
const address = adapter.computeAddress(addressSeed, devicePubkey);
|
|
1937
|
+
const wasDeployed = await adapter.isDeployed(address);
|
|
1938
|
+
if (!wasDeployed) {
|
|
1939
|
+
const func = adapter.buildDeploy(addressSeed, devicePubkey);
|
|
1940
|
+
await self.submitHostFunction(func, void 0);
|
|
1941
|
+
}
|
|
1942
|
+
await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
|
|
1943
|
+
const isSigner = await adapter.isAuthorizedSigner(address, devicePubkey, readSource);
|
|
1944
|
+
const wallet = build(address, isSigner ? "ready" : "needs-device-approval");
|
|
1945
|
+
wallet.isNewAccount = !wasDeployed && isSigner;
|
|
1946
|
+
return wallet;
|
|
1947
|
+
}
|
|
1948
|
+
/** Authorize an additional device signer (device-signed via `__check_auth`). */
|
|
1949
|
+
async addSigner(pubkey) {
|
|
1950
|
+
const func = this.adapter.buildAddSigner(this.address, pubkey);
|
|
1951
|
+
return this.submitHostFunction(func, this.address);
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Enroll a passkey as an approver (2FA-style step-up). Device-signed + gasless;
|
|
1955
|
+
* requires a ready device. Idempotent. Returns the passkey pubkey + tx hash.
|
|
1956
|
+
*/
|
|
1957
|
+
async enrollPasskey(passkey, params) {
|
|
1958
|
+
const enrolled = await passkey.enroll(params);
|
|
1959
|
+
const { transactionHash } = await this.addApprover(enrolled.publicKey);
|
|
1960
|
+
return { publicKey: enrolled.publicKey, transactionHash };
|
|
1961
|
+
}
|
|
1962
|
+
/** Register an already-enrolled passkey pubkey as an approver (gasless).
|
|
1963
|
+
* Idempotent. Lets one passkey be registered across chains without re-prompting. */
|
|
1964
|
+
async addApprover(pubkey) {
|
|
1965
|
+
if (this.status !== "ready") {
|
|
1966
|
+
throw new Error("kit/stellar: addApprover requires a ready, authorized device");
|
|
1967
|
+
}
|
|
1968
|
+
const readSource = await this.resolveSource();
|
|
1969
|
+
if (await this.adapter.isApprover(this.address, pubkey, readSource)) return {};
|
|
1970
|
+
const func = this.adapter.buildAddApprover(this.address, pubkey);
|
|
1971
|
+
const transactionHash = await this.submitHostFunction(func, this.address);
|
|
1972
|
+
return { transactionHash };
|
|
1973
|
+
}
|
|
1974
|
+
/** True if this account already has a passkey enrolled as an approver, so a
|
|
1975
|
+
* new device can be approved with the passkey instead of the email flow. */
|
|
1976
|
+
async hasPasskey() {
|
|
1977
|
+
const readSource = await this.resolveSource();
|
|
1978
|
+
return this.adapter.hasPasskeyApprover(this.address, readSource);
|
|
1979
|
+
}
|
|
1980
|
+
/** Re-read (from chain) whether THIS device is now an authorized signer.
|
|
1981
|
+
* Used to poll for readiness after a passkey approval before it's indexed. */
|
|
1982
|
+
async isReady() {
|
|
1983
|
+
const readSource = await this.resolveSource();
|
|
1984
|
+
return this.adapter.isAuthorizedSigner(this.address, this.devicePubkey, readSource);
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* From a fresh browser (status `needs-device-approval`), approve adding THIS
|
|
1988
|
+
* device using the user's synced passkey. Gasless via the relayer — the call
|
|
1989
|
+
* carries the WebAuthn assertion, so no device signature is needed. Returns the
|
|
1990
|
+
* tx hash. No trip back to an already-authorized device.
|
|
1991
|
+
*/
|
|
1992
|
+
async approveThisDeviceWithPasskey(passkey) {
|
|
1993
|
+
if (this.status === "ready") {
|
|
1994
|
+
throw new Error("kit/stellar: this device is already an authorized signer");
|
|
1995
|
+
}
|
|
1996
|
+
const { leaf, nonce } = await this.passkeyLeafForThisDevice();
|
|
1997
|
+
const leaves = [leaf];
|
|
1998
|
+
const assertion = await passkey.assert(batchChallenge(leaves));
|
|
1999
|
+
const { transactionHash } = await this.submitPasskeyApproval(assertion, leaves, 0, nonce);
|
|
2000
|
+
return transactionHash;
|
|
2001
|
+
}
|
|
2002
|
+
/** This device's leaf + passkey nonce for a (possibly multi-chain) batch. */
|
|
2003
|
+
async passkeyLeafForThisDevice() {
|
|
2004
|
+
const readSource = await this.resolveSource();
|
|
2005
|
+
const nonce = await this.adapter.passkeyNonce(this.address, readSource);
|
|
2006
|
+
return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
|
|
2007
|
+
}
|
|
2008
|
+
/** Submit `add_signer_via_passkey` given a shared assertion + batch position.
|
|
2009
|
+
* No device auth entry — authorized purely by the passkey assertion. */
|
|
2010
|
+
async submitPasskeyApproval(assertion, leaves, leafIndex, nonce) {
|
|
2011
|
+
const readSource = await this.resolveSource();
|
|
2012
|
+
const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
|
|
2013
|
+
const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
|
|
2014
|
+
let approver = null;
|
|
2015
|
+
for (const cand of candidates) {
|
|
2016
|
+
if (await this.adapter.isApprover(this.address, cand.publicKey, readSource)) {
|
|
2017
|
+
approver = cand.publicKey;
|
|
2018
|
+
break;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
if (!approver) throw new Error("kit/stellar: this passkey is not a registered approver");
|
|
2022
|
+
const func = this.adapter.buildAddSignerViaPasskey(
|
|
2023
|
+
this.address,
|
|
2024
|
+
this.devicePubkey,
|
|
2025
|
+
approver,
|
|
2026
|
+
nonce,
|
|
2027
|
+
leaves,
|
|
2028
|
+
leafIndex,
|
|
2029
|
+
assertion
|
|
2030
|
+
);
|
|
2031
|
+
return { transactionHash: await this.submitHostFunction(func, void 0) };
|
|
2032
|
+
}
|
|
2033
|
+
/** Move `amount` stroops of native XLM to `destination` (device-signed). */
|
|
2034
|
+
async execute(amount, destination) {
|
|
2035
|
+
return this.executeTransfer(NATIVE_SAC_ID[this.network], amount, destination);
|
|
2036
|
+
}
|
|
2037
|
+
/** Read this account's balance of `tokenId` (defaults to native XLM), in stroops. */
|
|
2038
|
+
async balance(tokenId = NATIVE_SAC_ID[this.network]) {
|
|
2039
|
+
const readSource = await this.resolveSource();
|
|
2040
|
+
return this.adapter.readBalance(tokenId, this.address, readSource);
|
|
2041
|
+
}
|
|
2042
|
+
/** Transfer `amount` of any SEP-41 token out of the account (device-signed). */
|
|
2043
|
+
async executeTransfer(tokenId, amount, destination) {
|
|
2044
|
+
if (this.status !== "ready") {
|
|
2045
|
+
throw new Error("kit/stellar: this device is not yet an authorized signer of the wallet");
|
|
2046
|
+
}
|
|
2047
|
+
const func = this.adapter.buildTransfer(tokenId, this.address, destination, amount);
|
|
2048
|
+
return this.submitHostFunction(func, this.address);
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* Register the backup signer derived from `code` as an authorized signer of
|
|
2052
|
+
* this account (device-signed). Idempotent. The code never leaves the device —
|
|
2053
|
+
* only the derived public key travels on-chain. Mirrors the other chains.
|
|
2054
|
+
*/
|
|
2055
|
+
async setupRecovery(code) {
|
|
2056
|
+
if (this.status !== "ready") {
|
|
2057
|
+
throw new Error("kit/stellar: setupRecovery requires a ready, registered device");
|
|
2058
|
+
}
|
|
2059
|
+
const { publicKey: backupPubkey } = deriveBackupKey(code);
|
|
2060
|
+
const readSource = await this.resolveSource();
|
|
2061
|
+
if (await this.adapter.isAuthorizedSigner(this.address, backupPubkey, readSource)) return void 0;
|
|
2062
|
+
return this.addSigner(backupPubkey);
|
|
2063
|
+
}
|
|
2064
|
+
/**
|
|
2065
|
+
* Recover an account after losing every device signer: derive the backup key
|
|
2066
|
+
* from `code`, use it (not the new device) to authorize `add_signer(newDevice)`,
|
|
2067
|
+
* and return a ready handle bound to the new device. The address is unchanged.
|
|
2068
|
+
*/
|
|
2069
|
+
static async recover(opts) {
|
|
2070
|
+
const signer = opts.createSigner ? await opts.createSigner(`${opts.identity.userId}:${opts.appSalt}`) : await WebCryptoSigner.loadOrCreate({ keyId: `${opts.identity.userId}:${opts.appSalt}` });
|
|
2071
|
+
const devicePubkey = await signer.getPublicKey();
|
|
2072
|
+
const backup = BackupSigner.fromCode(opts.code);
|
|
2073
|
+
const backupAdapter = new StellarAdapter({
|
|
2074
|
+
network: opts.network,
|
|
2075
|
+
rpcUrl: opts.rpcUrl,
|
|
2076
|
+
factoryId: opts.factoryId,
|
|
2077
|
+
signer: backup
|
|
2078
|
+
});
|
|
2079
|
+
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
2080
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
|
|
2081
|
+
const existing = await registry.lookup(opts.identity.userId);
|
|
2082
|
+
if (!existing) {
|
|
2083
|
+
throw new Error("kit/stellar: no account found for this identity \u2014 nothing to recover");
|
|
2084
|
+
}
|
|
2085
|
+
const relayer = opts.relayer ?? (opts.appId ? new StellarRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : void 0);
|
|
2086
|
+
const backupHandle = new _CavosStellar(
|
|
2087
|
+
opts.identity,
|
|
2088
|
+
existing.address,
|
|
2089
|
+
"ready",
|
|
2090
|
+
opts.network,
|
|
2091
|
+
backupAdapter,
|
|
2092
|
+
devicePubkey,
|
|
2093
|
+
relayer,
|
|
2094
|
+
opts.sourceKeypair
|
|
2095
|
+
);
|
|
2096
|
+
const readSource = await backupHandle.resolveSource();
|
|
2097
|
+
if (!await backupAdapter.isAuthorizedSigner(existing.address, devicePubkey, readSource)) {
|
|
2098
|
+
await backupHandle.addSigner(devicePubkey);
|
|
2099
|
+
}
|
|
2100
|
+
const adapter = new StellarAdapter({
|
|
2101
|
+
network: opts.network,
|
|
2102
|
+
rpcUrl: opts.rpcUrl,
|
|
2103
|
+
factoryId: opts.factoryId,
|
|
2104
|
+
signer
|
|
2105
|
+
});
|
|
2106
|
+
return new _CavosStellar(
|
|
2107
|
+
opts.identity,
|
|
2108
|
+
existing.address,
|
|
2109
|
+
"ready",
|
|
2110
|
+
opts.network,
|
|
2111
|
+
adapter,
|
|
2112
|
+
devicePubkey,
|
|
2113
|
+
relayer,
|
|
2114
|
+
opts.sourceKeypair
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
/** The transaction source/fee-payer G-address (relayer or self-funded). */
|
|
2118
|
+
async resolveSource() {
|
|
2119
|
+
if (this.relayer) return this.relayer.getSource();
|
|
2120
|
+
if (this.sourceKeypair) return this.sourceKeypair.publicKey();
|
|
2121
|
+
throw new Error("kit/stellar: a relayer (appId) or sourceKeypair is required");
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Build → simulate → device-sign auth → assemble → submit an invoke-contract
|
|
2125
|
+
* host function. `authAccount` is the account whose `__check_auth` must sign the
|
|
2126
|
+
* operation's Soroban auth entry (undefined for a plain factory deploy).
|
|
2127
|
+
*/
|
|
2128
|
+
async submitHostFunction(func, authAccount) {
|
|
2129
|
+
const server = this.adapter.server();
|
|
2130
|
+
const sourceAddr = await this.resolveSource();
|
|
2131
|
+
const simSource = new stellarSdk.Account(sourceAddr, "0");
|
|
2132
|
+
const unsignedOp = stellarSdk.Operation.invokeHostFunction({ func, auth: [] });
|
|
2133
|
+
const simTx = new stellarSdk.TransactionBuilder(simSource, {
|
|
2134
|
+
fee: stellarSdk.BASE_FEE,
|
|
2135
|
+
networkPassphrase: this.adapter.passphrase
|
|
2136
|
+
}).addOperation(unsignedOp).setTimeout(180).build();
|
|
2137
|
+
const sim = await server.simulateTransaction(simTx);
|
|
2138
|
+
if (stellarSdk.rpc.Api.isSimulationError(sim)) {
|
|
2139
|
+
throw new Error(`kit/stellar: simulation failed: ${sim.error}`);
|
|
2140
|
+
}
|
|
2141
|
+
const validUntil = (await server.getLatestLedger()).sequence + 100;
|
|
2142
|
+
const entries = sim.result?.auth ?? [];
|
|
2143
|
+
const signedAuth = [];
|
|
2144
|
+
for (const entry of entries) {
|
|
2145
|
+
if (authAccount && isAddressCredentialFor(entry, authAccount)) {
|
|
2146
|
+
signedAuth.push(await this.adapter.signAuthEntry(entry, validUntil));
|
|
2147
|
+
} else {
|
|
2148
|
+
signedAuth.push(entry);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
const account = await server.getAccount(sourceAddr);
|
|
2152
|
+
const finalOp = stellarSdk.Operation.invokeHostFunction({ func, auth: signedAuth });
|
|
2153
|
+
const built = new stellarSdk.TransactionBuilder(account, {
|
|
2154
|
+
fee: stellarSdk.BASE_FEE,
|
|
2155
|
+
networkPassphrase: this.adapter.passphrase
|
|
2156
|
+
}).addOperation(finalOp).setTimeout(180).build();
|
|
2157
|
+
const authSim = await server.simulateTransaction(built);
|
|
2158
|
+
if (stellarSdk.rpc.Api.isSimulationError(authSim)) {
|
|
2159
|
+
throw new Error(`kit/stellar: auth simulation failed: ${authSim.error}`);
|
|
2160
|
+
}
|
|
2161
|
+
const assembled = stellarSdk.rpc.assembleTransaction(built, authSim).build();
|
|
2162
|
+
if (this.relayer) {
|
|
2163
|
+
return this.relayer.submit(assembled.toXDR());
|
|
2164
|
+
}
|
|
2165
|
+
if (this.sourceKeypair) {
|
|
2166
|
+
assembled.sign(this.sourceKeypair);
|
|
2167
|
+
return this.sendAndConfirm(assembled);
|
|
2168
|
+
}
|
|
2169
|
+
throw new Error("kit/stellar: no relayer or sourceKeypair configured to submit");
|
|
2170
|
+
}
|
|
2171
|
+
/** Submit a signed tx via RPC and poll to confirmation. Returns the hash. */
|
|
2172
|
+
async sendAndConfirm(tx2) {
|
|
2173
|
+
const server = this.adapter.server();
|
|
2174
|
+
const sent = await server.sendTransaction(tx2);
|
|
2175
|
+
if (sent.status === "ERROR") {
|
|
2176
|
+
throw new Error(`kit/stellar: submit rejected: ${JSON.stringify(sent.errorResult)}`);
|
|
2177
|
+
}
|
|
2178
|
+
const hash6 = sent.hash;
|
|
2179
|
+
for (let i = 0; i < 30; i++) {
|
|
2180
|
+
const got = await server.getTransaction(hash6);
|
|
2181
|
+
if (got.status === stellarSdk.rpc.Api.GetTransactionStatus.SUCCESS) return hash6;
|
|
2182
|
+
if (got.status === stellarSdk.rpc.Api.GetTransactionStatus.FAILED) {
|
|
2183
|
+
throw new Error(`kit/stellar: tx ${hash6} failed`);
|
|
2184
|
+
}
|
|
2185
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
2186
|
+
}
|
|
2187
|
+
throw new Error(`kit/stellar: tx ${hash6} not confirmed in time`);
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
function isAddressCredentialFor(entry, accountAddress) {
|
|
2191
|
+
const creds = entry.credentials();
|
|
2192
|
+
if (creds.switch() !== stellarSdk.xdr.SorobanCredentialsType.sorobanCredentialsAddress()) return false;
|
|
2193
|
+
return stellarSdk.Address.fromScAddress(creds.address().address()).toString() === accountAddress;
|
|
2194
|
+
}
|
|
2195
|
+
var defaultRegistry2 = new InMemoryWalletRegistry();
|
|
2196
|
+
|
|
1193
2197
|
// src/recovery/HttpRecoveryClient.ts
|
|
1194
2198
|
function toHex2(n) {
|
|
1195
2199
|
return "0x" + n.toString(16);
|
|
@@ -1271,18 +2275,26 @@ var SOLANA_ENV = {
|
|
|
1271
2275
|
mainnet: "solana-mainnet",
|
|
1272
2276
|
testnet: "solana-devnet"
|
|
1273
2277
|
};
|
|
2278
|
+
var STELLAR_ENV = {
|
|
2279
|
+
mainnet: "stellar-mainnet",
|
|
2280
|
+
testnet: "stellar-testnet"
|
|
2281
|
+
};
|
|
1274
2282
|
var Cavos = class _Cavos {
|
|
1275
|
-
constructor(identity, address, status, account, adapter, devicePubkey) {
|
|
2283
|
+
constructor(identity, address, status, account, adapter, devicePubkey, paymaster) {
|
|
1276
2284
|
this.identity = identity;
|
|
1277
2285
|
this.address = address;
|
|
1278
2286
|
this.status = status;
|
|
1279
2287
|
this.account = account;
|
|
1280
2288
|
this.adapter = adapter;
|
|
1281
2289
|
this.devicePubkey = devicePubkey;
|
|
2290
|
+
this.paymaster = paymaster;
|
|
1282
2291
|
/** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
|
|
1283
2292
|
this.chain = "starknet";
|
|
1284
2293
|
/** Request id of the pending device-addition, when status is needs-device-approval. */
|
|
1285
2294
|
this.pendingRequestId = null;
|
|
2295
|
+
/** True when this connect just created & deployed a brand-new account (first
|
|
2296
|
+
* sign-up), so the UI can offer a one-time "secure your account" step. */
|
|
2297
|
+
this.isNewAccount = false;
|
|
1286
2298
|
}
|
|
1287
2299
|
/**
|
|
1288
2300
|
* Unified entry point. Pick a `chain` and an `network` environment; the kit
|
|
@@ -1311,6 +2323,22 @@ var Cavos = class _Cavos {
|
|
|
1311
2323
|
...opts.feePayer ? { feePayer: opts.feePayer } : {}
|
|
1312
2324
|
});
|
|
1313
2325
|
}
|
|
2326
|
+
if (opts.chain === "stellar") {
|
|
2327
|
+
return CavosStellar.connect({
|
|
2328
|
+
network: STELLAR_ENV[opts.network],
|
|
2329
|
+
...opts.auth ? { auth: opts.auth } : {},
|
|
2330
|
+
...opts.identity ? { identity: opts.identity } : {},
|
|
2331
|
+
appSalt: opts.appSalt,
|
|
2332
|
+
...opts.appId ? { appId: opts.appId } : {},
|
|
2333
|
+
...opts.backendUrl ? { backendUrl: opts.backendUrl } : {},
|
|
2334
|
+
...opts.registry ? { registry: opts.registry } : {},
|
|
2335
|
+
...opts.rpcUrl ? { rpcUrl: opts.rpcUrl } : {},
|
|
2336
|
+
...opts.factoryId ? { factoryId: opts.factoryId } : {},
|
|
2337
|
+
...opts.createSigner ? { createSigner: opts.createSigner } : {},
|
|
2338
|
+
...opts.stellarRelayer ? { relayer: opts.stellarRelayer } : {},
|
|
2339
|
+
...opts.stellarSourceKeypair ? { sourceKeypair: opts.stellarSourceKeypair } : {}
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
1314
2342
|
if (!opts.paymasterApiKey) {
|
|
1315
2343
|
throw new Error("kit: `paymasterApiKey` is required for Starknet connections");
|
|
1316
2344
|
}
|
|
@@ -1338,8 +2366,10 @@ var Cavos = class _Cavos {
|
|
|
1338
2366
|
const provider = new starknet.RpcProvider({
|
|
1339
2367
|
nodeUrl: opts.rpcUrl ?? STARKNET_NETWORKS[opts.network].rpcUrl
|
|
1340
2368
|
});
|
|
2369
|
+
const paymasterUrl = opts.paymasterUrl ?? CAVOS_PAYMASTER_URL[opts.network];
|
|
2370
|
+
const paymasterConfig = { url: paymasterUrl, apiKey: opts.paymasterApiKey };
|
|
1341
2371
|
const paymaster = new starknet.PaymasterRpc({
|
|
1342
|
-
nodeUrl:
|
|
2372
|
+
nodeUrl: paymasterUrl,
|
|
1343
2373
|
headers: { "x-paymaster-api-key": opts.paymasterApiKey }
|
|
1344
2374
|
});
|
|
1345
2375
|
const addressSeed = deriveAddressSeed({ userId: identity.userId, appSalt: opts.appSalt });
|
|
@@ -1354,26 +2384,27 @@ var Cavos = class _Cavos {
|
|
|
1354
2384
|
cairoVersion: "1"
|
|
1355
2385
|
});
|
|
1356
2386
|
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
1357
|
-
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) :
|
|
2387
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry3);
|
|
1358
2388
|
const recovery = opts.recovery ?? (opts.appId ? new HttpRecoveryClient({ baseUrl: backendUrl, appId: opts.appId }) : null);
|
|
1359
2389
|
const existing = await registry.lookup(identity.userId);
|
|
1360
2390
|
if (existing) {
|
|
1361
2391
|
const account2 = makeAccount(existing.address);
|
|
1362
2392
|
const isSigner2 = await adapter.isAuthorizedSigner(existing.address, devicePubkey);
|
|
1363
|
-
const
|
|
2393
|
+
const cavos2 = new _Cavos(
|
|
1364
2394
|
identity,
|
|
1365
2395
|
existing.address,
|
|
1366
2396
|
isSigner2 ? "ready" : "needs-device-approval",
|
|
1367
2397
|
account2,
|
|
1368
2398
|
adapter,
|
|
1369
|
-
devicePubkey
|
|
2399
|
+
devicePubkey,
|
|
2400
|
+
paymasterConfig
|
|
1370
2401
|
);
|
|
1371
2402
|
if (!isSigner2 && recovery) {
|
|
1372
2403
|
const dedup = lastDeviceRequest.get(identity.userId);
|
|
1373
2404
|
const fresh = dedup && Date.now() - dedup.requestedAt < DEVICE_REQUEST_DEDUP_MS;
|
|
1374
2405
|
try {
|
|
1375
2406
|
if (fresh) {
|
|
1376
|
-
|
|
2407
|
+
cavos2.pendingRequestId = dedup.requestId;
|
|
1377
2408
|
} else {
|
|
1378
2409
|
const { requestId } = await recovery.requestDeviceAddition({
|
|
1379
2410
|
userId: identity.userId,
|
|
@@ -1381,14 +2412,14 @@ var Cavos = class _Cavos {
|
|
|
1381
2412
|
newSigner: devicePubkey,
|
|
1382
2413
|
...identity.email ? { email: identity.email } : {}
|
|
1383
2414
|
});
|
|
1384
|
-
|
|
2415
|
+
cavos2.pendingRequestId = requestId;
|
|
1385
2416
|
lastDeviceRequest.set(identity.userId, { requestId, requestedAt: Date.now() });
|
|
1386
2417
|
}
|
|
1387
2418
|
} catch (e) {
|
|
1388
2419
|
console.warn("[Cavos] requestDeviceAddition failed:", e);
|
|
1389
2420
|
}
|
|
1390
2421
|
}
|
|
1391
|
-
return
|
|
2422
|
+
return cavos2;
|
|
1392
2423
|
}
|
|
1393
2424
|
const address = adapter.computeAddress({ addressSeed, initialSigner: devicePubkey });
|
|
1394
2425
|
const account = makeAccount(address);
|
|
@@ -1419,14 +2450,17 @@ var Cavos = class _Cavos {
|
|
|
1419
2450
|
console.warn("[Cavos] isAuthorizedSigner read failed:", e);
|
|
1420
2451
|
isSigner = !alreadyDeployed;
|
|
1421
2452
|
}
|
|
1422
|
-
|
|
2453
|
+
const cavos = new _Cavos(
|
|
1423
2454
|
identity,
|
|
1424
2455
|
address,
|
|
1425
2456
|
isSigner ? "ready" : "needs-device-approval",
|
|
1426
2457
|
account,
|
|
1427
2458
|
adapter,
|
|
1428
|
-
devicePubkey
|
|
2459
|
+
devicePubkey,
|
|
2460
|
+
paymasterConfig
|
|
1429
2461
|
);
|
|
2462
|
+
cavos.isNewAccount = !alreadyDeployed && isSigner;
|
|
2463
|
+
return cavos;
|
|
1430
2464
|
}
|
|
1431
2465
|
/** This device's public key (e.g. to request addition to an existing wallet). */
|
|
1432
2466
|
get publicKey() {
|
|
@@ -1446,6 +2480,108 @@ var Cavos = class _Cavos {
|
|
|
1446
2480
|
async addSigner(pubkey) {
|
|
1447
2481
|
return this.execute([this.adapter.buildAddSigner(this.address, pubkey)]);
|
|
1448
2482
|
}
|
|
2483
|
+
/**
|
|
2484
|
+
* Enroll a passkey as an APPROVER so the user can later add devices from any
|
|
2485
|
+
* browser (2FA-style step-up). Requires a ready device (the enrollment call is
|
|
2486
|
+
* device-signed and gasless). Idempotent: a no-op if the passkey is already an
|
|
2487
|
+
* approver. Call this whenever the app decides to prompt "turn on device
|
|
2488
|
+
* approvals". Returns the passkey's public key + the enrollment tx hash.
|
|
2489
|
+
*/
|
|
2490
|
+
async enrollPasskey(passkey, params) {
|
|
2491
|
+
const enrolled = await passkey.enroll(params);
|
|
2492
|
+
const { transactionHash } = await this.addApprover(enrolled.publicKey);
|
|
2493
|
+
return { publicKey: enrolled.publicKey, transactionHash };
|
|
2494
|
+
}
|
|
2495
|
+
/**
|
|
2496
|
+
* Register an ALREADY-enrolled passkey public key as an approver (gasless,
|
|
2497
|
+
* device-signed). Idempotent. Use this to register ONE passkey across multiple
|
|
2498
|
+
* chains without re-prompting `passkey.enroll()` on each: enroll once, then
|
|
2499
|
+
* call `addApprover(pubkey)` on each chain's wallet.
|
|
2500
|
+
*/
|
|
2501
|
+
async addApprover(pubkey) {
|
|
2502
|
+
if (this.status !== "ready") {
|
|
2503
|
+
throw new Error("kit: addApprover requires a ready, authorized device");
|
|
2504
|
+
}
|
|
2505
|
+
if (await this.adapter.isApprover(this.address, pubkey)) return {};
|
|
2506
|
+
const { transactionHash } = await this.execute([
|
|
2507
|
+
this.adapter.buildAddApprover(this.address, pubkey)
|
|
2508
|
+
]);
|
|
2509
|
+
try {
|
|
2510
|
+
await this.account.waitForTransaction(transactionHash);
|
|
2511
|
+
} catch (e) {
|
|
2512
|
+
console.warn("[Cavos] add_approver receipt wait failed:", e);
|
|
2513
|
+
}
|
|
2514
|
+
return { transactionHash };
|
|
2515
|
+
}
|
|
2516
|
+
/** True if this account already has a passkey enrolled as an approver, so a
|
|
2517
|
+
* new device can be approved with the passkey instead of the email flow. */
|
|
2518
|
+
async hasPasskey() {
|
|
2519
|
+
return this.adapter.hasPasskeyApprover(this.address);
|
|
2520
|
+
}
|
|
2521
|
+
/** Re-read (from chain) whether THIS device is now an authorized signer.
|
|
2522
|
+
* Cheap and side-effect free — used to poll for readiness after a passkey /
|
|
2523
|
+
* device approval submits, before the new signer is indexed. */
|
|
2524
|
+
async isReady() {
|
|
2525
|
+
return this.adapter.isAuthorizedSigner(this.address, this.devicePubkey);
|
|
2526
|
+
}
|
|
2527
|
+
/**
|
|
2528
|
+
* From a brand-new browser (status `needs-device-approval`), use the user's
|
|
2529
|
+
* synced passkey to authorize adding THIS device — no trip back to an already-
|
|
2530
|
+
* authorized device.
|
|
2531
|
+
*
|
|
2532
|
+
* `add_signer_via_passkey` is a public external authorized by the embedded
|
|
2533
|
+
* WebAuthn assertion (no device signature), so by default we sponsor it through
|
|
2534
|
+
* the Cavos paymaster's `paymaster_executeDirectTransaction` (the forwarder's
|
|
2535
|
+
* `execute_sponsored` runs a generic call — it does NOT require SNIP-9). Pass a
|
|
2536
|
+
* custom `submit` to route it through your own relayer instead. Returns the tx.
|
|
2537
|
+
*/
|
|
2538
|
+
async approveThisDeviceWithPasskey(opts) {
|
|
2539
|
+
if (this.status === "ready") {
|
|
2540
|
+
throw new Error("kit: this device is already an authorized signer");
|
|
2541
|
+
}
|
|
2542
|
+
const { leaf, nonce } = await this.passkeyLeafForThisDevice();
|
|
2543
|
+
const leaves = [leaf];
|
|
2544
|
+
const assertion = await opts.passkey.assert(batchChallenge(leaves));
|
|
2545
|
+
return this.submitPasskeyApproval(assertion, leaves, 0, nonce, opts.submit);
|
|
2546
|
+
}
|
|
2547
|
+
/** This device's leaf + the current passkey nonce, for a (possibly multi-chain)
|
|
2548
|
+
* passkey approval batch. See `approveDeviceEverywhere`. */
|
|
2549
|
+
async passkeyLeafForThisDevice() {
|
|
2550
|
+
const nonce = await this.adapter.getPasskeyNonce(this.address);
|
|
2551
|
+
return { leaf: this.adapter.passkeyLeaf(this.devicePubkey, nonce), nonce };
|
|
2552
|
+
}
|
|
2553
|
+
/** Submit `add_signer_via_passkey` given a (shared) assertion + this chain's
|
|
2554
|
+
* position in the batch. The assertion doesn't carry the passkey pubkey, so we
|
|
2555
|
+
* recover both candidates and pick the enrolled approver via the on-chain view
|
|
2556
|
+
* (no backend). Defaults to sponsoring through the paymaster. */
|
|
2557
|
+
async submitPasskeyApproval(assertion, leaves, leafIndex, nonce, submit) {
|
|
2558
|
+
const digest = webauthnDigest(assertion.authenticatorData, assertion.clientDataJSON);
|
|
2559
|
+
const candidates = recoverCandidatePublicKeys(assertion.r, assertion.s, digest);
|
|
2560
|
+
let yParity = null;
|
|
2561
|
+
for (const cand of candidates) {
|
|
2562
|
+
if (await this.adapter.isApprover(this.address, cand.publicKey)) {
|
|
2563
|
+
yParity = cand.yParity;
|
|
2564
|
+
break;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
if (yParity === null) {
|
|
2568
|
+
throw new Error("kit: this passkey is not a registered approver of the wallet");
|
|
2569
|
+
}
|
|
2570
|
+
const call = this.adapter.buildAddSignerViaPasskey(
|
|
2571
|
+
this.address,
|
|
2572
|
+
this.devicePubkey,
|
|
2573
|
+
nonce,
|
|
2574
|
+
leaves,
|
|
2575
|
+
leafIndex,
|
|
2576
|
+
assertion,
|
|
2577
|
+
yParity
|
|
2578
|
+
);
|
|
2579
|
+
if (submit) return submit(call);
|
|
2580
|
+
if (!this.paymaster) {
|
|
2581
|
+
throw new Error("kit: no paymaster configured \u2014 pass a `submit` relayer to approveThisDeviceWithPasskey");
|
|
2582
|
+
}
|
|
2583
|
+
return paymasterExecuteDirect(this.paymaster, this.address, call);
|
|
2584
|
+
}
|
|
1449
2585
|
/**
|
|
1450
2586
|
* Register a self-custodial backup signer derived from `code`, so the account
|
|
1451
2587
|
* can be recovered after the user loses every device. Idempotent: if the
|
|
@@ -1487,7 +2623,7 @@ var Cavos = class _Cavos {
|
|
|
1487
2623
|
const backup = BackupSigner.fromCode(opts.code);
|
|
1488
2624
|
const backupAdapter = new StarknetAdapter({ classHash, signer: backup, provider });
|
|
1489
2625
|
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
1490
|
-
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network }) :
|
|
2626
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network }) : defaultRegistry3);
|
|
1491
2627
|
const existing = await registry.lookup(opts.identity.userId);
|
|
1492
2628
|
if (!existing) {
|
|
1493
2629
|
throw new Error("kit: no account found for this identity \u2014 nothing to recover");
|
|
@@ -1522,7 +2658,7 @@ var Cavos = class _Cavos {
|
|
|
1522
2658
|
return new _Cavos(opts.identity, existing.address, "ready", account, adapter, devicePubkey);
|
|
1523
2659
|
}
|
|
1524
2660
|
};
|
|
1525
|
-
var
|
|
2661
|
+
var defaultRegistry3 = new InMemoryWalletRegistry();
|
|
1526
2662
|
var DEVICE_REQUEST_DEDUP_MS = 5 * 60 * 1e3;
|
|
1527
2663
|
var lastDeviceRequest = /* @__PURE__ */ new Map();
|
|
1528
2664
|
async function isDeployed(provider, address) {
|
|
@@ -1533,6 +2669,40 @@ async function isDeployed(provider, address) {
|
|
|
1533
2669
|
return false;
|
|
1534
2670
|
}
|
|
1535
2671
|
}
|
|
2672
|
+
async function paymasterExecuteDirect(paymaster, userAddress, call) {
|
|
2673
|
+
const body = {
|
|
2674
|
+
jsonrpc: "2.0",
|
|
2675
|
+
id: 1,
|
|
2676
|
+
method: "paymaster_executeDirectTransaction",
|
|
2677
|
+
params: {
|
|
2678
|
+
transaction: {
|
|
2679
|
+
type: "invoke",
|
|
2680
|
+
invoke: {
|
|
2681
|
+
user_address: userAddress,
|
|
2682
|
+
execute_from_outside_call: {
|
|
2683
|
+
to: call.contractAddress,
|
|
2684
|
+
selector: starknet.hash.getSelectorFromName(call.entrypoint),
|
|
2685
|
+
calldata: call.calldata.map((c) => starknet.num.toHex(c))
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
},
|
|
2689
|
+
parameters: { version: "0x1", fee_mode: { mode: "sponsored" } }
|
|
2690
|
+
}
|
|
2691
|
+
};
|
|
2692
|
+
const res = await fetch(paymaster.url, {
|
|
2693
|
+
method: "POST",
|
|
2694
|
+
headers: {
|
|
2695
|
+
"Content-Type": "application/json",
|
|
2696
|
+
...paymaster.apiKey ? { "x-paymaster-api-key": paymaster.apiKey } : {}
|
|
2697
|
+
},
|
|
2698
|
+
body: JSON.stringify(body)
|
|
2699
|
+
});
|
|
2700
|
+
const json = await res.json();
|
|
2701
|
+
if (json.error) {
|
|
2702
|
+
throw new Error(`kit: paymaster passkey approval failed: ${JSON.stringify(json.error)}`);
|
|
2703
|
+
}
|
|
2704
|
+
return { transactionHash: json.result?.transaction_hash ?? json.result?.tracking_id };
|
|
2705
|
+
}
|
|
1536
2706
|
var CavosAuth = class {
|
|
1537
2707
|
constructor(opts = {}) {
|
|
1538
2708
|
this.opts = opts;
|
|
@@ -1670,6 +2840,90 @@ function bytesToChunks(bytes) {
|
|
|
1670
2840
|
for (const b of bytes.subarray(0, 31)) w = w << 8n | BigInt(b);
|
|
1671
2841
|
return w;
|
|
1672
2842
|
}
|
|
2843
|
+
var PasskeySigner = class {
|
|
2844
|
+
constructor(opts = {}) {
|
|
2845
|
+
if (typeof window === "undefined" || !navigator.credentials) {
|
|
2846
|
+
throw new Error("kit/passkey: WebAuthn is only available in a browser");
|
|
2847
|
+
}
|
|
2848
|
+
this.rpId = opts.rpId ?? window.location.hostname;
|
|
2849
|
+
this.rpName = opts.rpName ?? this.rpId;
|
|
2850
|
+
if (isIpAddress(this.rpId)) {
|
|
2851
|
+
throw new Error(
|
|
2852
|
+
`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.)`
|
|
2853
|
+
);
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
/** True if this platform advertises a usable passkey (platform authenticator). */
|
|
2857
|
+
static async isSupported() {
|
|
2858
|
+
if (typeof window === "undefined" || !window.PublicKeyCredential) return false;
|
|
2859
|
+
try {
|
|
2860
|
+
return await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
|
2861
|
+
} catch {
|
|
2862
|
+
return false;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
/** Create a new synced passkey and return its P-256 public key. */
|
|
2866
|
+
async enroll(params) {
|
|
2867
|
+
const challenge = crypto.getRandomValues(new Uint8Array(32));
|
|
2868
|
+
const cred = await navigator.credentials.create({
|
|
2869
|
+
publicKey: {
|
|
2870
|
+
challenge: buf(challenge),
|
|
2871
|
+
rp: { id: this.rpId, name: this.rpName },
|
|
2872
|
+
user: {
|
|
2873
|
+
id: buf(userHandle(params.userId)),
|
|
2874
|
+
name: params.userName,
|
|
2875
|
+
displayName: params.displayName ?? params.userName
|
|
2876
|
+
},
|
|
2877
|
+
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
|
|
2878
|
+
// ES256 (P-256)
|
|
2879
|
+
authenticatorSelection: {
|
|
2880
|
+
residentKey: "required",
|
|
2881
|
+
requireResidentKey: true,
|
|
2882
|
+
userVerification: "preferred"
|
|
2883
|
+
},
|
|
2884
|
+
attestation: "none"
|
|
2885
|
+
}
|
|
2886
|
+
});
|
|
2887
|
+
if (!cred) throw new Error("kit/passkey: enrollment cancelled");
|
|
2888
|
+
const response = cred.response;
|
|
2889
|
+
const spki = new Uint8Array(response.getPublicKey());
|
|
2890
|
+
return { publicKey: spkiToPublicKey(spki), credentialId: new Uint8Array(cred.rawId) };
|
|
2891
|
+
}
|
|
2892
|
+
/**
|
|
2893
|
+
* Produce a WebAuthn assertion over `challenge` (a 32-byte value the caller
|
|
2894
|
+
* derives from the signer being added + the on-chain nonce). Uses discoverable
|
|
2895
|
+
* credentials — no `allowCredentials` — so it works on a brand-new browser.
|
|
2896
|
+
*/
|
|
2897
|
+
async assert(challenge) {
|
|
2898
|
+
const cred = await navigator.credentials.get({
|
|
2899
|
+
publicKey: {
|
|
2900
|
+
challenge: buf(challenge),
|
|
2901
|
+
rpId: this.rpId,
|
|
2902
|
+
allowCredentials: [],
|
|
2903
|
+
userVerification: "preferred"
|
|
2904
|
+
}
|
|
2905
|
+
});
|
|
2906
|
+
if (!cred) throw new Error("kit/passkey: assertion cancelled");
|
|
2907
|
+
const response = cred.response;
|
|
2908
|
+
const authenticatorData = new Uint8Array(response.authenticatorData);
|
|
2909
|
+
const clientDataJSON = new Uint8Array(response.clientDataJSON);
|
|
2910
|
+
const { r, s } = derToRs(new Uint8Array(response.signature));
|
|
2911
|
+
const challengeOffset = challengeOffsetOf(clientDataJSON, base64urlEncode(challenge));
|
|
2912
|
+
return { authenticatorData, clientDataJSON, r, s, challengeOffset };
|
|
2913
|
+
}
|
|
2914
|
+
};
|
|
2915
|
+
function isIpAddress(host) {
|
|
2916
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true;
|
|
2917
|
+
if (host.includes(":")) return true;
|
|
2918
|
+
return false;
|
|
2919
|
+
}
|
|
2920
|
+
function userHandle(userId) {
|
|
2921
|
+
const bytes = new TextEncoder().encode(userId);
|
|
2922
|
+
return bytes.length <= 64 ? bytes : sha256.sha256(bytes);
|
|
2923
|
+
}
|
|
2924
|
+
function buf(bytes) {
|
|
2925
|
+
return bytes.slice();
|
|
2926
|
+
}
|
|
1673
2927
|
var cavosLogoBase64 = "iVBORw0KGgoAAAANSUhEUgAAADMAAAA/CAYAAABNY/BRAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAlZSURBVHgB7VpbaCRZGf6r6lR3VV+STieZTBKCmZlO59LDisZBR2Q3D8v6MMrCgswirIqi4LI+7OKyIDp4YUAUXWFdxBurIIw+rcIgroMsKqvoMATdzSbpJEwmM8nk0rn3pbrrVJ39T3X3TE/S6a5TXVn2Yb88hKquc/nPOd/3X6oAjgfK4ODA47FY7AO1N9vb20/395+8wH+HY4AM/kOJx+PhcLjtjzs7O5naHyRJ2orFOq/29PRoeEnAZ/htDJ+g1dvbu8mYDWjUAztg27bM73d1da7jJQWfDfLTGN4XHR4aulE0siDhXz3g7oBRyAWHh5P/gbJBvs3Br474zO2hoTM/YGB9CJhEGj8sKcyi5xKJ05fwkoFP8MsY5dTAwMOKrDxvW7aryeGRYyoh3xkYOPkR8EkQ/DBGQUIH9Wjk79Qsihwb2TRN2haN/7evry+I1yq0iFaNcQjf1dW1VTLyaIgkRmjGSNHI0Y6O2BpemdCiILRiDOcJ5UQuGnlZ2JD73RAUBB37eQNaFASvDR2pQuX6LrOtc3yFoQU4gmDT88OJxAvQgiB4NUbp7+//mCTL37Ityxc14sIhK/L3sd8PgkdB8GIMSSQSSizW/i9qGn76CZlSk8ba2ycHBwe5GAjvtuhE+AA0GAzs4jmn3nlSH6wiCOGwvgpl/gjtkIgxFcIPv1EyCorksyE1wxAjX4jgOK/jhQUCc3T7oEP4kWTiG8w2P85aJHzTwSTggjCRTJx+FgQEwa0xysDAwDhI8mUkqg3vAlBYbELUH+O4KXDJHzfGkPHxcaktGrlOqZCHh62trXzt9d7eXl5AeOWSWaJtbdE3K2M2NajZxBzCF4vGtsE9PHPPE9u2SlAmcS0MBswEt8DjbOSzNJVKuRKERsZUCD/0ulHIB0UJbzN2F+rOj22BACRJJsVCrm1kZPg1aCIIDXcGCf8cMHvCi4e3aGmy3n3DMCZBHIptmY8lk4mnGz10lDHE8cSS8iOLWsKEV4gC2dzelXq/FQrGH2RZ3M9y4SGK8jIGtUNwBH/q9UomJiYAPfwkpd48vKIQ3AH7rwCH081CofAnEgiCB8ilokEx3ZipzvPQAweuHcJnMhsbRj5niRC+Bjxcu4PFjB2o4yN2d3e3mc02+XMgDO5Q92lqbKyuINQa4xB+JJm8hoSPYK7uKdhTFEXOZ7PPw9E7Ku/u7b0gKbIEHsAFAZW1A+d5FQ4IwgMDjowkn2GMPuo9pGeUBDT79srK76GB515ZWfl1IKBR/jx4AK4Cr/JcSCROfan2ftUYnvqexf8vWR49PM7cVtQA2d3cPA/l7T/KGN6/sr29M4HcIeDpuOGWWNQOqIFfnThx4hRU+FP1rAxV4v+0VPREePQdVlDTsJ38+J3V1esuJmjh7vwbV/diIKjhKjMvBjmC0N3dNV8ZT3HO7djY2AaG9B2yCE8kpwbG1IAmMQZv5nK5R5aWlvYqHbsJWvjYEp6Iro6O9ms49kOmWUK7mCSSazKwqaZHtqam3u6RRkaGrimy+gnsWiCCtqlZMueyueyVXM74OVcoKB8tC8ThtItEIifa26NfDunhJwMBNQHlhXVnFq5msWS8Vu2sFXhSpWPo5zjq5u/jfRyE1Nt74pOlkrXDPbebBqg2Cs3nd9u7u+cWFxcNqKgSePQXFfCxOdkZqlu4WCwmQqFAhFJ3fWLgit7RjhFJIp09PZ1/wZAd3ELpjGMGLcPIcHLdpMVfLizc+nbNzyJGyRUDdF1XL+l69Cso9zFgFqYQplMMcDUfRYWt7c3POE8nk0OvYL7wOVwbUUVAUQSm6SGZlgqXZ+dufhPKBXA32aTzzdCZMz9Ug8GvY626KsNCqoa7YkuK8ovZ2bmv8obcAHt0dHSxZOT6eSAH4rCxGol9Bm5Mz8ycq/bZ4HnHt6RSo/8rGoWzFW/iKfIgajA9m06P8fbVQcn09PSgFopyQ9zn6PchY/IkmSXjw5gNvgTNnZ2VTCZ/UzGEz8GDj2BUC0WUiiFOjFftxHm/iCnIQEDTVCw6eCWzhOWhZ5ADoQYTlPEtdEwl8ufREE+OEudnBTSdrK2t9UF5l53ou3ZAury8vFIq0aeIQqrqIgqpVCwwXI8XG7S3u7vjLxtGgS+YF2MYUQNKMVd4IpPJ8Be990KoQ6s3Pz//O0lRX2VeXy1gq0i47Slo0B5zmSclt1J1ANiKx6JXFm7dehUO8PKgMU4oPTMz8wQqVAYjcy/Jk2RjIMs/YIDDKy9hQJnCXEQGD7tiO4TXbqfT6c/Cfd90D/XONd82FUPqvrIgiGeD1DQhFNIuwuHdYRgZX3R8iDAY1ZHwqJaDUJb1Q7w+iqR8NGl7e21EDerC2SCeA9D10IV6v4VC4U8xW/gE25jEkY2NjQSUCV93NRpJIl1ZycxZlvk12WWoUwtN00br3VdV9RQIQiEqOuXiF9bX129Cg5yp6STT6YWfoh/9Gy62qFy31bvphCsC4DzBsf+cXlj8LTQRpWbGOIIwOzv7qB7S90X4U6lNawduh0U0Eg2x9VB4Oz03V/0SqiVjOPi2kmyucDKohQlI7g2Kx+OB2utoNKq51jCJEz4s89weKt8bNGvilgsUw30zu7MzjrGQ5/IQL4C4fNRWiUZ2dzcfgkpx0k0jEWJbt1ZWJlGpLskeq5FuwXMrrPE+e+fO2lsgUCQRVSkJQ+3vYQ50ncdHcBzA48Uk5R+z8/M/AdF0AMTgRNgYqX4UHVjRa3n1KOAZxGJixEQP/whUImGR9l7KM06Evbq6hoIQEhIEhzNHsgZDej3EFYtL971IWARea0342iOTzxeyDxPiCIIrYvOsEOprAOPCkslsnp+amuK74ekIt1I4s27evP1Pm9kvYoQg0OwwDVBQMINilzE/4Z86euZiq1VAOZ2ef46QwDSv+YInMCrL5Mbs3ByvH3BL3/WvmqrgR0J9e3o6pelREBcERoN6BGZm07xu4Nl/VeFHfdb5gg/fADiCwIsM7pqhIRhRYI2sEzwS/iD8KjbT/f397b3dvU+rgaCb6j3jL6b2s9nHMLPlX3H44rP8rJyzpeXlq6i9r0CzgNApCsk/w928Bj4Z4vQL/oEbIGOE8EVM6JaOdidYWyJkGiPxp6FO6vteg1NETKVSWHbFkP9BRPn92uf8xHG8oHHez9+9e/esacbjtT9gShBbXV0drYzrayjE8Q6F7uAm3ZgyLQAAAABJRU5ErkJggg==";
|
|
1674
2928
|
var STYLE_ID = "cavos-modal-styles-v2";
|
|
1675
2929
|
function injectStyles() {
|
|
@@ -1773,6 +3027,17 @@ var EmailIcon = () => /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", heig
|
|
|
1773
3027
|
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "4", width: "20", height: "16", rx: "2" }),
|
|
1774
3028
|
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m22 7-8.97 5.7a1.94 1.94 0 01-2.06 0L2 7" })
|
|
1775
3029
|
] });
|
|
3030
|
+
var FingerprintIcon = ({ size = 22, color = "currentColor" }) => /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3031
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 12C2 6.5 6.5 2 12 2a10 10 0 0 1 8 4" }),
|
|
3032
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 19.5C5.5 18 6 15 6 12c0-.7.12-1.37.34-2" }),
|
|
3033
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M17.29 21.02c.12-.6.43-2.3.5-3.02" }),
|
|
3034
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4" }),
|
|
3035
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8.65 22c.21-.66.45-1.32.57-2" }),
|
|
3036
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 13.12c0 2.38 0 6.38-1 8.88" }),
|
|
3037
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 16h.01" }),
|
|
3038
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21.8 16c.2-2 .131-5.354 0-6" }),
|
|
3039
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 6.8a6 6 0 0 1 9 5.2c0 .47 0 1.17-.02 2" })
|
|
3040
|
+
] });
|
|
1776
3041
|
var CavosLogo = ({ invert }) => /* @__PURE__ */ jsxRuntime.jsx("img", { src: `data:image/png;base64,${cavosLogoBase64}`, alt: "Cavos", style: { width: "auto", height: "16px", objectFit: "contain", opacity: 0.55, flexShrink: 0, filter: invert ? "invert(1)" : "none" } });
|
|
1777
3042
|
function Spinner({ size = 16, color = "#888" }) {
|
|
1778
3043
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -1923,7 +3188,11 @@ function CavosAuthModal({
|
|
|
1923
3188
|
authError,
|
|
1924
3189
|
clearAuthError,
|
|
1925
3190
|
resendDeviceApproval,
|
|
1926
|
-
recover
|
|
3191
|
+
recover,
|
|
3192
|
+
passkeySupported,
|
|
3193
|
+
enrollPasskeyDefault,
|
|
3194
|
+
approveDeviceWithPasskey,
|
|
3195
|
+
setupRecovery
|
|
1927
3196
|
} = useCavos();
|
|
1928
3197
|
const isMobile = useIsMobile();
|
|
1929
3198
|
const isLight = theme !== "dark";
|
|
@@ -1951,7 +3220,12 @@ function CavosAuthModal({
|
|
|
1951
3220
|
const [deviceResendBusy, setDeviceResendBusy] = react.useState(false);
|
|
1952
3221
|
const [recoverCode, setRecoverCode] = react.useState("");
|
|
1953
3222
|
const [recoverBusy, setRecoverBusy] = react.useState(false);
|
|
3223
|
+
const [pkBusy, setPkBusy] = react.useState(false);
|
|
3224
|
+
const [pkError, setPkError] = react.useState("");
|
|
3225
|
+
const [savedRecoveryCode, setSavedRecoveryCode] = react.useState("");
|
|
3226
|
+
const [copied, setCopied] = react.useState(false);
|
|
1954
3227
|
const doneHandledRef = react.useRef(false);
|
|
3228
|
+
const secureHandledRef = react.useRef(false);
|
|
1955
3229
|
const closeTimerRef = react.useRef(null);
|
|
1956
3230
|
const otpBoxRefs = react.useRef([null, null, null, null, null, null]);
|
|
1957
3231
|
const setOtpDigit = (i, raw) => {
|
|
@@ -2012,6 +3286,7 @@ function CavosAuthModal({
|
|
|
2012
3286
|
setOtpCode("");
|
|
2013
3287
|
setError("");
|
|
2014
3288
|
doneHandledRef.current = false;
|
|
3289
|
+
secureHandledRef.current = false;
|
|
2015
3290
|
}, 1600);
|
|
2016
3291
|
}, [onClose]);
|
|
2017
3292
|
react.useEffect(() => {
|
|
@@ -2022,13 +3297,26 @@ function CavosAuthModal({
|
|
|
2022
3297
|
doneHandledRef.current = false;
|
|
2023
3298
|
}
|
|
2024
3299
|
if (isAuthenticated && address && walletStatus.isReady) {
|
|
2025
|
-
|
|
3300
|
+
if (walletStatus.isNewAccount && !secureHandledRef.current) {
|
|
3301
|
+
if (screen !== "secure-account" && screen !== "recovery-code" && !doneHandledRef.current) {
|
|
3302
|
+
setScreen("secure-account");
|
|
3303
|
+
}
|
|
3304
|
+
} else {
|
|
3305
|
+
triggerDone(address);
|
|
3306
|
+
}
|
|
2026
3307
|
}
|
|
2027
|
-
if (walletStatus.
|
|
2028
|
-
|
|
2029
|
-
|
|
3308
|
+
if (walletStatus.needsDeviceApproval && // A deploy in progress owns the screen; never fight the deploying branch
|
|
3309
|
+
// above (both flags true would oscillate deploying ↔ approval → loop).
|
|
3310
|
+
!walletStatus.isDeploying && screen !== "recover" && screen !== "device-approval" && screen !== "passkey-approval") {
|
|
3311
|
+
if (walletStatus.hasPasskey && passkeySupported) {
|
|
3312
|
+
setScreen("passkey-approval");
|
|
3313
|
+
doneHandledRef.current = false;
|
|
3314
|
+
} else if (walletStatus.awaitingApproval) {
|
|
3315
|
+
setScreen("device-approval");
|
|
3316
|
+
doneHandledRef.current = false;
|
|
3317
|
+
}
|
|
2030
3318
|
}
|
|
2031
|
-
}, [open, isAuthenticated, address, walletStatus.isReady, walletStatus.isDeploying, walletStatus.awaitingApproval, screen, triggerDone]);
|
|
3319
|
+
}, [open, isAuthenticated, address, walletStatus.isReady, walletStatus.isDeploying, walletStatus.awaitingApproval, walletStatus.needsDeviceApproval, walletStatus.hasPasskey, walletStatus.isNewAccount, passkeySupported, screen, triggerDone]);
|
|
2032
3320
|
react.useEffect(() => () => {
|
|
2033
3321
|
if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
|
|
2034
3322
|
}, []);
|
|
@@ -2038,9 +3326,61 @@ function CavosAuthModal({
|
|
|
2038
3326
|
setEmail("");
|
|
2039
3327
|
setOtpCode("");
|
|
2040
3328
|
setError("");
|
|
3329
|
+
setPkError("");
|
|
3330
|
+
setSavedRecoveryCode("");
|
|
3331
|
+
setCopied(false);
|
|
2041
3332
|
doneHandledRef.current = false;
|
|
3333
|
+
secureHandledRef.current = false;
|
|
2042
3334
|
onClose();
|
|
2043
3335
|
};
|
|
3336
|
+
const finishSecureStep = () => {
|
|
3337
|
+
secureHandledRef.current = true;
|
|
3338
|
+
if (address) triggerDone(address);
|
|
3339
|
+
};
|
|
3340
|
+
const handleSetupPasskey = async () => {
|
|
3341
|
+
setPkBusy(true);
|
|
3342
|
+
setPkError("");
|
|
3343
|
+
try {
|
|
3344
|
+
await enrollPasskeyDefault();
|
|
3345
|
+
finishSecureStep();
|
|
3346
|
+
} catch (e) {
|
|
3347
|
+
setPkError(e instanceof Error ? e.message : "We couldn't set up your passkey. Try again.");
|
|
3348
|
+
} finally {
|
|
3349
|
+
setPkBusy(false);
|
|
3350
|
+
}
|
|
3351
|
+
};
|
|
3352
|
+
const handleSaveRecovery = async () => {
|
|
3353
|
+
setPkBusy(true);
|
|
3354
|
+
setPkError("");
|
|
3355
|
+
try {
|
|
3356
|
+
const code = await setupRecovery();
|
|
3357
|
+
setSavedRecoveryCode(code);
|
|
3358
|
+
setScreen("recovery-code");
|
|
3359
|
+
} catch (e) {
|
|
3360
|
+
setPkError(e instanceof Error ? e.message : "We couldn't create your recovery phrase. Try again.");
|
|
3361
|
+
} finally {
|
|
3362
|
+
setPkBusy(false);
|
|
3363
|
+
}
|
|
3364
|
+
};
|
|
3365
|
+
const handleCopyRecovery = async () => {
|
|
3366
|
+
try {
|
|
3367
|
+
await navigator.clipboard.writeText(savedRecoveryCode);
|
|
3368
|
+
setCopied(true);
|
|
3369
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
3370
|
+
} catch {
|
|
3371
|
+
}
|
|
3372
|
+
};
|
|
3373
|
+
const handlePasskeyApprove = async () => {
|
|
3374
|
+
setPkBusy(true);
|
|
3375
|
+
setPkError("");
|
|
3376
|
+
try {
|
|
3377
|
+
await approveDeviceWithPasskey();
|
|
3378
|
+
setPkBusy(false);
|
|
3379
|
+
} catch (e) {
|
|
3380
|
+
setPkError(e instanceof Error ? e.message : "We couldn't verify your passkey. Try again or use email.");
|
|
3381
|
+
setPkBusy(false);
|
|
3382
|
+
}
|
|
3383
|
+
};
|
|
2044
3384
|
const handleOAuth = async (provider) => {
|
|
2045
3385
|
setError("");
|
|
2046
3386
|
setBusy(true);
|
|
@@ -2131,6 +3471,85 @@ function CavosAuthModal({
|
|
|
2131
3471
|
setDeviceResendBusy(false);
|
|
2132
3472
|
}
|
|
2133
3473
|
};
|
|
3474
|
+
if (screen === "secure-account") {
|
|
3475
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: overlay, "data-cavos-theme": theme, role: "dialog", "aria-modal": true, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { ...card, position: "relative" }, children: [
|
|
3476
|
+
isMobile && /* @__PURE__ */ jsxRuntime.jsx("div", { style: handle }),
|
|
3477
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: isMobile ? "28px 24px 28px" : "44px 24px 28px", textAlign: "center" }, children: [
|
|
3478
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { width: 48, height: 48, borderRadius: "50%", margin: "0 auto 16px", background: isLight ? "rgba(0,0,0,0.04)" : "rgba(255,255,255,0.06)", border: `1px solid ${isLight ? "rgba(0,0,0,0.08)" : "rgba(255,255,255,0.1)"}`, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: textColor, strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3479
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" }),
|
|
3480
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 12l2 2 4-4" })
|
|
3481
|
+
] }) }),
|
|
3482
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { style: { margin: "0 0 8px", fontSize: "17px", fontWeight: 600, color: textColor, letterSpacing: "-0.02em" }, children: "Keep your account safe" }),
|
|
3483
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "0 0 24px", fontSize: "13px", color: subTextColor, lineHeight: 1.55 }, children: "Set up a passkey so you can sign in on a new phone or computer in one tap. It takes a few seconds." }),
|
|
3484
|
+
pkError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { background: errBg, border: `1px solid ${errBorder}`, borderRadius: "10px", padding: "9px 13px", fontSize: "13px", color: errColor, marginBottom: "14px", textAlign: "left" }, children: pkError }),
|
|
3485
|
+
passkeySupported ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3486
|
+
/* @__PURE__ */ jsxRuntime.jsxs("button", { className: "cavos-primary-btn", style: { width: "100%", padding: "13px", borderRadius: "12px", border: "none", background: primaryColor, color: "#fff", fontSize: "14px", fontWeight: 600, cursor: pkBusy ? "default" : "pointer", fontFamily: "inherit", display: "flex", alignItems: "center", justifyContent: "center", gap: "9px", opacity: pkBusy ? 0.65 : 1, transition: "opacity 0.15s, transform 0.1s" }, onClick: handleSetupPasskey, disabled: pkBusy, children: [
|
|
3487
|
+
pkBusy ? /* @__PURE__ */ jsxRuntime.jsx(Spinner, { size: 16, color: "#fff" }) : /* @__PURE__ */ jsxRuntime.jsx(FingerprintIcon, { size: 18, color: "#fff" }),
|
|
3488
|
+
pkBusy ? "Setting up\u2026" : "Set up a passkey"
|
|
3489
|
+
] }),
|
|
3490
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "10px 0 0", fontSize: "12px", color: subTextColor, lineHeight: 1.5 }, children: "Uses Face ID, Touch ID, or your device PIN." }),
|
|
3491
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: "12px", margin: "18px 0" }, children: [
|
|
3492
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cavos-divider-line", style: { background: isLight ? "rgba(0,0,0,0.08)" : "rgba(255,255,255,0.1)" } }),
|
|
3493
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "11px", color: subTextColor, textTransform: "uppercase", letterSpacing: "0.06em" }, children: "or" }),
|
|
3494
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cavos-divider-line", style: { background: isLight ? "rgba(0,0,0,0.08)" : "rgba(255,255,255,0.1)" } })
|
|
3495
|
+
] }),
|
|
3496
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "cavos-submit-btn", style: { width: "100%", padding: "12px", borderRadius: "12px", border: inputBorder, background: "transparent", color: textColor, fontSize: "14px", fontWeight: 500, cursor: pkBusy ? "default" : "pointer", fontFamily: "inherit", transition: "background 0.15s", opacity: pkBusy ? 0.6 : 1 }, onClick: handleSaveRecovery, disabled: pkBusy, children: "Save a recovery phrase instead" })
|
|
3497
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("button", { className: "cavos-primary-btn", style: { width: "100%", padding: "13px", borderRadius: "12px", border: "none", background: primaryColor, color: "#fff", fontSize: "14px", fontWeight: 600, cursor: pkBusy ? "default" : "pointer", fontFamily: "inherit", display: "flex", alignItems: "center", justifyContent: "center", gap: "9px", opacity: pkBusy ? 0.65 : 1, transition: "opacity 0.15s" }, onClick: handleSaveRecovery, disabled: pkBusy, children: [
|
|
3498
|
+
pkBusy && /* @__PURE__ */ jsxRuntime.jsx(Spinner, { size: 16, color: "#fff" }),
|
|
3499
|
+
pkBusy ? "Setting up\u2026" : "Save a recovery phrase"
|
|
3500
|
+
] }),
|
|
3501
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "cavos-sub-btn", style: { background: "none", border: "none", cursor: pkBusy ? "default" : "pointer", fontSize: "13px", color: subTextColor, width: "100%", textAlign: "center", padding: "18px 0 0", fontFamily: "inherit", transition: "color 0.15s", opacity: pkBusy ? 0.6 : 1 }, onClick: finishSecureStep, disabled: pkBusy, children: "Skip for now" })
|
|
3502
|
+
] }),
|
|
3503
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: footer, children: [
|
|
3504
|
+
/* @__PURE__ */ jsxRuntime.jsx(CavosLogo, { invert: !isLight }),
|
|
3505
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Secured by Cavos" })
|
|
3506
|
+
] })
|
|
3507
|
+
] }) });
|
|
3508
|
+
}
|
|
3509
|
+
if (screen === "recovery-code") {
|
|
3510
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: overlay, "data-cavos-theme": theme, role: "dialog", "aria-modal": true, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { ...card, position: "relative" }, children: [
|
|
3511
|
+
isMobile && /* @__PURE__ */ jsxRuntime.jsx("div", { style: handle }),
|
|
3512
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: isMobile ? "28px 24px 28px" : "44px 24px 28px", textAlign: "center" }, children: [
|
|
3513
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { style: { margin: "0 0 8px", fontSize: "17px", fontWeight: 600, color: textColor, letterSpacing: "-0.02em" }, children: "Save your recovery phrase" }),
|
|
3514
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "0 0 18px", fontSize: "13px", color: subTextColor, lineHeight: 1.55 }, children: "Write this down and keep it somewhere safe. It's the only way to get back in if you lose your devices, and we can't recover it for you." }),
|
|
3515
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { background: isLight ? "rgba(0,0,0,0.03)" : "rgba(255,255,255,0.05)", border: `1px solid ${isLight ? "rgba(0,0,0,0.08)" : "rgba(255,255,255,0.1)"}`, borderRadius: "12px", padding: "16px", marginBottom: "12px", fontSize: "14px", fontWeight: 500, color: textColor, wordBreak: "break-word", lineHeight: 1.6, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", textAlign: "center" }, children: savedRecoveryCode }),
|
|
3516
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "cavos-provider", style: { ...pBtn, marginBottom: "10px" }, onClick: handleCopyRecovery, children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: copied ? "Copied \u2713" : "Copy to clipboard" }) }),
|
|
3517
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "cavos-primary-btn", style: { width: "100%", padding: "12px", borderRadius: "12px", border: "none", background: primaryColor, color: "#fff", fontSize: "14px", fontWeight: 500, cursor: "pointer", fontFamily: "inherit", transition: "opacity 0.15s" }, onClick: finishSecureStep, children: "I've saved it" })
|
|
3518
|
+
] }),
|
|
3519
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: footer, children: [
|
|
3520
|
+
/* @__PURE__ */ jsxRuntime.jsx(CavosLogo, { invert: !isLight }),
|
|
3521
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Secured by Cavos" })
|
|
3522
|
+
] })
|
|
3523
|
+
] }) });
|
|
3524
|
+
}
|
|
3525
|
+
if (screen === "passkey-approval") {
|
|
3526
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: overlay, "data-cavos-theme": theme, role: "dialog", "aria-modal": true, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { ...card, position: "relative" }, children: [
|
|
3527
|
+
isMobile && /* @__PURE__ */ jsxRuntime.jsx("div", { style: handle }),
|
|
3528
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "cavos-close", style: close, onClick: handleClose, "aria-label": "Close", children: /* @__PURE__ */ jsxRuntime.jsx(CloseX, {}) }),
|
|
3529
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: isMobile ? "28px 24px 32px" : "48px 24px 32px", textAlign: "center" }, children: [
|
|
3530
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { width: 48, height: 48, borderRadius: "50%", margin: "0 auto 16px", background: isLight ? "rgba(0,0,0,0.04)" : "rgba(255,255,255,0.06)", border: `1px solid ${isLight ? "rgba(0,0,0,0.08)" : "rgba(255,255,255,0.1)"}`, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx(FingerprintIcon, { size: 24, color: textColor }) }),
|
|
3531
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { style: { margin: "0 0 8px", fontSize: "17px", fontWeight: 600, color: textColor, letterSpacing: "-0.02em" }, children: "Verify it's you" }),
|
|
3532
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "0 0 20px", fontSize: "13px", color: subTextColor, lineHeight: 1.55 }, children: "Confirm with Face ID, Touch ID, or your device PIN to add this device to your account." }),
|
|
3533
|
+
pkError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { background: errBg, border: `1px solid ${errBorder}`, borderRadius: "10px", padding: "9px 13px", fontSize: "13px", color: errColor, marginBottom: "14px" }, children: pkError }),
|
|
3534
|
+
/* @__PURE__ */ jsxRuntime.jsxs("button", { className: "cavos-primary-btn", style: { width: "100%", padding: "12px", borderRadius: "12px", border: "none", background: primaryColor, color: "#fff", fontSize: "14px", fontWeight: 500, cursor: pkBusy ? "default" : "pointer", fontFamily: "inherit", display: "flex", alignItems: "center", justifyContent: "center", gap: "8px", opacity: pkBusy ? 0.65 : 1 }, onClick: handlePasskeyApprove, disabled: pkBusy, children: [
|
|
3535
|
+
pkBusy && /* @__PURE__ */ jsxRuntime.jsx(Spinner, { size: 15, color: "#fff" }),
|
|
3536
|
+
pkBusy ? "Verifying\u2026" : "Continue with passkey"
|
|
3537
|
+
] }),
|
|
3538
|
+
walletStatus.awaitingApproval && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "cavos-sub-btn", style: { background: "none", border: "none", cursor: "pointer", fontSize: "13px", color: subTextColor, width: "100%", textAlign: "center", padding: "16px 0 0", fontFamily: "inherit", transition: "color 0.15s" }, onClick: () => {
|
|
3539
|
+
setScreen("device-approval");
|
|
3540
|
+
setPkError("");
|
|
3541
|
+
}, children: "Approve by email instead" }),
|
|
3542
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "cavos-sub-btn", style: { background: "none", border: "none", cursor: "pointer", fontSize: "13px", color: subTextColor, width: "100%", textAlign: "center", padding: `${walletStatus.awaitingApproval ? "10px" : "16px"} 0 0`, fontFamily: "inherit", transition: "color 0.15s" }, onClick: () => {
|
|
3543
|
+
setScreen("recover");
|
|
3544
|
+
setPkError("");
|
|
3545
|
+
}, children: "Use a recovery phrase" })
|
|
3546
|
+
] }),
|
|
3547
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: footer, children: [
|
|
3548
|
+
/* @__PURE__ */ jsxRuntime.jsx(CavosLogo, { invert: !isLight }),
|
|
3549
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Secured by Cavos" })
|
|
3550
|
+
] })
|
|
3551
|
+
] }) });
|
|
3552
|
+
}
|
|
2134
3553
|
if (screen === "device-approval") {
|
|
2135
3554
|
const expired = walletStatus.awaitingApproval && !walletStatus.pendingRequestId;
|
|
2136
3555
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: overlay, "data-cavos-theme": theme, role: "dialog", "aria-modal": true, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { ...card, position: "relative" }, children: [
|
|
@@ -2190,7 +3609,7 @@ function CavosAuthModal({
|
|
|
2190
3609
|
}, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { ...card, position: "relative" }, children: [
|
|
2191
3610
|
isMobile && /* @__PURE__ */ jsxRuntime.jsx("div", { style: handle }),
|
|
2192
3611
|
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "cavos-close", style: { ...close, left: "16px", right: "auto" }, onClick: () => {
|
|
2193
|
-
setScreen("device-approval");
|
|
3612
|
+
setScreen(walletStatus.hasPasskey && passkeySupported ? "passkey-approval" : "device-approval");
|
|
2194
3613
|
setError("");
|
|
2195
3614
|
setRecoverCode("");
|
|
2196
3615
|
}, "aria-label": "Back", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 12H5M12 5l-7 7 7 7" }) }) }),
|
|
@@ -2429,23 +3848,36 @@ var INITIAL_STATUS = {
|
|
|
2429
3848
|
isReady: false,
|
|
2430
3849
|
needsDeviceApproval: false,
|
|
2431
3850
|
awaitingApproval: false,
|
|
2432
|
-
pendingRequestId: null
|
|
3851
|
+
pendingRequestId: null,
|
|
3852
|
+
hasPasskey: false,
|
|
3853
|
+
isNewAccount: false
|
|
2433
3854
|
};
|
|
2434
3855
|
function CavosProvider({ config, modal, children }) {
|
|
2435
3856
|
const [auth] = react.useState(
|
|
2436
3857
|
() => new CavosAuth({ appId: config.appId, backendUrl: config.authBackendUrl })
|
|
2437
3858
|
);
|
|
2438
|
-
const [
|
|
3859
|
+
const [wallet, setWallet] = react.useState(null);
|
|
2439
3860
|
const [identity, setIdentity] = react.useState(null);
|
|
2440
3861
|
const [walletStatus, setWalletStatus] = react.useState(INITIAL_STATUS);
|
|
2441
3862
|
const [isLoading, setIsLoading] = react.useState(false);
|
|
2442
3863
|
const [authError, setAuthError] = react.useState(null);
|
|
2443
3864
|
const [modalOpen, setModalOpen] = react.useState(false);
|
|
3865
|
+
const [passkeySupported, setPasskeySupported] = react.useState(false);
|
|
2444
3866
|
const [branding, setBranding] = react.useState({});
|
|
3867
|
+
react.useEffect(() => {
|
|
3868
|
+
let cancelled = false;
|
|
3869
|
+
PasskeySigner.isSupported().then((ok) => {
|
|
3870
|
+
if (!cancelled) setPasskeySupported(ok);
|
|
3871
|
+
}).catch(() => {
|
|
3872
|
+
});
|
|
3873
|
+
return () => {
|
|
3874
|
+
cancelled = true;
|
|
3875
|
+
};
|
|
3876
|
+
}, []);
|
|
2445
3877
|
const configRef = react.useRef(config);
|
|
2446
3878
|
react.useEffect(() => {
|
|
2447
3879
|
configRef.current = config;
|
|
2448
|
-
}
|
|
3880
|
+
});
|
|
2449
3881
|
react.useEffect(() => {
|
|
2450
3882
|
if (!config.appId || typeof window === "undefined") return;
|
|
2451
3883
|
const base = config.authBackendUrl ?? "https://cavos.xyz";
|
|
@@ -2459,41 +3891,52 @@ function CavosProvider({ config, modal, children }) {
|
|
|
2459
3891
|
const closeModal = react.useCallback(() => setModalOpen(false), []);
|
|
2460
3892
|
const clearAuthError = react.useCallback(() => setAuthError(null), []);
|
|
2461
3893
|
const resendDeviceApproval = react.useCallback(async () => {
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
3894
|
+
const cfg = configRef.current;
|
|
3895
|
+
if (!identity || !wallet || wallet.chain !== "starknet" || !wallet.pendingRequestId) return;
|
|
3896
|
+
const backendUrl = cfg.authBackendUrl ?? "https://cavos.xyz";
|
|
3897
|
+
if (!cfg.appId) return;
|
|
3898
|
+
const recovery = new HttpRecoveryClient({ baseUrl: backendUrl, appId: cfg.appId });
|
|
2466
3899
|
await recovery.requestDeviceAddition({
|
|
2467
3900
|
userId: identity.userId,
|
|
2468
|
-
accountAddress:
|
|
2469
|
-
newSigner:
|
|
3901
|
+
accountAddress: wallet.address,
|
|
3902
|
+
newSigner: wallet.publicKey,
|
|
2470
3903
|
...identity.email ? { email: identity.email } : {}
|
|
2471
3904
|
});
|
|
2472
|
-
}, [identity,
|
|
2473
|
-
const connect = react.useCallback(async (id) => {
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
3905
|
+
}, [identity, wallet]);
|
|
3906
|
+
const connect = react.useCallback(async (id, opts) => {
|
|
3907
|
+
const cfg = configRef.current;
|
|
3908
|
+
if (!opts?.silent) setWalletStatus({ ...INITIAL_STATUS, isDeploying: true });
|
|
3909
|
+
const w = await Cavos.connect({
|
|
3910
|
+
chain: cfg.chain ?? "starknet",
|
|
3911
|
+
network: cfg.network,
|
|
2478
3912
|
identity: id,
|
|
2479
|
-
appSalt:
|
|
2480
|
-
...
|
|
2481
|
-
...
|
|
2482
|
-
...
|
|
2483
|
-
...
|
|
3913
|
+
appSalt: cfg.appSalt,
|
|
3914
|
+
...cfg.paymasterApiKey ? { paymasterApiKey: cfg.paymasterApiKey } : {},
|
|
3915
|
+
...cfg.appId ? { appId: cfg.appId } : {},
|
|
3916
|
+
...cfg.authBackendUrl ? { backendUrl: cfg.authBackendUrl } : {},
|
|
3917
|
+
...cfg.rpcUrl ? { rpcUrl: cfg.rpcUrl } : {}
|
|
2484
3918
|
});
|
|
2485
|
-
|
|
3919
|
+
setWallet(w);
|
|
2486
3920
|
setIdentity(id);
|
|
2487
|
-
const pendingRequestId =
|
|
3921
|
+
const pendingRequestId = w.chain === "starknet" ? w.pendingRequestId : null;
|
|
3922
|
+
let hasPasskey = false;
|
|
3923
|
+
if (w.status === "needs-device-approval") {
|
|
3924
|
+
try {
|
|
3925
|
+
hasPasskey = await w.hasPasskey();
|
|
3926
|
+
} catch {
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
2488
3929
|
setWalletStatus({
|
|
2489
3930
|
isDeploying: false,
|
|
2490
|
-
isReady:
|
|
2491
|
-
needsDeviceApproval:
|
|
2492
|
-
awaitingApproval:
|
|
2493
|
-
pendingRequestId
|
|
3931
|
+
isReady: w.status === "ready",
|
|
3932
|
+
needsDeviceApproval: w.status === "needs-device-approval",
|
|
3933
|
+
awaitingApproval: w.status === "needs-device-approval" && !!pendingRequestId,
|
|
3934
|
+
pendingRequestId,
|
|
3935
|
+
hasPasskey,
|
|
3936
|
+
isNewAccount: w.isNewAccount
|
|
2494
3937
|
});
|
|
2495
|
-
modal?.onSuccess?.(
|
|
2496
|
-
return
|
|
3938
|
+
modal?.onSuccess?.(w.address);
|
|
3939
|
+
return w;
|
|
2497
3940
|
}, [modal]);
|
|
2498
3941
|
react.useEffect(() => {
|
|
2499
3942
|
if (typeof window === "undefined") return;
|
|
@@ -2545,57 +3988,122 @@ function CavosProvider({ config, modal, children }) {
|
|
|
2545
3988
|
await connect(id);
|
|
2546
3989
|
}, [auth, connect]);
|
|
2547
3990
|
const execute = react.useCallback(async (calls) => {
|
|
2548
|
-
if (!
|
|
2549
|
-
if (
|
|
3991
|
+
if (!wallet) throw new Error("Not logged in");
|
|
3992
|
+
if (wallet.chain !== "starknet") {
|
|
2550
3993
|
throw new Error(
|
|
2551
|
-
"kit: useCavos().execute(calls) is Starknet-only. On Solana use the `wallet` handle: wallet.execute(amount, dest)."
|
|
3994
|
+
"kit: useCavos().execute(calls) is Starknet-only. On Solana/Stellar use the `wallet` handle: wallet.execute(amount, dest)."
|
|
2552
3995
|
);
|
|
2553
3996
|
}
|
|
2554
|
-
return
|
|
2555
|
-
}, [
|
|
3997
|
+
return wallet.execute(calls);
|
|
3998
|
+
}, [wallet]);
|
|
2556
3999
|
const addSigner = react.useCallback(
|
|
2557
4000
|
async (pubkey) => {
|
|
2558
|
-
if (!
|
|
2559
|
-
if (
|
|
2560
|
-
throw new Error("kit: addSigner via useCavos() is Starknet-only; use the `wallet` handle on
|
|
4001
|
+
if (!wallet) throw new Error("Not logged in");
|
|
4002
|
+
if (wallet.chain !== "starknet") {
|
|
4003
|
+
throw new Error("kit: addSigner via useCavos() is Starknet-only; use the `wallet` handle on other chains.");
|
|
2561
4004
|
}
|
|
2562
|
-
return
|
|
4005
|
+
return wallet.addSigner(pubkey);
|
|
2563
4006
|
},
|
|
2564
|
-
[
|
|
4007
|
+
[wallet]
|
|
2565
4008
|
);
|
|
4009
|
+
const enrollPasskey = react.useCallback(
|
|
4010
|
+
async (passkey, params) => {
|
|
4011
|
+
if (!wallet) throw new Error("Not logged in");
|
|
4012
|
+
return wallet.enrollPasskey(passkey, params);
|
|
4013
|
+
},
|
|
4014
|
+
[wallet]
|
|
4015
|
+
);
|
|
4016
|
+
const rpName = branding.appName ?? modal?.appName ?? "Cavos";
|
|
4017
|
+
const enrollPasskeyDefault = react.useCallback(async () => {
|
|
4018
|
+
if (!wallet || !identity) throw new Error("Not logged in");
|
|
4019
|
+
if (wallet.status !== "ready") throw new Error("kit: no ready device to enroll a passkey on");
|
|
4020
|
+
const passkey = new PasskeySigner({ rpName });
|
|
4021
|
+
await wallet.enrollPasskey(passkey, {
|
|
4022
|
+
userId: identity.userId,
|
|
4023
|
+
userName: identity.email ?? identity.userId,
|
|
4024
|
+
...identity.email ? { displayName: identity.email } : {}
|
|
4025
|
+
});
|
|
4026
|
+
}, [wallet, identity, rpName]);
|
|
4027
|
+
const approveDeviceWithPasskey = react.useCallback(async () => {
|
|
4028
|
+
if (!wallet || !identity) throw new Error("Not logged in");
|
|
4029
|
+
if (wallet.status !== "needs-device-approval") {
|
|
4030
|
+
await connect(identity);
|
|
4031
|
+
return;
|
|
4032
|
+
}
|
|
4033
|
+
const passkey = new PasskeySigner({ rpName });
|
|
4034
|
+
if (wallet.chain === "starknet") {
|
|
4035
|
+
await wallet.approveThisDeviceWithPasskey({ passkey });
|
|
4036
|
+
} else {
|
|
4037
|
+
await wallet.approveThisDeviceWithPasskey(passkey);
|
|
4038
|
+
}
|
|
4039
|
+
setWalletStatus((s) => ({ ...s, isDeploying: true, needsDeviceApproval: false, awaitingApproval: false }));
|
|
4040
|
+
const deadline = Date.now() + 6e4;
|
|
4041
|
+
for (; ; ) {
|
|
4042
|
+
let ready = false;
|
|
4043
|
+
try {
|
|
4044
|
+
ready = await wallet.isReady();
|
|
4045
|
+
} catch {
|
|
4046
|
+
}
|
|
4047
|
+
if (ready) break;
|
|
4048
|
+
if (Date.now() > deadline) {
|
|
4049
|
+
setWalletStatus((s) => ({ ...s, isDeploying: false, needsDeviceApproval: true }));
|
|
4050
|
+
throw new Error(
|
|
4051
|
+
"Your device is being added, but it's taking longer than usual. Please try again in a moment."
|
|
4052
|
+
);
|
|
4053
|
+
}
|
|
4054
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
4055
|
+
}
|
|
4056
|
+
await connect(identity, { silent: true });
|
|
4057
|
+
}, [wallet, identity, rpName, connect]);
|
|
2566
4058
|
const setupRecovery = react.useCallback(async () => {
|
|
2567
|
-
if (!
|
|
4059
|
+
if (!wallet) throw new Error("Not logged in");
|
|
2568
4060
|
const code = generateRecoveryCode();
|
|
2569
|
-
await
|
|
4061
|
+
await wallet.setupRecovery(code);
|
|
2570
4062
|
return code;
|
|
2571
|
-
}, [
|
|
4063
|
+
}, [wallet]);
|
|
2572
4064
|
const recover = react.useCallback(async (code) => {
|
|
2573
4065
|
if (!identity) throw new Error("Sign in first so we know which account to recover.");
|
|
4066
|
+
const cfg = configRef.current;
|
|
2574
4067
|
setAuthError(null);
|
|
2575
4068
|
setWalletStatus({ ...INITIAL_STATUS, isDeploying: true });
|
|
2576
4069
|
try {
|
|
2577
|
-
const chain =
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
4070
|
+
const chain = cfg.chain ?? "starknet";
|
|
4071
|
+
let w;
|
|
4072
|
+
if (chain === "solana") {
|
|
4073
|
+
w = await CavosSolana.recover({
|
|
4074
|
+
code,
|
|
4075
|
+
identity,
|
|
4076
|
+
network: cfg.network === "mainnet" ? "solana-mainnet" : "solana-devnet",
|
|
4077
|
+
appSalt: cfg.appSalt,
|
|
4078
|
+
...cfg.appId ? { appId: cfg.appId } : {},
|
|
4079
|
+
...cfg.authBackendUrl ? { backendUrl: cfg.authBackendUrl } : {},
|
|
4080
|
+
...cfg.rpcUrl ? { rpcUrl: cfg.rpcUrl } : {}
|
|
4081
|
+
});
|
|
4082
|
+
} else if (chain === "stellar") {
|
|
4083
|
+
w = await CavosStellar.recover({
|
|
4084
|
+
code,
|
|
4085
|
+
identity,
|
|
4086
|
+
network: cfg.network === "mainnet" ? "stellar-mainnet" : "stellar-testnet",
|
|
4087
|
+
appSalt: cfg.appSalt,
|
|
4088
|
+
...cfg.appId ? { appId: cfg.appId } : {},
|
|
4089
|
+
...cfg.authBackendUrl ? { backendUrl: cfg.authBackendUrl } : {},
|
|
4090
|
+
...cfg.rpcUrl ? { rpcUrl: cfg.rpcUrl } : {}
|
|
4091
|
+
});
|
|
4092
|
+
} else {
|
|
4093
|
+
w = await Cavos.recover({
|
|
4094
|
+
code,
|
|
4095
|
+
identity,
|
|
4096
|
+
network: cfg.network,
|
|
4097
|
+
appSalt: cfg.appSalt,
|
|
4098
|
+
paymasterApiKey: cfg.paymasterApiKey ?? "",
|
|
4099
|
+
...cfg.appId ? { appId: cfg.appId } : {},
|
|
4100
|
+
...cfg.authBackendUrl ? { backendUrl: cfg.authBackendUrl } : {},
|
|
4101
|
+
...cfg.rpcUrl ? { rpcUrl: cfg.rpcUrl } : {}
|
|
4102
|
+
});
|
|
4103
|
+
}
|
|
4104
|
+
setWallet(w);
|
|
2597
4105
|
setWalletStatus({ ...INITIAL_STATUS, isReady: true });
|
|
2598
|
-
modal?.onSuccess?.(
|
|
4106
|
+
modal?.onSuccess?.(w.address);
|
|
2599
4107
|
} catch (e) {
|
|
2600
4108
|
const msg = e instanceof Error ? e.message : "Recovery failed. Check your code and try again.";
|
|
2601
4109
|
setAuthError(msg);
|
|
@@ -2605,9 +4113,10 @@ function CavosProvider({ config, modal, children }) {
|
|
|
2605
4113
|
}, [identity, modal]);
|
|
2606
4114
|
react.useEffect(() => {
|
|
2607
4115
|
if (!walletStatus.awaitingApproval || !walletStatus.pendingRequestId || !identity) return;
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
const
|
|
4116
|
+
const cfg = configRef.current;
|
|
4117
|
+
if (!cfg.appId) return;
|
|
4118
|
+
const backendUrl = cfg.authBackendUrl ?? "https://cavos.xyz";
|
|
4119
|
+
const recovery = new HttpRecoveryClient({ baseUrl: backendUrl, appId: cfg.appId });
|
|
2611
4120
|
const requestId = walletStatus.pendingRequestId;
|
|
2612
4121
|
let cancelled = false;
|
|
2613
4122
|
const tick = async () => {
|
|
@@ -2631,7 +4140,7 @@ function CavosProvider({ config, modal, children }) {
|
|
|
2631
4140
|
};
|
|
2632
4141
|
}, [walletStatus.awaitingApproval, walletStatus.pendingRequestId, identity, connect]);
|
|
2633
4142
|
const logout = react.useCallback(() => {
|
|
2634
|
-
|
|
4143
|
+
setWallet(null);
|
|
2635
4144
|
setIdentity(null);
|
|
2636
4145
|
setWalletStatus(INITIAL_STATUS);
|
|
2637
4146
|
setAuthError(null);
|
|
@@ -2639,11 +4148,11 @@ function CavosProvider({ config, modal, children }) {
|
|
|
2639
4148
|
const value = {
|
|
2640
4149
|
openModal,
|
|
2641
4150
|
closeModal,
|
|
2642
|
-
isAuthenticated: !!
|
|
4151
|
+
isAuthenticated: !!wallet,
|
|
2643
4152
|
user: identity ? { userId: identity.userId, email: identity.email, provider: identity.provider } : null,
|
|
2644
4153
|
chain: config.chain ?? "starknet",
|
|
2645
|
-
wallet
|
|
2646
|
-
address:
|
|
4154
|
+
wallet,
|
|
4155
|
+
address: wallet?.address ?? null,
|
|
2647
4156
|
walletStatus,
|
|
2648
4157
|
isLoading,
|
|
2649
4158
|
authError,
|
|
@@ -2655,6 +4164,10 @@ function CavosProvider({ config, modal, children }) {
|
|
|
2655
4164
|
handleCallback,
|
|
2656
4165
|
execute,
|
|
2657
4166
|
addSigner,
|
|
4167
|
+
enrollPasskey,
|
|
4168
|
+
passkeySupported,
|
|
4169
|
+
enrollPasskeyDefault,
|
|
4170
|
+
approveDeviceWithPasskey,
|
|
2658
4171
|
resendDeviceApproval,
|
|
2659
4172
|
setupRecovery,
|
|
2660
4173
|
recover,
|