@cavos/kit 0.0.3 → 0.0.5
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-BH2_tOQ2.d.mts → Cavos-D20EtgOK.d.mts} +312 -182
- package/dist/{Cavos-BH2_tOQ2.d.ts → Cavos-D20EtgOK.d.ts} +312 -182
- package/dist/{chunk-BNGLH3Q3.mjs → chunk-M5BGBODC.mjs} +1660 -582
- package/dist/chunk-M5BGBODC.mjs.map +1 -0
- package/dist/index.d.mts +293 -89
- package/dist/index.d.ts +293 -89
- package/dist/index.js +1601 -593
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -88
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.d.mts +42 -29
- package/dist/react/index.d.ts +42 -29
- package/dist/react/index.js +2002 -655
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +353 -92
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-BNGLH3Q3.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -27,8 +27,8 @@ function bytesToHex(bytes) {
|
|
|
27
27
|
return s;
|
|
28
28
|
}
|
|
29
29
|
function hexToBytes(hex) {
|
|
30
|
-
const
|
|
31
|
-
const padded =
|
|
30
|
+
const clean2 = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
31
|
+
const padded = clean2.length % 2 ? "0" + clean2 : clean2;
|
|
32
32
|
const out = new Uint8Array(padded.length / 2);
|
|
33
33
|
for (let i = 0; i < out.length; i++) out[i] = parseInt(padded.slice(i * 2, i * 2 + 2), 16);
|
|
34
34
|
return out;
|
|
@@ -182,7 +182,7 @@ var CAVOS_PAYMASTER_URL = {
|
|
|
182
182
|
mainnet: "https://paymaster.cavos.xyz"
|
|
183
183
|
};
|
|
184
184
|
var DEVICE_ACCOUNT_CLASS_HASH = {
|
|
185
|
-
sepolia: "
|
|
185
|
+
sepolia: "0x765f000b7021577aa0d6c206fb3d8ac73571686b3e76b9dc9b6d59a372e8c2c",
|
|
186
186
|
mainnet: "0x1840aded59e8a0d2b440a134cb9079a7fc11b06c77f58ed189ab436a034ca6a"
|
|
187
187
|
};
|
|
188
188
|
|
|
@@ -192,21 +192,34 @@ var StarknetAdapter = class {
|
|
|
192
192
|
this.opts = opts;
|
|
193
193
|
this.chain = "starknet";
|
|
194
194
|
}
|
|
195
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Deterministic address = f(addressSeed) ONLY. The device pubkey is NOT
|
|
197
|
+
* part of the derivation — anti-squatting is the integrator's responsibility
|
|
198
|
+
* (keep `appSalt` secret; deploy on first login). This makes the address
|
|
199
|
+
* recomputable by the user from (userId, appSalt) alone, even after losing
|
|
200
|
+
* every device.
|
|
201
|
+
*
|
|
202
|
+
* `initialSigner` in `ComputeAddressParams` is IGNORED on Starknet (kept in
|
|
203
|
+
* the shared type for Solana/Stellar, which still include it).
|
|
204
|
+
*/
|
|
205
|
+
computeAddress({ addressSeed, salt }) {
|
|
196
206
|
return starknet.hash.calculateContractAddressFromHash(
|
|
197
207
|
starknet.num.toHex(salt ?? addressSeed),
|
|
198
208
|
this.opts.classHash,
|
|
199
|
-
this.constructorCalldata(addressSeed
|
|
209
|
+
this.constructorCalldata(addressSeed),
|
|
200
210
|
0
|
|
201
211
|
// deployerAddress 0 => deterministic counterfactual address
|
|
202
212
|
);
|
|
203
213
|
}
|
|
204
|
-
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
214
|
+
/**
|
|
215
|
+
* UDC deploy call. The constructor takes ONLY the seed — no device pubkey —
|
|
216
|
+
* so the account is born with no signers. The caller MUST follow up with
|
|
217
|
+
* `buildInitialize` in the same multicall (or a separate tx) to register the
|
|
218
|
+
* first device signer; otherwise the account is unusable.
|
|
219
|
+
*/
|
|
207
220
|
buildDeploy(params) {
|
|
208
221
|
const salt = params.salt ?? params.addressSeed;
|
|
209
|
-
const calldata = this.constructorCalldata(params.addressSeed
|
|
222
|
+
const calldata = this.constructorCalldata(params.addressSeed);
|
|
210
223
|
return [
|
|
211
224
|
{
|
|
212
225
|
contractAddress: UDC_ADDRESS,
|
|
@@ -222,9 +235,22 @@ var StarknetAdapter = class {
|
|
|
222
235
|
}
|
|
223
236
|
];
|
|
224
237
|
}
|
|
225
|
-
/** Constructor calldata: [address_seed
|
|
226
|
-
constructorCalldata(addressSeed
|
|
227
|
-
return [starknet.num.toHex(addressSeed)
|
|
238
|
+
/** Constructor calldata: [address_seed]. Device pubkey is registered post-deploy via initialize. */
|
|
239
|
+
constructorCalldata(addressSeed) {
|
|
240
|
+
return [starknet.num.toHex(addressSeed)];
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* `initialize` call: registers the first device signer. Callable only while
|
|
244
|
+
* the account has no signers (one-shot). In production this is bundled with
|
|
245
|
+
* the UDC deploy in a single sponsored multicall — see `connectStarknet`.
|
|
246
|
+
* Anti-squatting is NOT enforced on-chain.
|
|
247
|
+
*/
|
|
248
|
+
buildInitialize(accountAddress, devicePubkey) {
|
|
249
|
+
return {
|
|
250
|
+
contractAddress: accountAddress,
|
|
251
|
+
entrypoint: "initialize",
|
|
252
|
+
calldata: pubkeyCalldata(devicePubkey)
|
|
253
|
+
};
|
|
228
254
|
}
|
|
229
255
|
buildAddSigner(accountAddress, signer) {
|
|
230
256
|
return { contractAddress: accountAddress, entrypoint: "add_signer", calldata: pubkeyCalldata(signer) };
|
|
@@ -262,6 +288,17 @@ var StarknetAdapter = class {
|
|
|
262
288
|
});
|
|
263
289
|
return BigInt(res[0] ?? 0) !== 0n;
|
|
264
290
|
}
|
|
291
|
+
/** True if the account has at least one passkey registered as an approver.
|
|
292
|
+
* Lets the UI decide whether to offer passkey approval before prompting. */
|
|
293
|
+
async hasPasskeyApprover(accountAddress) {
|
|
294
|
+
if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
|
|
295
|
+
const res = await this.opts.provider.callContract({
|
|
296
|
+
contractAddress: accountAddress,
|
|
297
|
+
entrypoint: "get_approver_count",
|
|
298
|
+
calldata: []
|
|
299
|
+
});
|
|
300
|
+
return BigInt(res[0] ?? 0) > 0n;
|
|
301
|
+
}
|
|
265
302
|
async getPasskeyNonce(accountAddress) {
|
|
266
303
|
if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
|
|
267
304
|
const res = await this.opts.provider.callContract({
|
|
@@ -449,26 +486,30 @@ var SolanaAdapter = class {
|
|
|
449
486
|
this.chain = "solana";
|
|
450
487
|
this.programId = new web3_js.PublicKey(opts.programId ?? DEVICE_ACCOUNT_PROGRAM_ID);
|
|
451
488
|
}
|
|
452
|
-
/**
|
|
453
|
-
|
|
454
|
-
|
|
489
|
+
/**
|
|
490
|
+
* Deterministic account address: PDA of [ACCOUNT_SEED, addressSeed] — the
|
|
491
|
+
* device pubkey is NOT part of the seeds, so the address is recomputable
|
|
492
|
+
* from (userId, appSalt) alone. Anti-squatting is the integrator's
|
|
493
|
+
* responsibility (keep `appSalt` secret; deploy on first login).
|
|
494
|
+
*/
|
|
495
|
+
computeAddress(addressSeed) {
|
|
496
|
+
return this.pda(addressSeed).toBase58();
|
|
455
497
|
}
|
|
456
|
-
pda(addressSeed
|
|
498
|
+
pda(addressSeed) {
|
|
457
499
|
const [pda] = web3_js.PublicKey.findProgramAddressSync(
|
|
458
|
-
[
|
|
459
|
-
Buffer.from(ACCOUNT_SEED),
|
|
460
|
-
Buffer.from(addressSeed),
|
|
461
|
-
Buffer.from(initialCompressed.slice(1, 33))
|
|
462
|
-
// x-coordinate
|
|
463
|
-
],
|
|
500
|
+
[Buffer.from(ACCOUNT_SEED), Buffer.from(addressSeed)],
|
|
464
501
|
this.programId
|
|
465
502
|
);
|
|
466
503
|
return pda;
|
|
467
504
|
}
|
|
468
|
-
/**
|
|
505
|
+
/**
|
|
506
|
+
* `initialize` instruction: creates the account PDA and registers the first
|
|
507
|
+
* device signer. No attestation is required — anti-squatting is NOT enforced
|
|
508
|
+
* on-chain.
|
|
509
|
+
*/
|
|
469
510
|
buildInitialize(addressSeed, payer, initialSigner) {
|
|
470
511
|
const initialCompressed = compressedPubkey(initialSigner);
|
|
471
|
-
const account = this.pda(addressSeed
|
|
512
|
+
const account = this.pda(addressSeed);
|
|
472
513
|
const data = Buffer.concat([
|
|
473
514
|
anchorDiscriminator("initialize"),
|
|
474
515
|
Buffer.from(addressSeed),
|
|
@@ -476,7 +517,7 @@ var SolanaAdapter = class {
|
|
|
476
517
|
Buffer.from(initialCompressed)
|
|
477
518
|
// [u8;33]
|
|
478
519
|
]);
|
|
479
|
-
|
|
520
|
+
const programIx = new web3_js.TransactionInstruction({
|
|
480
521
|
programId: this.programId,
|
|
481
522
|
keys: [
|
|
482
523
|
{ pubkey: account, isSigner: false, isWritable: true },
|
|
@@ -485,6 +526,7 @@ var SolanaAdapter = class {
|
|
|
485
526
|
],
|
|
486
527
|
data
|
|
487
528
|
});
|
|
529
|
+
return [programIx];
|
|
488
530
|
}
|
|
489
531
|
/** `[precompile, add_signer]` bundle, authorized by an existing device signer. */
|
|
490
532
|
async buildAddSigner(account, newSigner) {
|
|
@@ -600,6 +642,11 @@ var SolanaAdapter = class {
|
|
|
600
642
|
return [precompileIx, ix];
|
|
601
643
|
}
|
|
602
644
|
/** Read whether `passkey` is a registered approver. */
|
|
645
|
+
/** True if the account has at least one passkey registered as an approver. */
|
|
646
|
+
async hasPasskeyApprover(account) {
|
|
647
|
+
const approvers = await this.fetchApprovers(new web3_js.PublicKey(account));
|
|
648
|
+
return approvers.length > 0;
|
|
649
|
+
}
|
|
603
650
|
async isApprover(account, passkey) {
|
|
604
651
|
const approvers = await this.fetchApprovers(new web3_js.PublicKey(account));
|
|
605
652
|
const target = Buffer.from(compressedPubkey(passkey)).toString("hex");
|
|
@@ -818,8 +865,8 @@ function u64le(n) {
|
|
|
818
865
|
new DataView(b.buffer, b.byteOffset, 8).setBigUint64(0, BigInt(n), true);
|
|
819
866
|
return b;
|
|
820
867
|
}
|
|
821
|
-
function readU64le(
|
|
822
|
-
return new DataView(
|
|
868
|
+
function readU64le(buf3, offset) {
|
|
869
|
+
return new DataView(buf3.buffer, buf3.byteOffset, buf3.length).getBigUint64(
|
|
823
870
|
offset,
|
|
824
871
|
true
|
|
825
872
|
);
|
|
@@ -878,11 +925,11 @@ var SolanaRelayer = class {
|
|
|
878
925
|
async send(instructions) {
|
|
879
926
|
const feePayer = await this.getFeePayer();
|
|
880
927
|
const { blockhash } = await this.opts.connection.getLatestBlockhash("confirmed");
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
const serialized =
|
|
928
|
+
const tx3 = new web3_js.Transaction();
|
|
929
|
+
tx3.feePayer = feePayer;
|
|
930
|
+
tx3.recentBlockhash = blockhash;
|
|
931
|
+
tx3.add(...instructions);
|
|
932
|
+
const serialized = tx3.serialize({ requireAllSignatures: false, verifySignatures: false }).toString("base64");
|
|
886
933
|
const res = await fetch(`${this.opts.baseUrl}/api/solana/relay`, {
|
|
887
934
|
method: "POST",
|
|
888
935
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1292,6 +1339,8 @@ var CavosSolana = class _CavosSolana {
|
|
|
1292
1339
|
this.feePayer = feePayer;
|
|
1293
1340
|
/** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
|
|
1294
1341
|
this.chain = "solana";
|
|
1342
|
+
/** True when this connect just created a brand-new account (first sign-up). */
|
|
1343
|
+
this.isNewAccount = false;
|
|
1295
1344
|
}
|
|
1296
1345
|
get publicKey() {
|
|
1297
1346
|
return this.devicePubkey;
|
|
@@ -1326,23 +1375,23 @@ var CavosSolana = class _CavosSolana {
|
|
|
1326
1375
|
opts.feePayer
|
|
1327
1376
|
);
|
|
1328
1377
|
}
|
|
1329
|
-
const address = adapter.computeAddress(addressSeed
|
|
1378
|
+
const address = adapter.computeAddress(addressSeed);
|
|
1330
1379
|
const deployed = await connection.getAccountInfo(new web3_js.PublicKey(address)) !== null;
|
|
1331
1380
|
if (!deployed) {
|
|
1332
1381
|
if (relayer) {
|
|
1333
1382
|
const payer = await relayer.getFeePayer();
|
|
1334
|
-
const
|
|
1335
|
-
await relayer.send(
|
|
1383
|
+
const ixs = adapter.buildInitialize(addressSeed, payer.toBase58(), devicePubkey);
|
|
1384
|
+
await relayer.send(ixs);
|
|
1336
1385
|
} else if (opts.feePayer) {
|
|
1337
|
-
const
|
|
1338
|
-
await web3_js.sendAndConfirmTransaction(connection, new web3_js.Transaction().add(
|
|
1386
|
+
const ixs = adapter.buildInitialize(addressSeed, opts.feePayer.publicKey.toBase58(), devicePubkey);
|
|
1387
|
+
await web3_js.sendAndConfirmTransaction(connection, new web3_js.Transaction().add(...ixs), [opts.feePayer]);
|
|
1339
1388
|
} else {
|
|
1340
1389
|
throw new Error("kit/solana: a relayer (appId) or feePayer is required to initialize a new account");
|
|
1341
1390
|
}
|
|
1342
1391
|
}
|
|
1343
1392
|
await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
|
|
1344
1393
|
const isSigner = await adapter.isAuthorizedSigner(address, devicePubkey);
|
|
1345
|
-
|
|
1394
|
+
const wallet = new _CavosSolana(
|
|
1346
1395
|
identity,
|
|
1347
1396
|
address,
|
|
1348
1397
|
isSigner ? "ready" : "needs-device-approval",
|
|
@@ -1352,6 +1401,8 @@ var CavosSolana = class _CavosSolana {
|
|
|
1352
1401
|
relayer,
|
|
1353
1402
|
opts.feePayer
|
|
1354
1403
|
);
|
|
1404
|
+
wallet.isNewAccount = !deployed && isSigner;
|
|
1405
|
+
return wallet;
|
|
1355
1406
|
}
|
|
1356
1407
|
/** Authorize an additional device signer (device-signed via precompile). */
|
|
1357
1408
|
async addSigner(pubkey) {
|
|
@@ -1378,6 +1429,16 @@ var CavosSolana = class _CavosSolana {
|
|
|
1378
1429
|
const transactionHash = await this.send(ixs);
|
|
1379
1430
|
return { transactionHash };
|
|
1380
1431
|
}
|
|
1432
|
+
/** True if this account already has a passkey enrolled as an approver, so a
|
|
1433
|
+
* new device can be approved with the passkey instead of the email flow. */
|
|
1434
|
+
async hasPasskey() {
|
|
1435
|
+
return this.adapter.hasPasskeyApprover(this.address);
|
|
1436
|
+
}
|
|
1437
|
+
/** Re-read (from chain) whether THIS device is now an authorized signer.
|
|
1438
|
+
* Used to poll for readiness after a passkey approval before it's indexed. */
|
|
1439
|
+
async isReady() {
|
|
1440
|
+
return this.adapter.isAuthorizedSigner(this.address, this.devicePubkey);
|
|
1441
|
+
}
|
|
1381
1442
|
/**
|
|
1382
1443
|
* From a fresh browser (status `needs-device-approval`), approve adding THIS
|
|
1383
1444
|
* device with the user's synced passkey. Gasless via the relayer — the bundle
|
|
@@ -1422,12 +1483,12 @@ var CavosSolana = class _CavosSolana {
|
|
|
1422
1483
|
return { transactionHash: await this.send(ixs) };
|
|
1423
1484
|
}
|
|
1424
1485
|
/** Move `amount` lamports out of the account to `destination` (device-signed). */
|
|
1425
|
-
async execute(amount, destination) {
|
|
1486
|
+
async execute(amount, destination, opts) {
|
|
1426
1487
|
if (this.status !== "ready") {
|
|
1427
1488
|
throw new Error("kit/solana: this device is not yet an authorized signer of the wallet");
|
|
1428
1489
|
}
|
|
1429
1490
|
const ixs = await this.adapter.buildExecuteTransfer(this.address, destination, amount);
|
|
1430
|
-
return this.send(ixs);
|
|
1491
|
+
return this.send(ixs, opts);
|
|
1431
1492
|
}
|
|
1432
1493
|
/**
|
|
1433
1494
|
* Run arbitrary CPI `instructions` with the account PDA as signer (device-
|
|
@@ -1437,14 +1498,16 @@ var CavosSolana = class _CavosSolana {
|
|
|
1437
1498
|
*
|
|
1438
1499
|
* What the relayer will sponsor is constrained by the app's Solana program
|
|
1439
1500
|
* allowlist (configured in the dashboard) — programs outside the allowlist are
|
|
1440
|
-
* rejected before co-signing.
|
|
1501
|
+
* rejected before co-signing. Pass `{ sponsored: false }` to bypass the relayer
|
|
1502
|
+
* and pay the fee from a configured `feePayer` (e.g. for allowlisted programs
|
|
1503
|
+
* the relayer rejects, or to test the device signature end-to-end).
|
|
1441
1504
|
*/
|
|
1442
|
-
async executeInstructions(instructions) {
|
|
1505
|
+
async executeInstructions(instructions, opts) {
|
|
1443
1506
|
if (this.status !== "ready") {
|
|
1444
1507
|
throw new Error("kit/solana: this device is not yet an authorized signer of the wallet");
|
|
1445
1508
|
}
|
|
1446
1509
|
const ixs = await this.adapter.buildExecute(this.address, instructions);
|
|
1447
|
-
return this.send(ixs);
|
|
1510
|
+
return this.send(ixs, opts);
|
|
1448
1511
|
}
|
|
1449
1512
|
/**
|
|
1450
1513
|
* Register the backup signer derived from `code` as an authorized signer of this
|
|
@@ -1523,304 +1586,984 @@ var CavosSolana = class _CavosSolana {
|
|
|
1523
1586
|
opts.feePayer
|
|
1524
1587
|
);
|
|
1525
1588
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1589
|
+
/**
|
|
1590
|
+
* Submit a built instruction bundle. Sponsored by default (relayer pays the
|
|
1591
|
+
* fee); pass `{ sponsored: false }` to self-fund via the configured `feePayer`.
|
|
1592
|
+
* The device signature is embedded inside the secp256r1 precompile instruction,
|
|
1593
|
+
* NOT applied as a Solana tx signature — so switching only changes who pays,
|
|
1594
|
+
* never the signing identity.
|
|
1595
|
+
*/
|
|
1596
|
+
async send(ixs, opts) {
|
|
1597
|
+
const sponsored = opts?.sponsored !== false;
|
|
1598
|
+
if (sponsored && this.relayer) return this.relayer.send(ixs);
|
|
1528
1599
|
if (this.feePayer) {
|
|
1529
1600
|
return web3_js.sendAndConfirmTransaction(this.connection, new web3_js.Transaction().add(...ixs), [this.feePayer]);
|
|
1530
1601
|
}
|
|
1531
|
-
throw new Error(
|
|
1602
|
+
throw new Error(
|
|
1603
|
+
`kit/solana: cannot ${sponsored ? "sponsor" : "self-fund"} \u2014 no ${sponsored ? "relayer" : "feePayer"} configured`
|
|
1604
|
+
);
|
|
1532
1605
|
}
|
|
1533
1606
|
};
|
|
1534
1607
|
var defaultRegistry = new InMemoryWalletRegistry();
|
|
1535
1608
|
|
|
1536
1609
|
// src/chains/stellar/constants.ts
|
|
1537
|
-
var FACTORY_CONTRACT_ID = {
|
|
1538
|
-
// Re-deployed 2026-07-01 with the passkey-approval device-account wasm (batched
|
|
1539
|
-
// multi-chain challenge). The factory pins the wasm hash immutably, so a new
|
|
1540
|
-
// wasm needs a new factory → new account addresses; testnet has no prod wallets.
|
|
1541
|
-
"stellar-testnet": "CBCJIODXIEBOXXD66KCUCF7ZDYJARKI4ZIVQOVWPULOBH5XGNCDP6W3I",
|
|
1542
|
-
// Set once the factory is deployed to mainnet (its address differs — network id
|
|
1543
|
-
// is part of contract-address derivation).
|
|
1544
|
-
"stellar-mainnet": ""
|
|
1545
|
-
};
|
|
1546
|
-
var DEVICE_ACCOUNT_WASM_HASH = {
|
|
1547
|
-
"stellar-testnet": "2671b085578e59a385ef5a5664e42f0450322fe3249539f588e1263ed5a31dce",
|
|
1548
|
-
"stellar-mainnet": ""
|
|
1549
|
-
};
|
|
1550
1610
|
var STELLAR_NETWORKS = {
|
|
1551
1611
|
"stellar-testnet": {
|
|
1552
|
-
rpcUrl: "https://soroban-testnet.stellar.org",
|
|
1553
1612
|
passphrase: "Test SDF Network ; September 2015"
|
|
1554
1613
|
},
|
|
1555
1614
|
"stellar-mainnet": {
|
|
1556
|
-
rpcUrl: "https://soroban-rpc.mainnet.stellar.gateway.fm",
|
|
1557
1615
|
passphrase: "Public Global Stellar Network ; September 2015"
|
|
1558
1616
|
}
|
|
1559
1617
|
};
|
|
1560
|
-
var
|
|
1561
|
-
"stellar-testnet": "
|
|
1562
|
-
"stellar-mainnet": "
|
|
1618
|
+
var HORIZON_URL = {
|
|
1619
|
+
"stellar-testnet": "https://horizon-testnet.stellar.org",
|
|
1620
|
+
"stellar-mainnet": "https://horizon.stellar.org"
|
|
1621
|
+
};
|
|
1622
|
+
var XLM_DECIMALS = 7;
|
|
1623
|
+
|
|
1624
|
+
// node_modules/@noble/ciphers/esm/utils.js
|
|
1625
|
+
function isBytes(a) {
|
|
1626
|
+
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array";
|
|
1627
|
+
}
|
|
1628
|
+
function abytes(b, ...lengths) {
|
|
1629
|
+
if (!isBytes(b))
|
|
1630
|
+
throw new Error("Uint8Array expected");
|
|
1631
|
+
if (lengths.length > 0 && !lengths.includes(b.length))
|
|
1632
|
+
throw new Error("Uint8Array expected of length " + lengths + ", got length=" + b.length);
|
|
1633
|
+
}
|
|
1634
|
+
function aexists(instance, checkFinished = true) {
|
|
1635
|
+
if (instance.destroyed)
|
|
1636
|
+
throw new Error("Hash instance has been destroyed");
|
|
1637
|
+
if (checkFinished && instance.finished)
|
|
1638
|
+
throw new Error("Hash#digest() has already been called");
|
|
1639
|
+
}
|
|
1640
|
+
function aoutput(out, instance) {
|
|
1641
|
+
abytes(out);
|
|
1642
|
+
const min = instance.outputLen;
|
|
1643
|
+
if (out.length < min) {
|
|
1644
|
+
throw new Error("digestInto() expects output buffer of length at least " + min);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
function u8(arr) {
|
|
1648
|
+
return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
1649
|
+
}
|
|
1650
|
+
function u32(arr) {
|
|
1651
|
+
return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
|
|
1652
|
+
}
|
|
1653
|
+
function clean(...arrays) {
|
|
1654
|
+
for (let i = 0; i < arrays.length; i++) {
|
|
1655
|
+
arrays[i].fill(0);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
function createView(arr) {
|
|
1659
|
+
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
1660
|
+
}
|
|
1661
|
+
var isLE = /* @__PURE__ */ (() => new Uint8Array(new Uint32Array([287454020]).buffer)[0] === 68)();
|
|
1662
|
+
function utf8ToBytes(str) {
|
|
1663
|
+
if (typeof str !== "string")
|
|
1664
|
+
throw new Error("string expected");
|
|
1665
|
+
return new Uint8Array(new TextEncoder().encode(str));
|
|
1666
|
+
}
|
|
1667
|
+
function toBytes(data) {
|
|
1668
|
+
if (typeof data === "string")
|
|
1669
|
+
data = utf8ToBytes(data);
|
|
1670
|
+
else if (isBytes(data))
|
|
1671
|
+
data = copyBytes(data);
|
|
1672
|
+
else
|
|
1673
|
+
throw new Error("Uint8Array expected, got " + typeof data);
|
|
1674
|
+
return data;
|
|
1675
|
+
}
|
|
1676
|
+
function equalBytes(a, b) {
|
|
1677
|
+
if (a.length !== b.length)
|
|
1678
|
+
return false;
|
|
1679
|
+
let diff = 0;
|
|
1680
|
+
for (let i = 0; i < a.length; i++)
|
|
1681
|
+
diff |= a[i] ^ b[i];
|
|
1682
|
+
return diff === 0;
|
|
1683
|
+
}
|
|
1684
|
+
var wrapCipher = /* @__NO_SIDE_EFFECTS__ */ (params, constructor) => {
|
|
1685
|
+
function wrappedCipher(key, ...args) {
|
|
1686
|
+
abytes(key);
|
|
1687
|
+
if (!isLE)
|
|
1688
|
+
throw new Error("Non little-endian hardware is not yet supported");
|
|
1689
|
+
if (params.nonceLength !== void 0) {
|
|
1690
|
+
const nonce = args[0];
|
|
1691
|
+
if (!nonce)
|
|
1692
|
+
throw new Error("nonce / iv required");
|
|
1693
|
+
if (params.varSizeNonce)
|
|
1694
|
+
abytes(nonce);
|
|
1695
|
+
else
|
|
1696
|
+
abytes(nonce, params.nonceLength);
|
|
1697
|
+
}
|
|
1698
|
+
const tagl = params.tagLength;
|
|
1699
|
+
if (tagl && args[1] !== void 0) {
|
|
1700
|
+
abytes(args[1]);
|
|
1701
|
+
}
|
|
1702
|
+
const cipher = constructor(key, ...args);
|
|
1703
|
+
const checkOutput = (fnLength, output) => {
|
|
1704
|
+
if (output !== void 0) {
|
|
1705
|
+
if (fnLength !== 2)
|
|
1706
|
+
throw new Error("cipher output not supported");
|
|
1707
|
+
abytes(output);
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
let called = false;
|
|
1711
|
+
const wrCipher = {
|
|
1712
|
+
encrypt(data, output) {
|
|
1713
|
+
if (called)
|
|
1714
|
+
throw new Error("cannot encrypt() twice with same key + nonce");
|
|
1715
|
+
called = true;
|
|
1716
|
+
abytes(data);
|
|
1717
|
+
checkOutput(cipher.encrypt.length, output);
|
|
1718
|
+
return cipher.encrypt(data, output);
|
|
1719
|
+
},
|
|
1720
|
+
decrypt(data, output) {
|
|
1721
|
+
abytes(data);
|
|
1722
|
+
if (tagl && data.length < tagl)
|
|
1723
|
+
throw new Error("invalid ciphertext length: smaller than tagLength=" + tagl);
|
|
1724
|
+
checkOutput(cipher.decrypt.length, output);
|
|
1725
|
+
return cipher.decrypt(data, output);
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
return wrCipher;
|
|
1729
|
+
}
|
|
1730
|
+
Object.assign(wrappedCipher, params);
|
|
1731
|
+
return wrappedCipher;
|
|
1563
1732
|
};
|
|
1733
|
+
function getOutput(expectedLength, out, onlyAligned = true) {
|
|
1734
|
+
if (out === void 0)
|
|
1735
|
+
return new Uint8Array(expectedLength);
|
|
1736
|
+
if (out.length !== expectedLength)
|
|
1737
|
+
throw new Error("invalid output length, expected " + expectedLength + ", got: " + out.length);
|
|
1738
|
+
if (onlyAligned && !isAligned32(out))
|
|
1739
|
+
throw new Error("invalid output, must be aligned");
|
|
1740
|
+
return out;
|
|
1741
|
+
}
|
|
1742
|
+
function setBigUint64(view, byteOffset, value, isLE2) {
|
|
1743
|
+
if (typeof view.setBigUint64 === "function")
|
|
1744
|
+
return view.setBigUint64(byteOffset, value, isLE2);
|
|
1745
|
+
const _32n = BigInt(32);
|
|
1746
|
+
const _u32_max = BigInt(4294967295);
|
|
1747
|
+
const wh = Number(value >> _32n & _u32_max);
|
|
1748
|
+
const wl = Number(value & _u32_max);
|
|
1749
|
+
const h = 0;
|
|
1750
|
+
const l = 4;
|
|
1751
|
+
view.setUint32(byteOffset + h, wh, isLE2);
|
|
1752
|
+
view.setUint32(byteOffset + l, wl, isLE2);
|
|
1753
|
+
}
|
|
1754
|
+
function u64Lengths(dataLength, aadLength, isLE2) {
|
|
1755
|
+
const num5 = new Uint8Array(16);
|
|
1756
|
+
const view = createView(num5);
|
|
1757
|
+
setBigUint64(view, 0, BigInt(aadLength), isLE2);
|
|
1758
|
+
setBigUint64(view, 8, BigInt(dataLength), isLE2);
|
|
1759
|
+
return num5;
|
|
1760
|
+
}
|
|
1761
|
+
function isAligned32(bytes) {
|
|
1762
|
+
return bytes.byteOffset % 4 === 0;
|
|
1763
|
+
}
|
|
1764
|
+
function copyBytes(bytes) {
|
|
1765
|
+
return Uint8Array.from(bytes);
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// node_modules/@noble/ciphers/esm/_polyval.js
|
|
1769
|
+
var BLOCK_SIZE = 16;
|
|
1770
|
+
var ZEROS16 = /* @__PURE__ */ new Uint8Array(16);
|
|
1771
|
+
var ZEROS32 = u32(ZEROS16);
|
|
1772
|
+
var POLY = 225;
|
|
1773
|
+
var mul2 = (s0, s1, s2, s3) => {
|
|
1774
|
+
const hiBit = s3 & 1;
|
|
1775
|
+
return {
|
|
1776
|
+
s3: s2 << 31 | s3 >>> 1,
|
|
1777
|
+
s2: s1 << 31 | s2 >>> 1,
|
|
1778
|
+
s1: s0 << 31 | s1 >>> 1,
|
|
1779
|
+
s0: s0 >>> 1 ^ POLY << 24 & -(hiBit & 1)
|
|
1780
|
+
// reduce % poly
|
|
1781
|
+
};
|
|
1782
|
+
};
|
|
1783
|
+
var swapLE = (n) => (n >>> 0 & 255) << 24 | (n >>> 8 & 255) << 16 | (n >>> 16 & 255) << 8 | n >>> 24 & 255 | 0;
|
|
1784
|
+
function _toGHASHKey(k) {
|
|
1785
|
+
k.reverse();
|
|
1786
|
+
const hiBit = k[15] & 1;
|
|
1787
|
+
let carry = 0;
|
|
1788
|
+
for (let i = 0; i < k.length; i++) {
|
|
1789
|
+
const t = k[i];
|
|
1790
|
+
k[i] = t >>> 1 | carry;
|
|
1791
|
+
carry = (t & 1) << 7;
|
|
1792
|
+
}
|
|
1793
|
+
k[0] ^= -hiBit & 225;
|
|
1794
|
+
return k;
|
|
1795
|
+
}
|
|
1796
|
+
var estimateWindow = (bytes) => {
|
|
1797
|
+
if (bytes > 64 * 1024)
|
|
1798
|
+
return 8;
|
|
1799
|
+
if (bytes > 1024)
|
|
1800
|
+
return 4;
|
|
1801
|
+
return 2;
|
|
1802
|
+
};
|
|
1803
|
+
var GHASH = class {
|
|
1804
|
+
// We select bits per window adaptively based on expectedLength
|
|
1805
|
+
constructor(key, expectedLength) {
|
|
1806
|
+
this.blockLen = BLOCK_SIZE;
|
|
1807
|
+
this.outputLen = BLOCK_SIZE;
|
|
1808
|
+
this.s0 = 0;
|
|
1809
|
+
this.s1 = 0;
|
|
1810
|
+
this.s2 = 0;
|
|
1811
|
+
this.s3 = 0;
|
|
1812
|
+
this.finished = false;
|
|
1813
|
+
key = toBytes(key);
|
|
1814
|
+
abytes(key, 16);
|
|
1815
|
+
const kView = createView(key);
|
|
1816
|
+
let k0 = kView.getUint32(0, false);
|
|
1817
|
+
let k1 = kView.getUint32(4, false);
|
|
1818
|
+
let k2 = kView.getUint32(8, false);
|
|
1819
|
+
let k3 = kView.getUint32(12, false);
|
|
1820
|
+
const doubles = [];
|
|
1821
|
+
for (let i = 0; i < 128; i++) {
|
|
1822
|
+
doubles.push({ s0: swapLE(k0), s1: swapLE(k1), s2: swapLE(k2), s3: swapLE(k3) });
|
|
1823
|
+
({ s0: k0, s1: k1, s2: k2, s3: k3 } = mul2(k0, k1, k2, k3));
|
|
1824
|
+
}
|
|
1825
|
+
const W = estimateWindow(expectedLength || 1024);
|
|
1826
|
+
if (![1, 2, 4, 8].includes(W))
|
|
1827
|
+
throw new Error("ghash: invalid window size, expected 2, 4 or 8");
|
|
1828
|
+
this.W = W;
|
|
1829
|
+
const bits = 128;
|
|
1830
|
+
const windows = bits / W;
|
|
1831
|
+
const windowSize = this.windowSize = 2 ** W;
|
|
1832
|
+
const items = [];
|
|
1833
|
+
for (let w = 0; w < windows; w++) {
|
|
1834
|
+
for (let byte = 0; byte < windowSize; byte++) {
|
|
1835
|
+
let s0 = 0, s1 = 0, s2 = 0, s3 = 0;
|
|
1836
|
+
for (let j = 0; j < W; j++) {
|
|
1837
|
+
const bit = byte >>> W - j - 1 & 1;
|
|
1838
|
+
if (!bit)
|
|
1839
|
+
continue;
|
|
1840
|
+
const { s0: d0, s1: d1, s2: d2, s3: d3 } = doubles[W * w + j];
|
|
1841
|
+
s0 ^= d0, s1 ^= d1, s2 ^= d2, s3 ^= d3;
|
|
1842
|
+
}
|
|
1843
|
+
items.push({ s0, s1, s2, s3 });
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
this.t = items;
|
|
1847
|
+
}
|
|
1848
|
+
_updateBlock(s0, s1, s2, s3) {
|
|
1849
|
+
s0 ^= this.s0, s1 ^= this.s1, s2 ^= this.s2, s3 ^= this.s3;
|
|
1850
|
+
const { W, t, windowSize } = this;
|
|
1851
|
+
let o0 = 0, o1 = 0, o2 = 0, o3 = 0;
|
|
1852
|
+
const mask = (1 << W) - 1;
|
|
1853
|
+
let w = 0;
|
|
1854
|
+
for (const num5 of [s0, s1, s2, s3]) {
|
|
1855
|
+
for (let bytePos = 0; bytePos < 4; bytePos++) {
|
|
1856
|
+
const byte = num5 >>> 8 * bytePos & 255;
|
|
1857
|
+
for (let bitPos = 8 / W - 1; bitPos >= 0; bitPos--) {
|
|
1858
|
+
const bit = byte >>> W * bitPos & mask;
|
|
1859
|
+
const { s0: e0, s1: e1, s2: e2, s3: e3 } = t[w * windowSize + bit];
|
|
1860
|
+
o0 ^= e0, o1 ^= e1, o2 ^= e2, o3 ^= e3;
|
|
1861
|
+
w += 1;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
this.s0 = o0;
|
|
1866
|
+
this.s1 = o1;
|
|
1867
|
+
this.s2 = o2;
|
|
1868
|
+
this.s3 = o3;
|
|
1869
|
+
}
|
|
1870
|
+
update(data) {
|
|
1871
|
+
aexists(this);
|
|
1872
|
+
data = toBytes(data);
|
|
1873
|
+
abytes(data);
|
|
1874
|
+
const b32 = u32(data);
|
|
1875
|
+
const blocks = Math.floor(data.length / BLOCK_SIZE);
|
|
1876
|
+
const left = data.length % BLOCK_SIZE;
|
|
1877
|
+
for (let i = 0; i < blocks; i++) {
|
|
1878
|
+
this._updateBlock(b32[i * 4 + 0], b32[i * 4 + 1], b32[i * 4 + 2], b32[i * 4 + 3]);
|
|
1879
|
+
}
|
|
1880
|
+
if (left) {
|
|
1881
|
+
ZEROS16.set(data.subarray(blocks * BLOCK_SIZE));
|
|
1882
|
+
this._updateBlock(ZEROS32[0], ZEROS32[1], ZEROS32[2], ZEROS32[3]);
|
|
1883
|
+
clean(ZEROS32);
|
|
1884
|
+
}
|
|
1885
|
+
return this;
|
|
1886
|
+
}
|
|
1887
|
+
destroy() {
|
|
1888
|
+
const { t } = this;
|
|
1889
|
+
for (const elm of t) {
|
|
1890
|
+
elm.s0 = 0, elm.s1 = 0, elm.s2 = 0, elm.s3 = 0;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
digestInto(out) {
|
|
1894
|
+
aexists(this);
|
|
1895
|
+
aoutput(out, this);
|
|
1896
|
+
this.finished = true;
|
|
1897
|
+
const { s0, s1, s2, s3 } = this;
|
|
1898
|
+
const o32 = u32(out);
|
|
1899
|
+
o32[0] = s0;
|
|
1900
|
+
o32[1] = s1;
|
|
1901
|
+
o32[2] = s2;
|
|
1902
|
+
o32[3] = s3;
|
|
1903
|
+
return out;
|
|
1904
|
+
}
|
|
1905
|
+
digest() {
|
|
1906
|
+
const res = new Uint8Array(BLOCK_SIZE);
|
|
1907
|
+
this.digestInto(res);
|
|
1908
|
+
this.destroy();
|
|
1909
|
+
return res;
|
|
1910
|
+
}
|
|
1911
|
+
};
|
|
1912
|
+
var Polyval = class extends GHASH {
|
|
1913
|
+
constructor(key, expectedLength) {
|
|
1914
|
+
key = toBytes(key);
|
|
1915
|
+
abytes(key);
|
|
1916
|
+
const ghKey = _toGHASHKey(copyBytes(key));
|
|
1917
|
+
super(ghKey, expectedLength);
|
|
1918
|
+
clean(ghKey);
|
|
1919
|
+
}
|
|
1920
|
+
update(data) {
|
|
1921
|
+
data = toBytes(data);
|
|
1922
|
+
aexists(this);
|
|
1923
|
+
const b32 = u32(data);
|
|
1924
|
+
const left = data.length % BLOCK_SIZE;
|
|
1925
|
+
const blocks = Math.floor(data.length / BLOCK_SIZE);
|
|
1926
|
+
for (let i = 0; i < blocks; i++) {
|
|
1927
|
+
this._updateBlock(swapLE(b32[i * 4 + 3]), swapLE(b32[i * 4 + 2]), swapLE(b32[i * 4 + 1]), swapLE(b32[i * 4 + 0]));
|
|
1928
|
+
}
|
|
1929
|
+
if (left) {
|
|
1930
|
+
ZEROS16.set(data.subarray(blocks * BLOCK_SIZE));
|
|
1931
|
+
this._updateBlock(swapLE(ZEROS32[3]), swapLE(ZEROS32[2]), swapLE(ZEROS32[1]), swapLE(ZEROS32[0]));
|
|
1932
|
+
clean(ZEROS32);
|
|
1933
|
+
}
|
|
1934
|
+
return this;
|
|
1935
|
+
}
|
|
1936
|
+
digestInto(out) {
|
|
1937
|
+
aexists(this);
|
|
1938
|
+
aoutput(out, this);
|
|
1939
|
+
this.finished = true;
|
|
1940
|
+
const { s0, s1, s2, s3 } = this;
|
|
1941
|
+
const o32 = u32(out);
|
|
1942
|
+
o32[0] = s0;
|
|
1943
|
+
o32[1] = s1;
|
|
1944
|
+
o32[2] = s2;
|
|
1945
|
+
o32[3] = s3;
|
|
1946
|
+
return out.reverse();
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
function wrapConstructorWithKey(hashCons) {
|
|
1950
|
+
const hashC = (msg, key) => hashCons(key, msg.length).update(toBytes(msg)).digest();
|
|
1951
|
+
const tmp = hashCons(new Uint8Array(16), 0);
|
|
1952
|
+
hashC.outputLen = tmp.outputLen;
|
|
1953
|
+
hashC.blockLen = tmp.blockLen;
|
|
1954
|
+
hashC.create = (key, expectedLength) => hashCons(key, expectedLength);
|
|
1955
|
+
return hashC;
|
|
1956
|
+
}
|
|
1957
|
+
var ghash = wrapConstructorWithKey((key, expectedLength) => new GHASH(key, expectedLength));
|
|
1958
|
+
wrapConstructorWithKey((key, expectedLength) => new Polyval(key, expectedLength));
|
|
1959
|
+
|
|
1960
|
+
// node_modules/@noble/ciphers/esm/aes.js
|
|
1961
|
+
var BLOCK_SIZE2 = 16;
|
|
1962
|
+
var BLOCK_SIZE32 = 4;
|
|
1963
|
+
var EMPTY_BLOCK = /* @__PURE__ */ new Uint8Array(BLOCK_SIZE2);
|
|
1964
|
+
var POLY2 = 283;
|
|
1965
|
+
function mul22(n) {
|
|
1966
|
+
return n << 1 ^ POLY2 & -(n >> 7);
|
|
1967
|
+
}
|
|
1968
|
+
function mul(a, b) {
|
|
1969
|
+
let res = 0;
|
|
1970
|
+
for (; b > 0; b >>= 1) {
|
|
1971
|
+
res ^= a & -(b & 1);
|
|
1972
|
+
a = mul22(a);
|
|
1973
|
+
}
|
|
1974
|
+
return res;
|
|
1975
|
+
}
|
|
1976
|
+
var sbox = /* @__PURE__ */ (() => {
|
|
1977
|
+
const t = new Uint8Array(256);
|
|
1978
|
+
for (let i = 0, x = 1; i < 256; i++, x ^= mul22(x))
|
|
1979
|
+
t[i] = x;
|
|
1980
|
+
const box = new Uint8Array(256);
|
|
1981
|
+
box[0] = 99;
|
|
1982
|
+
for (let i = 0; i < 255; i++) {
|
|
1983
|
+
let x = t[255 - i];
|
|
1984
|
+
x |= x << 8;
|
|
1985
|
+
box[t[i]] = (x ^ x >> 4 ^ x >> 5 ^ x >> 6 ^ x >> 7 ^ 99) & 255;
|
|
1986
|
+
}
|
|
1987
|
+
clean(t);
|
|
1988
|
+
return box;
|
|
1989
|
+
})();
|
|
1990
|
+
var rotr32_8 = (n) => n << 24 | n >>> 8;
|
|
1991
|
+
var rotl32_8 = (n) => n << 8 | n >>> 24;
|
|
1992
|
+
function genTtable(sbox2, fn) {
|
|
1993
|
+
if (sbox2.length !== 256)
|
|
1994
|
+
throw new Error("Wrong sbox length");
|
|
1995
|
+
const T0 = new Uint32Array(256).map((_, j) => fn(sbox2[j]));
|
|
1996
|
+
const T1 = T0.map(rotl32_8);
|
|
1997
|
+
const T2 = T1.map(rotl32_8);
|
|
1998
|
+
const T3 = T2.map(rotl32_8);
|
|
1999
|
+
const T01 = new Uint32Array(256 * 256);
|
|
2000
|
+
const T23 = new Uint32Array(256 * 256);
|
|
2001
|
+
const sbox22 = new Uint16Array(256 * 256);
|
|
2002
|
+
for (let i = 0; i < 256; i++) {
|
|
2003
|
+
for (let j = 0; j < 256; j++) {
|
|
2004
|
+
const idx = i * 256 + j;
|
|
2005
|
+
T01[idx] = T0[i] ^ T1[j];
|
|
2006
|
+
T23[idx] = T2[i] ^ T3[j];
|
|
2007
|
+
sbox22[idx] = sbox2[i] << 8 | sbox2[j];
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
return { sbox: sbox2, sbox2: sbox22, T0, T1, T2, T3, T01, T23 };
|
|
2011
|
+
}
|
|
2012
|
+
var tableEncoding = /* @__PURE__ */ genTtable(sbox, (s) => mul(s, 3) << 24 | s << 16 | s << 8 | mul(s, 2));
|
|
2013
|
+
var xPowers = /* @__PURE__ */ (() => {
|
|
2014
|
+
const p = new Uint8Array(16);
|
|
2015
|
+
for (let i = 0, x = 1; i < 16; i++, x = mul22(x))
|
|
2016
|
+
p[i] = x;
|
|
2017
|
+
return p;
|
|
2018
|
+
})();
|
|
2019
|
+
function expandKeyLE(key) {
|
|
2020
|
+
abytes(key);
|
|
2021
|
+
const len = key.length;
|
|
2022
|
+
if (![16, 24, 32].includes(len))
|
|
2023
|
+
throw new Error("aes: invalid key size, should be 16, 24 or 32, got " + len);
|
|
2024
|
+
const { sbox2 } = tableEncoding;
|
|
2025
|
+
const toClean = [];
|
|
2026
|
+
if (!isAligned32(key))
|
|
2027
|
+
toClean.push(key = copyBytes(key));
|
|
2028
|
+
const k32 = u32(key);
|
|
2029
|
+
const Nk = k32.length;
|
|
2030
|
+
const subByte = (n) => applySbox(sbox2, n, n, n, n);
|
|
2031
|
+
const xk = new Uint32Array(len + 28);
|
|
2032
|
+
xk.set(k32);
|
|
2033
|
+
for (let i = Nk; i < xk.length; i++) {
|
|
2034
|
+
let t = xk[i - 1];
|
|
2035
|
+
if (i % Nk === 0)
|
|
2036
|
+
t = subByte(rotr32_8(t)) ^ xPowers[i / Nk - 1];
|
|
2037
|
+
else if (Nk > 6 && i % Nk === 4)
|
|
2038
|
+
t = subByte(t);
|
|
2039
|
+
xk[i] = xk[i - Nk] ^ t;
|
|
2040
|
+
}
|
|
2041
|
+
clean(...toClean);
|
|
2042
|
+
return xk;
|
|
2043
|
+
}
|
|
2044
|
+
function apply0123(T01, T23, s0, s1, s2, s3) {
|
|
2045
|
+
return T01[s0 << 8 & 65280 | s1 >>> 8 & 255] ^ T23[s2 >>> 8 & 65280 | s3 >>> 24 & 255];
|
|
2046
|
+
}
|
|
2047
|
+
function applySbox(sbox2, s0, s1, s2, s3) {
|
|
2048
|
+
return sbox2[s0 & 255 | s1 & 65280] | sbox2[s2 >>> 16 & 255 | s3 >>> 16 & 65280] << 16;
|
|
2049
|
+
}
|
|
2050
|
+
function encrypt(xk, s0, s1, s2, s3) {
|
|
2051
|
+
const { sbox2, T01, T23 } = tableEncoding;
|
|
2052
|
+
let k = 0;
|
|
2053
|
+
s0 ^= xk[k++], s1 ^= xk[k++], s2 ^= xk[k++], s3 ^= xk[k++];
|
|
2054
|
+
const rounds = xk.length / 4 - 2;
|
|
2055
|
+
for (let i = 0; i < rounds; i++) {
|
|
2056
|
+
const t02 = xk[k++] ^ apply0123(T01, T23, s0, s1, s2, s3);
|
|
2057
|
+
const t12 = xk[k++] ^ apply0123(T01, T23, s1, s2, s3, s0);
|
|
2058
|
+
const t22 = xk[k++] ^ apply0123(T01, T23, s2, s3, s0, s1);
|
|
2059
|
+
const t32 = xk[k++] ^ apply0123(T01, T23, s3, s0, s1, s2);
|
|
2060
|
+
s0 = t02, s1 = t12, s2 = t22, s3 = t32;
|
|
2061
|
+
}
|
|
2062
|
+
const t0 = xk[k++] ^ applySbox(sbox2, s0, s1, s2, s3);
|
|
2063
|
+
const t1 = xk[k++] ^ applySbox(sbox2, s1, s2, s3, s0);
|
|
2064
|
+
const t2 = xk[k++] ^ applySbox(sbox2, s2, s3, s0, s1);
|
|
2065
|
+
const t3 = xk[k++] ^ applySbox(sbox2, s3, s0, s1, s2);
|
|
2066
|
+
return { s0: t0, s1: t1, s2: t2, s3: t3 };
|
|
2067
|
+
}
|
|
2068
|
+
function ctr32(xk, isLE2, nonce, src, dst) {
|
|
2069
|
+
abytes(nonce, BLOCK_SIZE2);
|
|
2070
|
+
abytes(src);
|
|
2071
|
+
dst = getOutput(src.length, dst);
|
|
2072
|
+
const ctr = nonce;
|
|
2073
|
+
const c32 = u32(ctr);
|
|
2074
|
+
const view = createView(ctr);
|
|
2075
|
+
const src32 = u32(src);
|
|
2076
|
+
const dst32 = u32(dst);
|
|
2077
|
+
const ctrPos = 12;
|
|
2078
|
+
const srcLen = src.length;
|
|
2079
|
+
let ctrNum = view.getUint32(ctrPos, isLE2);
|
|
2080
|
+
let { s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]);
|
|
2081
|
+
for (let i = 0; i + 4 <= src32.length; i += 4) {
|
|
2082
|
+
dst32[i + 0] = src32[i + 0] ^ s0;
|
|
2083
|
+
dst32[i + 1] = src32[i + 1] ^ s1;
|
|
2084
|
+
dst32[i + 2] = src32[i + 2] ^ s2;
|
|
2085
|
+
dst32[i + 3] = src32[i + 3] ^ s3;
|
|
2086
|
+
ctrNum = ctrNum + 1 >>> 0;
|
|
2087
|
+
view.setUint32(ctrPos, ctrNum, isLE2);
|
|
2088
|
+
({ s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]));
|
|
2089
|
+
}
|
|
2090
|
+
const start = BLOCK_SIZE2 * Math.floor(src32.length / BLOCK_SIZE32);
|
|
2091
|
+
if (start < srcLen) {
|
|
2092
|
+
const b32 = new Uint32Array([s0, s1, s2, s3]);
|
|
2093
|
+
const buf3 = u8(b32);
|
|
2094
|
+
for (let i = start, pos = 0; i < srcLen; i++, pos++)
|
|
2095
|
+
dst[i] = src[i] ^ buf3[pos];
|
|
2096
|
+
clean(b32);
|
|
2097
|
+
}
|
|
2098
|
+
return dst;
|
|
2099
|
+
}
|
|
2100
|
+
function computeTag(fn, isLE2, key, data, AAD) {
|
|
2101
|
+
const aadLength = AAD ? AAD.length : 0;
|
|
2102
|
+
const h = fn.create(key, data.length + aadLength);
|
|
2103
|
+
if (AAD)
|
|
2104
|
+
h.update(AAD);
|
|
2105
|
+
const num5 = u64Lengths(8 * data.length, 8 * aadLength, isLE2);
|
|
2106
|
+
h.update(data);
|
|
2107
|
+
h.update(num5);
|
|
2108
|
+
const res = h.digest();
|
|
2109
|
+
clean(num5);
|
|
2110
|
+
return res;
|
|
2111
|
+
}
|
|
2112
|
+
var gcm = /* @__PURE__ */ wrapCipher({ blockSize: 16, nonceLength: 12, tagLength: 16, varSizeNonce: true }, function aesgcm(key, nonce, AAD) {
|
|
2113
|
+
if (nonce.length < 8)
|
|
2114
|
+
throw new Error("aes/gcm: invalid nonce length");
|
|
2115
|
+
const tagLength = 16;
|
|
2116
|
+
function _computeTag(authKey, tagMask, data) {
|
|
2117
|
+
const tag = computeTag(ghash, false, authKey, data, AAD);
|
|
2118
|
+
for (let i = 0; i < tagMask.length; i++)
|
|
2119
|
+
tag[i] ^= tagMask[i];
|
|
2120
|
+
return tag;
|
|
2121
|
+
}
|
|
2122
|
+
function deriveKeys() {
|
|
2123
|
+
const xk = expandKeyLE(key);
|
|
2124
|
+
const authKey = EMPTY_BLOCK.slice();
|
|
2125
|
+
const counter = EMPTY_BLOCK.slice();
|
|
2126
|
+
ctr32(xk, false, counter, counter, authKey);
|
|
2127
|
+
if (nonce.length === 12) {
|
|
2128
|
+
counter.set(nonce);
|
|
2129
|
+
} else {
|
|
2130
|
+
const nonceLen = EMPTY_BLOCK.slice();
|
|
2131
|
+
const view = createView(nonceLen);
|
|
2132
|
+
setBigUint64(view, 8, BigInt(nonce.length * 8), false);
|
|
2133
|
+
const g = ghash.create(authKey).update(nonce).update(nonceLen);
|
|
2134
|
+
g.digestInto(counter);
|
|
2135
|
+
g.destroy();
|
|
2136
|
+
}
|
|
2137
|
+
const tagMask = ctr32(xk, false, counter, EMPTY_BLOCK);
|
|
2138
|
+
return { xk, authKey, counter, tagMask };
|
|
2139
|
+
}
|
|
2140
|
+
return {
|
|
2141
|
+
encrypt(plaintext) {
|
|
2142
|
+
const { xk, authKey, counter, tagMask } = deriveKeys();
|
|
2143
|
+
const out = new Uint8Array(plaintext.length + tagLength);
|
|
2144
|
+
const toClean = [xk, authKey, counter, tagMask];
|
|
2145
|
+
if (!isAligned32(plaintext))
|
|
2146
|
+
toClean.push(plaintext = copyBytes(plaintext));
|
|
2147
|
+
ctr32(xk, false, counter, plaintext, out.subarray(0, plaintext.length));
|
|
2148
|
+
const tag = _computeTag(authKey, tagMask, out.subarray(0, out.length - tagLength));
|
|
2149
|
+
toClean.push(tag);
|
|
2150
|
+
out.set(tag, plaintext.length);
|
|
2151
|
+
clean(...toClean);
|
|
2152
|
+
return out;
|
|
2153
|
+
},
|
|
2154
|
+
decrypt(ciphertext) {
|
|
2155
|
+
const { xk, authKey, counter, tagMask } = deriveKeys();
|
|
2156
|
+
const toClean = [xk, authKey, tagMask, counter];
|
|
2157
|
+
if (!isAligned32(ciphertext))
|
|
2158
|
+
toClean.push(ciphertext = copyBytes(ciphertext));
|
|
2159
|
+
const data = ciphertext.subarray(0, -tagLength);
|
|
2160
|
+
const passedTag = ciphertext.subarray(-tagLength);
|
|
2161
|
+
const tag = _computeTag(authKey, tagMask, data);
|
|
2162
|
+
toClean.push(tag);
|
|
2163
|
+
if (!equalBytes(tag, passedTag))
|
|
2164
|
+
throw new Error("aes/gcm: invalid ghash tag");
|
|
2165
|
+
const out = ctr32(xk, false, counter, data);
|
|
2166
|
+
clean(...toClean);
|
|
2167
|
+
return out;
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
});
|
|
2171
|
+
var NONCE_LEN = 12;
|
|
2172
|
+
var RECOVERY_INFO = "cavos-stellar-dek-recovery";
|
|
2173
|
+
var PASSKEY_INFO = "cavos-stellar-dek-passkey";
|
|
2174
|
+
var ECIES_INFO = "cavos-stellar-dek-ecies";
|
|
2175
|
+
var RECOVERY_PBKDF2_ITERATIONS = 21e4;
|
|
2176
|
+
var RECOVERY_KDF_SALT = "cavos-stellar-recovery-v1";
|
|
2177
|
+
function generateDEK() {
|
|
2178
|
+
return utils.randomBytes(32);
|
|
2179
|
+
}
|
|
2180
|
+
function sealControlSeed(controlSeed, dek) {
|
|
2181
|
+
const nonce = utils.randomBytes(NONCE_LEN);
|
|
2182
|
+
const ct = gcm(dek, nonce).encrypt(controlSeed);
|
|
2183
|
+
return concat(nonce, ct);
|
|
2184
|
+
}
|
|
2185
|
+
function openControlSeed(sealed, dek) {
|
|
2186
|
+
const { nonce, ct } = splitNonce(sealed);
|
|
2187
|
+
return gcm(dek, nonce).decrypt(ct);
|
|
2188
|
+
}
|
|
2189
|
+
function wrapDEK(dek, kek) {
|
|
2190
|
+
const nonce = utils.randomBytes(NONCE_LEN);
|
|
2191
|
+
return concat(nonce, gcm(kek, nonce).encrypt(dek));
|
|
2192
|
+
}
|
|
2193
|
+
function unwrapDEK(wrapped, kek) {
|
|
2194
|
+
const { nonce, ct } = splitNonce(wrapped);
|
|
2195
|
+
return gcm(kek, nonce).decrypt(ct);
|
|
2196
|
+
}
|
|
2197
|
+
function deriveRecoveryKEK(code) {
|
|
2198
|
+
const normalised = code.trim().replace(/\s+/g, " ").toLowerCase();
|
|
2199
|
+
if (!normalised) throw new Error("kit/stellar: recovery code is empty");
|
|
2200
|
+
const stretched = pbkdf2.pbkdf2(sha256.sha256, new TextEncoder().encode(normalised), new TextEncoder().encode(RECOVERY_KDF_SALT), {
|
|
2201
|
+
c: RECOVERY_PBKDF2_ITERATIONS,
|
|
2202
|
+
dkLen: 32
|
|
2203
|
+
});
|
|
2204
|
+
return hkdf.hkdf(sha256.sha256, stretched, void 0, RECOVERY_INFO, 32);
|
|
2205
|
+
}
|
|
2206
|
+
function derivePasskeyKEK(prfOutput) {
|
|
2207
|
+
if (prfOutput.length < 32) throw new Error("kit/stellar: passkey PRF output too short");
|
|
2208
|
+
return hkdf.hkdf(sha256.sha256, prfOutput, void 0, PASSKEY_INFO, 32);
|
|
2209
|
+
}
|
|
2210
|
+
function eciesWrapDEK(dek, recipientPubSec1) {
|
|
2211
|
+
const ephPriv = p256.p256.utils.randomPrivateKey();
|
|
2212
|
+
const ephPubCompressed = p256.p256.getPublicKey(ephPriv, true);
|
|
2213
|
+
const kek = eciesKEK(p256.p256.getSharedSecret(ephPriv, recipientPubSec1, false), ephPubCompressed);
|
|
2214
|
+
const wrapped = wrapDEK(dek, kek);
|
|
2215
|
+
return concat(ephPubCompressed, wrapped);
|
|
2216
|
+
}
|
|
2217
|
+
function eciesUnwrapDEK(blob, recipientPrivScalar) {
|
|
2218
|
+
const ephPubCompressed = blob.subarray(0, 33);
|
|
2219
|
+
const wrapped = blob.subarray(33);
|
|
2220
|
+
const kek = eciesKEK(p256.p256.getSharedSecret(recipientPrivScalar, ephPubCompressed, false), ephPubCompressed);
|
|
2221
|
+
return unwrapDEK(wrapped, kek);
|
|
2222
|
+
}
|
|
2223
|
+
function eciesKEK(sharedUncompressed, ephPubCompressed) {
|
|
2224
|
+
return eciesKEKFromX(sharedUncompressed.subarray(1, 33), ephPubCompressed);
|
|
2225
|
+
}
|
|
2226
|
+
function eciesKEKFromX(sharedX, ephPubCompressed) {
|
|
2227
|
+
return hkdf.hkdf(sha256.sha256, sharedX, ephPubCompressed, ECIES_INFO, 32);
|
|
2228
|
+
}
|
|
2229
|
+
var DATA_ENTRY_MAX = 64;
|
|
2230
|
+
function chunkTo64(blob) {
|
|
2231
|
+
if (blob.length <= DATA_ENTRY_MAX) return [blob];
|
|
2232
|
+
const chunks = [];
|
|
2233
|
+
for (let i = 0; i < blob.length; i += DATA_ENTRY_MAX) {
|
|
2234
|
+
chunks.push(blob.subarray(i, i + DATA_ENTRY_MAX));
|
|
2235
|
+
}
|
|
2236
|
+
return chunks;
|
|
2237
|
+
}
|
|
2238
|
+
function unchunk(chunks) {
|
|
2239
|
+
const total = chunks.reduce((n, c) => n + c.length, 0);
|
|
2240
|
+
const out = new Uint8Array(total);
|
|
2241
|
+
let off = 0;
|
|
2242
|
+
for (const c of chunks) {
|
|
2243
|
+
out.set(c, off);
|
|
2244
|
+
off += c.length;
|
|
2245
|
+
}
|
|
2246
|
+
return out;
|
|
2247
|
+
}
|
|
2248
|
+
function concat(a, b) {
|
|
2249
|
+
const out = new Uint8Array(a.length + b.length);
|
|
2250
|
+
out.set(a, 0);
|
|
2251
|
+
out.set(b, a.length);
|
|
2252
|
+
return out;
|
|
2253
|
+
}
|
|
2254
|
+
function splitNonce(blob) {
|
|
2255
|
+
return { nonce: blob.subarray(0, NONCE_LEN), ct: blob.subarray(NONCE_LEN) };
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
// src/chains/stellar/datamap.ts
|
|
2259
|
+
var CT_BASE = "cv:ct";
|
|
2260
|
+
var PASSKEY_BASE = "cv:wp";
|
|
2261
|
+
var RECOVERY_BASE = "cv:wr";
|
|
2262
|
+
var VERSION_KEY = "cv:v";
|
|
2263
|
+
var ENVELOPE_VERSION = 1;
|
|
2264
|
+
function deviceBase(slot) {
|
|
2265
|
+
return `cv:wd:${slot}`;
|
|
2266
|
+
}
|
|
2267
|
+
function toDataEntries(env) {
|
|
2268
|
+
const out = {};
|
|
2269
|
+
writeChunked(out, CT_BASE, env.ct);
|
|
2270
|
+
for (const [slot, blob] of Object.entries(env.deviceWraps)) {
|
|
2271
|
+
writeChunked(out, deviceBase(slot), blob);
|
|
2272
|
+
}
|
|
2273
|
+
if (env.passkeyWrap) writeChunked(out, PASSKEY_BASE, env.passkeyWrap);
|
|
2274
|
+
if (env.recoveryWrap) writeChunked(out, RECOVERY_BASE, env.recoveryWrap);
|
|
2275
|
+
out[VERSION_KEY] = Uint8Array.of(ENVELOPE_VERSION);
|
|
2276
|
+
return out;
|
|
2277
|
+
}
|
|
2278
|
+
function fromDataEntries(entries) {
|
|
2279
|
+
const ct = readChunked(entries, CT_BASE);
|
|
2280
|
+
if (!ct) throw new Error("kit/stellar: account has no control-seed ciphertext (cv:ct)");
|
|
2281
|
+
const deviceWraps = {};
|
|
2282
|
+
const seenSlots = /* @__PURE__ */ new Set();
|
|
2283
|
+
for (const key of Object.keys(entries)) {
|
|
2284
|
+
const m = key.match(/^cv:wd:([^/]+)\/\d+$/);
|
|
2285
|
+
if (m) seenSlots.add(m[1]);
|
|
2286
|
+
}
|
|
2287
|
+
for (const slot of seenSlots) {
|
|
2288
|
+
const blob = readChunked(entries, deviceBase(slot));
|
|
2289
|
+
if (blob) deviceWraps[slot] = blob;
|
|
2290
|
+
}
|
|
2291
|
+
return {
|
|
2292
|
+
ct,
|
|
2293
|
+
deviceWraps,
|
|
2294
|
+
passkeyWrap: readChunked(entries, PASSKEY_BASE),
|
|
2295
|
+
recoveryWrap: readChunked(entries, RECOVERY_BASE)
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
function deviceWrapEntries(slot, blob) {
|
|
2299
|
+
const out = {};
|
|
2300
|
+
writeChunked(out, deviceBase(slot), blob);
|
|
2301
|
+
return out;
|
|
2302
|
+
}
|
|
2303
|
+
function writeChunked(out, base, blob) {
|
|
2304
|
+
const chunks = chunkTo64(blob);
|
|
2305
|
+
chunks.forEach((chunk, i) => {
|
|
2306
|
+
out[`${base}/${i}`] = chunk;
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
function readChunked(entries, base) {
|
|
2310
|
+
const chunks = [];
|
|
2311
|
+
for (let i = 0; ; i++) {
|
|
2312
|
+
const chunk = entries[`${base}/${i}`];
|
|
2313
|
+
if (!chunk) break;
|
|
2314
|
+
chunks.push(chunk);
|
|
2315
|
+
}
|
|
2316
|
+
return chunks.length ? unchunk(chunks) : void 0;
|
|
2317
|
+
}
|
|
1564
2318
|
|
|
1565
2319
|
// src/chains/stellar/StellarAdapter.ts
|
|
1566
|
-
var
|
|
2320
|
+
var TX_TIMEOUT = 180;
|
|
1567
2321
|
var StellarAdapter = class {
|
|
1568
2322
|
constructor(opts) {
|
|
1569
2323
|
this.chain = "stellar";
|
|
1570
2324
|
this.network = opts.network;
|
|
1571
2325
|
this.passphrase = STELLAR_NETWORKS[opts.network].passphrase;
|
|
1572
|
-
this.
|
|
1573
|
-
this.factoryId = opts.factoryId ?? FACTORY_CONTRACT_ID[opts.network];
|
|
1574
|
-
if (!this.factoryId) {
|
|
1575
|
-
throw new Error(`kit/stellar: no factory contract id configured for ${opts.network}`);
|
|
1576
|
-
}
|
|
1577
|
-
this.signer = opts.signer;
|
|
2326
|
+
this.horizonUrl = opts.horizonUrl ?? HORIZON_URL[opts.network];
|
|
1578
2327
|
}
|
|
1579
2328
|
server() {
|
|
1580
2329
|
if (!this._server) {
|
|
1581
|
-
this._server = new stellarSdk.
|
|
1582
|
-
allowHttp: this.
|
|
2330
|
+
this._server = new stellarSdk.Horizon.Server(this.horizonUrl, {
|
|
2331
|
+
allowHttp: this.horizonUrl.startsWith("http://")
|
|
1583
2332
|
});
|
|
1584
2333
|
}
|
|
1585
2334
|
return this._server;
|
|
1586
2335
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
2336
|
+
/** Whether the classic account already exists on-chain. */
|
|
2337
|
+
async isDeployed(address) {
|
|
2338
|
+
try {
|
|
2339
|
+
await this.server().loadAccount(address);
|
|
2340
|
+
return true;
|
|
2341
|
+
} catch (e) {
|
|
2342
|
+
if (isNotFound(e)) return false;
|
|
2343
|
+
throw e;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
/** Read the account's `MANAGE_DATA` entries as raw bytes (name → value). */
|
|
2347
|
+
async loadDataEntries(address) {
|
|
2348
|
+
const account = await this.server().loadAccount(address);
|
|
2349
|
+
const out = {};
|
|
2350
|
+
for (const [name, b64] of Object.entries(account.data_attr ?? {})) {
|
|
2351
|
+
out[name] = new Uint8Array(Buffer.from(b64, "base64"));
|
|
2352
|
+
}
|
|
2353
|
+
return out;
|
|
2354
|
+
}
|
|
2355
|
+
/** Native XLM balance in stroops. Returns 0n if the account doesn't exist. */
|
|
2356
|
+
async balance(address) {
|
|
2357
|
+
try {
|
|
2358
|
+
const account = await this.server().loadAccount(address);
|
|
2359
|
+
const native = account.balances.find((b) => b.asset_type === "native");
|
|
2360
|
+
return native ? toStroops(native.balance) : 0n;
|
|
2361
|
+
} catch (e) {
|
|
2362
|
+
if (isNotFound(e)) return 0n;
|
|
2363
|
+
throw e;
|
|
2364
|
+
}
|
|
1589
2365
|
}
|
|
1590
2366
|
/**
|
|
1591
|
-
*
|
|
1592
|
-
*
|
|
1593
|
-
* `
|
|
1594
|
-
* `
|
|
2367
|
+
* Build the account-creation transaction (source = funder, the relayer or a
|
|
2368
|
+
* self-funded keypair):
|
|
2369
|
+
* 1. `createAccount` funds the deterministic `G…` master address,
|
|
2370
|
+
* 2. `manageData` writes the control-key envelope entries (authorized by the
|
|
2371
|
+
* still-weight-1 master),
|
|
2372
|
+
* 3. `setOptions` adds the control signer (weight 1), sets all thresholds to
|
|
2373
|
+
* 1, and zeroes the master weight — after this the master can never sign.
|
|
2374
|
+
*
|
|
2375
|
+
* The returned tx must be signed by BOTH the master keypair (for the account's
|
|
2376
|
+
* own ops) and the funder (source + fee). Sponsorship of reserves is layered on
|
|
2377
|
+
* in Phase 3.
|
|
1595
2378
|
*/
|
|
1596
|
-
|
|
1597
|
-
const
|
|
1598
|
-
const
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
)
|
|
2379
|
+
async buildCreateTx(params) {
|
|
2380
|
+
const funderAccount = await this.server().loadAccount(params.funder);
|
|
2381
|
+
const builder = new stellarSdk.TransactionBuilder(funderAccount, {
|
|
2382
|
+
fee: stellarSdk.BASE_FEE,
|
|
2383
|
+
networkPassphrase: this.passphrase
|
|
2384
|
+
});
|
|
2385
|
+
builder.addOperation(
|
|
2386
|
+
stellarSdk.Operation.createAccount({
|
|
2387
|
+
destination: params.masterAddress,
|
|
2388
|
+
startingBalance: fromStroops(params.startingBalance)
|
|
1607
2389
|
})
|
|
1608
2390
|
);
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
return
|
|
1625
|
-
}
|
|
1626
|
-
/** Host function: `account.remove_signer(signer)` (requires device auth). */
|
|
1627
|
-
buildRemoveSigner(accountAddress, signer) {
|
|
1628
|
-
return invokeFunc(accountAddress, "remove_signer", [bytesScVal(sec1Pubkey(signer))]);
|
|
1629
|
-
}
|
|
1630
|
-
/** Host function: `account.add_approver(passkey)` (requires device auth). */
|
|
1631
|
-
buildAddApprover(accountAddress, passkey) {
|
|
1632
|
-
return invokeFunc(accountAddress, "add_approver", [bytesScVal(sec1Pubkey(passkey))]);
|
|
1633
|
-
}
|
|
1634
|
-
/** Host function: `account.remove_approver(passkey)` (requires device auth). */
|
|
1635
|
-
buildRemoveApprover(accountAddress, passkey) {
|
|
1636
|
-
return invokeFunc(accountAddress, "remove_approver", [bytesScVal(sec1Pubkey(passkey))]);
|
|
1637
|
-
}
|
|
1638
|
-
/** This chain's leaf for approving `add_signer(newSigner)` at `nonce`:
|
|
1639
|
-
* `sha256(sec1(new_signer) || nonce_be8)`. The batch challenge the passkey signs
|
|
1640
|
-
* is `sha256(concat(leaves))` across chains. */
|
|
1641
|
-
passkeyLeaf(newSigner, nonce) {
|
|
1642
|
-
const msg = new Uint8Array(65 + 8);
|
|
1643
|
-
msg.set(sec1Pubkey(newSigner), 0);
|
|
1644
|
-
const n = new Uint8Array(8);
|
|
1645
|
-
let v = nonce;
|
|
1646
|
-
for (let i = 7; i >= 0; i--) {
|
|
1647
|
-
n[i] = Number(v & 0xffn);
|
|
1648
|
-
v >>= 8n;
|
|
1649
|
-
}
|
|
1650
|
-
msg.set(n, 65);
|
|
1651
|
-
return sha256.sha256(msg);
|
|
1652
|
-
}
|
|
1653
|
-
/** Host function: passkey-authorized `add_signer_via_passkey` (no device auth —
|
|
1654
|
-
* authorized by the embedded WebAuthn assertion, so any relayer can submit).
|
|
1655
|
-
* `leaves`/`leafIndex` place this chain's leaf in the multi-chain batch. */
|
|
1656
|
-
buildAddSignerViaPasskey(accountAddress, newSigner, passkey, nonce, leaves, leafIndex, assertion) {
|
|
1657
|
-
const sig = encodeLowSSignature2({ r: assertion.r, s: assertion.s});
|
|
1658
|
-
const leavesScVal = stellarSdk.xdr.ScVal.scvVec(leaves.map((l) => bytesScVal(l)));
|
|
1659
|
-
return invokeFunc(accountAddress, "add_signer_via_passkey", [
|
|
1660
|
-
bytesScVal(sec1Pubkey(newSigner)),
|
|
1661
|
-
bytesScVal(sec1Pubkey(passkey)),
|
|
1662
|
-
stellarSdk.nativeToScVal(nonce, { type: "u64" }),
|
|
1663
|
-
leavesScVal,
|
|
1664
|
-
stellarSdk.nativeToScVal(leafIndex, { type: "u32" }),
|
|
1665
|
-
bytesScVal(assertion.authenticatorData),
|
|
1666
|
-
bytesScVal(assertion.clientDataJSON),
|
|
1667
|
-
stellarSdk.nativeToScVal(assertion.challengeOffset, { type: "u32" }),
|
|
1668
|
-
bytesScVal(sig)
|
|
1669
|
-
]);
|
|
1670
|
-
}
|
|
1671
|
-
/** Read whether `passkey` is a registered approver (read-only simulation). */
|
|
1672
|
-
async isApprover(accountAddress, passkey, readSource) {
|
|
1673
|
-
if (!await this.isDeployed(accountAddress)) return false;
|
|
1674
|
-
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1675
|
-
const src = new Account3(readSource, "0");
|
|
1676
|
-
const op = stellarSdk.Operation.invokeHostFunction({
|
|
1677
|
-
func: invokeFunc(accountAddress, "is_approver", [bytesScVal(sec1Pubkey(passkey))]),
|
|
1678
|
-
auth: []
|
|
1679
|
-
});
|
|
1680
|
-
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1681
|
-
const sim = await this.server().simulateTransaction(tx2);
|
|
1682
|
-
if (stellarSdk.rpc.Api.isSimulationError(sim)) {
|
|
1683
|
-
throw new Error(`kit/stellar: is_approver simulation failed: ${sim.error}`);
|
|
1684
|
-
}
|
|
1685
|
-
if (!sim.result?.retval) return false;
|
|
1686
|
-
return stellarSdk.scValToNative(sim.result.retval) === true;
|
|
1687
|
-
}
|
|
1688
|
-
/** Read the current passkey-approval nonce (read-only simulation). */
|
|
1689
|
-
async passkeyNonce(accountAddress, readSource) {
|
|
1690
|
-
if (!await this.isDeployed(accountAddress)) return 0n;
|
|
1691
|
-
const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1692
|
-
const src = new Account3(readSource, "0");
|
|
1693
|
-
const op = stellarSdk.Operation.invokeHostFunction({
|
|
1694
|
-
func: invokeFunc(accountAddress, "passkey_nonce", []),
|
|
1695
|
-
auth: []
|
|
1696
|
-
});
|
|
1697
|
-
const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
|
|
1698
|
-
const sim = await this.server().simulateTransaction(tx2);
|
|
1699
|
-
if (stellarSdk.rpc.Api.isSimulationError(sim)) {
|
|
1700
|
-
throw new Error(`kit/stellar: passkey_nonce simulation failed: ${sim.error}`);
|
|
1701
|
-
}
|
|
1702
|
-
if (!sim.result?.retval) return 0n;
|
|
1703
|
-
return BigInt(stellarSdk.scValToNative(sim.result.retval));
|
|
1704
|
-
}
|
|
1705
|
-
/** Host function: SEP-41 `token.transfer(from=account, to, amount)` (device auth). */
|
|
1706
|
-
buildTransfer(tokenId, accountAddress, destination, amount) {
|
|
1707
|
-
return invokeFunc(tokenId, "transfer", [
|
|
1708
|
-
new stellarSdk.Address(accountAddress).toScVal(),
|
|
1709
|
-
new stellarSdk.Address(destination).toScVal(),
|
|
1710
|
-
stellarSdk.nativeToScVal(amount, { type: "i128" })
|
|
1711
|
-
]);
|
|
2391
|
+
for (const [name, value] of Object.entries(toDataEntries(params.envelope))) {
|
|
2392
|
+
builder.addOperation(
|
|
2393
|
+
stellarSdk.Operation.manageData({ name, value: Buffer.from(value), source: params.masterAddress })
|
|
2394
|
+
);
|
|
2395
|
+
}
|
|
2396
|
+
builder.addOperation(
|
|
2397
|
+
stellarSdk.Operation.setOptions({
|
|
2398
|
+
source: params.masterAddress,
|
|
2399
|
+
masterWeight: 0,
|
|
2400
|
+
lowThreshold: 1,
|
|
2401
|
+
medThreshold: 1,
|
|
2402
|
+
highThreshold: 1,
|
|
2403
|
+
signer: { ed25519PublicKey: params.controlAddress, weight: 1 }
|
|
2404
|
+
})
|
|
2405
|
+
);
|
|
2406
|
+
return builder.setTimeout(TX_TIMEOUT).build();
|
|
1712
2407
|
}
|
|
1713
2408
|
/**
|
|
1714
|
-
*
|
|
1715
|
-
*
|
|
1716
|
-
*
|
|
1717
|
-
*
|
|
2409
|
+
* Build a **sponsored** account-creation transaction whose source is the
|
|
2410
|
+
* relayer. Wraps the account setup in `beginSponsoringFutureReserves` /
|
|
2411
|
+
* `endSponsoringFutureReserves`, so the relayer (not the user) pays every
|
|
2412
|
+
* reserve — the account is created with a 0 starting balance and holds no
|
|
2413
|
+
* locked XLM of the user's. Ops:
|
|
2414
|
+
* 0. beginSponsoringFutureReserves(G) source = relayer
|
|
2415
|
+
* 1. createAccount(G, 0) source = relayer
|
|
2416
|
+
* 2. manageData(cv:… envelope) source = G (master-signed)
|
|
2417
|
+
* 3. setOptions(control signer, master → 0) source = G
|
|
2418
|
+
* 4. endSponsoringFutureReserves() source = G
|
|
2419
|
+
*
|
|
2420
|
+
* Signed by the master (for the G ops, while it's still weight 1); the relayer
|
|
2421
|
+
* co-signs (source + fee + sponsorship) before submitting.
|
|
1718
2422
|
*/
|
|
1719
|
-
async
|
|
1720
|
-
const
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
2423
|
+
async buildSponsoredCreateTx(params) {
|
|
2424
|
+
const relayerAccount = await this.server().loadAccount(params.relayer);
|
|
2425
|
+
const builder = new stellarSdk.TransactionBuilder(relayerAccount, {
|
|
2426
|
+
fee: stellarSdk.BASE_FEE,
|
|
2427
|
+
networkPassphrase: this.passphrase
|
|
2428
|
+
});
|
|
2429
|
+
builder.addOperation(
|
|
2430
|
+
stellarSdk.Operation.beginSponsoringFutureReserves({ sponsoredId: params.masterAddress, source: params.relayer })
|
|
2431
|
+
);
|
|
2432
|
+
builder.addOperation(
|
|
2433
|
+
stellarSdk.Operation.createAccount({ destination: params.masterAddress, startingBalance: "0", source: params.relayer })
|
|
2434
|
+
);
|
|
2435
|
+
for (const [name, value] of Object.entries(toDataEntries(params.envelope))) {
|
|
2436
|
+
builder.addOperation(
|
|
2437
|
+
stellarSdk.Operation.manageData({ name, value: Buffer.from(value), source: params.masterAddress })
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
builder.addOperation(
|
|
2441
|
+
stellarSdk.Operation.setOptions({
|
|
2442
|
+
source: params.masterAddress,
|
|
2443
|
+
masterWeight: 0,
|
|
2444
|
+
lowThreshold: 1,
|
|
2445
|
+
medThreshold: 1,
|
|
2446
|
+
highThreshold: 1,
|
|
2447
|
+
signer: { ed25519PublicKey: params.controlAddress, weight: 1 }
|
|
1728
2448
|
})
|
|
1729
2449
|
);
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
const pubkey = await this.signer.getPublicKey();
|
|
1733
|
-
addrCreds.signature(deviceSignatureScVal(pubkey, sig));
|
|
1734
|
-
return entry;
|
|
2450
|
+
builder.addOperation(stellarSdk.Operation.endSponsoringFutureReserves({ source: params.masterAddress }));
|
|
2451
|
+
return builder.setTimeout(TX_TIMEOUT).build();
|
|
1735
2452
|
}
|
|
1736
2453
|
/**
|
|
1737
|
-
*
|
|
1738
|
-
* `
|
|
1739
|
-
*
|
|
2454
|
+
* Build a classic native-XLM payment as an *inner* transaction whose source is
|
|
2455
|
+
* the account itself (`G…`), signed by the control key. Wrap it in a fee-bump
|
|
2456
|
+
* (see `wrapFeeBump`) so the relayer pays the fee — gasless.
|
|
1740
2457
|
*/
|
|
1741
|
-
async
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
const sim = await this.server().simulateTransaction(tx2);
|
|
1751
|
-
if (stellarSdk.rpc.Api.isSimulationError(sim) || !sim.result?.retval) return 0n;
|
|
1752
|
-
return BigInt(stellarSdk.scValToNative(sim.result.retval));
|
|
2458
|
+
async buildPaymentTx(params) {
|
|
2459
|
+
const source = await this.server().loadAccount(params.from);
|
|
2460
|
+
return new stellarSdk.TransactionBuilder(source, { fee: stellarSdk.BASE_FEE, networkPassphrase: this.passphrase }).addOperation(
|
|
2461
|
+
stellarSdk.Operation.payment({
|
|
2462
|
+
destination: params.to,
|
|
2463
|
+
asset: stellarSdk.Asset.native(),
|
|
2464
|
+
amount: fromStroops(params.amount)
|
|
2465
|
+
})
|
|
2466
|
+
).setTimeout(TX_TIMEOUT).build();
|
|
1753
2467
|
}
|
|
1754
|
-
/**
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
} catch {
|
|
1764
|
-
return false;
|
|
2468
|
+
/**
|
|
2469
|
+
* Build a data-entry write (e.g. re-wrapping the DEK for a newly approved
|
|
2470
|
+
* device) as an inner tx sourced by the account, signed by the control key.
|
|
2471
|
+
*/
|
|
2472
|
+
async buildDataTx(params) {
|
|
2473
|
+
const source = await this.server().loadAccount(params.account);
|
|
2474
|
+
const builder = new stellarSdk.TransactionBuilder(source, { fee: stellarSdk.BASE_FEE, networkPassphrase: this.passphrase });
|
|
2475
|
+
for (const [name, value] of Object.entries(params.entries)) {
|
|
2476
|
+
builder.addOperation(stellarSdk.Operation.manageData({ name, value: Buffer.from(value) }));
|
|
1765
2477
|
}
|
|
2478
|
+
return builder.setTimeout(TX_TIMEOUT).build();
|
|
1766
2479
|
}
|
|
1767
2480
|
/**
|
|
1768
|
-
*
|
|
1769
|
-
*
|
|
1770
|
-
*
|
|
2481
|
+
* Build a **sponsored** data-entry write whose source is the relayer, so the
|
|
2482
|
+
* relayer (not the 0-balance account) pays the reserve of any NEW subentry.
|
|
2483
|
+
* Adding a factor (passkey / recovery) or a device slot creates a data entry
|
|
2484
|
+
* that needs ~0.5 XLM of reserve; a sponsored account holds no XLM, so — like
|
|
2485
|
+
* account creation — the write must be wrapped in begin/endSponsoringFuture
|
|
2486
|
+
* reserves with the relayer as sponsor. Ops:
|
|
2487
|
+
* 0. beginSponsoringFutureReserves(account) source = relayer
|
|
2488
|
+
* 1..n. manageData(cv:…) source = account (control-signed)
|
|
2489
|
+
* last. endSponsoringFutureReserves() source = account
|
|
2490
|
+
*
|
|
2491
|
+
* Signed by the control key (account ops) + the relayer (source + fee + sponsor).
|
|
1771
2492
|
*/
|
|
1772
|
-
async
|
|
1773
|
-
|
|
1774
|
-
const
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
func: invokeFunc(accountAddress, "is_authorized", [bytesScVal(sec1Pubkey(signer))]),
|
|
1778
|
-
auth: []
|
|
2493
|
+
async buildSponsoredDataTx(params) {
|
|
2494
|
+
const relayerAccount = await this.server().loadAccount(params.relayer);
|
|
2495
|
+
const builder = new stellarSdk.TransactionBuilder(relayerAccount, {
|
|
2496
|
+
fee: stellarSdk.BASE_FEE,
|
|
2497
|
+
networkPassphrase: this.passphrase
|
|
1779
2498
|
});
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
2499
|
+
builder.addOperation(
|
|
2500
|
+
stellarSdk.Operation.beginSponsoringFutureReserves({ sponsoredId: params.account, source: params.relayer })
|
|
2501
|
+
);
|
|
2502
|
+
for (const [name, value] of Object.entries(params.entries)) {
|
|
2503
|
+
builder.addOperation(stellarSdk.Operation.manageData({ name, value: Buffer.from(value), source: params.account }));
|
|
2504
|
+
}
|
|
2505
|
+
builder.addOperation(stellarSdk.Operation.endSponsoringFutureReserves({ source: params.account }));
|
|
2506
|
+
return builder.setTimeout(TX_TIMEOUT).build();
|
|
2507
|
+
}
|
|
2508
|
+
/** Wrap a control-signed inner tx in a fee-bump whose fee source is `feeSource`
|
|
2509
|
+
* (the relayer). The inner tx pays nothing; the relayer pays all fees. */
|
|
2510
|
+
wrapFeeBump(inner, feeSource) {
|
|
2511
|
+
return stellarSdk.TransactionBuilder.buildFeeBumpTransaction(feeSource, stellarSdk.BASE_FEE, inner, this.passphrase);
|
|
2512
|
+
}
|
|
2513
|
+
/** Submit a fully-signed classic transaction and return its hash. Throws with
|
|
2514
|
+
* the Horizon result codes on failure. */
|
|
2515
|
+
async submit(tx3) {
|
|
2516
|
+
try {
|
|
2517
|
+
const res = await this.server().submitTransaction(tx3);
|
|
2518
|
+
return res.hash;
|
|
2519
|
+
} catch (e) {
|
|
2520
|
+
throw new Error(`kit/stellar: submit failed: ${horizonError(e)}`);
|
|
1784
2521
|
}
|
|
1785
|
-
|
|
1786
|
-
|
|
2522
|
+
}
|
|
2523
|
+
/** Build an `Account` handle for a known address + sequence (avoids a Horizon
|
|
2524
|
+
* round-trip when the caller already has the sequence). */
|
|
2525
|
+
accountAt(address, sequence) {
|
|
2526
|
+
return new stellarSdk.Account(address, sequence);
|
|
1787
2527
|
}
|
|
1788
2528
|
};
|
|
1789
|
-
function
|
|
1790
|
-
const
|
|
1791
|
-
|
|
1792
|
-
out.set(bigIntTo32Bytes(pk.x), 1);
|
|
1793
|
-
out.set(bigIntTo32Bytes(pk.y), 33);
|
|
1794
|
-
return out;
|
|
2529
|
+
function isNotFound(e) {
|
|
2530
|
+
const status = e?.response?.status ?? e?.status;
|
|
2531
|
+
return status === 404;
|
|
1795
2532
|
}
|
|
1796
|
-
function
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1799
|
-
out.set(bigIntTo32Bytes(sig.r), 0);
|
|
1800
|
-
out.set(bigIntTo32Bytes(lowS2), 32);
|
|
1801
|
-
return out;
|
|
2533
|
+
function horizonError(e) {
|
|
2534
|
+
const codes = e?.response?.data?.extras?.result_codes;
|
|
2535
|
+
return codes ? JSON.stringify(codes) : String(e?.message ?? e);
|
|
1802
2536
|
}
|
|
1803
|
-
function
|
|
1804
|
-
const
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
})
|
|
1820
|
-
);
|
|
2537
|
+
function toStroops(amount) {
|
|
2538
|
+
const [whole, frac = ""] = amount.split(".");
|
|
2539
|
+
const fracPadded = (frac + "0000000").slice(0, 7);
|
|
2540
|
+
return BigInt(whole) * 10000000n + BigInt(fracPadded);
|
|
2541
|
+
}
|
|
2542
|
+
function fromStroops(stroops) {
|
|
2543
|
+
const neg = stroops < 0n;
|
|
2544
|
+
const abs = neg ? -stroops : stroops;
|
|
2545
|
+
const whole = abs / 10000000n;
|
|
2546
|
+
const frac = (abs % 10000000n).toString().padStart(7, "0");
|
|
2547
|
+
return `${neg ? "-" : ""}${whole}.${frac}`;
|
|
2548
|
+
}
|
|
2549
|
+
var MASTER_HKDF_INFO = "cavos-stellar-master";
|
|
2550
|
+
function deriveStellarMasterSeed(identity) {
|
|
2551
|
+
const ikm = deriveAddressSeedStellar(identity);
|
|
2552
|
+
return hkdf.hkdf(sha256.sha256, ikm, void 0, MASTER_HKDF_INFO, 32);
|
|
1821
2553
|
}
|
|
1822
|
-
function
|
|
1823
|
-
return stellarSdk.
|
|
2554
|
+
function deriveStellarMasterKeypair(identity) {
|
|
2555
|
+
return stellarSdk.Keypair.fromRawEd25519Seed(Buffer.from(deriveStellarMasterSeed(identity)));
|
|
2556
|
+
}
|
|
2557
|
+
function deriveStellarAddress(identity) {
|
|
2558
|
+
return deriveStellarMasterKeypair(identity).publicKey();
|
|
2559
|
+
}
|
|
2560
|
+
function generateControlKey() {
|
|
2561
|
+
const seed = utils.randomBytes(32);
|
|
2562
|
+
return { keypair: stellarSdk.Keypair.fromRawEd25519Seed(Buffer.from(seed)), seed };
|
|
2563
|
+
}
|
|
2564
|
+
function controlKeypairFromSeed(seed) {
|
|
2565
|
+
if (seed.length !== 32) throw new Error("kit/stellar: control seed must be 32 bytes");
|
|
2566
|
+
return stellarSdk.Keypair.fromRawEd25519Seed(Buffer.from(seed));
|
|
1824
2567
|
}
|
|
1825
2568
|
|
|
1826
2569
|
// src/chains/stellar/StellarRelayer.ts
|
|
@@ -1828,7 +2571,7 @@ var StellarRelayer = class {
|
|
|
1828
2571
|
constructor(opts) {
|
|
1829
2572
|
this.opts = opts;
|
|
1830
2573
|
}
|
|
1831
|
-
/** The relayer's source/fee-payer G-account (fetched + cached
|
|
2574
|
+
/** The relayer's source/fee-payer/sponsor G-account (fetched + cached). */
|
|
1832
2575
|
async getSource() {
|
|
1833
2576
|
if (this.source) return this.source;
|
|
1834
2577
|
const res = await fetch(`${this.opts.baseUrl}/api/stellar/relay?network=${this.opts.network}`);
|
|
@@ -1837,17 +2580,16 @@ var StellarRelayer = class {
|
|
|
1837
2580
|
this.source = fee_payer;
|
|
1838
2581
|
return this.source;
|
|
1839
2582
|
}
|
|
1840
|
-
/**
|
|
1841
|
-
*
|
|
1842
|
-
|
|
1843
|
-
*/
|
|
1844
|
-
async submit(transactionXdr) {
|
|
2583
|
+
/** POST a (partially) signed transaction XDR for the relayer to co-sign + submit.
|
|
2584
|
+
* `kind` selects the validation gate. Returns the confirmed transaction hash. */
|
|
2585
|
+
async submit(kind, transactionXdr) {
|
|
1845
2586
|
const res = await fetch(`${this.opts.baseUrl}/api/stellar/relay`, {
|
|
1846
2587
|
method: "POST",
|
|
1847
2588
|
headers: { "Content-Type": "application/json" },
|
|
1848
2589
|
body: JSON.stringify({
|
|
1849
2590
|
app_id: this.opts.appId,
|
|
1850
2591
|
network: this.opts.network,
|
|
2592
|
+
kind,
|
|
1851
2593
|
transaction: transactionXdr
|
|
1852
2594
|
})
|
|
1853
2595
|
});
|
|
@@ -1855,296 +2597,415 @@ var StellarRelayer = class {
|
|
|
1855
2597
|
const detail = await res.text().catch(() => "");
|
|
1856
2598
|
throw new Error(`kit/stellar: relay failed (${res.status}) ${detail}`);
|
|
1857
2599
|
}
|
|
1858
|
-
const { hash:
|
|
1859
|
-
return
|
|
2600
|
+
const { hash: hash5 } = await res.json();
|
|
2601
|
+
return hash5;
|
|
1860
2602
|
}
|
|
1861
2603
|
};
|
|
1862
2604
|
|
|
1863
2605
|
// src/chains/stellar/CavosStellar.ts
|
|
2606
|
+
var DEFAULT_STARTING_BALANCE = 50000000n;
|
|
1864
2607
|
var CavosStellar = class _CavosStellar {
|
|
1865
|
-
constructor(identity, address, status, network, adapter,
|
|
2608
|
+
constructor(identity, address, status, network, adapter, deviceKey, control, dek, relayer) {
|
|
1866
2609
|
this.identity = identity;
|
|
1867
2610
|
this.address = address;
|
|
1868
|
-
this.status = status;
|
|
1869
2611
|
this.network = network;
|
|
1870
2612
|
this.adapter = adapter;
|
|
1871
|
-
this.
|
|
2613
|
+
this.deviceKey = deviceKey;
|
|
2614
|
+
this.control = control;
|
|
2615
|
+
this.dek = dek;
|
|
1872
2616
|
this.relayer = relayer;
|
|
1873
|
-
|
|
1874
|
-
|
|
2617
|
+
// Discriminant for the `CavosWallet` union. Classic `G…` IS the Stellar chain
|
|
2618
|
+
// now (the Soroban `C…` path was removed), so this is "stellar".
|
|
1875
2619
|
this.chain = "stellar";
|
|
2620
|
+
this.isNewAccount = false;
|
|
2621
|
+
this.statusValue = status;
|
|
1876
2622
|
}
|
|
1877
|
-
get
|
|
1878
|
-
return this.
|
|
2623
|
+
get status() {
|
|
2624
|
+
return this.statusValue;
|
|
1879
2625
|
}
|
|
1880
2626
|
static async connect(opts) {
|
|
1881
2627
|
const identity = opts.identity ?? await opts.auth?.authenticate();
|
|
1882
2628
|
if (!identity) throw new Error("kit/stellar: connect requires `identity` or `auth`");
|
|
1883
|
-
const
|
|
1884
|
-
const
|
|
1885
|
-
const
|
|
1886
|
-
|
|
1887
|
-
rpcUrl: opts.rpcUrl,
|
|
1888
|
-
factoryId: opts.factoryId,
|
|
1889
|
-
signer
|
|
1890
|
-
});
|
|
1891
|
-
const addressSeed = deriveAddressSeedStellar({ userId: identity.userId, appSalt: opts.appSalt });
|
|
2629
|
+
const adapter = new StellarAdapter({ network: opts.network, horizonUrl: opts.horizonUrl });
|
|
2630
|
+
const master = deriveStellarMasterKeypair({ userId: identity.userId, appSalt: opts.appSalt });
|
|
2631
|
+
const address = master.publicKey();
|
|
2632
|
+
const startingBalance = opts.startingBalance ?? DEFAULT_STARTING_BALANCE;
|
|
1892
2633
|
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
1893
|
-
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
|
|
1894
2634
|
const relayer = opts.relayer ?? (opts.appId ? new StellarRelayer({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : void 0);
|
|
1895
|
-
const build = (
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
2635
|
+
const build = (status, unlocked) => new _CavosStellar(
|
|
2636
|
+
identity,
|
|
2637
|
+
address,
|
|
2638
|
+
status,
|
|
2639
|
+
opts.network,
|
|
2640
|
+
adapter,
|
|
2641
|
+
opts.deviceKey,
|
|
2642
|
+
unlocked?.control,
|
|
2643
|
+
unlocked?.dek,
|
|
2644
|
+
relayer
|
|
2645
|
+
);
|
|
2646
|
+
if (await adapter.isDeployed(address)) {
|
|
2647
|
+
const unlocked = await unlockViaDevice(adapter, address, opts.deviceKey);
|
|
2648
|
+
return build(unlocked ? "ready" : "needs-device-approval", unlocked ?? void 0);
|
|
2649
|
+
}
|
|
2650
|
+
if (!relayer && !opts.sourceKeypair) {
|
|
2651
|
+
throw new Error("kit/stellar: a relayer (appId) or sourceKeypair is required to create the account");
|
|
2652
|
+
}
|
|
2653
|
+
const { keypair: control, seed: controlSeed } = generateControlKey();
|
|
2654
|
+
const dek = generateDEK();
|
|
2655
|
+
const envelope = {
|
|
2656
|
+
ct: sealControlSeed(controlSeed, dek),
|
|
2657
|
+
deviceWraps: { [opts.deviceKey.slotId()]: eciesWrapDEK(dek, opts.deviceKey.publicKeySec1()) }
|
|
2658
|
+
};
|
|
2659
|
+
if (relayer) {
|
|
2660
|
+
const relayerSource = await relayer.getSource();
|
|
2661
|
+
const tx3 = await adapter.buildSponsoredCreateTx({
|
|
2662
|
+
relayer: relayerSource,
|
|
2663
|
+
masterAddress: address,
|
|
2664
|
+
controlAddress: control.publicKey(),
|
|
2665
|
+
envelope
|
|
2666
|
+
});
|
|
2667
|
+
tx3.sign(master);
|
|
2668
|
+
await relayer.submit("create", tx3.toXDR());
|
|
2669
|
+
} else {
|
|
2670
|
+
const funder = opts.sourceKeypair;
|
|
2671
|
+
const tx3 = await adapter.buildCreateTx({
|
|
2672
|
+
funder: funder.publicKey(),
|
|
2673
|
+
masterAddress: address,
|
|
2674
|
+
controlAddress: control.publicKey(),
|
|
2675
|
+
envelope,
|
|
2676
|
+
startingBalance
|
|
2677
|
+
});
|
|
2678
|
+
tx3.sign(master, funder);
|
|
2679
|
+
await adapter.submit(tx3);
|
|
1902
2680
|
}
|
|
1903
|
-
const
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
2681
|
+
const wallet = build("ready", { control, dek });
|
|
2682
|
+
wallet.isNewAccount = true;
|
|
2683
|
+
return wallet;
|
|
2684
|
+
}
|
|
2685
|
+
/** Native XLM balance of the account, in stroops. */
|
|
2686
|
+
async balance() {
|
|
2687
|
+
return this.adapter.balance(this.address);
|
|
2688
|
+
}
|
|
2689
|
+
/** True if the account has a passkey factor enrolled (`cv:wp`), so a new device
|
|
2690
|
+
* can be approved with the passkey instead of a recovery code. Mirrors the
|
|
2691
|
+
* other chains' `hasPasskey()` for the React provider. */
|
|
2692
|
+
async hasPasskey() {
|
|
2693
|
+
try {
|
|
2694
|
+
const env = fromDataEntries(await this.adapter.loadDataEntries(this.address));
|
|
2695
|
+
return !!env.passkeyWrap;
|
|
2696
|
+
} catch {
|
|
2697
|
+
return false;
|
|
1907
2698
|
}
|
|
1908
|
-
await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
|
|
1909
|
-
const isSigner = await adapter.isAuthorizedSigner(address, devicePubkey, readSource);
|
|
1910
|
-
return build(address, isSigner ? "ready" : "needs-device-approval");
|
|
1911
2699
|
}
|
|
1912
|
-
/**
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2700
|
+
/** Whether the control key is unlocked on this device (status ready). Classic
|
|
2701
|
+
* approvals land synchronously via Horizon, so this reflects state immediately
|
|
2702
|
+
* (no indexing delay to poll for). */
|
|
2703
|
+
async isReady() {
|
|
2704
|
+
return this.statusValue === "ready";
|
|
1916
2705
|
}
|
|
1917
2706
|
/**
|
|
1918
|
-
*
|
|
1919
|
-
*
|
|
2707
|
+
* Move `amount` stroops of native XLM to `destination`, signed by the control
|
|
2708
|
+
* key. Sponsored by default (the relayer fee-bumps and pays the fee); pass
|
|
2709
|
+
* `{ sponsored: false }` to submit directly — the account pays its own (tiny)
|
|
2710
|
+
* fee from its XLM balance. The control key signs identically in both modes;
|
|
2711
|
+
* only the fee payer differs.
|
|
1920
2712
|
*/
|
|
1921
|
-
async
|
|
1922
|
-
const
|
|
1923
|
-
const
|
|
1924
|
-
return
|
|
1925
|
-
}
|
|
1926
|
-
/** Register an already-enrolled passkey pubkey as an approver (gasless).
|
|
1927
|
-
* Idempotent. Lets one passkey be registered across chains without re-prompting. */
|
|
1928
|
-
async addApprover(pubkey) {
|
|
1929
|
-
if (this.status !== "ready") {
|
|
1930
|
-
throw new Error("kit/stellar: addApprover requires a ready, authorized device");
|
|
1931
|
-
}
|
|
1932
|
-
const readSource = await this.resolveSource();
|
|
1933
|
-
if (await this.adapter.isApprover(this.address, pubkey, readSource)) return {};
|
|
1934
|
-
const func = this.adapter.buildAddApprover(this.address, pubkey);
|
|
1935
|
-
const transactionHash = await this.submitHostFunction(func, this.address);
|
|
1936
|
-
return { transactionHash };
|
|
2713
|
+
async execute(amount, destination, opts) {
|
|
2714
|
+
const control = this.requireControl();
|
|
2715
|
+
const inner = await this.adapter.buildPaymentTx({ from: this.address, to: destination, amount });
|
|
2716
|
+
return this.submitInner(inner, control, opts);
|
|
1937
2717
|
}
|
|
1938
2718
|
/**
|
|
1939
|
-
*
|
|
1940
|
-
*
|
|
1941
|
-
*
|
|
1942
|
-
*
|
|
2719
|
+
* Enroll a passkey as an unlock factor: wrap the DEK under the passkey's PRF
|
|
2720
|
+
* output and write the `cv:wp` entry. This is the synced anchor used to approve
|
|
2721
|
+
* a new device or recover — it survives device loss. Idempotent-ish: writing it
|
|
2722
|
+
* again just overwrites the wrap of the same DEK. Requires a ready device.
|
|
1943
2723
|
*/
|
|
1944
|
-
async
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
const { leaf, nonce } = await this.passkeyLeafForThisDevice();
|
|
1949
|
-
const leaves = [leaf];
|
|
1950
|
-
const assertion = await passkey.assert(batchChallenge(leaves));
|
|
1951
|
-
const { transactionHash } = await this.submitPasskeyApproval(assertion, leaves, 0, nonce);
|
|
1952
|
-
return transactionHash;
|
|
2724
|
+
async enrollPasskey(prfOutput) {
|
|
2725
|
+
const { control, dek } = this.requireUnlocked();
|
|
2726
|
+
const wrap = wrapDEK(dek, derivePasskeyKEK(prfOutput));
|
|
2727
|
+
return this.writeFactor(PASSKEY_BASE, wrap, control);
|
|
1953
2728
|
}
|
|
1954
|
-
/**
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
2729
|
+
/**
|
|
2730
|
+
* Set up a recovery code as an unlock factor: wrap the DEK under the code's KEK
|
|
2731
|
+
* and write the `cv:wr` entry. Optional in v1 — the integrating app decides when
|
|
2732
|
+
* to surface it. The code never leaves the device; only the wrap goes on-chain.
|
|
2733
|
+
* Requires a ready device.
|
|
2734
|
+
*/
|
|
2735
|
+
async setupRecovery(code) {
|
|
2736
|
+
const { control, dek } = this.requireUnlocked();
|
|
2737
|
+
const wrap = wrapDEK(dek, deriveRecoveryKEK(code));
|
|
2738
|
+
return this.writeFactor(RECOVERY_BASE, wrap, control);
|
|
1959
2739
|
}
|
|
1960
|
-
/**
|
|
1961
|
-
*
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
break;
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
if (!approver) throw new Error("kit/stellar: this passkey is not a registered approver");
|
|
1974
|
-
const func = this.adapter.buildAddSignerViaPasskey(
|
|
1975
|
-
this.address,
|
|
1976
|
-
this.devicePubkey,
|
|
1977
|
-
approver,
|
|
1978
|
-
nonce,
|
|
1979
|
-
leaves,
|
|
1980
|
-
leafIndex,
|
|
1981
|
-
assertion
|
|
2740
|
+
/**
|
|
2741
|
+
* From a new browser/device (`needs-device-approval`), approve THIS device using
|
|
2742
|
+
* the user's synced passkey: unlock the DEK via the passkey factor, then wrap it
|
|
2743
|
+
* to this device's slot so future sessions unlock silently. Flips status to
|
|
2744
|
+
* `ready`. No trip back to an already-authorized device.
|
|
2745
|
+
*/
|
|
2746
|
+
async approveThisDeviceWithPasskey(prfOutput) {
|
|
2747
|
+
return this.approveThisDevice(
|
|
2748
|
+
await unlockViaPasskey(this.adapter, this.address, prfOutput),
|
|
2749
|
+
"passkey"
|
|
1982
2750
|
);
|
|
1983
|
-
return { transactionHash: await this.submitHostFunction(func, void 0) };
|
|
1984
|
-
}
|
|
1985
|
-
/** Move `amount` stroops of native XLM to `destination` (device-signed). */
|
|
1986
|
-
async execute(amount, destination) {
|
|
1987
|
-
return this.executeTransfer(NATIVE_SAC_ID[this.network], amount, destination);
|
|
1988
2751
|
}
|
|
1989
|
-
/**
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
return this.
|
|
2752
|
+
/** Approve THIS device using the recovery code (same as the passkey path, for
|
|
2753
|
+
* the backup factor). */
|
|
2754
|
+
async approveThisDeviceWithRecovery(code) {
|
|
2755
|
+
return this.approveThisDevice(
|
|
2756
|
+
await unlockViaRecovery(this.adapter, this.address, code),
|
|
2757
|
+
"recovery code"
|
|
2758
|
+
);
|
|
1993
2759
|
}
|
|
1994
|
-
/**
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2760
|
+
/** The control key's public G address (the weight-1 real signer), for display. */
|
|
2761
|
+
get controlAddress() {
|
|
2762
|
+
return this.control?.publicKey();
|
|
2763
|
+
}
|
|
2764
|
+
// --- internals ----------------------------------------------------------
|
|
2765
|
+
async approveThisDevice(unlocked, factor) {
|
|
2766
|
+
if (this.statusValue === "ready") {
|
|
2767
|
+
throw new Error("kit/stellar: this device is already authorized");
|
|
2768
|
+
}
|
|
2769
|
+
if (!unlocked) {
|
|
2770
|
+
throw new Error(`kit/stellar: could not unlock the account with the ${factor} \u2014 wrong factor or not enrolled`);
|
|
2771
|
+
}
|
|
2772
|
+
const slot = this.deviceKey.slotId();
|
|
2773
|
+
const wrap = eciesWrapDEK(unlocked.dek, this.deviceKey.publicKeySec1());
|
|
2774
|
+
const hash5 = await this.submitDataWrite(deviceWrapEntries(slot, wrap), unlocked.control);
|
|
2775
|
+
this.control = unlocked.control;
|
|
2776
|
+
this.dek = unlocked.dek;
|
|
2777
|
+
this.statusValue = "ready";
|
|
2778
|
+
return hash5;
|
|
2779
|
+
}
|
|
2780
|
+
/** Write a single-factor wrap (passkey/recovery) into the account data entries,
|
|
2781
|
+
* signed by the control key. Overwrites cleanly if the base already existed and
|
|
2782
|
+
* the new blob has the same chunk count. */
|
|
2783
|
+
async writeFactor(base, wrap, control) {
|
|
2784
|
+
const entries = {};
|
|
2785
|
+
chunkTo64(wrap).forEach((chunk, i) => {
|
|
2786
|
+
entries[`${base}/${i}`] = chunk;
|
|
2787
|
+
});
|
|
2788
|
+
return this.submitDataWrite(entries, control);
|
|
2001
2789
|
}
|
|
2002
2790
|
/**
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
2005
|
-
*
|
|
2791
|
+
* Sign an inner (account-sourced) payment tx with the control key and submit it:
|
|
2792
|
+
* - sponsored (default) → with a relayer, wrap in a fee-bump (relayer pays
|
|
2793
|
+
* the fee) and POST; falls back to self-funded if no relayer;
|
|
2794
|
+
* - `{ sponsored: false }` → submit directly (the account pays its own fee).
|
|
2795
|
+
* Payments add no subentries, so no reserve sponsorship is needed here.
|
|
2006
2796
|
*/
|
|
2007
|
-
async
|
|
2008
|
-
|
|
2009
|
-
|
|
2797
|
+
async submitInner(inner, control, opts) {
|
|
2798
|
+
inner.sign(control);
|
|
2799
|
+
const sponsored = opts?.sponsored !== false;
|
|
2800
|
+
if (sponsored && this.relayer) {
|
|
2801
|
+
const feeSource = await this.relayer.getSource();
|
|
2802
|
+
const bump = this.adapter.wrapFeeBump(inner, feeSource);
|
|
2803
|
+
return this.relayer.submit("fee-bump", bump.toXDR());
|
|
2010
2804
|
}
|
|
2011
|
-
|
|
2012
|
-
const readSource = await this.resolveSource();
|
|
2013
|
-
if (await this.adapter.isAuthorizedSigner(this.address, backupPubkey, readSource)) return void 0;
|
|
2014
|
-
return this.addSigner(backupPubkey);
|
|
2805
|
+
return this.adapter.submit(inner);
|
|
2015
2806
|
}
|
|
2016
2807
|
/**
|
|
2017
|
-
*
|
|
2018
|
-
*
|
|
2019
|
-
*
|
|
2808
|
+
* Write data entries (add a factor / device slot) — which create NEW subentries
|
|
2809
|
+
* that each need ~0.5 XLM of reserve. A relayer-sponsored account holds no XLM,
|
|
2810
|
+
* so the write must be sponsored by the relayer (source + sponsor), exactly like
|
|
2811
|
+
* account creation — a plain fee-bump would fail with `op_low_reserve`.
|
|
2812
|
+
* - sponsored (default) → with a relayer, build a sponsored write (relayer
|
|
2813
|
+
* source + begin/end sponsoring), control-sign the account ops, relay
|
|
2814
|
+
* co-signs + submits; falls back to self-funded if no relayer;
|
|
2815
|
+
* - `{ sponsored: false }` → the account writes directly (it must hold its
|
|
2816
|
+
* own reserve for the new subentries).
|
|
2020
2817
|
*/
|
|
2021
|
-
|
|
2022
|
-
const
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
|
|
2033
|
-
const existing = await registry.lookup(opts.identity.userId);
|
|
2034
|
-
if (!existing) {
|
|
2035
|
-
throw new Error("kit/stellar: no account found for this identity \u2014 nothing to recover");
|
|
2818
|
+
async submitDataWrite(entries, control, opts) {
|
|
2819
|
+
const sponsored = opts?.sponsored !== false;
|
|
2820
|
+
if (sponsored && this.relayer) {
|
|
2821
|
+
const relayerSource = await this.relayer.getSource();
|
|
2822
|
+
const tx4 = await this.adapter.buildSponsoredDataTx({
|
|
2823
|
+
relayer: relayerSource,
|
|
2824
|
+
account: this.address,
|
|
2825
|
+
entries
|
|
2826
|
+
});
|
|
2827
|
+
tx4.sign(control);
|
|
2828
|
+
return this.relayer.submit("sponsored-data", tx4.toXDR());
|
|
2036
2829
|
}
|
|
2037
|
-
const
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
devicePubkey,
|
|
2045
|
-
relayer,
|
|
2046
|
-
opts.sourceKeypair
|
|
2047
|
-
);
|
|
2048
|
-
const readSource = await backupHandle.resolveSource();
|
|
2049
|
-
if (!await backupAdapter.isAuthorizedSigner(existing.address, devicePubkey, readSource)) {
|
|
2050
|
-
await backupHandle.addSigner(devicePubkey);
|
|
2830
|
+
const tx3 = await this.adapter.buildDataTx({ account: this.address, entries });
|
|
2831
|
+
tx3.sign(control);
|
|
2832
|
+
return this.adapter.submit(tx3);
|
|
2833
|
+
}
|
|
2834
|
+
requireControl() {
|
|
2835
|
+
if (this.statusValue !== "ready" || !this.control) {
|
|
2836
|
+
throw new Error("kit/stellar: control key not unlocked on this device (needs approval)");
|
|
2051
2837
|
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2838
|
+
return this.control;
|
|
2839
|
+
}
|
|
2840
|
+
requireUnlocked() {
|
|
2841
|
+
const control = this.requireControl();
|
|
2842
|
+
if (!this.dek) throw new Error("kit/stellar: DEK unavailable on this device");
|
|
2843
|
+
return { control, dek: this.dek };
|
|
2844
|
+
}
|
|
2845
|
+
};
|
|
2846
|
+
async function unlockViaDevice(adapter, address, deviceKey) {
|
|
2847
|
+
const env = await loadEnvelope(adapter, address);
|
|
2848
|
+
const wrap = env.deviceWraps[deviceKey.slotId()];
|
|
2849
|
+
if (!wrap) return null;
|
|
2850
|
+
try {
|
|
2851
|
+
const dek = await deviceKey.unwrap(wrap);
|
|
2852
|
+
return openControl(env, dek);
|
|
2853
|
+
} catch {
|
|
2854
|
+
return null;
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
async function unlockViaPasskey(adapter, address, prfOutput) {
|
|
2858
|
+
const env = await loadEnvelope(adapter, address);
|
|
2859
|
+
if (!env.passkeyWrap) return null;
|
|
2860
|
+
try {
|
|
2861
|
+
const dek = unwrapDEK(env.passkeyWrap, derivePasskeyKEK(prfOutput));
|
|
2862
|
+
return openControl(env, dek);
|
|
2863
|
+
} catch {
|
|
2864
|
+
return null;
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
async function unlockViaRecovery(adapter, address, code) {
|
|
2868
|
+
const env = await loadEnvelope(adapter, address);
|
|
2869
|
+
if (!env.recoveryWrap) return null;
|
|
2870
|
+
try {
|
|
2871
|
+
const dek = unwrapDEK(env.recoveryWrap, deriveRecoveryKEK(code));
|
|
2872
|
+
return openControl(env, dek);
|
|
2873
|
+
} catch {
|
|
2874
|
+
return null;
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
async function loadEnvelope(adapter, address) {
|
|
2878
|
+
return fromDataEntries(await adapter.loadDataEntries(address));
|
|
2879
|
+
}
|
|
2880
|
+
function openControl(env, dek) {
|
|
2881
|
+
const controlSeed = openControlSeed(env.ct, dek);
|
|
2882
|
+
return { control: controlKeypairFromSeed(controlSeed), dek };
|
|
2883
|
+
}
|
|
2884
|
+
var LocalDeviceUnwrapKey = class _LocalDeviceUnwrapKey {
|
|
2885
|
+
constructor(scalar) {
|
|
2886
|
+
this.scalar = scalar;
|
|
2887
|
+
}
|
|
2888
|
+
/** Generate a fresh device unwrap key. */
|
|
2889
|
+
static generate() {
|
|
2890
|
+
return new _LocalDeviceUnwrapKey(p256.p256.utils.randomPrivateKey());
|
|
2891
|
+
}
|
|
2892
|
+
/** Rebuild from a persisted 32-byte scalar. */
|
|
2893
|
+
static fromScalar(scalar) {
|
|
2894
|
+
if (scalar.length !== 32) throw new Error("kit/stellar: device unwrap scalar must be 32 bytes");
|
|
2895
|
+
return new _LocalDeviceUnwrapKey(scalar);
|
|
2896
|
+
}
|
|
2897
|
+
/** The raw scalar, for the caller to persist in secure storage. */
|
|
2898
|
+
export() {
|
|
2899
|
+
return this.scalar;
|
|
2900
|
+
}
|
|
2901
|
+
publicKeySec1() {
|
|
2902
|
+
return p256.p256.getPublicKey(this.scalar, false);
|
|
2903
|
+
}
|
|
2904
|
+
slotId() {
|
|
2905
|
+
return deviceSlotId(this.publicKeySec1());
|
|
2906
|
+
}
|
|
2907
|
+
async unwrap(blob) {
|
|
2908
|
+
return eciesUnwrapDEK(blob, this.scalar);
|
|
2909
|
+
}
|
|
2910
|
+
};
|
|
2911
|
+
function deviceSlotId(publicKeySec1) {
|
|
2912
|
+
const h = sha256.sha256(publicKeySec1);
|
|
2913
|
+
let s = "";
|
|
2914
|
+
for (const b of h.subarray(0, 4)) s += b.toString(16).padStart(2, "0");
|
|
2915
|
+
return s;
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
// src/chains/stellar/WebCryptoDeviceUnwrapKey.ts
|
|
2919
|
+
var IDB_NAME2 = "cavos-kit-stellar";
|
|
2920
|
+
var IDB_STORE2 = "unwrap-keys";
|
|
2921
|
+
var WebCryptoDeviceUnwrapKey = class _WebCryptoDeviceUnwrapKey {
|
|
2922
|
+
constructor(privateKey, publicRaw, keyId) {
|
|
2923
|
+
this.privateKey = privateKey;
|
|
2924
|
+
this.publicRaw = publicRaw;
|
|
2925
|
+
this.keyId = keyId;
|
|
2926
|
+
}
|
|
2927
|
+
/** Create a fresh device unwrap key and persist it. */
|
|
2928
|
+
static async create(opts) {
|
|
2929
|
+
assertSecureContext2();
|
|
2930
|
+
const pair = await crypto.subtle.generateKey(
|
|
2931
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
2932
|
+
false,
|
|
2933
|
+
// private key is NON-extractable
|
|
2934
|
+
["deriveBits"]
|
|
2067
2935
|
);
|
|
2936
|
+
const publicRaw = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
|
|
2937
|
+
await idbPut2(opts.keyId, { privateKey: pair.privateKey, publicRaw });
|
|
2938
|
+
return new _WebCryptoDeviceUnwrapKey(pair.privateKey, publicRaw, opts.keyId);
|
|
2068
2939
|
}
|
|
2069
|
-
/**
|
|
2070
|
-
async
|
|
2071
|
-
|
|
2072
|
-
if (
|
|
2073
|
-
|
|
2940
|
+
/** Load an existing device unwrap key, or null if none exists yet. */
|
|
2941
|
+
static async load(opts) {
|
|
2942
|
+
const rec = await idbGet2(opts.keyId);
|
|
2943
|
+
if (!rec) return null;
|
|
2944
|
+
return new _WebCryptoDeviceUnwrapKey(rec.privateKey, rec.publicRaw, opts.keyId);
|
|
2074
2945
|
}
|
|
2075
|
-
/**
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
const
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
2103
|
-
const account = await server.getAccount(sourceAddr);
|
|
2104
|
-
const finalOp = stellarSdk.Operation.invokeHostFunction({ func, auth: signedAuth });
|
|
2105
|
-
const built = new stellarSdk.TransactionBuilder(account, {
|
|
2106
|
-
fee: stellarSdk.BASE_FEE,
|
|
2107
|
-
networkPassphrase: this.adapter.passphrase
|
|
2108
|
-
}).addOperation(finalOp).setTimeout(180).build();
|
|
2109
|
-
const authSim = await server.simulateTransaction(built);
|
|
2110
|
-
if (stellarSdk.rpc.Api.isSimulationError(authSim)) {
|
|
2111
|
-
throw new Error(`kit/stellar: auth simulation failed: ${authSim.error}`);
|
|
2112
|
-
}
|
|
2113
|
-
const assembled = stellarSdk.rpc.assembleTransaction(built, authSim).build();
|
|
2114
|
-
if (this.relayer) {
|
|
2115
|
-
return this.relayer.submit(assembled.toXDR());
|
|
2116
|
-
}
|
|
2117
|
-
if (this.sourceKeypair) {
|
|
2118
|
-
assembled.sign(this.sourceKeypair);
|
|
2119
|
-
return this.sendAndConfirm(assembled);
|
|
2120
|
-
}
|
|
2121
|
-
throw new Error("kit/stellar: no relayer or sourceKeypair configured to submit");
|
|
2122
|
-
}
|
|
2123
|
-
/** Submit a signed tx via RPC and poll to confirmation. Returns the hash. */
|
|
2124
|
-
async sendAndConfirm(tx2) {
|
|
2125
|
-
const server = this.adapter.server();
|
|
2126
|
-
const sent = await server.sendTransaction(tx2);
|
|
2127
|
-
if (sent.status === "ERROR") {
|
|
2128
|
-
throw new Error(`kit/stellar: submit rejected: ${JSON.stringify(sent.errorResult)}`);
|
|
2129
|
-
}
|
|
2130
|
-
const hash6 = sent.hash;
|
|
2131
|
-
for (let i = 0; i < 30; i++) {
|
|
2132
|
-
const got = await server.getTransaction(hash6);
|
|
2133
|
-
if (got.status === stellarSdk.rpc.Api.GetTransactionStatus.SUCCESS) return hash6;
|
|
2134
|
-
if (got.status === stellarSdk.rpc.Api.GetTransactionStatus.FAILED) {
|
|
2135
|
-
throw new Error(`kit/stellar: tx ${hash6} failed`);
|
|
2136
|
-
}
|
|
2137
|
-
await new Promise((r) => setTimeout(r, 1e3));
|
|
2138
|
-
}
|
|
2139
|
-
throw new Error(`kit/stellar: tx ${hash6} not confirmed in time`);
|
|
2946
|
+
/** Load the device unwrap key, creating one on first use. */
|
|
2947
|
+
static async loadOrCreate(opts) {
|
|
2948
|
+
return await _WebCryptoDeviceUnwrapKey.load(opts) ?? await _WebCryptoDeviceUnwrapKey.create(opts);
|
|
2949
|
+
}
|
|
2950
|
+
publicKeySec1() {
|
|
2951
|
+
return this.publicRaw;
|
|
2952
|
+
}
|
|
2953
|
+
slotId() {
|
|
2954
|
+
return deviceSlotId(this.publicRaw);
|
|
2955
|
+
}
|
|
2956
|
+
async unwrap(blob) {
|
|
2957
|
+
const ephPubCompressed = blob.subarray(0, 33);
|
|
2958
|
+
const wrapped = blob.subarray(33);
|
|
2959
|
+
const ephUncompressed = p256.p256.ProjectivePoint.fromHex(ephPubCompressed).toRawBytes(false);
|
|
2960
|
+
const ephKey = await crypto.subtle.importKey(
|
|
2961
|
+
"raw",
|
|
2962
|
+
ephUncompressed,
|
|
2963
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
2964
|
+
false,
|
|
2965
|
+
[]
|
|
2966
|
+
);
|
|
2967
|
+
const sharedX = new Uint8Array(
|
|
2968
|
+
await crypto.subtle.deriveBits({ name: "ECDH", public: ephKey }, this.privateKey, 256)
|
|
2969
|
+
);
|
|
2970
|
+
const kek = eciesKEKFromX(sharedX, ephPubCompressed);
|
|
2971
|
+
return unwrapDEK(wrapped, kek);
|
|
2140
2972
|
}
|
|
2141
2973
|
};
|
|
2142
|
-
function
|
|
2143
|
-
const
|
|
2144
|
-
if (
|
|
2145
|
-
|
|
2974
|
+
function assertSecureContext2() {
|
|
2975
|
+
const ok = typeof crypto !== "undefined" && typeof crypto.subtle !== "undefined" && (typeof window === "undefined" || window.isSecureContext);
|
|
2976
|
+
if (!ok) {
|
|
2977
|
+
throw new Error(
|
|
2978
|
+
"Cavos: WebCrypto is unavailable. Device keys require a secure context \u2014 use HTTPS, or http://localhost."
|
|
2979
|
+
);
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
function openDb2() {
|
|
2983
|
+
return new Promise((resolve, reject) => {
|
|
2984
|
+
const req = indexedDB.open(IDB_NAME2, 1);
|
|
2985
|
+
req.onupgradeneeded = () => req.result.createObjectStore(IDB_STORE2);
|
|
2986
|
+
req.onsuccess = () => resolve(req.result);
|
|
2987
|
+
req.onerror = () => reject(req.error);
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2990
|
+
async function idbPut2(keyId, value) {
|
|
2991
|
+
const db = await openDb2();
|
|
2992
|
+
await tx2(db, "readwrite", (store) => store.put(value, keyId));
|
|
2993
|
+
db.close();
|
|
2994
|
+
}
|
|
2995
|
+
async function idbGet2(keyId) {
|
|
2996
|
+
const db = await openDb2();
|
|
2997
|
+
const result = await tx2(db, "readonly", (store) => store.get(keyId));
|
|
2998
|
+
db.close();
|
|
2999
|
+
return result ?? null;
|
|
3000
|
+
}
|
|
3001
|
+
function tx2(db, mode, run) {
|
|
3002
|
+
return new Promise((resolve, reject) => {
|
|
3003
|
+
const store = db.transaction(IDB_STORE2, mode).objectStore(IDB_STORE2);
|
|
3004
|
+
const req = run(store);
|
|
3005
|
+
req.onsuccess = () => resolve(req.result);
|
|
3006
|
+
req.onerror = () => reject(req.error);
|
|
3007
|
+
});
|
|
2146
3008
|
}
|
|
2147
|
-
var defaultRegistry2 = new InMemoryWalletRegistry();
|
|
2148
3009
|
|
|
2149
3010
|
// src/recovery/HttpRecoveryClient.ts
|
|
2150
3011
|
function toHex2(n) {
|
|
@@ -2244,6 +3105,9 @@ var Cavos = class _Cavos {
|
|
|
2244
3105
|
this.chain = "starknet";
|
|
2245
3106
|
/** Request id of the pending device-addition, when status is needs-device-approval. */
|
|
2246
3107
|
this.pendingRequestId = null;
|
|
3108
|
+
/** True when this connect just created & deployed a brand-new account (first
|
|
3109
|
+
* sign-up), so the UI can offer a one-time "secure your account" step. */
|
|
3110
|
+
this.isNewAccount = false;
|
|
2247
3111
|
}
|
|
2248
3112
|
/**
|
|
2249
3113
|
* Unified entry point. Pick a `chain` and an `network` environment; the kit
|
|
@@ -2273,17 +3137,16 @@ var Cavos = class _Cavos {
|
|
|
2273
3137
|
});
|
|
2274
3138
|
}
|
|
2275
3139
|
if (opts.chain === "stellar") {
|
|
3140
|
+
const identity = opts.identity ?? (opts.auth ? await opts.auth.authenticate() : void 0);
|
|
3141
|
+
if (!identity) throw new Error("kit: Stellar connect requires `identity` or `auth`");
|
|
3142
|
+
const deviceKey = opts.stellarDeviceKey ?? await WebCryptoDeviceUnwrapKey.loadOrCreate({ keyId: `${identity.userId}:${opts.appSalt}` });
|
|
2276
3143
|
return CavosStellar.connect({
|
|
2277
3144
|
network: STELLAR_ENV[opts.network],
|
|
2278
|
-
|
|
2279
|
-
...opts.identity ? { identity: opts.identity } : {},
|
|
3145
|
+
identity,
|
|
2280
3146
|
appSalt: opts.appSalt,
|
|
3147
|
+
deviceKey,
|
|
2281
3148
|
...opts.appId ? { appId: opts.appId } : {},
|
|
2282
3149
|
...opts.backendUrl ? { backendUrl: opts.backendUrl } : {},
|
|
2283
|
-
...opts.registry ? { registry: opts.registry } : {},
|
|
2284
|
-
...opts.rpcUrl ? { rpcUrl: opts.rpcUrl } : {},
|
|
2285
|
-
...opts.factoryId ? { factoryId: opts.factoryId } : {},
|
|
2286
|
-
...opts.createSigner ? { createSigner: opts.createSigner } : {},
|
|
2287
3150
|
...opts.stellarRelayer ? { relayer: opts.stellarRelayer } : {},
|
|
2288
3151
|
...opts.stellarSourceKeypair ? { sourceKeypair: opts.stellarSourceKeypair } : {}
|
|
2289
3152
|
});
|
|
@@ -2333,13 +3196,13 @@ var Cavos = class _Cavos {
|
|
|
2333
3196
|
cairoVersion: "1"
|
|
2334
3197
|
});
|
|
2335
3198
|
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
2336
|
-
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) :
|
|
3199
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network: opts.network }) : defaultRegistry2);
|
|
2337
3200
|
const recovery = opts.recovery ?? (opts.appId ? new HttpRecoveryClient({ baseUrl: backendUrl, appId: opts.appId }) : null);
|
|
2338
3201
|
const existing = await registry.lookup(identity.userId);
|
|
2339
3202
|
if (existing) {
|
|
2340
3203
|
const account2 = makeAccount(existing.address);
|
|
2341
3204
|
const isSigner2 = await adapter.isAuthorizedSigner(existing.address, devicePubkey);
|
|
2342
|
-
const
|
|
3205
|
+
const cavos2 = new _Cavos(
|
|
2343
3206
|
identity,
|
|
2344
3207
|
existing.address,
|
|
2345
3208
|
isSigner2 ? "ready" : "needs-device-approval",
|
|
@@ -2353,7 +3216,7 @@ var Cavos = class _Cavos {
|
|
|
2353
3216
|
const fresh = dedup && Date.now() - dedup.requestedAt < DEVICE_REQUEST_DEDUP_MS;
|
|
2354
3217
|
try {
|
|
2355
3218
|
if (fresh) {
|
|
2356
|
-
|
|
3219
|
+
cavos2.pendingRequestId = dedup.requestId;
|
|
2357
3220
|
} else {
|
|
2358
3221
|
const { requestId } = await recovery.requestDeviceAddition({
|
|
2359
3222
|
userId: identity.userId,
|
|
@@ -2361,16 +3224,16 @@ var Cavos = class _Cavos {
|
|
|
2361
3224
|
newSigner: devicePubkey,
|
|
2362
3225
|
...identity.email ? { email: identity.email } : {}
|
|
2363
3226
|
});
|
|
2364
|
-
|
|
3227
|
+
cavos2.pendingRequestId = requestId;
|
|
2365
3228
|
lastDeviceRequest.set(identity.userId, { requestId, requestedAt: Date.now() });
|
|
2366
3229
|
}
|
|
2367
3230
|
} catch (e) {
|
|
2368
3231
|
console.warn("[Cavos] requestDeviceAddition failed:", e);
|
|
2369
3232
|
}
|
|
2370
3233
|
}
|
|
2371
|
-
return
|
|
3234
|
+
return cavos2;
|
|
2372
3235
|
}
|
|
2373
|
-
const address = adapter.computeAddress({ addressSeed
|
|
3236
|
+
const address = adapter.computeAddress({ addressSeed });
|
|
2374
3237
|
const account = makeAccount(address);
|
|
2375
3238
|
const alreadyDeployed = await isDeployed(provider, address);
|
|
2376
3239
|
if (!alreadyDeployed) {
|
|
@@ -2378,10 +3241,11 @@ var Cavos = class _Cavos {
|
|
|
2378
3241
|
address,
|
|
2379
3242
|
class_hash: classHash,
|
|
2380
3243
|
salt: starknet.num.toHex(addressSeed),
|
|
2381
|
-
calldata: adapter.constructorCalldata(addressSeed
|
|
3244
|
+
calldata: adapter.constructorCalldata(addressSeed),
|
|
2382
3245
|
version: 1
|
|
2383
3246
|
};
|
|
2384
|
-
const
|
|
3247
|
+
const initCall = adapter.buildInitialize(address, devicePubkey);
|
|
3248
|
+
const deployRes = await account.executePaymasterTransaction([initCall], {
|
|
2385
3249
|
feeMode: { mode: "sponsored" },
|
|
2386
3250
|
deploymentData
|
|
2387
3251
|
});
|
|
@@ -2399,7 +3263,7 @@ var Cavos = class _Cavos {
|
|
|
2399
3263
|
console.warn("[Cavos] isAuthorizedSigner read failed:", e);
|
|
2400
3264
|
isSigner = !alreadyDeployed;
|
|
2401
3265
|
}
|
|
2402
|
-
|
|
3266
|
+
const cavos = new _Cavos(
|
|
2403
3267
|
identity,
|
|
2404
3268
|
address,
|
|
2405
3269
|
isSigner ? "ready" : "needs-device-approval",
|
|
@@ -2408,24 +3272,33 @@ var Cavos = class _Cavos {
|
|
|
2408
3272
|
devicePubkey,
|
|
2409
3273
|
paymasterConfig
|
|
2410
3274
|
);
|
|
3275
|
+
cavos.isNewAccount = !alreadyDeployed && isSigner;
|
|
3276
|
+
return cavos;
|
|
2411
3277
|
}
|
|
2412
3278
|
/** This device's public key (e.g. to request addition to an existing wallet). */
|
|
2413
3279
|
get publicKey() {
|
|
2414
3280
|
return this.devicePubkey;
|
|
2415
3281
|
}
|
|
2416
3282
|
/** Execute a sponsored (gasless) multicall, signed silently by the device. */
|
|
2417
|
-
async execute(calls) {
|
|
3283
|
+
async execute(calls, opts) {
|
|
2418
3284
|
if (this.status !== "ready") {
|
|
2419
3285
|
throw new Error("kit: this device is not yet an authorized signer of the wallet");
|
|
2420
3286
|
}
|
|
3287
|
+
if (opts?.sponsored === false) {
|
|
3288
|
+
const res2 = await this.account.execute(calls);
|
|
3289
|
+
return { transactionHash: res2.transaction_hash };
|
|
3290
|
+
}
|
|
2421
3291
|
const res = await this.account.executePaymasterTransaction(calls, {
|
|
2422
3292
|
feeMode: { mode: "sponsored" }
|
|
2423
3293
|
});
|
|
2424
3294
|
return { transactionHash: res.transaction_hash };
|
|
2425
3295
|
}
|
|
2426
|
-
/**
|
|
2427
|
-
|
|
2428
|
-
|
|
3296
|
+
/**
|
|
3297
|
+
* Authorize an additional device signer. Sponsored by default; pass
|
|
3298
|
+
* `{ sponsored: false }` to pay the fee from the account's own ETH balance.
|
|
3299
|
+
*/
|
|
3300
|
+
async addSigner(pubkey, opts) {
|
|
3301
|
+
return this.execute([this.adapter.buildAddSigner(this.address, pubkey)], opts);
|
|
2429
3302
|
}
|
|
2430
3303
|
/**
|
|
2431
3304
|
* Enroll a passkey as an APPROVER so the user can later add devices from any
|
|
@@ -2434,27 +3307,45 @@ var Cavos = class _Cavos {
|
|
|
2434
3307
|
* approver. Call this whenever the app decides to prompt "turn on device
|
|
2435
3308
|
* approvals". Returns the passkey's public key + the enrollment tx hash.
|
|
2436
3309
|
*/
|
|
2437
|
-
async enrollPasskey(passkey, params) {
|
|
3310
|
+
async enrollPasskey(passkey, params, opts) {
|
|
2438
3311
|
const enrolled = await passkey.enroll(params);
|
|
2439
|
-
const { transactionHash } = await this.addApprover(enrolled.publicKey);
|
|
3312
|
+
const { transactionHash } = await this.addApprover(enrolled.publicKey, opts);
|
|
2440
3313
|
return { publicKey: enrolled.publicKey, transactionHash };
|
|
2441
3314
|
}
|
|
2442
3315
|
/**
|
|
2443
|
-
* Register an ALREADY-enrolled passkey public key as an approver (gasless
|
|
2444
|
-
* device-signed). Idempotent. Use this to register ONE passkey across
|
|
2445
|
-
* chains without re-prompting `passkey.enroll()` on each: enroll once,
|
|
2446
|
-
* call `addApprover(pubkey)` on each chain's wallet.
|
|
3316
|
+
* Register an ALREADY-enrolled passkey public key as an approver (gasless by
|
|
3317
|
+
* default, device-signed). Idempotent. Use this to register ONE passkey across
|
|
3318
|
+
* multiple chains without re-prompting `passkey.enroll()` on each: enroll once,
|
|
3319
|
+
* then call `addApprover(pubkey)` on each chain's wallet. Pass
|
|
3320
|
+
* `{ sponsored: false }` to pay the fee from the account's own balance.
|
|
2447
3321
|
*/
|
|
2448
|
-
async addApprover(pubkey) {
|
|
3322
|
+
async addApprover(pubkey, opts) {
|
|
2449
3323
|
if (this.status !== "ready") {
|
|
2450
3324
|
throw new Error("kit: addApprover requires a ready, authorized device");
|
|
2451
3325
|
}
|
|
2452
3326
|
if (await this.adapter.isApprover(this.address, pubkey)) return {};
|
|
2453
|
-
const { transactionHash } = await this.execute(
|
|
2454
|
-
this.adapter.buildAddApprover(this.address, pubkey)
|
|
2455
|
-
|
|
3327
|
+
const { transactionHash } = await this.execute(
|
|
3328
|
+
[this.adapter.buildAddApprover(this.address, pubkey)],
|
|
3329
|
+
opts
|
|
3330
|
+
);
|
|
3331
|
+
try {
|
|
3332
|
+
await this.account.waitForTransaction(transactionHash);
|
|
3333
|
+
} catch (e) {
|
|
3334
|
+
console.warn("[Cavos] add_approver receipt wait failed:", e);
|
|
3335
|
+
}
|
|
2456
3336
|
return { transactionHash };
|
|
2457
3337
|
}
|
|
3338
|
+
/** True if this account already has a passkey enrolled as an approver, so a
|
|
3339
|
+
* new device can be approved with the passkey instead of the email flow. */
|
|
3340
|
+
async hasPasskey() {
|
|
3341
|
+
return this.adapter.hasPasskeyApprover(this.address);
|
|
3342
|
+
}
|
|
3343
|
+
/** Re-read (from chain) whether THIS device is now an authorized signer.
|
|
3344
|
+
* Cheap and side-effect free — used to poll for readiness after a passkey /
|
|
3345
|
+
* device approval submits, before the new signer is indexed. */
|
|
3346
|
+
async isReady() {
|
|
3347
|
+
return this.adapter.isAuthorizedSigner(this.address, this.devicePubkey);
|
|
3348
|
+
}
|
|
2458
3349
|
/**
|
|
2459
3350
|
* From a brand-new browser (status `needs-device-approval`), use the user's
|
|
2460
3351
|
* synced passkey to authorize adding THIS device — no trip back to an already-
|
|
@@ -2523,11 +3414,11 @@ var Cavos = class _Cavos {
|
|
|
2523
3414
|
* add_signer (gasless). Returns the transaction hash (or undefined when the
|
|
2524
3415
|
* backup was already set up).
|
|
2525
3416
|
*/
|
|
2526
|
-
async setupRecovery(code) {
|
|
3417
|
+
async setupRecovery(code, opts) {
|
|
2527
3418
|
const { publicKey: backupPubkey } = deriveBackupKey(code);
|
|
2528
3419
|
const already = await this.adapter.isAuthorizedSigner(this.address, backupPubkey);
|
|
2529
3420
|
if (already) return void 0;
|
|
2530
|
-
return this.addSigner(backupPubkey);
|
|
3421
|
+
return this.addSigner(backupPubkey, opts);
|
|
2531
3422
|
}
|
|
2532
3423
|
/**
|
|
2533
3424
|
* Recover an account after losing every device signer. Derives the backup key
|
|
@@ -2554,11 +3445,11 @@ var Cavos = class _Cavos {
|
|
|
2554
3445
|
const backup = BackupSigner.fromCode(opts.code);
|
|
2555
3446
|
const backupAdapter = new StarknetAdapter({ classHash, signer: backup, provider });
|
|
2556
3447
|
const backendUrl = opts.backendUrl ?? "https://cavos.xyz";
|
|
2557
|
-
const
|
|
2558
|
-
|
|
2559
|
-
if (!existing) {
|
|
3448
|
+
const address = opts.address ?? await lookupAddress(opts, backendUrl, network);
|
|
3449
|
+
if (!address) {
|
|
2560
3450
|
throw new Error("kit: no account found for this identity \u2014 nothing to recover");
|
|
2561
3451
|
}
|
|
3452
|
+
const existing = { address };
|
|
2562
3453
|
const backupAccount = new starknet.Account({
|
|
2563
3454
|
provider,
|
|
2564
3455
|
address: existing.address,
|
|
@@ -2589,7 +3480,7 @@ var Cavos = class _Cavos {
|
|
|
2589
3480
|
return new _Cavos(opts.identity, existing.address, "ready", account, adapter, devicePubkey);
|
|
2590
3481
|
}
|
|
2591
3482
|
};
|
|
2592
|
-
var
|
|
3483
|
+
var defaultRegistry2 = new InMemoryWalletRegistry();
|
|
2593
3484
|
var DEVICE_REQUEST_DEDUP_MS = 5 * 60 * 1e3;
|
|
2594
3485
|
var lastDeviceRequest = /* @__PURE__ */ new Map();
|
|
2595
3486
|
async function isDeployed(provider, address) {
|
|
@@ -2600,23 +3491,26 @@ async function isDeployed(provider, address) {
|
|
|
2600
3491
|
return false;
|
|
2601
3492
|
}
|
|
2602
3493
|
}
|
|
3494
|
+
async function lookupAddress(opts, backendUrl, network) {
|
|
3495
|
+
const registry = opts.registry ?? (opts.appId ? new HttpWalletRegistry({ baseUrl: backendUrl, appId: opts.appId, network }) : defaultRegistry2);
|
|
3496
|
+
const existing = await registry.lookup(opts.identity.userId);
|
|
3497
|
+
return existing?.address ?? null;
|
|
3498
|
+
}
|
|
2603
3499
|
async function approveDeviceEverywhere(wallets, passkey) {
|
|
2604
3500
|
const targets = wallets.filter((w) => w.status === "needs-device-approval");
|
|
2605
3501
|
if (targets.length === 0) return [];
|
|
2606
3502
|
const infos = await Promise.all(targets.map((w) => w.passkeyLeafForThisDevice()));
|
|
2607
3503
|
const leaves = infos.map((i) => i.leaf);
|
|
2608
3504
|
const assertion = await passkey.assert(batchChallenge(leaves));
|
|
2609
|
-
const
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
i,
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
}
|
|
2619
|
-
return out;
|
|
3505
|
+
const settled = await Promise.allSettled(
|
|
3506
|
+
targets.map((w, i) => w.submitPasskeyApproval(assertion, leaves, i, infos[i].nonce))
|
|
3507
|
+
);
|
|
3508
|
+
return settled.map(
|
|
3509
|
+
(r, i) => r.status === "fulfilled" ? { chain: targets[i].chain, transactionHash: r.value.transactionHash } : {
|
|
3510
|
+
chain: targets[i].chain,
|
|
3511
|
+
error: r.reason instanceof Error ? r.reason.message : String(r.reason)
|
|
3512
|
+
}
|
|
3513
|
+
);
|
|
2620
3514
|
}
|
|
2621
3515
|
async function paymasterExecuteDirect(paymaster, userAddress, call) {
|
|
2622
3516
|
const body = {
|
|
@@ -2799,6 +3693,101 @@ function bytesToChunks(bytes) {
|
|
|
2799
3693
|
for (const b of bytes.subarray(0, 31)) w = w << 8n | BigInt(b);
|
|
2800
3694
|
return w;
|
|
2801
3695
|
}
|
|
3696
|
+
var PRF_SALT = sha256.sha256(new TextEncoder().encode("cavos-stellar-prf-v1"));
|
|
3697
|
+
var PasskeyPrf = class {
|
|
3698
|
+
constructor(opts = {}) {
|
|
3699
|
+
if (typeof window === "undefined" || !navigator.credentials) {
|
|
3700
|
+
throw new Error("kit/passkey-prf: WebAuthn is only available in a browser");
|
|
3701
|
+
}
|
|
3702
|
+
this.rpId = opts.rpId ?? window.location.hostname;
|
|
3703
|
+
this.rpName = opts.rpName ?? this.rpId;
|
|
3704
|
+
if (isIpAddress(this.rpId)) {
|
|
3705
|
+
throw new Error(
|
|
3706
|
+
`kit/passkey-prf: passkeys can't use an IP address as the domain ("${this.rpId}"). Use http://localhost, a real HTTPS domain, or a tunnel \u2014 or pass an explicit \`rpId\`.`
|
|
3707
|
+
);
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
/** True if this platform advertises a usable passkey (platform authenticator). */
|
|
3711
|
+
static async isSupported() {
|
|
3712
|
+
if (typeof window === "undefined" || !window.PublicKeyCredential) return false;
|
|
3713
|
+
try {
|
|
3714
|
+
return await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
|
3715
|
+
} catch {
|
|
3716
|
+
return false;
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
/**
|
|
3720
|
+
* Create a new synced, discoverable passkey with the PRF extension enabled, then
|
|
3721
|
+
* immediately read its PRF secret. Returns the credential id and the 32-byte
|
|
3722
|
+
* secret so the caller can enroll the passkey factor in one step. Some
|
|
3723
|
+
* authenticators don't return PRF results on create — in that case `secret` is
|
|
3724
|
+
* undefined and the caller should follow up with `getSecret()`.
|
|
3725
|
+
*/
|
|
3726
|
+
async enroll(params) {
|
|
3727
|
+
const cred = await navigator.credentials.create({
|
|
3728
|
+
publicKey: {
|
|
3729
|
+
challenge: buf(crypto.getRandomValues(new Uint8Array(32))),
|
|
3730
|
+
rp: { id: this.rpId, name: this.rpName },
|
|
3731
|
+
user: {
|
|
3732
|
+
id: buf(userHandle(params.userId)),
|
|
3733
|
+
name: params.userName,
|
|
3734
|
+
displayName: params.displayName ?? params.userName
|
|
3735
|
+
},
|
|
3736
|
+
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
|
|
3737
|
+
// ES256 (P-256)
|
|
3738
|
+
authenticatorSelection: {
|
|
3739
|
+
residentKey: "required",
|
|
3740
|
+
requireResidentKey: true,
|
|
3741
|
+
userVerification: "required"
|
|
3742
|
+
},
|
|
3743
|
+
attestation: "none",
|
|
3744
|
+
extensions: { prf: { eval: { first: buf(PRF_SALT) } } }
|
|
3745
|
+
}
|
|
3746
|
+
});
|
|
3747
|
+
if (!cred) throw new Error("kit/passkey-prf: enrollment cancelled");
|
|
3748
|
+
return { credentialId: new Uint8Array(cred.rawId), secret: readPrf(cred) };
|
|
3749
|
+
}
|
|
3750
|
+
/**
|
|
3751
|
+
* Get the passkey's 32-byte PRF secret. Uses discoverable credentials (no
|
|
3752
|
+
* `allowCredentials`), so it works from a brand-new browser — the OS shows the
|
|
3753
|
+
* synced passkey picker. Throws if the authenticator doesn't support PRF.
|
|
3754
|
+
*/
|
|
3755
|
+
async getSecret() {
|
|
3756
|
+
const cred = await navigator.credentials.get({
|
|
3757
|
+
publicKey: {
|
|
3758
|
+
challenge: buf(crypto.getRandomValues(new Uint8Array(32))),
|
|
3759
|
+
rpId: this.rpId,
|
|
3760
|
+
allowCredentials: [],
|
|
3761
|
+
userVerification: "required",
|
|
3762
|
+
extensions: { prf: { eval: { first: buf(PRF_SALT) } } }
|
|
3763
|
+
}
|
|
3764
|
+
});
|
|
3765
|
+
if (!cred) throw new Error("kit/passkey-prf: assertion cancelled");
|
|
3766
|
+
const secret = readPrf(cred);
|
|
3767
|
+
if (!secret) {
|
|
3768
|
+
throw new Error(
|
|
3769
|
+
"kit/passkey-prf: this authenticator did not return a PRF result \u2014 PRF is unsupported here"
|
|
3770
|
+
);
|
|
3771
|
+
}
|
|
3772
|
+
return secret;
|
|
3773
|
+
}
|
|
3774
|
+
};
|
|
3775
|
+
function readPrf(cred) {
|
|
3776
|
+
const results = cred.getClientExtensionResults().prf?.results?.first;
|
|
3777
|
+
return results ? new Uint8Array(results) : void 0;
|
|
3778
|
+
}
|
|
3779
|
+
function isIpAddress(host) {
|
|
3780
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true;
|
|
3781
|
+
if (host.includes(":")) return true;
|
|
3782
|
+
return false;
|
|
3783
|
+
}
|
|
3784
|
+
function userHandle(userId) {
|
|
3785
|
+
const bytes = new TextEncoder().encode(userId);
|
|
3786
|
+
return bytes.length <= 64 ? bytes : sha256.sha256(bytes);
|
|
3787
|
+
}
|
|
3788
|
+
function buf(bytes) {
|
|
3789
|
+
return bytes.slice();
|
|
3790
|
+
}
|
|
2802
3791
|
var PasskeySigner = class {
|
|
2803
3792
|
constructor(opts = {}) {
|
|
2804
3793
|
if (typeof window === "undefined" || !navigator.credentials) {
|
|
@@ -2806,7 +3795,7 @@ var PasskeySigner = class {
|
|
|
2806
3795
|
}
|
|
2807
3796
|
this.rpId = opts.rpId ?? window.location.hostname;
|
|
2808
3797
|
this.rpName = opts.rpName ?? this.rpId;
|
|
2809
|
-
if (
|
|
3798
|
+
if (isIpAddress2(this.rpId)) {
|
|
2810
3799
|
throw new Error(
|
|
2811
3800
|
`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.)`
|
|
2812
3801
|
);
|
|
@@ -2826,10 +3815,10 @@ var PasskeySigner = class {
|
|
|
2826
3815
|
const challenge = crypto.getRandomValues(new Uint8Array(32));
|
|
2827
3816
|
const cred = await navigator.credentials.create({
|
|
2828
3817
|
publicKey: {
|
|
2829
|
-
challenge:
|
|
3818
|
+
challenge: buf2(challenge),
|
|
2830
3819
|
rp: { id: this.rpId, name: this.rpName },
|
|
2831
3820
|
user: {
|
|
2832
|
-
id:
|
|
3821
|
+
id: buf2(userHandle2(params.userId)),
|
|
2833
3822
|
name: params.userName,
|
|
2834
3823
|
displayName: params.displayName ?? params.userName
|
|
2835
3824
|
},
|
|
@@ -2856,7 +3845,7 @@ var PasskeySigner = class {
|
|
|
2856
3845
|
async assert(challenge) {
|
|
2857
3846
|
const cred = await navigator.credentials.get({
|
|
2858
3847
|
publicKey: {
|
|
2859
|
-
challenge:
|
|
3848
|
+
challenge: buf2(challenge),
|
|
2860
3849
|
rpId: this.rpId,
|
|
2861
3850
|
allowCredentials: [],
|
|
2862
3851
|
userVerification: "preferred"
|
|
@@ -2871,18 +3860,23 @@ var PasskeySigner = class {
|
|
|
2871
3860
|
return { authenticatorData, clientDataJSON, r, s, challengeOffset };
|
|
2872
3861
|
}
|
|
2873
3862
|
};
|
|
2874
|
-
function
|
|
3863
|
+
function isIpAddress2(host) {
|
|
2875
3864
|
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true;
|
|
2876
3865
|
if (host.includes(":")) return true;
|
|
2877
3866
|
return false;
|
|
2878
3867
|
}
|
|
2879
|
-
function
|
|
3868
|
+
function userHandle2(userId) {
|
|
2880
3869
|
const bytes = new TextEncoder().encode(userId);
|
|
2881
3870
|
return bytes.length <= 64 ? bytes : sha256.sha256(bytes);
|
|
2882
3871
|
}
|
|
2883
|
-
function
|
|
3872
|
+
function buf2(bytes) {
|
|
2884
3873
|
return bytes.slice();
|
|
2885
3874
|
}
|
|
3875
|
+
/*! Bundled license information:
|
|
3876
|
+
|
|
3877
|
+
@noble/ciphers/esm/utils.js:
|
|
3878
|
+
(*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) *)
|
|
3879
|
+
*/
|
|
2886
3880
|
|
|
2887
3881
|
exports.BackupSigner = BackupSigner;
|
|
2888
3882
|
exports.Cavos = Cavos;
|
|
@@ -2891,12 +3885,12 @@ exports.CavosSolana = CavosSolana;
|
|
|
2891
3885
|
exports.CavosStellar = CavosStellar;
|
|
2892
3886
|
exports.DEVICE_ACCOUNT_CLASS_HASH = DEVICE_ACCOUNT_CLASS_HASH;
|
|
2893
3887
|
exports.DEVICE_ACCOUNT_PROGRAM_ID = DEVICE_ACCOUNT_PROGRAM_ID;
|
|
2894
|
-
exports.
|
|
2895
|
-
exports.FACTORY_CONTRACT_ID = FACTORY_CONTRACT_ID;
|
|
3888
|
+
exports.HORIZON_URL = HORIZON_URL;
|
|
2896
3889
|
exports.HttpRecoveryClient = HttpRecoveryClient;
|
|
2897
3890
|
exports.HttpWalletRegistry = HttpWalletRegistry;
|
|
2898
3891
|
exports.InMemoryWalletRegistry = InMemoryWalletRegistry;
|
|
2899
|
-
exports.
|
|
3892
|
+
exports.LocalDeviceUnwrapKey = LocalDeviceUnwrapKey;
|
|
3893
|
+
exports.PasskeyPrf = PasskeyPrf;
|
|
2900
3894
|
exports.PasskeySigner = PasskeySigner;
|
|
2901
3895
|
exports.SECP256R1_PROGRAM_ID = SECP256R1_PROGRAM_ID;
|
|
2902
3896
|
exports.SOLANA_NETWORKS = SOLANA_NETWORKS;
|
|
@@ -2910,7 +3904,9 @@ exports.StaticIdentity = StaticIdentity;
|
|
|
2910
3904
|
exports.StellarAdapter = StellarAdapter;
|
|
2911
3905
|
exports.StellarRelayer = StellarRelayer;
|
|
2912
3906
|
exports.UDC_ADDRESS = UDC_ADDRESS;
|
|
3907
|
+
exports.WebCryptoDeviceUnwrapKey = WebCryptoDeviceUnwrapKey;
|
|
2913
3908
|
exports.WebCryptoSigner = WebCryptoSigner;
|
|
3909
|
+
exports.XLM_DECIMALS = XLM_DECIMALS;
|
|
2914
3910
|
exports.anchorDiscriminator = anchorDiscriminator;
|
|
2915
3911
|
exports.approveDeviceEverywhere = approveDeviceEverywhere;
|
|
2916
3912
|
exports.base64urlEncode = base64urlEncode;
|
|
@@ -2924,18 +3920,30 @@ exports.deriveAddressSeed = deriveAddressSeed;
|
|
|
2924
3920
|
exports.deriveAddressSeedSolana = deriveAddressSeedSolana;
|
|
2925
3921
|
exports.deriveAddressSeedStellar = deriveAddressSeedStellar;
|
|
2926
3922
|
exports.deriveBackupKey = deriveBackupKey;
|
|
2927
|
-
exports.
|
|
3923
|
+
exports.derivePasskeyKEK = derivePasskeyKEK;
|
|
3924
|
+
exports.deriveRecoveryKEK = deriveRecoveryKEK;
|
|
3925
|
+
exports.deriveStellarAddress = deriveStellarAddress;
|
|
3926
|
+
exports.deriveStellarMasterKeypair = deriveStellarMasterKeypair;
|
|
3927
|
+
exports.deviceSlotId = deviceSlotId;
|
|
3928
|
+
exports.eciesUnwrapDEK = eciesUnwrapDEK;
|
|
3929
|
+
exports.eciesWrapDEK = eciesWrapDEK;
|
|
2928
3930
|
exports.encodeLowSSignature = encodeLowSSignature;
|
|
2929
|
-
exports.
|
|
3931
|
+
exports.fromDataEntries = fromDataEntries;
|
|
3932
|
+
exports.generateControlKey = generateControlKey;
|
|
3933
|
+
exports.generateDEK = generateDEK;
|
|
2930
3934
|
exports.generateRecoveryCode = generateRecoveryCode;
|
|
2931
3935
|
exports.hexToBytes = hexToBytes;
|
|
2932
3936
|
exports.lowS = lowS;
|
|
3937
|
+
exports.openControlSeed = openControlSeed;
|
|
2933
3938
|
exports.recoverCandidatePublicKeys = recoverCandidatePublicKeys;
|
|
2934
3939
|
exports.recoverYParity = recoverYParity;
|
|
2935
|
-
exports.
|
|
3940
|
+
exports.sealControlSeed = sealControlSeed;
|
|
2936
3941
|
exports.serializeInstructions = serializeInstructions;
|
|
2937
3942
|
exports.signatureToFelts = signatureToFelts;
|
|
3943
|
+
exports.toDataEntries = toDataEntries;
|
|
2938
3944
|
exports.u256ToFelts = u256ToFelts;
|
|
3945
|
+
exports.unwrapDEK = unwrapDEK;
|
|
2939
3946
|
exports.webauthnDigest = webauthnDigest;
|
|
3947
|
+
exports.wrapDEK = wrapDEK;
|
|
2940
3948
|
//# sourceMappingURL=index.js.map
|
|
2941
3949
|
//# sourceMappingURL=index.js.map
|